Componentes

sitelo-ui es una biblioteca de componentes para sitelo. Cada componente es una función que devuelve una cadena de HTML, así que encaja directamente en un árbol de javascript-to-html sin nada en medio: sin compilador, sin runtime, sin hidratación. Lo que construyes es lo que acaba en dist/.

Viene con sitelo, en el punto de entrada sitelo/ui.

Primeros pasos

npm install sitelo javascript-to-html

Pon styles() en el head y llama a los componentes en el body. Esa es toda la configuración:

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

export default () => html({ lang: 'es' },
  head(
    meta({ charset: 'utf-8' }),
    meta({ name: 'viewport', content: 'width=device-width, initial-scale=1' }),
    title('Mi sitio'),
    styles(),
  ),
  body(
    container({ size: 'md' },
      stack({ gap: 'md' },
        heading({ level: 1 }, 'Hola'),
        text({ variant: 'lead' }, 'Una página construida con componentes.'),
        button({ href: '/docs' }, 'Leer la documentación'),
      ),
    ),
  ),
)

Los nombres de los componentes coinciden a propósito con lo que renderizan, lo que hace que unos cuantos — button, input, table, link, code, select, progress — choquen con las funciones de elemento de javascript-to-html. Importa la biblioteca como espacio de nombres cuando necesites ambas:

import * as ui from 'sitelo/ui'

ui.card(
  ui.cardHeader({ title: 'Rutas', subtitle: 'Basado en archivos' }),
  ui.cardBody(ui.text('src/about.ht.js pasa a ser /about.')),
  ui.cardFooter({ divided: true }, ui.button({ size: 'sm' }, 'Leer más')),
)

La convención de llamada

Cada componente recibe un objeto de props opcional seguido de sus hijos, exactamente como un elemento de javascript-to-html. Las props que el componente entiende se consumen por nombre; todo lo demás pasa al elemento renderizado como atributo, así que id, data-*, aria-* y los atributos de evento funcionan sin que la biblioteca tenga que enumerarlos:

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

Una prop cuyo valor el componente no reconoce — variant: 'nonsense' — vuelve al valor por defecto en lugar de lanzar un error. Una errata cosmética no debería romper una compilación.

Estilos

styles() devuelve un elemento <style> con la hoja completa, minificada. Son unos 7 kB por la red y no puede faltar en dist/, por eso es la opción por defecto. Si prefieres enlazarla una vez y que el navegador la cachee entre páginas, importa el CSS desde un archivo de entrada empaquetado y Vite lo emitirá:

// src/main.js — empaquetado por Vite, cacheado entre páginas
import 'sitelo/ui/styles.css'

Usa una u otra, no las dos.

Temas

Todo se controla con propiedades personalizadas de CSS en :root: cinco paletas, una escala de espaciado, radios, tipografía y sombras. theme() escribe sus sobrescrituras y acepta nombres en camelCase (radiusMd--su-radius-md), objetos de paleta o propiedades personalizadas literales:

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

head(
  styles(),
  // Después de styles(), para que estos ganen.
  theme({
    primary: { base: '#5b5bd6', hover: '#4a4ac4', fg: '#ffffff' },
    radiusMd: '2px',
    fontSans: '"Inter", system-ui, sans-serif',
  }, {
    dark: { primary: { base: '#8f8ff0' } },
  }),
)

El modo oscuro se resuelve solo a partir de prefers-color-scheme. Poner data-theme o data-su-theme en light o dark en cualquier ancestro lo anula, que es justo lo que hace themeToggle():

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

head(
  themeScript(), // aplica la elección guardada antes del primer pintado
  styles(),
)

// …en cualquier parte del body
themeToggle()

JavaScript, y lo poco que hay

La mayoría de los componentes no necesita ninguno. El modal y el panel lateral son elementos popover, así que el navegador se encarga de abrirlos, del fondo, del clic fuera y de Escape. El acordeón es <details name>. Los menús son <details>. Los tooltips son CSS.

Cuatro cosas sí quieren un script, y cada una va a buscarlo por su cuenta:

No hay nada que añadir a tu archivo de entrada: el import es el atributo del evento:

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

