Skip to content

Source unique des composants ​

Décision ​

@sia-ui/react-web est la source des composants web. Le registre installable n'est plus une implémentation concurrente : il devient une projection de react-web, produite au build.

Jusqu'ici le dépôt contenait deux implémentations web sans lien entre elles :

  • packages/registry/templates/ — 88 composants copiés chez l'utilisateur, sans dépendance, avec leur propre cn, formatDate et formatCurrency, et un unique fichier CSS. Aucun n'importe @sia-ui/*.
  • packages/react-web/ — composants distribués comme package, avec un CSS par composant.

Maintenir les deux revient à écrire chaque composant deux fois. Et le registre ne mène nulle part côté mobile : on ne copie pas un <span> dans une <View>.

Architecture visée ​

Un composant n'est écrit qu'une fois, dans react-web. Il sort par deux canaux :

  • package — pnpm add @sia-ui/react-web, mis à jour par semver;
  • registre — sia-ui add badge, copié chez l'utilisateur qui en devient propriétaire.

Ce que les deux plateformes partagent — et ce qu'elles ne partagent pas ​

Il serait tentant de placer un contrat de props par composant dans un paquet commun, que chaque implémentation étendrait. Une forme de props ne traverse pas les plateformes : le web étend HTMLAttributes et prend className, le natif étend ViewProps et prend style; un tooltip web se place avec position: fixed, un tooltip natif avec un calque.

Chaque composant déclare donc ses props chez lui, comme le fait shadcn, et comme l'exige le canal de copie : un contrat importé obligerait à installer un paquet pour obtenir un type.

Ce qui se partage réellement :

QuoiOùPourquoi
Le vocabulaire — ton, taille, variante@sia-ui/tokens22 composants s'en servent, le natif s'en servira
Le comportement — machines à états, placement, calculs@sia-ui/headlessidentique des deux côtés, c'est le vrai partage
Le client HTTP@sia-ui/apiaucun rapport avec le rendu
Le contrat de formulaire@sia-ui/headlessaucun rapport avec le rendu
La forme des propsnulle partelle est propre à chaque plateforme

État actuel ​

@sia-ui/react-web existe désormais comme package du workspace : il est installé, typé, construit et couvert par la CI. Il exporte neuf composants.

ComposantÉtat
Badgeexporté
Dividerexporté
Labelexporté
Skeletonexporté
Switchexporté
Textexporté
Drawerexporté
Sliderexporté
Tooltipexporté
DateTimePickerattend Calendar, Input et Timer
Formattend Button; dépend de react-hook-form et @hookform/resolvers/zod
Fieldpas d'implémentation, seulement un styles.css et une story

DateTimePicker, Form et Field restent exclus de src/index.ts et du tsconfig.json tant que leurs dépendances internes n'existent pas.

Portal, usePortal et useOutsideClick ne vivent pas ici mais dans @sia-ui/react : ce sont des primitives de runtime, utiles à une application comme à ce package. De même, cn vient de @sia-ui/utils. Un composant ne réimplémente jamais ce qu'une couche inférieure expose déjà.

Cas de Form et Field ​

Ces deux composants importent react-hook-form et @hookform/resolvers/zod. Le parti pris du projet est de se brancher sur ces librairies, pas d'en dépendre : Field doit rester entièrement contrôlé — name, value, onChange, onBlur, error, ref — et l'adaptateur vers react-hook-form doit vivre à côté, optionnel. À reprendre avant de les réintégrer.

Migration ​

L'ordre de travail est mécanique, composant par composant :

  1. écrire le contrat dans packages/core/src/<composant>.ts s'il n'existe pas, et l'exporter depuis src/index.ts;
  2. porter le template du registre vers packages/react-web/src/components/, en typant les props sur le contrat;
  3. extraire ses règles de templates/styles/sia-ui.css vers un styles.css propre au composant;
  4. exporter le composant depuis react-web/src/index.ts et ajouter sa story;
  5. une fois la projection en place, supprimer le template manuel du registre.

Tant que la projection n'existe pas, les deux implémentations coexistent. Les stories de react-web sont préfixées Package · react-web/ pour éviter les collisions d'identifiants avec celles du registre.

Distribution : pas de projection ​

Le registre a d'abord été pensé sans aucune dépendance, ce qui imposait de générer les templates : inliner le vocabulaire de @sia-ui/tokens, le runtime de @sia-ui/react, et réécrire chaque import. Un générateur a été écrit et fonctionnait — la projection compilait avec "paths": {}, sans qu'aucun @sia-ui/* soit résolvable.

Il a été abandonné. Cette contrainte n'était pas nécessaire : shadcn lui-même ne la respecte pas, son button.tsx importe @radix-ui/react-slot et class-variance-authority, et registry.json porte depuis toujours un champ dependencies pour les paquets npm.

Un template a donc le droit d'importer @sia-ui/tokens, @sia-ui/utils, @sia-ui/headless et @sia-ui/react. La CLI copie le fichier source tel quel et annonce les dépendances à installer. Aucun code n'est généré, aucun n'est dupliqué.

La CLI ​

bash
sia-ui init              écrit sia-ui.json et la feuille de style
sia-ui list              liste les entrées, marquées [pkg] quand elles viennent du package
sia-ui add <nom...>      installe, avec ses dépendances de registre
sia-ui add <nom> --overwrite   écrase une copie existante

Elle lit désormais sia-ui.json — components, lib, styles — au lieu d'écrire en dur dans src/, et affiche les paquets npm à installer :

add  app/ui/Label/index.tsx
add  app/ui/Label/styles.css

À installer :
  pnpm add @sia-ui/headless @sia-ui/react @sia-ui/tokens @sia-ui/utils

Le fichier installé est exactement le fichier source de react-web, octet pour octet. C'est ce que permet le droit d'importer @sia-ui/*, et c'est ce qui garantit qu'il n'y a plus qu'une implémentation.

Le schéma des entrées ​

Chaque entrée porte maintenant :

ChampRôle
versionversion de l'entrée, indépendante de celle du package
statusexperimental, stable, deprecated ou removed
sourcepackage (copié depuis react-web) ou template (écrit à la main)
dependenciespaquets npm annoncés par la CLI, jamais installés d'office

La CLI refuse une entrée removed, prévient sur une entrée deprecated et affiche son remplacement.

Pourquoi 0.1.0 et pas 1.0.0 ​

Toutes les entrées et tous les paquets sont à 0.1.0. Rien n'est publié : en semver, le 0. majeur veut dire « l'API n'est pas figée, une rupture peut arriver en mineure ».

C'est ce qu'il faut ici, parce que plusieurs décisions sont encore ouvertes et qu'elles seront chacune une rupture :

  • les sept composants qui existent des deux côtés, à arbitrer;
  • le sort de ConfigProvider;
  • Field et Form, à détacher de react-hook-form;
  • les 85 entrées qui passeront de template à package.

Passer en 1.0.0 maintenant reviendrait à s'engager sur une API qu'on sait devoir changer, et à brûler 2.0.0, 3.0.0, 4.0.0 sur des décisions internes que personne n'a encore consommées.

1.0.0 est le premier engagement public : il vient en phase 7, quand la migration est finie et les arbitrages rendus.

Migration : faite ​

Les 88 entrées historiques ont rejoint packages/react-web/src/components/. packages/registry/templates/ n'existe plus.

AvantAprès
Entrées du registre88 template + 2 package87, toutes package
Composants dans react-web988
Feuille de styleun fichier de 202 lignes68 feuilles par composant + une commune
Fichiers copiés pour crud-page12, dont 3 de bibliothèque9 composants, 0 bibliothèque

Déplacer par étapes n'a pas servi ​

L'ordre imposé par le graphe de dépendances ne valait que pour un déplacement composant par composant. En les déplaçant tous ensemble, les imports voisins se résolvent entre eux à l'arrivée : l'ordre n'a plus d'objet.

Le découpage de la feuille de style ​

609 règles, réparties automatiquement : une règle part avec un composant si lui seul peut la revendiquer, d'après les classes que son fichier cite. Sinon elle reste dans src/styles/shared.css — variables de thème, animations partagées, ajustements responsives transverses.

571 règles ont trouvé un composant, 146 sont communes. Les 38 restantes appartenaient à slider, tooltip et drawer, dont la version de react-web a été retenue : leur CSS est remplacé, pas perdu.

L'invariant a été vérifié : aucune règle du registre n'a disparu sans arbitrage explicite.

Les arbitrages ​

ComposantRetenuPourquoi
slider, tooltip, drawerreact-webdéjà câblés sur @sia-ui/headless
badge, divider, skeleton, switchregistreversions plus riches — tons, chargement
field, date-time-pickerregistreles brouillons de react-web étaient incomplets
textsupprimédoublon de typography, plus complet

Les fonctions que le registre avait en plus ont été récupérées plutôt que perdues : minDistance est descendu dans useSliderState, showValue, formatValue, startLabel et endLabel dans le contrat ISliderProps, et TooltipProps reste exporté comme alias avec un content facultatif.

Ce qui reste dehors ​

Form est exclu du barrel et du typage : il importe react-hook-form et @hookform/resolvers/zod en dur, ce que la règle du projet interdit. Il attend d'être repris en composant contrôlé, avec l'adaptateur à côté.

Règle ​

Un composant web ne s'écrit plus dans packages/registry/templates/. Une contribution qui ajoute un template sans passer par react-web est à refuser.

Publié sous licence MIT.