Temas

Todos los componentes leen las mismas propiedades personalizadas, así que un tema no es más que un conjunto de anulaciones sobre :root: sin paso de compilación, sin archivo de configuración y sin ningún componente al que haya que avisar.

Meter los estilos

styles() devuelve un <link> a un solo archivo, que el navegador cachea en todas las páginas del sitio. No hay nada que configurar ni nada que copiar: el plugin de sitelo lo sirve en dev y lo escribe en la build, en la misma base que el runtime de los componentes.

import { styles } from 'sitelo/ui'

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

El nombre lleva un hash del contenido, así que puedes servirlo como immutable y aun así publicar un cambio. Pasa { hash: false } para un /su/ui.css sin más, o base para apuntar el enlace a una copia que alojes tú.

{ inline: true } mete la hoja entera en un <style>: unos 11 kB en gzip en cada página, pero ni una petición extra ni nada que pueda faltar en dist/. Es el mejor trato para una página suelta; el enlace se paga solo en la segunda página que lee una visita.

head(
  title('Mi sitio'),
  styles({ inline: true }),
)

El resto de la familia te da las piezas. stylesheet() devuelve el CSS crudo como cadena —para alojar la hoja donde sitelo no llega, o para escribirla tú donde quieras— y stylesUrl() solo la URL, para un elemento link propio.

Presets

Un preset cambia todo el aspecto de una vez. styles({ preset: 'neumorphism' }) enlaza una segunda hoja justo después de la principal — servida, con hash y en caché en las mismas condiciones — y cada componente de la página la sigue, sin que tengas que tocar el marcado.

import { styles } from 'sitelo/ui'

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

Cada preset tiene su propia página, con todos los componentes reestilizados en directo: Neumorfismo, Neobrutalismo, Superneon.

inline incrusta las dos hojas, stylesheet({ preset }) las devuelve como una sola cadena, y un nombre que no es un preset lanza un error con la lista de los que existen.

Anular tokens

theme() escribe las anulaciones. Las claves son nombres de token en camelCase, objetos de paleta o propiedades personalizadas literales — y va después de styles(), así que manda.

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 acotados

Un selector acota las anulaciones a un subárbol en vez de a toda la página. Eso es lo que hacen los tres paneles de abajo: los mismos componentes, tres paletas distintas, una sola página.

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

Modo oscuro

El oscuro se resuelve por su cuenta a partir de prefers-color-scheme. Un data-theme o data-su-theme explícito con valor light o dark en cualquier ancestro lo sobrescribe, que es como las demos de este sitio siguen al conmutador de la barra superior.

Pasa dark para anulaciones que solo deban aplicarse allí. Cubre el atributo y la media query de una vez.

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

Qué hay para anular

Cinco paletas de nueve ranuras cada una, una escala de espaciado, tipografía, radios, sombras y los colores de superficie. Cada una es una propiedad personalizada: abre la hoja de estilos, o el inspector de tu navegador, y están todas en :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 clave en camelCase pasa a ser una propiedad en kebab-case: radiusMd es --su-radius-md, y fontSans es --su-font-sans. Un objeto anidado se expande igual — { primary: { softFg: … } } pone --su-primary-soft-fg — y una clave que ya empiece por -- se usa exactamente tal cual, que es la vía de escape para lo que el mapeo no cubra.

Una paleta tiene nueve ranuras: base, hover, active, fg, soft, softHover, softFg, border y ring. Define solo las que vayas a cambiar.

Contraste

Las paletas que vienen de serie superan el AA de las WCAG contra las superficies sobre las que se apoyan, en ambos temas, y hay un test en el repositorio que tumba la compilación si eso deja de ser cierto. Un tema tuyo no está cubierto por él: comprueba tu fg contra tu base antes de publicarlo.

Props

styles():

PropTipoPor defectoDescripción
preset'neumorphism' | 'neubrutalism' | 'superneon'—Reestiliza todos los componentes con un preset, enlazado o incrustado después de la hoja principal.
inlinebooleanfalseEmite el CSS en sí en vez de un enlace a él.
hashbooleantrueAñade al nombre del archivo un hash del contenido. Solo enlazado.
basestring'/su/'Apunta la URL a otro sitio; esa copia la alojas tú. Solo enlazado.
minifybooleantrueQuita comentarios y espacios. Solo en línea.
noncestring—Nonce de CSP para el elemento emitido.

stylesUrl() acepta base y hash; stylesheet() acepta minify y preset.

theme(tokens, options):

PropTipoPor defectoDescripción
selectorstring':root'Acota las anulaciones a un subárbol.
darkobject—Anulaciones aplicadas solo en modo oscuro.
noncestring—Nonce de CSP.