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
O styles() devolve um <link> para um único ficheiro, que o browser guarda em cache entre páginas; o plugin do sitelo serve-o em dev e escreve-o na build, com um nome que leva um hash do conteúdo e podes servir como immutable. Com { inline: true } devolve antes um <style> com a folha completa: cerca de 11 kB na rede, sem pedido extra e sem nada que possa desaparecer de dist/:
import { styles } from 'sitelo/ui'
head(
// a folha inteira na página, sem pedido nenhum
styles({ inline: true }),
)A folha cobre todos os componentes da biblioteca, e um site usa uma mão-cheia. pruneCss: true no sitelo.config.js escreve apenas as regras que a build pode de facto aplicar: o plugin lê as classes su- das páginas que acabou de escrever — e dos scripts ao lado, pelas classes que um toast ou um passo acrescentam depois — e descarta toda a regra cujo seletor nomeie uma que nada usa. Decide-se pelo resultado, não pelos imports, por isso button({ variant: 'soft' }) mantém .su-btn--soft e um button() simples não. Uma folha ligada é aparada contra o site inteiro e recebe um hash novo, com cada <link> reescrito a condizer; uma inline é aparada à sua própria página. Uma dúzia de componentes fica em 2–3 kB gzip. Em dev serve-se a folha completa; a marcação que a build nunca vê, como a de uma ilha de servidor, precisa das suas classes em keep:
export default {
pruneCss: true,
// ou nomeia classes que a build nunca vê; `*` para um prefixo
// pruneCss: { keep: ['su-card', 'su-btn*'] },
}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()Ou muda o aspeto todo de uma vez: styles({ preset: 'neumorphism' }) liga a folha de um preset logo a seguir à principal, todos os componentes a seguem, e o theme() continua a funcionar por cima. Podes vê-lo ao vivo na página Neumorfismo.
JavaScript, e o pouco que é preciso
A maioria dos componentes não precisa de nenhum. O modal e a gaveta são elementos <dialog> que um botão abre com um command, por isso é o browser que trata do fundo, do foco, do Escape e do clique fora. O acordeão é um <details name>. Os menus são <details>. As dicas são CSS. Os separadores cujos painéis trocam no lugar são um grupo de rádios: cada separador é uma <label>, e o painel que se segue ao rádio marcado é o que o CSS mostra.
Três coisas querem mesmo um script, e cada uma vai buscá-lo sozinha:
- 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))">
<svg class="su-icon" …></svg>
</button>O sitelo serve esses módulos a partir de /su/ (ou /repo/su/ com --base /repo/) 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 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 <dialog> e o seu gatilho é qualquer botão cujo commandfor aponte para o seu id:
import { button, modal } from 'sitelo/ui'
button({ commandfor: 'confirm', command: 'show-modal' }, '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, com um grupo de rádios e CSS.
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, 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, stylesUrl, 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.