sitelo sirve esos módulos desde /su/ mientras desarrollas y copia al build solo los que tus páginas referencian de verdad. Cada uno pesa bastante menos de un kilobyte, ninguno se descarga antes de la primera interacción y hasta entonces todos los componentes se renderizan bien: las pestañas con panel muestran el que el servidor marcó como activo, los menús se abren y se cierran solos y el botón de descartar no hace nada.

La excepción es toast(), porque nada en la página lo dispara por ti:

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

Ejemplos

Los formularios conectan solos sus etiquetas, ids, textos de ayuda y mensajes de error:

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

card(
  cardBody(
    stack({ gap: 'md' },
      textField({ label: 'Correo', name: 'email', type: 'email', help: 'Nunca se comparte.' }),
      textField({ label: 'Sitio', name: 'site', startAdornment: 'https://', error: 'No es una URL.' }),
      selectField({ label: 'Plan', name: 'plan', options: ['Gratis', 'Pro'], value: 'Pro' }),
    ),
  ),
  cardFooter({ divided: true }, button({ type: 'submit' }, 'Guardar')),
)

Un modal es un popover y su disparador es cualquier botón que apunte a su id:

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

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

modal({
  id: 'confirm',
  title: '¿Eliminar esta página?',
  footer: button({ color: 'danger' }, 'Eliminar'),
}, 'Esto no se puede deshacer.')

Las pestañas vienen en dos formas: enlaces o paneles.

// Pestañas de enlace: una página por pestaña, sin ningún script.
tabs({ items: [
  { label: 'Docs', href: '/docs', active: true },
  { label: 'API', href: '/api' },
] })

// Pestañas con panel: cambian en el sitio y piden solas el código para hacerlo.
tabs({ value: 'use', items: [
  { id: 'install', label: 'Instalar', panel: code('npm install sitelo') },
  { id: 'use', label: 'Usar', panel: code("import * as ui from 'sitelo/ui'") },
] })

Las tablas reciben columns y rows, con una función render allí donde una celda necesita más que un valor:

table({
  striped: true,
  columns: [
    { key: 'page', header: 'Página' },
    { key: 'size', header: 'Tamaño', align: 'end' },
    { header: 'Estado', render: (row) => chip({ color: row.ok ? 'success' : 'danger' }, row.ok ? 'ok' : 'falló') },
  ],
  rows: pages,
})

El directorio examples/ui del repositorio renderiza todos los componentes en una sola página: es la forma más rápida de ver el conjunto completo.

Referencia de componentes

Todas las exportaciones, por grupo. Las props están tipadas: sitelo/ui incluye archivos .d.ts, así que el editor completa variant, color y size tanto en JavaScript como en TypeScript.

GrupoComponentes
Maquetacióncontainer, stack, grid, divider, card, cardHeader, cardTitle, cardSubtitle, cardMedia, cardBody, cardFooter, aspectRatio
Tipografíatext, heading, link, code, inlineCode, kbd, visuallyHidden, prose
Entradasbutton, iconButton, buttonGroup, field, input, textarea, select, textField, textareaField, selectField, checkbox, radio, toggle, choiceGroup, slider, sliderField, toggleButton, toggleGroup
Visualización de datosavatar, avatarGroup, badge, chip, tooltip, table, list, listItem, figure
Feedbackalert, progress, spinner, skeleton, toasts, empty
Navegaciónbreadcrumbs, pagination, tabs, appBar, appBarNav, appBarSpacer, appBarActions, navLink, themeToggle
Superposicionesmodal, drawer, closeButton, menu, menuItem, menuSeparator, accordion, accordionItem, collapsible
Seccioneshero, footer, siteFooter, footerColumn, footerBottom, stat, statGroup, steps, timeline, timelineItem, mockup
Estilosstyles, stylesheet, theme, themeScript

Dos nombres se apartan de lo esperable: el interruptor es toggle, porque switch es una palabra reservada y no puede ser un enlace de importación; y el enlace con estilo se exporta como link y como textLink, para poder convivir con el link de javascript-to-html. table, input, select y progress tienen la misma salida de emergencia: dataTable, textInput, selectField, progressBar.