Temas

Todos os componentes leem as mesmas propriedades personalizadas, por isso um tema é apenas um conjunto de sobreposições em :root — sem passo de construção, sem ficheiro de configuração, e sem nenhum componente a quem seja preciso avisar.

Meter os estilos

O styles() devolve um <link> para um único ficheiro, que o navegador guarda em cache em todas as páginas do site. Não há nada para configurar nem nada para copiar: o plugin do sitelo serve-o em dev e escreve-o na build, na mesma base que o runtime dos componentes.

import { styles } from 'sitelo/ui'

head(
  title('O meu site'),
  styles(),
)
// <link rel="stylesheet" href="/su/ui-c9428b65.css">

O nome leva um hash do conteúdo, por isso podes servi-lo como immutable e ainda assim publicar uma alteração. Passa { hash: false } para um simples /su/ui.css, ou base para apontar a ligação a uma cópia que alojes tu.

{ inline: true } põe antes a folha inteira num <style>: cerca de 11 kB em gzip em cada página, mas sem pedido extra e sem nada que possa faltar em dist/. É a melhor troca para uma página só; a ligação paga o seu pedido na segunda página que alguém lê.

head(
  title('O meu site'),
  styles({ inline: true }),
)

O resto da família dá-te as peças. O stylesheet() devolve o CSS em bruto como cadeia — para alojares a folha onde o sitelo não chega, ou para a escreveres tu em algum lado — e o stylesUrl() só o URL, para um elemento link teu.

Presets

Um preset muda o aspeto todo de uma vez. styles({ preset: 'neumorphism' }) liga uma segunda folha logo a seguir à principal — servida, com hash e em cache nas mesmas condições — e todos os componentes da página a seguem, sem nada a mudar na marcação.

import { styles } from 'sitelo/ui'

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

Cada preset tem a sua própria página, com todos os componentes redesenhados ao vivo: Neumorfismo, Neobrutalismo, Superneon.

A opção inline põe as duas folhas inline, o stylesheet({ preset }) devolve-as como uma só string, e um nome que não é um preset lança um erro com a lista dos que existem.

Sobrepor tokens

O theme() escreve as sobreposições. As chaves são nomes de tokens em camelCase, objetos de paleta, ou propriedades personalizadas literais — e vem depois do styles(), por isso ganha.

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',
  }),
)

Temas com âmbito

Um selector limita as sobreposições a uma subárvore em vez da página toda. É isso que os três painéis abaixo fazem — os mesmos componentes, três paletas diferentes, uma só página.

índigo
rosa
arredondado
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' }, 'índigo'),
        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' }, 'arredondado'),
        button({ block: true }, 'Primary'),
        button({ variant: 'soft', block: true }, 'Soft'),
      ))),
    ),
  ),
)

Modo escuro

O escuro resolve-se sozinho a partir do prefers-color-scheme. Um data-theme ou data-su-theme explícito com light ou dark em qualquer antepassado sobrepõe-se — é assim que as demonstrações deste site seguem o alternador da barra de topo.

Passa dark para sobreposições que só devem valer lá. Cobre o atributo e a media query de uma vez.

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

O que há para sobrepor

Cinco paletas de nove ranhuras cada, uma escala de espaçamento, tipos de letra, raios, sombras e as cores das superfícies. Cada uma delas é uma propriedade personalizada — abre a folha de estilos, ou o inspetor do teu navegador, e estão todas em :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),
      ),
    ),
  ),
)

Nomes

Uma chave em camelCase torna-se uma propriedade em kebab-case: radiusMd é --su-radius-md, fontSans é --su-font-sans. Um objeto aninhado expande-se da mesma maneira — { primary: { softFg: … } } define --su-primary-soft-fg — e uma chave que já comece por -- é usada exatamente como está escrita, o que é a saída de emergência para tudo o que o mapeamento não cobre.

Uma paleta tem nove ranhuras: base, hover, active, fg, soft, softHover, softFg, border e ring. Define apenas as que estiveres a mudar.

Contraste

As paletas que vêm incluídas cumprem o AA das WCAG contra as superfícies em que assentam, nos dois temas, e há um teste no repositório que falha a construção se isso deixar de ser verdade. Um tema teu não está coberto por ele — confere o teu fg contra o teu base antes de o publicares.

Props

styles():

PropTipoPredefiniçãoDescrição
preset'neumorphism' | 'neubrutalism' | 'superneon'—Muda o estilo de todos os componentes com um preset, ligado ou inline depois da folha principal.
inlinebooleanfalseEmite o próprio CSS em vez de uma ligação para ele.
hashbooleantrueAcrescenta ao nome do ficheiro um hash do conteúdo. Só ligado.
basestring'/su/'Aponta o URL para outro lado; essa cópia alojas tu. Só ligado.
minifybooleantrueRemove comentários e espaços. Só em linha.
noncestring—Nonce de CSP para o elemento emitido.

O stylesUrl() aceita base e hash; o stylesheet() aceita minify e preset.

theme(tokens, options):

PropTipoPredefiniçãoDescrição
selectorstring':root'Limita as sobreposições a uma subárvore.
darkobject—Sobreposições aplicadas só no modo escuro.
noncestring—Nonce de CSP.