Composants

sitelo-ui est une bibliothèque de composants pour sitelo. Chaque composant est une fonction qui renvoie une chaîne de HTML : il s’imbrique donc directement dans un arbre javascript-to-html, sans rien entre les deux — pas de compilateur, pas de runtime, pas d’hydratation. Ce que vous écrivez est exactement ce qui atterrit dans dist/.

Elle est livrée avec sitelo, sous le point d’entrée sitelo/ui.

Démarrage

npm install sitelo javascript-to-html

Placez styles() dans le head et appelez les composants dans le body. C’est toute la configuration :

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

export default () => html({ lang: 'fr' },
  head(
    meta({ charset: 'utf-8' }),
    meta({ name: 'viewport', content: 'width=device-width, initial-scale=1' }),
    title('Mon site'),
    styles(),
  ),
  body(
    container({ size: 'md' },
      stack({ gap: 'md' },
        heading({ level: 1 }, 'Bonjour'),
        text({ variant: 'lead' }, 'Une page construite à partir de composants.'),
        button({ href: '/docs' }, 'Lire la documentation'),
      ),
    ),
  ),
)

Les noms des composants correspondent volontairement à ce qu’ils rendent, ce qui fait que quelques-uns — button, input, table, link, code, select, progress — entrent en collision avec les fonctions d’élément de javascript-to-html. Importez la bibliothèque comme espace de noms quand vous avez besoin des deux :

import * as ui from 'sitelo/ui'

ui.card(
  ui.cardHeader({ title: 'Routage', subtitle: 'Basé sur les fichiers' }),
  ui.cardBody(ui.text('src/about.ht.js devient /about.')),
  ui.cardFooter({ divided: true }, ui.button({ size: 'sm' }, 'En savoir plus')),
)

La convention d’appel

Chaque composant prend un objet de props optionnel suivi de ses enfants, exactement comme un élément javascript-to-html. Les props que le composant comprend sont consommées par leur nom ; tout le reste est transmis à l’élément rendu sous forme d’attribut, si bien que id, data-*, aria-* et les attributs d’événement fonctionnent sans que la bibliothèque ait à les énumérer :

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

Une prop dont le composant ne reconnaît pas la valeur — variant: 'nonsense' — retombe sur la valeur par défaut au lieu de lever une erreur. Une faute de frappe cosmétique ne devrait pas faire échouer un build.

Styles

styles() renvoie un élément <style> contenant toute la feuille, minifiée. Cela représente environ 7 ko sur le réseau et ne peut pas disparaître de dist/ : c’est pourquoi c’est le comportement par défaut. Si vous préférez la lier une seule fois et laisser le navigateur la mettre en cache d’une page à l’autre, importez le CSS depuis un fichier d’entrée groupé et Vite l’émettra :

// src/main.js — inclus dans le bundle par Vite, mis en cache entre les pages
import 'sitelo/ui/styles.css'

Utilisez l’un ou l’autre, pas les deux.

Thèmes

Tout repose sur des propriétés personnalisées CSS déclarées sur :root : cinq palettes, une échelle d’espacement, des rayons, la typographie et les ombres. theme() écrit leurs surcharges et accepte des noms en camelCase (radiusMd--su-radius-md), des objets de palette ou des propriétés personnalisées littérales :

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

head(
  styles(),
  // Après styles(), pour que ces valeurs l’emportent.
  theme({
    primary: { base: '#5b5bd6', hover: '#4a4ac4', fg: '#ffffff' },
    radiusMd: '2px',
    fontSans: '"Inter", system-ui, sans-serif',
  }, {
    dark: { primary: { base: '#8f8ff0' } },
  }),
)

Le mode sombre se résout tout seul à partir de prefers-color-scheme. Donner à data-theme ou data-su-theme la valeur light ou dark sur n’importe quel ancêtre l’emporte — c’est exactement ce que fait themeToggle() :

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

head(
  themeScript(), // applique le choix enregistré avant le premier rendu
  styles(),
)

// …n’importe où dans le body
themeToggle()

JavaScript, et le peu qu’il en faut

La plupart des composants n’en ont pas besoin. La modale et le tiroir sont des éléments popover : le navigateur gère l’ouverture, l’arrière-plan, le clic à l’extérieur et Échap. L’accordéon est un <details name>. Les menus sont des <details>. Les infobulles sont en CSS.

