Komponenty

sitelo-ui to biblioteka komponentów dla sitelo. Każdy komponent jest funkcją zwracającą ciąg HTML, więc zagnieżdża się wprost w drzewie javascript-to-html bez niczego po drodze — bez kompilatora, bez runtime’u, bez hydratacji. To, co budujesz, ląduje w dist/.

Jest dołączone do sitelo, pod punktem wejścia sitelo/ui.

Szybki start

npm install sitelo javascript-to-html

Wstaw styles() do head i wywołuj komponenty w body. To cała konfiguracja:

import { body, head, html, meta, title } from 'javascript-to-html'
import { styles, container, stack, heading, text, button } from 'sitelo/ui'

export default () => html({ lang: 'pl' },
  head(
    meta({ charset: 'utf-8' }),
    meta({ name: 'viewport', content: 'width=device-width, initial-scale=1' }),
    title('Moja strona'),
    styles(),
  ),
  body(
    container({ size: 'md' },
      stack({ gap: 'md' },
        heading({ level: 1 }, 'Cześć'),
        text({ variant: 'lead' }, 'Strona zbudowana z komponentów.'),
        button({ href: '/docs' }, 'Przeczytaj dokumentację'),
      ),
    ),
  ),
)

Nazwy komponentów celowo odpowiadają temu, co renderują, co oznacza, że kilka z nich — button, input, table, link, code, select, progress — zderza się z funkcjami elementów z javascript-to-html. Gdy potrzebujesz obu, zaimportuj bibliotekę jako przestrzeń nazw:

import * as ui from 'sitelo/ui'

ui.card(
  ui.cardHeader({ title: 'Routing', subtitle: 'Oparty na plikach' }),
  ui.cardBody(ui.text('src/about.ht.js staje się /about.')),
  ui.cardFooter({ divided: true }, ui.button({ size: 'sm' }, 'Czytaj dalej')),
)

Konwencja wywołania

Każdy komponent przyjmuje opcjonalny obiekt propsów, a po nim dzieci — dokładnie jak element javascript-to-html. Propsy, które komponent rozumie, są konsumowane po nazwie; cała reszta przechodzi na renderowany element jako atrybut, więc id, data-*, aria-* i atrybuty zdarzeń działają bez tego, żeby biblioteka musiała je wyliczać:

button({ id: 'save', 'data-analytics': 'save-click', onclick: 'save()' }, 'Zapisz')
// <button type="button" id="save" data-analytics="save-click" onclick="save()" class="su-btn …">

Props, którego wartości komponent nie rozpoznaje — variant: 'nonsense' — wraca do wartości domyślnej, zamiast rzucać błędem. Kosmetyczna literówka nie powinna przerywać buildu.

Style

styles() zwraca <link> do jednego pliku, który przeglądarka buforuje na wszystkich stronach; wtyczka sitelo serwuje go w dev i zapisuje do buildu, pod nazwą z hashem treści, którą możesz serwować jako immutable. { inline: true } zwraca zamiast tego <style> z całym arkuszem — około 11 kB w sieci, żadnego dodatkowego żądania i nic, co mogłoby zginąć z dist/:

import { styles } from 'sitelo/ui'

head(
  // cały arkusz w stronie — żadnego żądania
  styles({ inline: true }),
)

Arkusz obejmuje każdy komponent biblioteki, a witryna używa garstki. pruneCss: true w sitelo.config.js zapisuje tylko te reguły, które build potrafi dopasować: wtyczka czyta klasy su- ze stron, które właśnie zapisała — i ze skryptów obok nich, dla klas dokładanych później przez powiadomienie albo krok — i odrzuca każdą regułę, której selektor wymienia klasę, której nikt nie nosi. Decyduje o tym wynik, a nie importy, więc button({ variant: 'soft' }) zachowuje .su-btn--soft, a zwykły button() nie. Podlinkowany arkusz jest przycinany względem całej witryny i dostaje nowy hash, a każdy <link> zostaje przepisany; wbudowany przycina się do własnej strony. Kilkanaście komponentów to 2–3 kB po gzipie. Dev serwuje cały arkusz; znaczniki, których build nigdy nie widzi — na przykład z wyspy serwerowej — wymagają wymienienia swoich klas w keep:

