Componenti

sitelo-ui è una libreria di componenti per sitelo. Ogni componente è una funzione che restituisce una stringa di HTML, quindi si annida direttamente in un albero javascript-to-html senza nulla in mezzo — nessun compilatore, nessun runtime, nessuna idratazione. Quello che costruisci è quello che finisce in dist/.

Arriva insieme a sitelo, sotto il punto di ingresso sitelo/ui.

Avvio rapido

npm install sitelo javascript-to-html

Metti styles() nella head e chiama i componenti nel body. La configurazione è tutta qui:

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

export default () => html({ lang: 'it' },
  head(
    meta({ charset: 'utf-8' }),
    meta({ name: 'viewport', content: 'width=device-width, initial-scale=1' }),
    title('Il mio sito'),
    styles(),
  ),
  body(
    container({ size: 'md' },
      stack({ gap: 'md' },
        heading({ level: 1 }, 'Ciao'),
        text({ variant: 'lead' }, 'Una pagina costruita con i componenti.'),
        button({ href: '/docs' }, 'Leggi la documentazione'),
      ),
    ),
  ),
)

I nomi dei componenti combaciano di proposito con ciò che renderizzano, il che significa che alcuni di essi — button, input, table, link, code, select, progress — vanno a sbattere contro le funzioni-elemento di javascript-to-html. Importa la libreria come namespace quando ti servono entrambe:

import * as ui from 'sitelo/ui'

ui.card(
  ui.cardHeader({ title: 'Routing', subtitle: 'Basato sui file' }),
  ui.cardBody(ui.text('src/about.ht.js diventa /about.')),
  ui.cardFooter({ divided: true }, ui.button({ size: 'sm' }, 'Leggi di più')),
)

La convenzione di chiamata

Ogni componente prende un oggetto di props facoltativo seguito dai figli, esattamente come un elemento di javascript-to-html. Le props che il componente conosce vengono consumate per nome; tutto il resto passa all’elemento renderizzato come attributo, quindi id, data-*, aria-* e gli attributi di evento funzionano senza che la libreria debba elencarli:

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

Una prop di cui il componente non riconosce il valore — variant: 'nonsense' — ripiega sul valore predefinito invece di sollevare un errore. Un refuso estetico non deve far fallire una build.

Stili

styles() restituisce un <link> a un solo file, che il browser tiene in cache fra una pagina e l’altra; il plugin di sitelo lo serve in sviluppo e lo scrive nella build, sotto un nome con hash del contenuto che puoi servire come immutable. { inline: true } restituisce invece uno <style> che contiene l’intero foglio — circa 11 kB sulla rete, nessuna richiesta in più, e niente che possa sparire da dist/:

import { styles } from 'sitelo/ui'

head(
  // tutto il foglio di stile nella pagina — nessuna richiesta
  styles({ inline: true }),
)

Il foglio copre ogni componente della libreria, e un sito ne usa una manciata. pruneCss: true in sitelo.config.js scrive solo le regole che la build riesce ad agganciare: il plugin legge le classi su- dalle pagine che ha appena scritto — e dagli script accanto a esse, per le classi che un toast o uno step aggiungono dopo — e scarta ogni regola il cui selettore ne nomina una che nessuno porta. Si decide dall’output più che dagli import, quindi button({ variant: 'soft' }) tiene .su-btn--soft e un semplice button() no. Un foglio collegato viene potato rispetto a tutto il sito e riceve un nuovo hash, con ogni <link> riscritto di conseguenza; uno incorporato viene potato sulla sua sola pagina. Una dozzina di componenti fa 2–3 kB gzippati. Lo sviluppo serve il foglio intero; il markup che la build non vede mai, come quello di un’island server, ha bisogno che le sue classi siano nominate in keep:

export default {
  pruneCss: true,
  // oppure indica classi che la build non vede mai; `*` per un prefisso
  // pruneCss: { keep: ['su-card', 'su-btn*'] },
}

Temi

Tutto è guidato da proprietà personalizzate CSS su :root — cinque palette, una scala di spaziature, raggi, caratteri e ombre. theme() scrive i loro override, e accetta nomi in camelCase (radiusMd → --su-radius-md), oggetti palette, oppure proprietà personalizzate letterali:

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

head(
  styles(),
  // Dopo styles(), così vincono queste.
  theme({
    primary: { base: '#5b5bd6', hover: '#4a4ac4', fg: '#ffffff' },
    radiusMd: '2px',
    fontSans: '"Inter", system-ui, sans-serif',
  }, {
    dark: { primary: { base: '#8f8ff0' } },
  }),
)

La modalità scura si risolve da sola a partire da prefers-color-scheme. Impostare data-theme o data-su-theme a light o dark su un qualunque antenato la scavalca — ed è quello che fa themeToggle():

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

head(
  themeScript(), // applica la scelta salvata prima del primo paint
  styles(),
)

// …in qualunque punto del body
themeToggle()

Oppure cambia tutto l’aspetto in un colpo solo: styles({ preset: 'neumorphism' }) collega il foglio di un preset subito dopo quello principale, ogni componente lo segue e theme() continua a funzionare sopra. Lo vedi dal vivo nella pagina Neumorfismo.

JavaScript, e quanto poco ce n’è

