Componentes
Nesta página
O sitelo-ui é uma biblioteca de componentes para o sitelo. Cada componente é uma função que devolve uma string de HTML, por isso encaixa diretamente numa árvore javascript-to-html sem nada pelo meio — sem compilador, sem runtime, sem hidratação. O que constróis é o que fica em dist/.
Vem com o sitelo, no ponto de entrada sitelo/ui.
Primeiros passos
npm install sitelo javascript-to-htmlPõe styles() no head e chama os componentes no body. A configuração é toda esta:
import { body, head, html, meta, title } from 'javascript-to-html'
import { styles, container, stack, heading, text, button } from 'sitelo/ui'
export default () => html({ lang: 'pt' },
head(
meta({ charset: 'utf-8' }),
meta({ name: 'viewport', content: 'width=device-width, initial-scale=1' }),
title('O meu site'),
styles(),
),
body(
container({ size: 'md' },
stack({ gap: 'md' },
heading({ level: 1 }, 'Olá'),
text({ variant: 'lead' }, 'Uma página construída a partir de componentes.'),
button({ href: '/docs' }, 'Ler a documentação'),
),
),
),
)Os nomes dos componentes correspondem de propósito àquilo que renderizam, o que faz com que alguns deles — button, input, table, link, code, select, progress — colidam com as funções de elemento do javascript-to-html. Importa a biblioteca como espaço de nomes quando precisares das duas:
import * as ui from 'sitelo/ui'
ui.card(
ui.cardHeader({ title: 'Rotas', subtitle: 'Baseado em ficheiros' }),
ui.cardBody(ui.text('src/about.ht.js passa a ser /about.')),
ui.cardFooter({ divided: true }, ui.button({ size: 'sm' }, 'Saber mais')),
)A convenção de chamada
Cada componente recebe um objeto de props opcional seguido dos filhos, tal como um elemento do javascript-to-html. As props que o componente conhece são consumidas pelo nome; tudo o resto passa para o elemento renderizado como atributo, por isso id, data-*, aria-* e os atributos de evento funcionam sem que a biblioteca tenha de os enumerar:
button({ id: 'save', 'data-analytics': 'save-click', onclick: 'save()' }, 'Guardar')
// <button type="button" id="save" data-analytics="save-click" onclick="save()" class="su-btn …">Uma prop cujo valor o componente não reconhece — variant: 'nonsense' — recai no valor por omissão em vez de lançar um erro. Uma gralha cosmética não deve fazer falhar uma compilação.
Estilos
styles() devolve um elemento <style> com a folha completa, minificada. São cerca de 7 kB na rede e não pode desaparecer de dist/ — é por isso que é a opção por omissão. Se preferires ligá-la uma só vez e deixar o browser guardá-la em cache entre páginas, importa o CSS a partir de um ficheiro de entrada empacotado e o Vite emite-o:
// src/main.js — empacotado pelo Vite, em cache entre páginas
import 'sitelo/ui/styles.css'Usa uma ou outra, não as duas.
Temas
Tudo é controlado por propriedades personalizadas de CSS em :root: cinco paletas, uma escala de espaçamento, raios, tipografia e sombras. theme() escreve as substituições e aceita nomes em camelCase (radiusMd → --su-radius-md), objetos de paleta ou propriedades personalizadas literais:
import { styles, theme } from 'sitelo/ui'
head(
styles(),
// Depois de styles(), para que estes prevaleçam.
theme({
primary: { base: '#5b5bd6', hover: '#4a4ac4', fg: '#ffffff' },
radiusMd: '2px',
fontSans: '"Inter", system-ui, sans-serif',
}, {
dark: { primary: { base: '#8f8ff0' } },
}),
)O modo escuro resolve-se sozinho a partir de prefers-color-scheme. Definir data-theme ou data-su-theme como light ou dark em qualquer ascendente sobrepõe-se a isso — e é exatamente o que themeToggle() faz:
import { styles, themeScript, themeToggle } from 'sitelo/ui'
head(
themeScript(), // aplica a escolha guardada antes da primeira pintura
styles(),
)
// …em qualquer sítio do body
themeToggle()JavaScript, e o pouco que é preciso
A maioria dos componentes não precisa de nenhum. O modal e a gaveta são elementos popover, por isso é o browser que trata da abertura, do fundo, do clique fora e do Escape. O acordeão é um <details name>. Os menus são <details>. As dicas são CSS.
Quatro coisas querem mesmo um script, e cada uma vai buscá-lo sozinha:
- os separadores cujos painéis trocam no lugar
- o botão de fechar de um alerta que se pode dispensar
- fechar um menu com um clique fora ou com Escape
- o alternador de tema
Não há nada a acrescentar ao teu ficheiro de entrada — o import é o atributo do evento:
<!-- rendered by alert({ dismissible: true }) -->
<button class="su-alert-dismiss"
onclick="import('/su/alert.js').then(m=>m.dismiss(this))">
×
</button>O sitelo serve esses módulos a partir de /su/ enquanto desenvolves e copia para o build apenas aqueles que as tuas páginas referenciam de facto. Cada um tem bem menos de um kilobyte, nenhum é descarregado antes da primeira interação e, até lá, todos os componentes renderizam corretamente: os separadores com painel mostram aquele que o servidor marcou como ativo, os menus abrem e fecham sozinhos e o botão de dispensar não faz nada.
A exceção é toast(), porque nada na página o dispara por ti:
// src/main.js
import { toast } from 'sitelo/ui/client'Exemplos
Os formulários ligam sozinhos as suas etiquetas, ids, textos de ajuda e mensagens de erro:
import { card, cardBody, cardFooter, button, stack, textField, selectField } from 'sitelo/ui'
card(
cardBody(
stack({ gap: 'md' },
textField({ label: 'E-mail', name: 'email', type: 'email', help: 'Nunca é partilhado.' }),
textField({ label: 'Site', name: 'site', startAdornment: 'https://', error: 'Não é um URL.' }),
selectField({ label: 'Plano', name: 'plan', options: ['Grátis', 'Pro'], value: 'Pro' }),
),
),
cardFooter({ divided: true }, button({ type: 'submit' }, 'Guardar')),
)Um modal é um popover e o seu gatilho é qualquer botão que aponte para o seu id:
import { button, modal } from 'sitelo/ui'
button({ popovertarget: 'confirm' }, 'Eliminar…')
modal({
id: 'confirm',
title: 'Eliminar esta página?',
footer: button({ color: 'danger' }, 'Eliminar'),
}, 'Isto não pode ser desfeito.')Os separadores existem em duas formas — ligações ou painéis:
// Separadores de ligação: uma página por separador, sem qualquer script.
tabs({ items: [
{ label: 'Docs', href: '/docs', active: true },
{ label: 'API', href: '/api' },
] })
// Separadores com painel: trocam no lugar e vão buscar sozinhos o código.
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'") },
] })As tabelas recebem columns e rows, com uma função render onde uma célula precisar de mais do que um valor:
table({
striped: true,
columns: [
{ key: 'page', header: 'Página' },
{ key: 'size', header: 'Tamanho', align: 'end' },
{ header: 'Estado', render: (row) => chip({ color: row.ok ? 'success' : 'danger' }, row.ok ? 'ok' : 'falhou') },
],
rows: pages,
})A pasta examples/ui do repositório renderiza todos os componentes numa única página — é a forma mais rápida de ver o conjunto completo.
Referência de componentes
Todas as exportações, por grupo. As props têm tipos: o sitelo/ui traz ficheiros .d.ts, por isso o editor completa variant, color e size tanto em JavaScript como em TypeScript.
| Grupo | Componentes |
|---|---|
| Disposição | container, stack, grid, divider, card, cardHeader, cardTitle, cardSubtitle, cardMedia, cardBody, cardFooter, aspectRatio |
| Tipografia | 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 |
| Apresentação de dados | avatar, avatarGroup, badge, chip, tooltip, table, list, listItem, figure |
| Feedback | alert, progress, spinner, skeleton, toasts, empty |
| Navegação | breadcrumbs, pagination, tabs, appBar, appBarNav, appBarSpacer, appBarActions, navLink, themeToggle |
| Sobreposições | modal, drawer, closeButton, menu, menuItem, menuSeparator, accordion, accordionItem, collapsible |
| Secções | hero, footer, siteFooter, footerColumn, footerBottom, stat, statGroup, steps, timeline, timelineItem, mockup |
| Estilos | styles, stylesheet, theme, themeScript |
Dois nomes fogem ao esperado: o interruptor é toggle, porque switch é uma palavra reservada e não pode ser uma ligação de importação; e a ligação com estilo é exportada como link e como textLink, para poder conviver com o link do javascript-to-html. table, input, select e progress têm a mesma saída de emergência: dataTable, textInput, selectField, progressBar.