Quatre choses réclament vraiment un script, et chacune va le chercher elle-même :

Rien à ajouter à votre fichier d’entrée : l’import est l’attribut d’événement :

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

sitelo sert ces modules depuis /su/ pendant le développement et copie dans la build uniquement ceux que vos pages référencent réellement. Chacun pèse bien moins d’un kilo-octet, aucun n’est chargé avant la première interaction, et jusque-là chaque composant s’affiche correctement : les onglets à panneaux montrent celui que le serveur a marqué actif, les menus s’ouvrent et se ferment seuls, et le bouton de fermeture ne fait rien.

L’exception est toast(), car rien sur la page ne le déclenche pour vous :

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

Exemples

Les formulaires relient eux-mêmes leurs libellés, leurs ids, leurs textes d’aide et leurs messages d’erreur :

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: 'Jamais partagé.' }),
      textField({ label: 'Site', name: 'site', startAdornment: 'https://', error: 'Ce n’est pas une URL.' }),
      selectField({ label: 'Formule', name: 'plan', options: ['Gratuit', 'Pro'], value: 'Pro' }),
    ),
  ),
  cardFooter({ divided: true }, button({ type: 'submit' }, 'Enregistrer')),
)

Une modale est un popover, et son déclencheur est n’importe quel bouton qui pointe vers son id :

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

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

modal({
  id: 'confirm',
  title: 'Supprimer cette page ?',
  footer: button({ color: 'danger' }, 'Supprimer'),
}, 'Cette action est irréversible.')

Les onglets existent sous deux formes : des liens, ou des panneaux.

// Onglets liens : une page par onglet, aucun script.
tabs({ items: [
  { label: 'Docs', href: '/docs', active: true },
  { label: 'API', href: '/api' },
] })

// Onglets à panneaux : échange sur place, et ils vont chercher le code eux-mêmes.
tabs({ value: 'use', items: [
  { id: 'install', label: 'Installer', panel: code('npm install sitelo') },
  { id: 'use', label: 'Utiliser', panel: code("import * as ui from 'sitelo/ui'") },
] })

Les tableaux prennent columns et rows, avec une fonction render partout où une cellule demande plus qu’une valeur :

table({
  striped: true,
  columns: [
    { key: 'page', header: 'Page' },
    { key: 'size', header: 'Taille', align: 'end' },
    { header: 'Statut', render: (row) => chip({ color: row.ok ? 'success' : 'danger' }, row.ok ? 'ok' : 'échec') },
  ],
  rows: pages,
})

Le dossier examples/ui du dépôt affiche tous les composants sur une seule page — c’est le moyen le plus rapide de voir l’ensemble.

Référence des composants

Tous les exports, par groupe. Les props sont typées : sitelo/ui livre des fichiers .d.ts, de sorte qu’un éditeur complète variant, color et size aussi bien en JavaScript qu’en TypeScript.

GroupeComposants
Mise en pagecontainer, stack, grid, divider, card, cardHeader, cardTitle, cardSubtitle, cardMedia, cardBody, cardFooter, aspectRatio
Typographietext, heading, link, code, inlineCode, kbd, visuallyHidden, prose
Champsbutton, iconButton, buttonGroup, field, input, textarea, select, textField, textareaField, selectField, checkbox, radio, toggle, choiceGroup, slider, sliderField, toggleButton, toggleGroup
Affichage de donnéesavatar, avatarGroup, badge, chip, tooltip, table, list, listItem, figure
Retour d’informationalert, progress, spinner, skeleton, toasts, empty
Navigationbreadcrumbs, pagination, tabs, appBar, appBarNav, appBarSpacer, appBarActions, navLink, themeToggle
Superpositionsmodal, drawer, closeButton, menu, menuItem, menuSeparator, accordion, accordionItem, collapsible
Sectionshero, footer, siteFooter, footerColumn, footerBottom, stat, statGroup, steps, timeline, timelineItem, mockup
Stylesstyles, stylesheet, theme, themeScript

Deux noms s’écartent de ce à quoi on s’attend : l’interrupteur s’appelle toggle, parce que switch est un mot réservé et ne peut pas servir de liaison d’import ; et le lien stylé est exporté à la fois comme link et comme textLink, pour cohabiter avec le link de javascript-to-html. table, input, select et progress disposent de la même échappatoire : dataTable, textInput, selectField, progressBar.