export default {
  pruneCss: true,
  // albo wskaż klasy, których build nigdy nie widzi; `*` dla prefiksu
  // pruneCss: { keep: ['su-card', 'su-btn*'] },
}

Motywy

Wszystkim sterują własne właściwości CSS na :root — pięć palet, skala odstępów, promienie, krój i cienie. theme() zapisuje dla nich nadpisania i przyjmuje nazwy w camelCase (radiusMd → --su-radius-md), obiekty palet albo dosłowne właściwości własne:

import { styles, theme } from 'sitelo/ui'

head(
  styles(),
  // Po styles(), więc te wygrywają.
  theme({
    primary: { base: '#5b5bd6', hover: '#4a4ac4', fg: '#ffffff' },
    radiusMd: '2px',
    fontSans: '"Inter", system-ui, sans-serif',
  }, {
    dark: { primary: { base: '#8f8ff0' } },
  }),
)

Tryb ciemny rozstrzyga się sam na podstawie prefers-color-scheme. Ustawienie data-theme albo data-su-theme na light lub dark na dowolnym przodku to nadpisuje — i właśnie to robi themeToggle():

import { styles, themeScript, themeToggle } from 'sitelo/ui'

head(
  themeScript(), // stosuje zapisany wybór przed pierwszym malowaniem
  styles(),
)

// …gdziekolwiek w body
themeToggle()

Albo zmień cały wygląd za jednym razem: styles({ preset: 'neumorphism' }) dołącza arkusz presetu zaraz po głównym, każdy komponent za nim podąża, a theme() dalej działa na wierzchu. Na żywo zobaczysz to na stronie Neumorfizm.

JavaScript i jak mało go tu jest

Większość komponentów nie potrzebuje go wcale. Okno modalne i szuflada to elementy popover, więc otwieraniem, tłem, kliknięciem poza i Escape zajmuje się przeglądarka. Akordeon to <details name>. Menu to <details>. Podpowiedzi to CSS. Zakładki, których panele podmieniają się w miejscu, to grupa radio: każda zakładka to <label>, a panel następujący po zaznaczonym radiu jest tym, który CSS pokazuje.

Trzy rzeczy chcą skryptu i każda sama po niego sięga:

Nie ma czego dodawać do pliku wejściowego — importem jest atrybut zdarzenia:

<!-- rendered by alert({ dismissible: true }) -->
<button class="su-alert-dismiss"
        onclick="import('/su/alert.js').then(m=>m.dismiss(this))">
  &times;
</button>

sitelo serwuje te moduły z /su/ podczas pracy i kopiuje do buildu te, do których Twoje strony faktycznie się odwołują. Każdy waży znacznie poniżej kilobajta, żaden nie jest pobierany przed pierwszą interakcją, a każdy komponent renderuje się poprawnie, dopóki to nie nastąpi: menu otwierają się i zamykają same, a przycisk zamykania nic nie robi.

Wyjątkiem jest toast(), bo nic na stronie nie uruchamia go za Ciebie:

// src/main.js
import { toast } from 'sitelo/ui/client'

Przykłady

Formularze same łączą swoje etykiety, identyfikatory, teksty pomocy i komunikaty błędów:

import { card, cardBody, cardFooter, button, stack, textField, selectField } from 'sitelo/ui'

card(
  cardBody(
    stack({ gap: 'md' },
      textField({ label: 'E-mail', name: 'email', type: 'email', help: 'Nigdy nieudostępniany.' }),
      textField({ label: 'Strona', name: 'site', startAdornment: 'https://', error: 'To nie jest URL.' }),
      selectField({ label: 'Plan', name: 'plan', options: ['Darmowy', 'Pro'], value: 'Pro' }),
    ),
  ),
  cardFooter({ divided: true }, button({ type: 'submit' }, 'Zapisz')),
)

