Temi

Ogni componente legge le stesse proprietà personalizzate, quindi un tema è un insieme di override su :root — nessun passaggio di build, nessun file di configurazione, e nessun componente a cui vada detto qualcosa.

Portare dentro gli stili

styles() restituisce un <link> a un solo file, che il browser tiene in cache su tutte le pagine del sito. Non c’è niente da configurare e niente da copiare: il plugin di sitelo lo serve in sviluppo e lo scrive nella build, alla stessa base del runtime dei componenti.

import { styles } from 'sitelo/ui'

head(
  title('Il mio sito'),
  styles(),
)
// <link rel="stylesheet" href="/su/ui-c9428b65.css">

Il nome porta un hash del contenuto, quindi puoi servirlo come immutable e pubblicare comunque una modifica. Passa { hash: false } per un semplice /su/ui.css, oppure base per puntare il link a una copia che ospiti tu.

{ inline: true } mette invece tutto il foglio in uno <style> — circa 11 kB gzippati in ogni pagina, ma nessuna richiesta in più e niente che possa sparire da dist/. È il compromesso migliore per una pagina sola; il link si ripaga la propria richiesta alla seconda pagina che un visitatore legge.

head(
  title('Il mio sito'),
  styles({ inline: true }),
)

Il resto della famiglia ti passa i pezzi. stylesheet() restituisce il CSS grezzo come stringa — per ospitare il foglio in un posto che sitelo non raggiunge, o per scriverlo tu da qualche parte — e stylesUrl() il solo href, per un elemento link tuo.

Preset

Un preset cambia tutto l’aspetto in un colpo solo. styles({ preset: 'neumorphism' }) collega un secondo foglio subito dopo quello principale — servito, con hash e in cache alle stesse condizioni — e ogni componente della pagina lo segue, senza toccare il markup.

import { styles } from 'sitelo/ui'

head(
  title('Il mio sito'),
  styles({ preset: 'neumorphism' }),
)
// <link rel="stylesheet" href="/su/ui-c9428b65.css">
// <link rel="stylesheet" href="/su/neumorphism-5d0e7b91.css">

Ogni preset ha una pagina tutta sua, con ogni componente ridisegnato dal vivo: Neumorfismo, Neobrutalismo, Superneon.

inline mette inline entrambi i fogli, stylesheet({ preset }) li restituisce come un’unica stringa, e un nome che non è un preset lancia un errore con l’elenco di quelli che ci sono.

Scavalcare i token

theme() scrive gli override. Le chiavi sono nomi di token in camelCase, oggetti palette, o proprietà personalizzate letterali — e va dopo styles(), così vince.

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

head(
  styles(),
  theme({
    primary: { base: '#5b5bd6', hover: '#4a4ac4', active: '#3f3fb0', fg: '#ffffff' },
    radiusMd: '2px',
    fontSans: '"Inter", system-ui, sans-serif',
  }),
)

Temi circoscritti

Un selector circoscrive gli override a un sottoalbero invece che a tutta la pagina. È ciò che fanno i tre pannelli qui sotto — stessi componenti, tre palette diverse, una sola pagina.

indaco
rosa
arrotondato
fragment(
  theme({ primary: { base: '#5b5bd6', hover: '#4a4ac4', fg: '#ffffff', soft: '#e6e6fa', softFg: '#33338f', border: '#b9b9ee' } }, { selector: '.theme-indigo' }),
  theme({ primary: { base: '#b0357a', hover: '#962e68', fg: '#ffffff', soft: '#fbe4f0', softFg: '#7d1f53', border: '#f0a9ce' } }, { selector: '.theme-pink' }),
  theme({ radiusMd: '999px', radiusLg: '1.5rem' }, { selector: '.theme-round' }),
  grid({ min: '11rem' },
    div({ class: 'theme-indigo' },
      card(cardBody(stack({ gap: 'sm' },
        text({ variant: 'caption', tone: 'muted' }, 'indaco'),
        button({ block: true }, 'Primary'),
        button({ variant: 'soft', block: true }, 'Soft'),
      ))),
    ),
    div({ class: 'theme-pink' },
      card(cardBody(stack({ gap: 'sm' },
        text({ variant: 'caption', tone: 'muted' }, 'rosa'),
        button({ block: true }, 'Primary'),
        button({ variant: 'soft', block: true }, 'Soft'),
      ))),
    ),
    div({ class: 'theme-round' },
      card(cardBody(stack({ gap: 'sm' },
        text({ variant: 'caption', tone: 'muted' }, 'arrotondato'),
        button({ block: true }, 'Primary'),
        button({ variant: 'soft', block: true }, 'Soft'),
      ))),
    ),
  ),
)

