Registre et CLI
Pourquoi copier les composants
Les composants applicatifs changent souvent selon le produit. Les copier permet de les personnaliser sans forker une bibliothèque entière, tout en conservant une base commune et documentée.
Flux d'installation
Commandes
pnpm dlx @sia-ui/cli init # sia-ui.json et la feuille de styles
pnpm dlx @sia-ui/cli list # le catalogue, avec versions
pnpm dlx @sia-ui/cli list --outdated # ce qui a bougé depuis l'installation
pnpm dlx @sia-ui/cli add button page-header # installer, dépendances comprises
pnpm dlx @sia-ui/cli update [nom...] # mettre à jour, sans écraser les retouches
pnpm dlx @sia-ui/cli diff [nom...] # ce qui a été retouché localement
pnpm dlx @sia-ui/cli remove <nom...> # désinstaller
pnpm dlx @sia-ui/cli doctor # l'état de tout ce qui est installé| Option | Effet |
|---|---|
--overwrite | add : écraser un fichier déjà présent |
--force | update : écraser aussi les fichiers retouchés ; remove : passer outre ses refus |
--dry-run | update : montrer ce qui changerait, sans rien écrire |
--registry <url> | lire un registre servi en HTTP |
Après la copie
Un composant copié cesse de suivre les paquets : il vit dans le dépôt de qui l'a installé, qui le modifie comme il veut. C'est la contrepartie du modèle, et c'est aussi ce qui rendait le registre sans retour — installer, puis plus rien. Impossible de corriger ou de déprécier une entrée après coup.
sia-ui.lock.json, écrit à chaque add, referme cette impasse :
{
"format": 1,
"items": {
"badge": {
"version": "0.1.0",
"installedAt": "2026-09-22T21:15:12.846Z",
"files": {
"src/components/Badge/index.tsx": "sha256:24616e…"
}
}
}
}L'empreinte est celle du fichier tel qu'il a été copié. Comparée au disque, elle dit si l'utilisateur l'a retouché; comparée à la source du registre, elle dit si l'entrée a bougé depuis. C'est de là que les quatre commandes tirent tout ce qu'elles savent.
Les chemins y sont écrits en barres obliques, jamais à la mode Windows : le fichier est versionné dans le dépôt de l'utilisateur, et deux machines ne doivent pas décrire le même fichier avec deux clés différentes.
remove refuse deux choses par défaut : retirer une entrée dont une autre dépend encore, et supprimer un fichier retouché depuis l'installation. --force lève les deux — le travail de quelqu'un ne s'efface pas sans qu'il le demande.
Mettre à jour
pnpm dlx @sia-ui/cli@latest list --outdated # ce qui a une version plus récente
pnpm dlx @sia-ui/cli@latest update --dry-run # ce qui changerait
pnpm dlx @sia-ui/cli@latest update # l'appliquer@latest compte : le registre embarqué est figé au jour où la CLI a été publiée. Un projet branché sur le registre HTTP (registry dans sia-ui.json, ou --registry) le voit à jour sans changer de CLI.
Seules les entrées dont la version a changé sont concernées. update compare la version notée dans le verrou à celle du registre ; les numéros des paquets npm n'y jouent aucun rôle. Pour chaque fichier, l'empreinte du verrou dit s'il a été retouché :
| Fichier | update | update --force |
|---|---|---|
| intact depuis l'installation | remplacé | remplacé |
| retouché localement | gardé, signalé | remplacé |
| nouveau dans cette version | ajouté | ajouté |
| présent mais inconnu du verrou | gardé | remplacé |
| plus fourni par la nouvelle version | laissé, signalé | laissé, signalé |
Une entrée dont un fichier a été gardé reste en retard dans le verrou : la question se reposera au prochain update, au lieu d'être tenue pour réglée. sia-ui diff <nom> montre l'écart pour reprendre la modification à la main. Les dépendances de registre apparues avec une nouvelle version sont installées, les paquets npm annoncés, et la feuille commune suit.
Une modification doit changer la version
Puisque les projets ne voient une mise à jour que par la version de l'entrée, modifier components/Pagination/index.tsx sans monter pagination publie une correction que personne ne reçoit. pnpm check:registry-versions, lancé par la CI, l'interdit : il compare chaque fichier d'entrée à la dernière publication et échoue quand un fichier a changé alors que la version de son entrée est restée la même.
La référence est le commit qui a fixé la version publiée dans packages/registry/package.json — pas le tag seul, qui peut pointer sur le commit d'avant quand on publie depuis un arbre non commité. Entre deux releases, une entrée peut changer autant de fois qu'il faut : une montée de version suffit. Une entrée nouvelle passe toujours.
Entrées modifiées depuis b388ce8 (@sia-ui/registry@0.5.0) sans nouvelle version :
crud-page@0.3.0
packages/react-web/src/components/CrudPage/index.tsxMonter la version dans registry.json, puis expliquer le changement dans packages/registry/CHANGELOG.md — pnpm check:changelog refuse une version sans explication. Les deux contrôles se complètent : l'un exige qu'un changement porte une version, l'autre qu'une version porte une explication.
Le cycle de vie d'une entrée
add avertit sur une entrée deprecated et refuse une entrée removed, en nommant celle qui prend la suite quand elle existe. doctor signale les deux sur ce qui est déjà installé — c'est le seul moyen qu'une dépréciation atteigne quelqu'un qui a copié le fichier il y a six mois.
Chaque version d'entrée doit figurer dans packages/registry/CHANGELOG.md, publié avec le paquet : pnpm check:changelog le vérifie, et la CI le lance. Sans quoi list --outdated dirait « mettez à jour » sans que personne puisse savoir de quoi.
Le registre servi en HTTP
La copie embarquée dans la CLI fige le catalogue au jour de sa publication : corriger une entrée obligerait chacun à mettre la CLI à jour. Le registre est donc aussi servi en ligne, versionné par l'URL :
sia-ui add badge --registry https://beatjo.github.io/sia-ui-site/r/v1/r/v1/ est un contrat. Une forme incompatible naîtrait sous r/v2/, et les deux seraient servis côte à côte le temps que les projets suivent — c'est ce que veut dire « une CLI ancienne continue de répondre ». Le champ format du document dit la même chose depuis l'intérieur : une CLI qui lit le format 1 refuse le format 2 au lieu de l'interpréter de travers.
L'URL peut aussi vivre dans sia-ui.json, sous registry. Du plus explicite au plus implicite : l'option, puis le fichier de configuration, puis la copie embarquée.
Structure d'une entrée
{
"name": "accordion",
"description": "Collapsible sections with animated height.",
"type": "component",
"version": "0.1.0",
"status": "stable",
"source": "package",
"files": [
"components/Accordion/index.tsx",
"components/Accordion/styles.css"
],
"dependencies": ["@sia-ui/headless", "@sia-ui/utils"],
"registryDependencies": ["icons"]
}version est celle de l'entrée, pas celle des paquets. status vaut experimental, stable, deprecated ou removed; les deux derniers acceptent replacedBy et deprecatedReason.
Le fichier est tenu à la main, et rien dans le typage ne le protège — il est importé puis transtypé, donc un status: "stabel" passerait le build pour n'échouer que chez l'utilisateur. packages/registry/src/registry.test.ts vérifie chaque entrée; packages/cli/src/install.test.ts vérifie que les fichiers annoncés sont réellement servis.
Ce même essai vérifie aussi que chaque import relatif d'une entrée vise un fichier que add <nom> apporte : les siens, ou ceux de ses registryDependencies, transitivement. Dans le dépôt, tout le dossier components/ est présent et tout compile ; chez l'utilisateur, seul ce que l'entrée annonce arrive. C'est cet écart qui a laissé drawer — et avec lui app-shell — ininstallable : un ./context absent de files, un ../Icons sans dépendance à icons. Oublier un fichier ou une dépendance fait désormais échouer la CI.
Composants disponibles
| Nom | Niveau | Utilité |
|---|---|---|
typography | Primitive | Titres, textes et code inline |
button | Primitive | Action accessible avec chargement |
icon-button | Primitive | Action icône avec libellé accessible |
input | Primitive | Champ texte et état invalide |
search-input | Composant | Recherche contrôlée ou non avec effacement |
date-picker | Composant | Sélection native d'une date sérialisable |
time-picker | Composant | Sélection native d'une heure sérialisable |
textarea | Primitive | Saisie multiligne |
overlay | Primitive | Portail positionné avec collisions et fermeture |
select | Composant | Sélection recherchable locale ou distante |
checkbox | Primitive | Choix booléen avec description |
radio-group | Primitive | Choix exclusif groupé |
switch | Primitive | Activation booléenne contrôlée ou non |
badge | Primitive | Statut compact et sémantique |
statistic | Composant | Nombre, montant ou pourcentage localisé |
relative-time | Composant | Date relative sémantique auto-actualisée |
pagination | Composant | Pagination contrôlée et accessible |
alert | Composant | Feedback sémantique local |
skeleton | Primitive | Placeholder de chargement |
spinner | Primitive | Indicateur de chargement accessible |
modal | Composant | Dialogue accessible dans un portail |
confirm-dialog | Pattern | Confirmation d'une action sensible |
drawer | Composant | Panneau latéral accessible |
tabs | Composant | Navigation par onglets et clavier |
tooltip | Composant | Aide contextuelle au survol et focus |
card | Primitive | Conteneur de contenu composé |
divider | Primitive | Séparateur horizontal ou vertical |
avatar | Primitive | Image ou initiales d'un utilisateur |
container | Primitive | Largeur responsive et empilement |
field | Composant | Contrat universel des champs de formulaire |
form-field | Alias déprécié | Compatibilité temporaire vers field |
page-header | Pattern | En-tête cohérent pour les pages |
entity-meta | Pattern | Métadonnées compactes d'une entité |
error-state | Pattern | Erreur récupérable avec nouvelle tentative |
filters-bar | Pattern | Recherche, filtres actifs et actions |
stat-card | Pattern | Indicateur métier dans une carte |
kpi-grid | Pattern | Grille responsive d'indicateurs |
audit-meta | Pattern | Historique chronologique d'audit |
empty-state | Pattern | État vide avec action optionnelle |
async-state | Pattern | Chargement, erreur et état vide |
config-provider | Composant | Valeurs par défaut globales ou locales des composants |
Vagues 3 et 4
| Groupe | Entrées installables |
|---|---|
| Données | data-table, descriptions, timeline, tree, tree-select, transfer |
| Finance | currency-input, amount-display |
| Date et temps | date-range-filter, date-range-picker, time-range-picker, date-time-picker, calendar, mini-calendar, event-calendar, timer, countdown, month-picker, year-picker, week-picker, duration-display |
| Application | command-palette, app-shell, auth-layout, crud-page, permission-gate, unsaved-changes-guard |
| Interaction | file-upload, tour, resizable-panel, qr-code |
Le registre contient désormais 88 entrées, dépendances techniques incluses.
Utilitaires locaux copiés
L'entrée utils sert de façade locale pour les composants copiés. Elle évite de forcer une dépendance externe juste pour des besoins transverses simples.
Exemples actuellement fournis:
cncomposeEventHandlersformatDateformatTimeAgoformatCurrencytruncatenormalizeIdentifier
Ce que le projet hôte doit fournir
sia-ui.css, écrit parinit, pose les variables de thème et un resetbox-sizing: border-boxde spécificité nulle (:where(*)). Les composants supposentborder-box: sans lui, unwidth: 100%plus un padding déborde — 32 px de défilement horizontal sousAppShell. Toute règle du projet l'emporte sur ce reset.La feuille suit le registre, comme un composant : son empreinte est notée dans
sia-ui.lock.json(styles), et elle est lue à la même source que les composants. Sans ce suivi, les composants se mettaient à jour sans la feuille dont ils dépendent — le resetbox-sizingn'arrivait jamais.État de la feuille initaddupdateupdate --forcedoctorabsente copiée copiée copiée copiée signalée à jour — — — — — non retouchée, registre plus récent laissée mise à jour mise à jour mise à jour signalée retouchée localement laissée laissée laissée remplacée signalée copiée avant le suivi (CLI ≤ 0.5) laissée laissée laissée remplacée signalée initinitialise, et c'est tout ; un projet dont la feuille date d'une CLI 0.5 ou antérieure la remet d'aplomb une fois avecsia-ui update --force.eslint-plugin-react-hooks, si le projet lance ESLint sur les composants copiés. Deux d'entre eux (chart,require-session) portent uneslint-disable-next-line react-hooks/exhaustive-depsjustifié : sans le plugin, ESLint refuse une règle qu'il ne connaît pas. Avec sa configurationrecommended, les composants ne produisent aucun avertissementexhaustive-deps. Les règles du React Compiler (refs,purity,set-state-in-effect, activées parrecommendeddepuis la version 7) signalent encore une vingtaine de lignes dans une dizaine de composants — Prévu : les réécrire, ou documenter chaque exception.
Limites actuelles
La CLI n'installe pas les dépendances npm : elle les annonce, et laisse le gestionnaire de paquets du projet décider. Elle ne fusionne pas non plus les alias TypeScript, ni un fichier retouché avec sa nouvelle version — diff montre l'écart, la fusion reste manuelle.