Okno modalne to popover, a jego wyzwalaczem jest dowolny przycisk wskazujący na jego id:

import { button, modal } from 'sitelo/ui'

button({ popovertarget: 'confirm' }, 'Usuń…')

modal({
  id: 'confirm',
  title: 'Usunąć tę stronę?',
  footer: button({ color: 'danger' }, 'Usuń'),
}, 'Tego nie da się cofnąć.')

Zakładki występują w dwóch postaciach — odnośniki albo panele:

// Zakładki-odnośniki: jedna strona na zakładkę, bez skryptu.
tabs({ items: [
  { label: 'Dokumentacja', href: '/docs', active: true },
  { label: 'API', href: '/api' },
] })

// Zakładki-panele: podmiana w miejscu, na grupie radio i CSS.
tabs({ value: 'use', items: [
  { id: 'install', label: 'Instalacja', panel: code('npm install sitelo') },
  { id: 'use', label: 'Użycie', panel: code("import * as ui from 'sitelo/ui'") },
] })

Tabele przyjmują columns i rows, a funkcja render wchodzi wszędzie tam, gdzie komórka potrzebuje czegoś więcej niż wartości:

table({
  striped: true,
  columns: [
    { key: 'page', header: 'Strona' },
    { key: 'size', header: 'Rozmiar', align: 'end' },
    { header: 'Status', render: (row) => chip({ color: row.ok ? 'success' : 'danger' }, row.ok ? 'ok' : 'błąd') },
  ],
  rows: pages,
})

Katalog examples/ui w repozytorium renderuje każdy komponent na jednej stronie — to najszybszy sposób, by zobaczyć cały zestaw.

Referencja komponentów

Każdy eksport, grupami. Propsy są otypowane: sitelo/ui dostarcza pliki .d.ts, więc edytor podpowiada variant, color i size zarówno w JavaScripcie, jak i w TypeScripcie.

GrupaKomponenty
Układcontainer, stack, grid, divider, card, cardHeader, cardTitle, cardSubtitle, cardMedia, cardBody, cardFooter, aspectRatio
Typografiatext, heading, link, code, inlineCode, kbd, visuallyHidden, prose
Pola formularzabutton, iconButton, buttonGroup, field, input, textarea, select, textField, textareaField, selectField, checkbox, radio, toggle, choiceGroup, slider, sliderField, toggleButton, toggleGroup
Prezentacja danychavatar, avatarGroup, badge, chip, tooltip, table, list, listItem, figure
Komunikatyalert, progress, skeleton, toasts, empty
Nawigacjabreadcrumbs, pagination, tabs, appBar, appBarNav, appBarSpacer, appBarActions, navLink, themeToggle
Warstwymodal, drawer, closeButton, menu, menuItem, menuSeparator, accordion, accordionItem, collapsible
Sekcjehero, footer, siteFooter, footerColumn, footerBottom, stat, statGroup, steps, timeline, timelineItem, mockup
Stylowaniestyles, stylesheet, stylesUrl, theme, themeScript

Dwie nazwy odbiegają od oczekiwań: przełącznik to toggle, bo switch jest słowem zastrzeżonym i nie może być wiązaniem importu; a ostylowana kotwica jest eksportowana i jako link, i jako textLink, żeby mogła stać obok link z javascript-to-html. table, input, select i progress mają to samo wyjście awaryjne: dataTable, textInput, selectField, progressBar.

Dodatki

Drugi punkt wejścia, sitelo/ui-extras, mieści komponenty nie dla wszystkich — faktury i efekty, na początek ziarno filmowe. Każdy przynosi własny arkusz stylów, grainStyles() obok styles(), więc strona podlinkowuje tylko to, czego używa, a sitelo/ui-extras/client niesie ich wywołania po stronie strony. Są skatalogowane w sitelo UI extras.