Modalità scura

Lo scuro si risolve da sé a partire da prefers-color-scheme. Un data-theme o data-su-theme esplicito con valore light o dark su un qualunque antenato lo scavalca — ed è così che le demo di questo sito seguono il pulsante nella barra in alto.

Passa dark per override che devono valere solo lì. Copre in un colpo solo l’attributo e la media query.

theme({
  primary: { base: '#5b5bd6' },
}, {
  dark: { primary: { base: '#8f8ff0' } },
})

Che cosa c’è da scavalcare

Cinque palette di nove caselle ciascuna, una scala di spaziature, i caratteri, i raggi, le ombre e i colori delle superfici. Ognuno è una proprietà personalizzata — apri il foglio di stile, o l’ispettore del tuo browser, e sono tutte su :root.

primary
neutral
success
warning
danger
xs
sm
md
lg
xl
stack({ gap: 'md' },
  stack({ direction: 'row', gap: 'sm', wrap: true },
    ...['primary', 'neutral', 'success', 'warning', 'danger'].map((color) =>
      stack({ gap: 'xs', align: 'center' },
        div({ style: 'width: 3.5rem; height: 2rem; border-radius: 0.4rem; background: var(--su-' + color + ')' }),
        text({ variant: 'caption', tone: 'muted' }, color),
      ),
    ),
  ),
  stack({ direction: 'row', gap: 'sm', wrap: true, align: 'flex-end' },
    ...['xs', 'sm', 'md', 'lg', 'xl'].map((step) =>
      stack({ gap: 'xs', align: 'center' },
        div({ style: 'width: var(--su-space-' + step + '); height: 2rem; border-radius: 0.2rem; background: var(--su-neutral)' }),
        text({ variant: 'caption', tone: 'muted' }, step),
      ),
    ),
  ),
)

Nomenclatura

Una chiave in camelCase diventa una proprietà in kebab-case: radiusMd è --su-radius-md, fontSans è --su-font-sans. Un oggetto annidato si espande allo stesso modo — { primary: { softFg: … } } imposta --su-primary-soft-fg — e una chiave che inizia già con -- viene usata esattamente come è scritta, che è la via d’uscita per tutto ciò che la conversione non copre.

Una palette ha nove caselle: base, hover, active, fg, soft, softHover, softFg, border e ring. Imposta solo quelle che stai cambiando.

Contrasto

Le palette incluse superano il livello AA delle WCAG rispetto alle superfici su cui stanno, in entrambi i temi, e nel repository c’è un test che fa fallire la build se questo smette di essere vero. Un tema tuo non è coperto da quel test — controlla il tuo fg rispetto al tuo base prima di pubblicare.

Props

styles():

PropTipoPredefinitoDescrizione
preset'neumorphism' | 'neubrutalism' | 'superneon'—Ridisegna ogni componente con un preset, collegato o inline dopo il foglio principale.
inlinebooleanfalseEmetti il CSS stesso invece di un link a esso.
hashbooleantrueMetti un hash del contenuto nel nome del file. Solo forma collegata.
basestring'/su/'Punta l’URL altrove; quella copia la ospiti tu. Solo forma collegata.
minifybooleantrueTogli commenti e spazi. Solo forma inline.
noncestring—Nonce CSP per l’elemento emesso.

stylesUrl() accetta base e hash; stylesheet() accetta minify e preset.

theme(tokens, options):

PropTipoPredefinitoDescrizione
selectorstring':root'Circoscrive gli override a un sottoalbero.
darkobject—Override applicati solo in modalità scura.
noncestring—Nonce CSP.