Alla maggior parte dei componenti non serve affatto. Il modale e il pannello laterale sono elementi popover, quindi apertura, sfondo, clic fuori ed Escape li gestisce il browser. La fisarmonica è <details name>. I menu sono <details>. I tooltip sono CSS. Le schede i cui pannelli si scambiano sul posto sono un gruppo di radio: ogni scheda è una <label>, e il pannello che segue la radio selezionata è quello che il CSS mostra.

Tre cose vogliono uno script, e ciascuna se lo va a prendere da sé:

Non c’è niente da aggiungere al tuo file di ingresso — l’import è l’attributo di evento:

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

sitelo serve quei moduli da /su/ mentre sviluppi, e copia nella build quelli che le tue pagine referenziano davvero. Ognuno sta ben sotto il kilobyte, nessuno viene scaricato prima della prima interazione, e ogni componente si renderizza correttamente finché non lo è: i menu si aprono e si chiudono da soli, il pulsante di chiusura non fa nulla.

L’eccezione è toast(), perché non c’è niente nella pagina che lo faccia scattare al posto tuo:

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

Esempi

I form collegano da sé le proprie etichette, gli id, i testi di aiuto e i messaggi di errore:

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

card(
  cardBody(
    stack({ gap: 'md' },
      textField({ label: 'Email', name: 'email', type: 'email', help: 'Mai condivisa.' }),
      textField({ label: 'Sito', name: 'site', startAdornment: 'https://', error: 'Non è un URL.' }),
      selectField({ label: 'Piano', name: 'plan', options: ['Gratuito', 'Pro'], value: 'Pro' }),
    ),
  ),
  cardFooter({ divided: true }, button({ type: 'submit' }, 'Salva')),
)

Un modale è un popover e il suo innesco è un qualunque pulsante che punta al suo id:

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

button({ popovertarget: 'confirm' }, 'Elimina…')

modal({
  id: 'confirm',
  title: 'Eliminare questa pagina?',
  footer: button({ color: 'danger' }, 'Elimina'),
}, 'L’operazione non può essere annullata.')

Le schede arrivano in due forme — link, oppure pannelli:

// Schede-link: una pagina per scheda, nessuno script.
tabs({ items: [
  { label: 'Documentazione', href: '/docs', active: true },
  { label: 'API', href: '/api' },
] })

// Schede-pannello: scambio sul posto, con un gruppo di radio e CSS.
tabs({ value: 'use', items: [
  { id: 'install', label: 'Installazione', panel: code('npm install sitelo') },
  { id: 'use', label: 'Uso', panel: code("import * as ui from 'sitelo/ui'") },
] })

Le tabelle prendono columns e rows, con una funzione render ovunque una cella abbia bisogno di più di un valore:

table({
  striped: true,
  columns: [
    { key: 'page', header: 'Pagina' },
    { key: 'size', header: 'Dimensione', align: 'end' },
    { header: 'Stato', render: (row) => chip({ color: row.ok ? 'success' : 'danger' }, row.ok ? 'ok' : 'fallita') },
  ],
  rows: pages,
})

La cartella examples/ui nel repository renderizza ogni componente su una sola pagina — è il modo più rapido per vedere tutto l’insieme.

Riferimento dei componenti

Ogni export, per gruppo. Le props sono tipizzate: sitelo/ui include file .d.ts, quindi un editor completa variant, color e size al posto tuo tanto in JavaScript quanto in TypeScript.

GruppoComponenti
Layoutcontainer, stack, grid, divider, card, cardHeader, cardTitle, cardSubtitle, cardMedia, cardBody, cardFooter, aspectRatio
Tipografiatext, heading, link, code, inlineCode, kbd, visuallyHidden, prose
Campibutton, iconButton, buttonGroup, field, input, textarea, select, textField, textareaField, selectField, checkbox, radio, toggle, choiceGroup, slider, sliderField, toggleButton, toggleGroup
Visualizzazione datiavatar, avatarGroup, badge, chip, tooltip, table, list, listItem, figure
Feedbackalert, progress, skeleton, toasts, empty
Navigazionebreadcrumbs, pagination, tabs, appBar, appBarNav, appBarSpacer, appBarActions, navLink, themeToggle
Sovrapposizionimodal, drawer, closeButton, menu, menuItem, menuSeparator, accordion, accordionItem, collapsible
Sezionihero, footer, siteFooter, footerColumn, footerBottom, stat, statGroup, steps, timeline, timelineItem, mockup
Stilestyles, stylesheet, stylesUrl, theme, themeScript

Due nomi si discostano da quel che ti aspetteresti: l’interruttore è toggle, perché switch è una parola riservata e non può essere un binding di import; e l’ancora con stile è esportata sia come link sia come textLink, così può stare accanto al link di javascript-to-html. table, input, select e progress hanno la stessa via d’uscita: dataTable, textInput, selectField, progressBar.

Extra

Un secondo punto di ingresso, sitelo/ui-extras, contiene i componenti che non sono per tutti — texture ed effetti, a cominciare da una grana da pellicola. Ognuno porta un foglio di stile suo, grainStyles() accanto a styles(), così una pagina collega solo ciò che usa, e sitelo/ui-extras/client porta le loro chiamate lato pagina. Sono catalogati in sitelo UI extras.