Componentes
En esta página
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-htmlPon 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:
- las pestañas cuyos paneles cambian en el sitio
- el botón de cerrar de una alerta descartable
- cerrar un menú al hacer clic fuera o pulsar Escape
- el conmutador de tema
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))">
×
</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.
| Grupo | Componentes |
|---|---|
| Maquetación | container, stack, grid, divider, card, cardHeader, cardTitle, cardSubtitle, cardMedia, cardBody, cardFooter, aspectRatio |
| Tipografía | text, heading, link, code, inlineCode, kbd, visuallyHidden, prose |
| Entradas | button, iconButton, buttonGroup, field, input, textarea, select, textField, textareaField, selectField, checkbox, radio, toggle, choiceGroup, slider, sliderField, toggleButton, toggleGroup |
| Visualización de datos | avatar, avatarGroup, badge, chip, tooltip, table, list, listItem, figure |
| Feedback | alert, progress, spinner, skeleton, toasts, empty |
| Navegación | breadcrumbs, pagination, tabs, appBar, appBarNav, appBarSpacer, appBarActions, navLink, themeToggle |
| Superposiciones | modal, drawer, closeButton, menu, menuItem, menuSeparator, accordion, accordionItem, collapsible |
| Secciones | hero, footer, siteFooter, footerColumn, footerBottom, stat, statGroup, steps, timeline, timelineItem, mockup |
| Estilos | styles, 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.