Plusieurs applications, un seul système
L'échec habituel : une bibliothèque est construite pour l'application A, les boutons de l'application B sont 4 pixels plus hauts et 2 pixels plus arrondis, donc B fork le bouton "juste pour cette fois". Cinq applications plus tard, il y a six boutons, huit selects et trois modales, qui font tous le même travail.
Rendez le composant partagé basé sur des tokens : il comporte le comportement, le balisage et les états, et chaque thème fournit ses mesures. Consolidez ensuite application par application.
Composants basés sur des tokens
Lisez chaque mesure qui diffère entre les applications depuis un token de thème :
@theme {
--spacing-btn-sm: 1.75rem; --spacing-btn-px-sm: 0.625rem;
--spacing-btn-md: 2.25rem; --spacing-btn-px-md: 0.875rem;
--spacing-field: 2.125rem; /* hauteur input / select */
--text-label: 0.8125rem;
}
md: "h-btn-md px-btn-px-md text-sm",
icon: "size-btn-md",
Ensuite :
- Une suite surcharge les tokens une fois dans son thème, et chaque application de la suite obtient son propre contrôle mesuré.
- Le rayon, les remplissages et les ombres sont déjà des tokens (
rounded-field,bg-accent,--shadow-card). - Peignez les remplissages comme une couleur plus une classe décorative, par exemple
bg-accent accent-fill. Un thème qui ne définit que la décoration (un lustre) ne rendra alors jamais un bouton transparent.
Voir references/button.tsx.
Passe de consolidation, par application
-
Inventaire des copies locales. Grep chaque application pour
export function (Btn|Button|IconBtn|Select|Field|Input|Modal|Dialog|Sheet|Badge|Empty|Pager|Toast|Stat|Status|Spinner). Comptez les sites d'appel. -
Remplacez et supprimez là où le composant partagé correspond après thémage. Mappez les noms de variantes explicitement et écrivez le mappage :
Nom local Variante partagée primary,navyaccentsecondaryoutlinequiet,softsubtledanger(plein)danger-solid -
Gardez les éléments spécifiques à l'application comme spécifiques :
- widgets de domaine, graphiques et chrome d'application ;
- contrôles dont la forme est la marque (plaques iOS, contrôles plats Carbon, CTA pilulaires de 48px) ;
- contrôles avec un comportement manquant dans le composant partagé (case à cocher tri-état, chips supprimables, un élément de réinitialisation "Tout", filtres multi-sélection).
Un wrapper fin est acceptable lorsqu'il ajoute une chose. Notez pourquoi dans un commentaire.
-
Remplacez les
<select>natifs et les selects basés sur menu par un seul Select.- Radix Select réserve
""comme "aucune valeur" : utilisez un sentinelle comme"all"et mappez-le en retour. - Transmettez
aria-labelledbyetaria-describedby. - Les scripts de parcours qui appellent
selectOption()doivent cliquer sur le combobox, puis sur l'option.
- Radix Select réserve
-
Vérifiez chaque application par rapport à ses propres captures d'écran (voir
visual-regression-pass). Les différences réelles sont généralement intentionnelles : tailles, lustre, échelle de pression.
Pièges de tailwind-merge (cn())
tailwind-merge ne connaît que l'échelle intégrée de Tailwind. Les tokens personnalisés le brisent de trois manières silencieuses :
- Tokens d'espacement inconnus.
cn("h-btn-md", "h-10")garde les deux, car il ne sait pas queh-btn-mdest une hauteur, et l'ordre de la feuille de style détermine le gagnant. Unh-10appelant perdu au profit du token et un bouton de 40px rendu à 36px. Enregistrez les tokens :extendTailwindMerge({ extend: { theme: { spacing: ["btn-sm","btn-md","field", …] } } }). - Tailles de texte inconnues.
cn("text-meta", "text-ink")traitetext-metacomme une couleur et la supprime. Enregistrez les tailles :theme: { text: ["meta","micro","label"] }. - Ombres et couleurs personnalisées.
text-14à côté detext-t3, ou arbitraireshadow-[var(--x)]à côté d'une ombre nommée : le même problème. Lorsqu'une base de code a une échelle personnalisée fermée partout, utilisezclsxbrut.
Voir references/cn.ts.
Focus et validation
- Un style
aria-invalid:ring-*l'emporte surfocus-visible:ring-*et cache l'anneau de focus sur un champ non valide focalisé. Écrivezaria-invalid:not-focus-visible:ring-…. - Les
<button>nus dans les formulaires les soumettent. Si le bouton partagé ne définit pas par défauttype="button", chaque site d'appel converti qui ne soumet pas doit le définir.