Design System
La charte graphique d'un projet, les variables CSS qu'elle produit et les classes que les éditeurs écrivent : où vit chaque valeur, ce qu'elle devient dans la page, ce qui est permis et ce qui est refusé.
Mis à jour le
Vue d'ensemble
L'apparence d'un projet tient dans un seul fichier, la charte graphique web/configurations/brand.identity.json. Au rendu de chaque page, le framework la convertit en variables CSS (--brand, --text-primary, --space-4, --radius-md...) posées sur :root, puis sur [data-theme="light"] pour le thème clair. Les blocs ne lisent que ces variables : une valeur changée dans la charte change tout le site, sans retoucher un bloc.
Sur ces variables, le paquet @asynccode/ac_tools construit la feuille du design system : un squelette de mise en page, une grille de 12 colonnes, des classes d'espacement, de visibilité, de typographie, de tons, de rayons et d'ombres. Les éditeurs de l'Atelier posent ces classes sur les nœuds des arbres (tree.json, structure.json). Ce qu'aucune classe n'exprime part dans le styleJson du nœud, écrit en jetons et compilé dans le style.css du bloc.
Ce que vous écrivez, ce que la page reçoit
| Ce que vous écrivez | Ce que la page reçoit |
|---|---|
web/configurations/brand.identity.json | Un <style> dans le <head> : :root{...} pour le bloc dark, [data-theme="light"]{...} quand un bloc light existe. |
props.className (tree.json), classname (structure.json) | Les classes de la feuille du design system, générée au build à partir de la charte et réduite aux classes que le projet cite. |
styleJson (tree.json) | Des règles entre /* forge:nodes:start */ et /* forge:nodes:end */ dans le style.css du bloc, une règle par nœud stylé. |
web/index.css | Le CSS global du projet, placé dans la couche @layer app-globals. |
web/configurations/layouts/<id>/style.css, web/configurations/components/<id>/style.css | Du CSS hors couche, inséré page par page pour la mise en page et les blocs que la page monte. |
Une règle de bloc n'écrit jamais une couleur, une taille ou un rayon en dur : elle cite une variable. C'est ce qui permet au thème clair et aux retouches de la charte de s'appliquer partout.
Le fichier de charte
La charte vit dans web/configurations/brand.identity.json. Sa seule forme est un conteneur à trois clés : defaultTheme, dark et, en option, light. Le framework lit dark comme base des deux thèmes. Un fichier plat, avec les groupes à la racine, n'est pas lu : getBrand reçoit un bloc vide et n'émet que ses valeurs de repli, sans aucune couleur.
{
"defaultTheme": "light",
"dark": {
"palette": { ... },
"surface": { ... },
"ink": { ... },
"fonts": { ... },
"typography": { ... },
"radii": { ... },
"shadows": { ... },
"motion": { ... },
"spacing": { ... },
"stroke": { ... },
"grid": { ... },
"layers": { ... },
"effects": { ... }
},
"light": {
"palette": { ... },
"surface": { ... },
"ink": { ... },
"shadows": { ... }
}
}| Clé | Rôle |
|---|---|
defaultTheme | Thème d'un premier visiteur : "light" ou "dark". Toute autre valeur, ou l'absence, vaut "dark". Le choix enregistré du visiteur passe avant. |
dark | Le bloc complet. Il fait le thème sombre et sert de base au thème clair. |
light | Facultatif. Seulement ce qui change en clair : en pratique palette, surface, ink, shadows, et media pour un logo propre au clair. |
id, name | Identifient la charte. getBrand ne les lit pas. |
La fusion du thème clair
Le thème clair n'est pas lu seul : le framework fusionne light sur une copie de dark, en profondeur. Un objet imbriqué se fusionne clé par clé (light.palette.brand remplace dark.palette.brand et garde les autres couleurs), une valeur simple remplace. Le résultat passe par getBrand comme le bloc sombre, et le jeu complet des variables est émis une seconde fois sous [data-theme="light"].
Trois réglages ne se lisent que dans dark : les feuilles de polices (fonts.sans.import, fonts.mono.import, fonts.display.import, chargées une fois dans le <head>), le favicon (media.favicon.url), et les couleurs posées sur le voile (--scrim-brand, --scrim-accent-2, calculées depuis la palette sombre dans les deux thèmes). Une famille de police changée dans light sans feuille chargée retombe sur sa pile de repli.
Les valeurs de la charte sont des données : avant d'entrer dans le <style>, chaque variable passe le même contrôle qu'une valeur de styleJson (section suivante sur le styleJson). Une valeur refusée (un ;, une accolade, un <, un url( ailleurs que dans le logo) est retirée, et ce qui la lit retombe sur son propre repli.
Le schéma, groupe par groupe
Chaque groupe ci-dessous vit dans dark (et, pour ce qui change, dans light). La colonne « Si absent » dit ce que getBrand émet quand la clé manque : une valeur de repli, une variable absente (retirée du <style>), ou une valeur invalide qui casse les déclarations qui la lisent.
palette : les couleurs
Des couleurs hexadécimales à six chiffres (#rrggbb). Chaque couleur produit aussi son canal -rgb.
| Clé | Rôle | Si absent |
|---|---|---|
brand | Couleur primaire (« Primaire » dans l'éditeur) : --brand, --tone-primary. | Variable absente |
brandSecondary | Couleur secondaire : --brand-secondary, --tone-secondary. | Variable absente |
accent | Premier accent : --accent, alias --ui-violet. | Variable absente |
accent2 | Deuxième accent : --accent-2, alias --ui-pink. | Variable absente |
accent3 | Troisième accent : --accent-3, alias --ui-cyan. | Variable absente |
accentSoft | Accent doux, pour un fond teinté : --accent-soft, alias --ui-pink-soft. | Variable absente |
success, warning, error, info | Les quatre états : --success, --warning, --error, --info, et les tons --tone-success, --tone-warning, --tone-danger, --tone-info. | Variable absente |
brandOn | Encre posée sur un aplat brand : --ink-text-on-brand, --tone-primary-on. | Calculée : #ffffff ou #0b0b12, la plus contrastée |
accentOn | Encre posée sur un aplat accent : --ink-text-on-accent. | Calculée : #ffffff ou #0b0b12, la plus contrastée |
primitiveAccent, fragmentAccent | Couleurs de l'interface d'édition : --ui-primitive, --ui-fragment. | info pour primitiveAccent, accentSoft pour fragmentAccent |
surface : les fonds
| Clé | Rôle | Si absent |
|---|---|---|
base | Fond de page : --bg-primary, --ui-bg-0, --sf-0. | Variable absente |
panel | Fond de panneau : --bg-secondary, --ui-bg-panel, --sf-1. | Variable absente |
elevated | Fond surélevé : --bg-tertiary, --ui-bg-elev, --sf-2. | Variable absente |
bg1 | --ui-bg-1. | base |
bg2 | --ui-bg-2. | elevated |
bgCard | Fond de carte : --ui-bg-card, --sf-3. | panel |
scrim | Point de départ du voile sombre --scrim. Se déclare dans dark seulement. | Calculé depuis la plus sombre de base et ink.text |
ink : l'encre et les filets
text est une couleur ; les six autres clés sont des opacités, de 0 à 1, appliquées à text. Toutes les nuances de texte et de filet dérivent donc d'une seule encre.
| Clé | Rôle | Si absent |
|---|---|---|
text | Encre principale : --text-primary, --ui-text, --ink-1. | #ffffff |
muted | --text-secondary, --ui-muted, --ink-2. | Valeur invalide : rgba(r, g, b, undefined) |
dim | --text-tertiary, --ui-dim, --ink-3. | Valeur invalide : rgba(r, g, b, undefined) |
faint | --ui-faint. | Valeur invalide : rgba(r, g, b, undefined) |
line | Filet courant : --ui-line, --line-1. | Valeur invalide : rgba(r, g, b, undefined) |
lineStrong | Filet appuyé : --ui-line-strong, --line-2. | Valeur invalide : rgba(r, g, b, undefined) |
lineFaint | Filet léger : --ui-line-faint, --line-0. | Valeur invalide : rgba(r, g, b, undefined) |
fonts : les polices
Trois rôles, sans, mono et display, chacun { family, import, fallback }. family est le nom de la famille. import est l'adresse de sa feuille Google Fonts (https://fonts.googleapis.com/css2?...), insérée en <link rel="stylesheet"> dans le <head>. fallback est la pile de repli, lue pour sans et display, limitée à des lettres, chiffres, espaces, virgules, guillemets et tirets (120 caractères).
| Clé | Rôle | Si absent |
|---|---|---|
sans.family | Police du texte : --font-sans, --ui-font-sans. | system-ui |
mono.family | Police à chasse fixe : --font-mono, --ui-font-mono. | ui-monospace |
display.family | Police des titres : --font-display. | La police sans |
sans.import, mono.import, display.import | Feuille de la police. Seules les adresses https:// sont chargées, et seulement depuis dark. | Aucune feuille : la famille ne s'affiche que si le poste du visiteur l'a |
sans.fallback, display.fallback | Pile de repli placée après la famille. | ui-sans-serif, -apple-system, system-ui, sans-serif |
Les démos autorisent fonts.googleapis.com et fonts.gstatic.com dans web/configurations/csp.json. Une police servie depuis un autre hôte doit y être ajoutée, sinon le navigateur bloque sa feuille.
typography : tailles, interlignes, rôles
La typographie est une échelle à 13 crans : 3xs, 2xs, xs, sm, base, lg, xl, 2xl, 3xl, 4xl, 5xl, 6xl, 7xl. Chaque cran porte une taille, un interligne et un interlettrage, parce que la bonne valeur dépend du cran.
| Clé | Rôle | Si absent |
|---|---|---|
sizes.3xs ... sizes.7xl | Taille en px (un nombre) : --text-3xs à --text-7xl. | L'échelle par défaut (voir les variables) |
leading.3xs ... leading.7xl | Interligne sans unité : --leading-3xs à --leading-7xl. | L'échelle par défaut |
tracking.3xs ... tracking.7xl | Interlettrage en em (un nombre) : --tracking-3xs à --tracking-7xl. | L'échelle par défaut |
trackingScale.tight, normal, wide | --tracking-tight, --tracking-normal, --tracking-wide. | -0.025, 0, 0.04 |
weights.regular, medium, semibold, bold | --weight-regular, --weight-medium, --weight-semibold, --weight-bold. | Variable absente |
lineHeights.tight | --leading-tight. | Variable absente |
lineHeights.base | --leading-base. C'est cette clé, et non leading.base, qui donne la variable. | Variable absente, même si leading.base existe |
roles.display, pageTitle, sectionTitle, lead | Les quatre rôles de titrage (ci-dessous). | display de 36 à 72 px, pageTitle de 32 à 48 px, sectionTitle cran 3xl, lead cran base |
Un rôle s'écrit de deux façons. { "step": "xl" } le fixe sur un cran de l'échelle. { "min": 56, "max": 96 } le rend fluide entre 375 et 1280 px de large, avec min > 0 et max >= min. Les deux formes acceptent leading et tracking pour forcer l'interligne et l'interlettrage. Sans eux, un rôle fluide prend ceux du plus grand cran dont la taille ne dépasse pas max. Un rôle mal formé reprend sa valeur par défaut.
radii : les coins
| Clé | Rôle | Si absent |
|---|---|---|
radius | Le cadran : un nombre en px dont se déduisent --radius-sm, --radius-md, --radius-lg et --radius-xl (voir les calculs). | 14 |
pill | Rayon des pastilles : --radius-full, --radius-badge, --radius-tag. | 9999 |
radius doit être un nombre JSON : "6", écrit en chaîne, est ignoré et le cadran revient à 14. Des clés radii.sm, radii.md, radii.lg ou radii.xl ne sont pas lues.
shadows : les ombres
Des valeurs box-shadow complètes, par exemple 0 4px 12px rgba(0, 0, 0, 0.32), 0 2px 4px rgba(0, 0, 0, 0.3).
| Clé | Rôle | Si absent |
|---|---|---|
xs, sm, md, ring | --shadow-xs, --shadow-sm, --shadow-md, --shadow-ring. | soft |
soft | --shadow-soft, repli des autres. | Variable absente |
lg | --shadow-lg. | Variable absente |
xl | --shadow-xl. | lg, puis soft |
brand | Ombre teintée de la marque : --shadow-brand. | Variable absente |
motion : les courbes et les durées
| Clé | Rôle | Si absent |
|---|---|---|
easeOut | --ease-out. | Variable absente |
easeSpring | --ease-spring. | easeOut |
easeInOut | --ease-in-out. | easeOut |
fast | --duration-fast, une durée CSS ("120ms"). | Variable absente |
base | --duration-base. | Variable absente |
slow | --duration-slow. | base |
spacing : l'échelle d'espacement
Des clés numériques, écrites en chaîne dans le JSON, vers des px : "0", "1", "2", "3", "4", "5", "6", "8", "10", "12", "16", "20", "24", "32", "40". Chaque clé n donne --space-n ; la clé "1" donne aussi l'unité --u.
| Clé | Rôle | Si absent |
|---|---|---|
0, 1, 2, 3, 4, 6, 8, 12, 16 | --space-0 à --space-16, et --u pour la clé 1. | Aucun repli : la valeur devient undefinedpx |
5, 10, 20, 24, 32, 40 | --space-5, --space-10, --space-20, --space-24, --space-32, --space-40. | 20, 40, 80, 96, 128, 160 |
stroke : les traits
| Clé | Rôle | Si absent |
|---|---|---|
thin | Trait fin, valeur CSS complète ("0.5px") : --stroke-thin. | Variable absente |
base | Trait courant : --stroke-base. | Variable absente |
grid : la grille et les largeurs
| Clé | Rôle | Si absent |
|---|---|---|
gutter | Gouttière en px : --gutter. | 24 |
columns | Nombre de pistes de .grid : --cols. Les classes de span s'arrêtent à 12. | 12 |
containers.sm, md, lg, xl, xxl | Largeur max de .container à chaque palier : --container-sm à --container-xxl. | 540, 720, 960, 1140, 1320 |
narrow | Colonne de lecture : --container-narrow. | 700 |
wide | Largeur large : --container-wide. | 1600 |
layers : l'empilement
| Clé | Rôle | Si absent |
|---|---|---|
sticky, banner, overlay, dropdown, modal, popover, toast, tooltip | --z-sticky à --z-tooltip. | 100, 200, 300, 400, 500, 600, 700, 800 |
effects : la matière des surfaces
| Clé | Rôle | Si absent |
|---|---|---|
surface | Matière par défaut d'un data-style vide : flat, outlined, elevated, soft ou glass. Inscrite dans la feuille du design system au build. | flat |
glassBlur | Flou de la matière glass, en px : --glass-blur, --surface-filter. | 14 |
glassAlpha | Opacité du fond glass : --glass-alpha. | 0.62 |
enableShadows, enableGradients, enableRounded | Lus par aucune variable : sans effet. | - |
media : logo et favicon
| Clé | Rôle | Si absent |
|---|---|---|
logo | { url, width, height } : le logo du thème. Donne --brand-logo-url et, si width et height sont des nombres, --brand-logo-ratio. | Pas de logo : le nom de marque s'affiche seul |
logoDark | Logo pris quand logo n'a pas d'url. | - |
favicon | { url } : l'icône de l'onglet, lue dans dark seulement. | Pas de <link rel="icon"> |
Les groupes sans repli
Un manque produit l'un de trois effets :
- Variable absente : une couleur de
paletteou desurface, les ombressoft,lgetbrand,motion.easeOut,fastetbase,weights.*,lineHeights.*,stroke.*. La variable n'est pas émise ; une déclaration qui la lit parvar()sans repli devient invalide, et la propriété reprend sa valeur héritée ou initiale. - Valeur invalide :
spacingpour les clés0,1,2,3,4,6,8,12,16(--space-4: undefinedpx,--u: undefinedpx), et les opacités deink(rgba(243, 241, 234, undefined)). La variable existe mais ne vaut rien : chaque marge, retrait, gouttière ou filet qui la lit disparaît. - Canal noir : le
-rgbd'une couleur absente, ou écrite autrement qu'en#rrggbb(#fff,rgb(...), un nom), vaut0, 0, 0.
typography.sizes et radii ont un repli : une taille manquante reprend l'échelle par défaut, un radius manquant vaut 14. Les deux groupes à remplir en entier sont spacing et ink.
Référence des variables CSS
getBrand produit 249 variables par thème, toujours sous les mêmes noms ; celles dont la source manque sont retirées du <style>. Les tableaux ci-dessous les donnent toutes, famille par famille, avec leur source dans la charte. Une variable en -rgb porte les trois canaux séparés par des virgules (210, 58, 79) et s'emploie dans rgba(var(--brand-rgb), 0.2).
Marque et accents
| Variable | Source dans la charte | Rôle |
|---|---|---|
--brand, --brand-rgb | palette.brand | Couleur primaire. |
--brand-secondary, --brand-secondary-rgb | palette.brandSecondary | Couleur secondaire. |
--accent, --accent-rgb | palette.accent | Premier accent. |
--accent-2, --accent-2-rgb | palette.accent2 | Deuxième accent. |
--accent-3, --accent-3-rgb | palette.accent3 | Troisième accent. |
--accent-soft, --accent-soft-rgb | palette.accentSoft | Accent doux, pour un fond teinté. |
--accents-muted | palette.accent, accent2, accent3 | 1 quand les trois accents sont presque gris (chroma OKLCH < 0,04), sinon 0. |
--ink-text-on-brand | palette.brandOn | Encre lisible sur un aplat --brand. |
--ink-text-on-accent | palette.accentOn | Encre lisible sur un aplat --accent. |
Alias de l'interface
Les mêmes couleurs sous les noms que lisent les fragments du framework. Elles basculent avec le thème comme les autres.
| Variable | Source dans la charte | Rôle |
|---|---|---|
--ui-violet, --ui-violet-rgb | palette.accent | Alias de --accent. |
--ui-pink, --ui-pink-rgb | palette.accent2 | Alias de --accent-2. |
--ui-pink-soft, --ui-pink-soft-rgb | palette.accentSoft | Alias de --accent-soft. |
--ui-cyan | palette.accent3 | Alias de --accent-3. |
--ui-primitive, --ui-primitive-rgb | palette.primitiveAccent | Couleur d'édition, repli palette.info. |
--ui-fragment | palette.fragmentAccent | Couleur d'édition, repli palette.accentSoft. |
États
| Variable | Source dans la charte | Rôle |
|---|---|---|
--success, --success-rgb | palette.success | Réussite. |
--warning, --warning-rgb | palette.warning | Avertissement. |
--error, --error-rgb | palette.error | Erreur. |
--info, --info-rgb | palette.info | Information. |
--ui-green, --ui-amber, --ui-red, --ui-blue | palette.success, warning, error, info | Alias des quatre états, dans cet ordre. |
Tons
Six tons sémantiques, chacun décliné en six variables. Dans le second tableau, * vaut primary, secondary, success, warning, danger ou info.
| Variable | Source dans la charte |
|---|---|
--tone-primary | palette.brand |
--tone-secondary | palette.brandSecondary |
--tone-success | palette.success |
--tone-warning | palette.warning |
--tone-danger | palette.error |
--tone-info | palette.info |
| Variable | Rôle |
|---|---|
--tone-* | L'aplat du ton. |
--tone-*-rgb | Ses canaux. |
--tone-*-on | Encre lisible sur l'aplat (palette.brandOn pour primary). |
--tone-*-subtle | Le ton en lavis : rgba(r, g, b, 0.12). |
--tone-*-on-subtle | Le ton ajusté pour se lire sur son lavis (4,5:1). |
--tone-*-emphasis | L'aplat poussé de 15 % vers son encre, pour le survol et l'appui. |
--tone-neutral-rgb | Fixe : 140, 140, 160. |
Surfaces et voile
| Variable | Source dans la charte | Rôle |
|---|---|---|
--bg-primary, --bg-primary-rgb | surface.base | Fond de page. |
--bg-secondary, --bg-secondary-rgb | surface.panel | Fond de panneau. |
--bg-tertiary, --bg-tertiary-rgb | surface.elevated | Fond surélevé. |
--ui-bg-0, --ui-bg-0-rgb | surface.base | Alias de --bg-primary. |
--ui-bg-1, --ui-bg-1-rgb | surface.bg1 | Repli surface.base. |
--ui-bg-2, --ui-bg-2-rgb | surface.bg2 | Repli surface.elevated. |
--ui-bg-card, --ui-bg-card-rgb | surface.bgCard | Fond de carte, repli surface.panel. |
--ui-bg-panel | surface.panel | Alias de --bg-secondary. |
--ui-bg-elev | surface.elevated | Alias de --bg-tertiary. |
--scrim, --scrim-rgb | surface.scrim | Voile sombre posé sur une image ou une bannière, le même dans les deux thèmes. |
--scrim-ink | surface.scrim | Encre lisible sur le voile. |
--scrim-brand, --scrim-brand-ink | palette.brand (dark) | La marque éclaircie pour se lire sur le voile, et son encre. |
--scrim-accent-2 | palette.accent2 (dark) | Le deuxième accent, ajusté de même. |
Encre et filets
| Variable | Source dans la charte | Rôle |
|---|---|---|
--text-primary, --text-primary-rgb | ink.text | Texte principal. |
--text-secondary | ink.muted | Texte secondaire : ink.text à l'opacité muted. |
--text-tertiary | ink.dim | Texte tertiaire. |
--ui-text, --ui-text-rgb | ink.text | Alias de --text-primary. |
--ui-muted | ink.muted | Alias de --text-secondary. |
--ui-dim | ink.dim | Alias de --text-tertiary. |
--ui-faint | ink.faint | Texte très effacé. |
--ui-line | ink.line | Filet courant. |
--ui-line-strong | ink.lineStrong | Filet appuyé. |
--ui-line-faint | ink.lineFaint | Filet léger. |
Polices
| Variable | Source dans la charte | Rôle |
|---|---|---|
--font-sans | fonts.sans | 'Famille', <fallback>. |
--font-display | fonts.display | Famille des titres ; la police sans à défaut. |
--font-mono | fonts.mono | 'Famille', ui-monospace, Menlo, Consolas, monospace. |
--ui-font-sans | fonts.sans.family | Même famille, pile fixe ui-sans-serif, system-ui, sans-serif. |
--ui-font-mono | fonts.mono.family | Même famille, pile fixe ui-monospace, Menlo, monospace. |
Échelle typographique
Chaque cran donne trois variables, lues dans typography.sizes, typography.leading et typography.tracking. Valeurs par défaut quand la clé manque :
| Cran | Variables | Par défaut (taille, interligne, interlettrage) |
|---|---|---|
3xs | --text-3xs, --leading-3xs, --tracking-3xs | 10px, 1.4, 0.02em |
2xs | --text-2xs, --leading-2xs, --tracking-2xs | 11px, 1.45, 0.015em |
xs | --text-xs, --leading-xs, --tracking-xs | 12px, 1.33, 0.01em |
sm | --text-sm, --leading-sm, --tracking-sm | 14px, 1.43, 0em |
base | --text-base, --leading-base, --tracking-base | 16px, 1.5, 0em |
lg | --text-lg, --leading-lg, --tracking-lg | 18px, 1.56, 0em |
xl | --text-xl, --leading-xl, --tracking-xl | 20px, 1.4, -0.01em |
2xl | --text-2xl, --leading-2xl, --tracking-2xl | 24px, 1.33, -0.015em |
3xl | --text-3xl, --leading-3xl, --tracking-3xl | 30px, 1.2, -0.02em |
4xl | --text-4xl, --leading-4xl, --tracking-4xl | 36px, 1.11, -0.025em |
5xl | --text-5xl, --leading-5xl, --tracking-5xl | 48px, 1, -0.03em |
6xl | --text-6xl, --leading-6xl, --tracking-6xl | 60px, 1, -0.035em |
7xl | --text-7xl, --leading-7xl, --tracking-7xl | 72px, 1, -0.04em |
--leading-base vient de typography.lineHeights.base, pas de leading.base : sans lineHeights.base, la variable n'est pas émise du tout, et fs-base perd son interligne.
Rôles de titrage
| Variable | Source dans la charte | Rôle |
|---|---|---|
--text-display, --leading-display, --tracking-display | typography.roles.display | Titre d'accroche. Par défaut fluide de 36 à 72 px. |
--text-page-title, --leading-page-title, --tracking-page-title | typography.roles.pageTitle | Titre de page. Par défaut fluide de 32 à 48 px. |
--text-section-title, --leading-section-title, --tracking-section-title | typography.roles.sectionTitle | Titre de section. Par défaut le cran 3xl. |
--text-lead, --leading-lead, --tracking-lead | typography.roles.lead | Chapeau sous un titre. Par défaut le cran base. |
Graisses, interlignes, interlettrage
| Variable | Source dans la charte | Rôle |
|---|---|---|
--weight-regular, --weight-medium, --weight-semibold, --weight-bold | typography.weights | Graisses nommées. |
--leading-tight | typography.lineHeights.tight | Interligne serré. |
--leading-base | typography.lineHeights.base | Interligne du texte courant. |
--tracking-tight, --tracking-normal, --tracking-wide | typography.trackingScale | Interlettrages nommés, en em. |
Rayons
| Variable | Source dans la charte | Rôle |
|---|---|---|
--radius-sm | radii.radius - 8 | Petit coin. |
--radius-md | radii.radius - 4 | Coin moyen. |
--radius-lg | radii.radius | Grand coin, la valeur du cadran. |
--radius-xl | radii.radius + 4 | Très grand coin. |
--radius-full | radii.pill | Pastille. |
--radius-btn, --radius-input | radii.radius - 8 | Boutons et champs de saisie. |
--radius-btn-lg | radii.radius - 4 | Grand bouton. |
--radius-badge, --radius-tag | radii.pill | Badges et étiquettes. |
Ombres
| Variable | Source dans la charte | Rôle |
|---|---|---|
--shadow-xs | shadows.xs | Élévation minimale. |
--shadow-sm | shadows.sm | Élévation faible. |
--shadow-soft | shadows.soft | Ombre diffuse, repli des autres. |
--shadow-md | shadows.md | Élévation moyenne. |
--shadow-lg | shadows.lg | Élévation forte. |
--shadow-xl | shadows.xl | Élévation maximale. |
--shadow-ring | shadows.ring | Anneau d'un pixel. |
--shadow-brand | shadows.brand | Ombre teintée de la marque. |
Mouvement
| Variable | Source dans la charte | Rôle |
|---|---|---|
--ease-out | motion.easeOut | Courbe de sortie. |
--ease-spring | motion.easeSpring | Courbe « spring ». |
--ease-in-out | motion.easeInOut | Courbe d'entrée et de sortie. |
--duration-fast | motion.fast | Durée courte (survol). |
--duration-base | motion.base | Durée courante. |
--duration-slow | motion.slow | Durée longue. |
Espacement et traits
| Variable | Source dans la charte | Rôle |
|---|---|---|
--space-0, --space-1, --space-2, --space-3, --space-4, --space-5, --space-6, --space-8, --space-10, --space-12, --space-16, --space-20, --space-24, --space-32, --space-40 | spacing.<n> | Échelle d'espacement, en px. |
--u | spacing.1 | Unité de base. |
--stroke-thin | stroke.thin | Trait fin. |
--stroke-base | stroke.base | Trait courant. |
Échelles numérotées
Des alias numérotés des mêmes valeurs, lus par la feuille du design system.
| Variable | Source dans la charte | Rôle |
|---|---|---|
--sf-0, --sf-1, --sf-2, --sf-3 | surface.base, panel, elevated, bgCard | Les quatre étages de surface. |
--line-0, --line-1, --line-2 | ink.lineFaint, line, lineStrong | Les trois filets. |
--ink-1, --ink-2, --ink-3 | ink.text, muted, dim | Les trois encres. |
Grille, couches, effets
| Variable | Source dans la charte | Rôle |
|---|---|---|
--gutter | grid.gutter | Gouttière, en px. |
--cols | grid.columns | Pistes de .grid. |
--container-sm, --container-md, --container-lg, --container-xl, --container-xxl | grid.containers | Largeur de .container par palier. |
--container-narrow | grid.narrow | Colonne de lecture. |
--container-wide | grid.wide | Largeur large (frame-wide). |
--z-sticky, --z-banner, --z-overlay, --z-dropdown, --z-modal, --z-popover, --z-toast, --z-tooltip | layers | Ordre d'empilement. |
--glass-blur, --surface-filter | effects.glassBlur | Flou de la matière glass (14px, blur(14px)). |
--glass-alpha | effects.glassAlpha | Opacité du fond glass. |
Logo
| Variable | Source dans la charte | Rôle |
|---|---|---|
--brand-logo-url | media.logo.url | url("..."), pour un background-image. |
--brand-logo-display | media.logo | inline-block avec un logo, none sans. |
--brand-logo-ratio | media.logo.width, height | Rapport largeur sur hauteur, 4 décimales. |
--brand-name-display | media.logo.width, height | none quand le ratio est connu, sinon inline. |
--brand-wordmark-display | media.logo.width, height | inline-block quand le ratio est connu, sinon none. |
Les cinq variables du logo et --accents-muted ne sont pas acceptées dans un styleJson. Le logo n'entre dans le CSS que par --brand-logo-url : aucune autre valeur url( n'est admise.
Variables de contexte
Celles-ci ne viennent pas de getBrand : la feuille du design system les pose selon le contexte du nœud. Elles sont permises dans un styleJson.
| Variable | Source dans la charte | Rôle |
|---|---|---|
--surface, --surface-up | [data-surface] | Fond du niveau courant et du niveau au-dessus (--sf-1 à --sf-3 selon la profondeur). |
--on-surface, --on-surface-2 | [data-surface] | Encre principale et secondaire (--text-primary, --text-secondary). |
--line | [data-surface] | Filet du niveau (--line-1, puis --line-2). |
--shadow | [data-surface] | Ombre du niveau (--shadow-sm, --shadow-md, --shadow-lg). |
--radius, --pad | [data-surface] | Rayon (--radius-md) et retrait (--space-4) d'une boîte. |
--radius-inner | [data-surface] | --radius moins --pad, borné à 0 : le rayon d'un enfant collé au bord. |
--container-w | grid.containers | Largeur courante de .container : 100%, puis --container-sm à --container-xxl à partir de chaque palier d'écran. |
--shell-clear | web/index.css | Dégagement sous une barre fixe, lu par scroll-padding-top (84 px si le projet ne le pose pas). |
--tone, --tone-rgb, --tone-on, --control-h, --control-pad-x, --control-font-size, --control-radius et --control-gap passent le contrôle d'un styleJson, mais ni getBrand ni la feuille du design system ne les définissent : sans un parent qui les pose, ils ne valent rien.
Ce que getBrand calcule
La plupart des variables recopient une valeur de la charte. Celles-ci sont calculées ; les connaître évite de les écrire à la main.
Le canal rgb
-rgb découpe une couleur #rrggbb en trois entiers séparés par une virgule et une espace. C'est la forme qui donne une transparence tirée de la charte, comme dans ce bloc d'une démo :
[data-forge-id="aboutHeader--crumbHome"][data-forge-id="aboutHeader--crumbHome"] { color: rgba(var(--bg-primary-rgb),0.72); text-decoration: none; }Toute autre écriture (#fff, rgb(...), red) donne 0, 0, 0, et l'encre calculée sur cette couleur devient #ffffff.
Les encres lisibles
--ink-text-on-brand, --ink-text-on-accent et --tone-*-on comparent le contraste du blanc et du noir sur l'aplat et gardent le meilleur : #ffffff ou #0b0b12. palette.brandOn et palette.accentOn imposent une autre encre.
Les tons
-subtle: l'aplat à 12 % d'opacité.-emphasis: l'aplat mélangé à 15 % vers son encre-on. Une marque claire fonce au survol, une marque sombre s'éclaircit, dans les deux thèmes.-on-subtle: le ton, poussé par pas de 5 % vers l'encre lisible jusqu'à 4,5:1 de contraste contresurface.basecouvert du lavis (20 pas au plus).
Le voile
--scrim part de surface.scrim, ou à défaut de la plus sombre de surface.base et ink.text, assombrie de moitié vers le noir. Il est ensuite assombri par pas de 5 % jusqu'à ce qu'un texte blanc garde 7:1 de contraste sur ce voile posé à 72 % sur du blanc. --scrim-brand et --scrim-accent-2 sont la marque et le deuxième accent de la palette sombre, éclaircis jusqu'à 4,5:1 sur le voile.
Le cadran de rayon
radii.radius est le seul réglage des coins, avec pill. Les crans s'en déduisent à pas de 4 px, bornés à 0 :
Cadran radii.radius | --radius-sm, -btn, -input | --radius-md, -btn-lg | --radius-lg | --radius-xl |
|---|---|---|---|---|
| 0 | 0px | 0px | 0px | 4px |
| 6 | 0px | 2px | 6px | 10px |
| 14 | 6px | 10px | 14px | 18px |
| 20 | 12px | 16px | 20px | 24px |
radius: 0--radius-sm0px--radius-md0px--radius-lg0px--radius-xl4pxradius: 6--radius-sm0px--radius-md2px--radius-lg6px--radius-xl10pxradius: 14--radius-sm6px--radius-md10px--radius-lg14px--radius-xl18pxradius: 20--radius-sm12px--radius-md16px--radius-lg20px--radius-xl24px14 est la valeur par défaut. --radius-full, --radius-badge et --radius-tag valent radii.pill, 9999 par défaut.
Les tailles fluides
Un rôle { min, max } devient un clamp() qui croît en ligne droite de min à 375 px de large jusqu'à max à 1280 px : pente (max - min) / 905, origine min - pente x 375. Avec min égal à max, la valeur est fixe. Les rôles de la démo de l'exemple complet donnent :
--text-display: clamp(56px, 4.42vw + 39.43px, 96px);
--text-page-title: clamp(48px, 3.536vw + 34.74px, 80px);
--text-section-title: clamp(38px, 2.431vw + 28.88px, 60px);
--text-lead: 20px;Les accents gris
Quand accent, accent2 et accent3 ont tous une chroma OKLCH inférieure à 0,04, --accents-muted vaut 1. Si c'est vrai dans les deux thèmes, la balise <html> reçoit data-accents="muted", qu'une règle peut cibler pour une charte monochrome.
Clair et sombre
Le Layout émet un <style> en tête de page : :root porte les variables du bloc dark, puis, si le fichier a un bloc light, [data-theme="light"] porte le jeu complet recalculé pour le clair. Les deux sélecteurs pèsent autant ; le second, écrit après, l'emporte dès que <html> porte data-theme="light".
:root{--brand:#d23a4f;--brand-rgb:210, 58, 79;...;--bg-primary:#1a211c;...}
[data-theme="light"]{--brand:#c42a3f;--brand-rgb:196, 42, 63;...;--bg-primary:#f2f4f5;...}Les couleurs que le client retouche depuis l'administration de contenu sont émises dans un second <style>, après celui-ci, sur :root et :root[data-theme="light"] : elles l'emportent dans les deux thèmes.
Le choix du thème avant l'affichage
Un script en ligne, dans le <head>, fixe le thème avant la première peinture, sans flash :
localStoragetheme-modevautauto: clair de 7 h à 18 h 59 (heure du poste), sombre sinon.- Sinon
localStoragetheme-preference:lightoudark, le dernier choix du visiteur. - Sinon
defaultThemede la charte.
Clair : <html data-theme="light">. Sombre : l'attribut est retiré, le sombre est l'état sans attribut. Le script écrit aussi data-default-theme. Le contrôleur ThemeManager, monté une fois, réapplique le thème après chaque navigation ; le fragment ThemeSwitcher écrit theme-mode et theme-preference.
Projet sombre seul
- Ne mettez pas de bloc
light. Aucune règle[data-theme="light"]n'est émise : même si l'attribut est posé, les variables restent celles dedark. - Laissez
defaultThemeà"dark", ou absent. - Ne montez pas de
ThemeSwitcher. - Ne figez pas les surfaces dans
web/index.css(--bg-primaryposé surbodypour forcer le sombre) : c'est la trace d'un bloclightresté dans la charte. Retirez le bloc.
Projet clair par défaut
"defaultTheme": "light" avec un bloc light : un premier visiteur voit le clair, le sombre reste accessible par la préférence. "defaultTheme": "light" sans bloc light pose l'attribut mais ne change aucune variable : le site reste sombre.
Une règle écrite à la main sous [data-theme='light'] ne vaut que pour le thème clair et se maintient à part. Dans un bloc, préférez les variables : elles basculent seules.
Le système de classes
La feuille du design system est générée par @asynccode/ac_tools (designSystemCss) à partir de la charte du projet, au build du site, et insérée dans le CSS global de chaque page. Elle déclare l'ordre des couches de toute la cascade :
@layer ds-reset, ds-tokens, ds-base, ds-grid, ds-components, app-globals, ds-utilities;Une couche déclarée plus tard l'emporte sur les précédentes, quelle que soit la spécificité, et tout CSS hors couche (le style.css d'un bloc, d'une vue, d'une mise en page) l'emporte sur toutes les couches. Les utilitaires (ds-utilities) battent donc web/index.css (app-globals) et perdent contre le style.css d'un bloc. Aucune classe du système ne porte !important.
Sur le site publié, la feuille ne garde que les classes dont le nom apparaît comme mot dans un fichier .js, .jsx ou .json de web/configurations ou du framework. Une classe assemblée à partir d'une donnée qui n'est écrite dans aucun fichier (une réponse d'API, par exemple) est retirée : elle s'affiche dans l'Aperçu, qui garde toute la feuille, et disparaît en ligne.
Le squelette
| Classe | Effet |
|---|---|
container | Largeur 100%, centrée, retrait latéral d'une demi-gouttière, max-width: var(--container-w). |
container-fluid | Pleine largeur, même retrait. |
container-narrow | Colonne de lecture : max-width: var(--container-narrow). |
full-bleed | Sort du conteneur : width: 100vw, d'un bord de la fenêtre à l'autre. |
grid | Grille CSS de var(--cols, 12) pistes égales, gouttière --gutter. Le parent est le conteneur ds-grid, chaque enfant un conteneur ds-cell. |
frame-narrow, frame-page, frame-wide | Retrait intérieur qui centre le contenu d'une bande sur --container-narrow, --container-w ou --container-wide ; le fond reste pleine largeur. |
frame-band-soft, frame-band-strong, frame-band-brand | Fond de bande : --bg-secondary, --bg-tertiary, --tone-primary-subtle. |
frame-space-s, frame-space-m, frame-space-l | Respiration verticale fluide d'une bande, un clamp() entre deux crans d'espacement. |
--container-w vaut 100%, puis, à partir de 576, 768, 992, 1200 et 1400 px de large, --container-sm, --container-md, --container-lg, --container-xl et --container-xxl.
La grille de 12 colonnes
Le parent porte grid, chaque enfant dit sa largeur en colonnes. Il n'y a pas de cols-N : la grille a toujours 12 pistes (--cols), et « trois colonnes » s'écrit avec trois enfants g-col-4.
| Classe | Effet |
|---|---|
g-col-1 ... g-col-12 | L'enfant couvre N pistes (grid-column: auto/span N). |
g-col-auto | Une piste, placement automatique. |
g-start-1 ... g-start-12, g-start-auto | Piste de départ de l'enfant (un décalage). |
g-col-<bp>-N, g-start-<bp>-N | Les mêmes à partir d'un palier de largeur de la grille ; <bp> vaut sm, md, lg, xl ou xxl. |
g-order-<bp>-first | L'enfant passe en tête à partir du palier. |
g-N, gx-N, gy-N | Gouttière de la grille (deux axes, colonnes, lignes) prise dans --space-N. Ne marchent que sur .grid. |
gap-N, gapx-N, gapy-N | gap, column-gap, row-gap sur n'importe quelle boîte flex ou grille. |
grid-balance, grid-balance-center | Sur le parent : quand la dernière rangée est incomplète, ses enfants g-col-6, g-col-4 ou g-col-3 s'élargissent pour la remplir, ou se centrent. |
Les crans N de gouttière et d'espacement : 0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 16, 20, 24, 32, 40. Exemple réel : une rangée de quatre principes, une colonne pleine sur une grille étroite, deux par rangée à partir de 20rem de grille, quatre à partir de 40rem.
{
"id": "items",
"primitive": "wrapper",
"styleJson": {
"base": { "alignItems": "start", "width": "100%" }
},
"props": { "className": "grid gap-6 w-100" },
"children": [
{
"id": "itemA",
"primitive": "wrapper",
"props": {
"className": "g-col-12 g-col-sm-6 g-col-lg-3 d-flex flex-column align-items-start gap-3"
},
"children": [ ... ]
},
...
]
}Deux axes de paliers
Les paliers portent les mêmes noms sur deux axes qui ne se mélangent pas : un md d'écran n'est pas un md de grille.
| Palier | Écran (@media) | Grille et cellule (@container) |
|---|---|---|
sm | 576px | 20rem |
md | 768px | 30rem |
lg | 992px | 40rem |
xl | 1200px | 50rem |
xxl | 1400px | 60rem |
- Écran :
@media (min-width: ...), la largeur de la fenêtre. Les utilitaires à palier (d-md-none,mt-lg-4,order-md-first...) et les blocssmàxxld'unstyleJson. - Grille :
@container ds-grid (min-width: ...), la largeur du.gridparent. Seulesg-col-<bp>-*,g-start-<bp>-*etg-order-<bp>-firsts'y rangent. Une grille posée dans une colonne étroite se replie donc comme sur un petit écran. - Cellule : un
styleJsonqui porte"containerQuery": trueécrit ses blocssmàxxlen@container ds-cell, la largeur de la cellule de grille ou de la bandeframe-*qui contient le nœud.cq-order-<bp>-firstsuit le même axe.
Les utilitaires
Une classe utilitaire fait une seule chose. La forme à palier insère le palier après le préfixe : d-none devient d-md-none, mt-4 devient mt-lg-4. Seules les familles marquées « palier » l'acceptent, et ce palier est celui de l'écran.
| Classes | Effet |
|---|---|
d-none, d-inline, d-inline-block, d-block, d-flex, d-inline-flex, d-grid | display, palier. C'est la famille de visibilité : d-none d-md-block masque sous 768 px. |
flex-row, flex-column, flex-row-reverse, flex-column-reverse, flex-wrap, flex-nowrap, flex-fill, flex-grow-0, flex-grow-1, flex-shrink-0, flex-shrink-1 | Direction, retour à la ligne, croissance ; palier. |
align-items-<v>, align-self-<v> (start, end, center, baseline, stretch) | Alignement sur l'axe secondaire ; palier. |
justify-content-<v> (start, end, center, between, around, evenly) | Répartition sur l'axe principal ; palier. |
m-N, mt-N, mb-N, ms-N, me-N, mx-N, my-N, m-auto ... | Marges en --space-N ou auto. t, b : haut, bas ; s, e : début et fin de ligne ; x, y : les deux côtés. Palier. |
p-N, pt-N, pb-N, ps-N, pe-N, px-N, py-N | Retraits en --space-N, mêmes côtés ; palier. |
gap-N | gap en --space-N ; palier. |
text-start, text-end, text-center, text-justify | Alignement du texte ; palier. |
text-lowercase, text-uppercase, text-capitalize, text-nowrap, text-wrap, text-balance, text-break | Casse et coupure des lignes. |
fs-3xs ... fs-7xl | Taille, interligne et interlettrage du cran (--text-*, --leading-*, --tracking-*). |
fw-regular, fw-medium, fw-semibold | --weight-*. Il n'y a pas de fw-bold. |
tracking-tight, tracking-normal, tracking-wide | --tracking-*. |
text-primary, text-secondary, text-success, text-warning, text-danger, text-info, bg-primary ... bg-info, bg-primary-subtle ... bg-info-subtle, border-primary ... border-info | Couleur, fond, lavis ou couleur de bordure du ton (--tone-*, --tone-*-subtle). |
text-body, text-muted, bg-surface, bg-surface-up | --on-surface, --on-surface-2, --surface, --surface-up. |
border, border-top, border-bottom, border-start, border-end, border-0, border-top-0 ... | Filet --stroke-base en --line-1, ou retiré. |
rounded-sm, rounded-md, rounded-lg, rounded-xl, rounded-full, rounded-0 | --radius-*. |
shadow-xs, shadow-sm, shadow-md, shadow-lg, shadow-xl, shadow-none | --shadow-*. |
position-static, position-relative, position-absolute, position-fixed, position-sticky | Positionnement. |
top-0, top-50, top-100, bottom-*, start-*, end-* | Décalages en %, côtés logiques pour start et end. |
w-25, w-50, w-75, w-100, w-auto, h-25 ... h-auto, mw-100, mh-100 | Largeur et hauteur en % ; w-* et h-* à palier. |
overflow-auto, overflow-hidden, overflow-visible, overflow-scroll | Débordement. |
order-0 ... order-5, order-first, order-last | Ordre d'affichage ; palier d'écran. |
opacity-0, opacity-25, opacity-50, opacity-75, opacity-100 | Opacité. |
z-sticky, z-banner, z-overlay, z-dropdown, z-modal, z-popover, z-toast, z-tooltip | z-index: var(--z-*). |
text-primary colore en --tone-primary, la couleur de marque, et non en --text-primary. Pour l'encre courante, utilisez text-body ; pour l'encre secondaire, text-muted.
Les aides et les recettes
Les aides sont toujours présentes, jamais élaguées : clearfix, text-bg-primary à text-bg-info (fond du ton avec son encre), link-primary à link-info, focus-ring, icon-link et icon-link-hover, translate-middle, translate-middle-x, translate-middle-y, ratio avec ratio-1x1, ratio-4x3, ratio-16x9, ratio-21x9, vstack, hstack, stretched-link, text-truncate, vr, visually-hidden, visually-hidden-focusable.
Des recettes de contenu (table, table-sm, table-striped, table-hover, figure, blockquote, list-unstyled, list-inline, img-thumbnail...), de formulaire (form-label, form-text, invalid-feedback, valid-feedback, form-range) et de composants (alert-primary à alert-info, accordion, modal, offcanvas, list-group, progress, toast, popover, tooltip, breadcrumb, spinner-border, navbar...) complètent la feuille. Les composants sont ajoutés page par page, seulement quand la page cite leur classe. Les familles btn*, badge* et form-control vivent dans le CSS global du framework.
Le contexte : data-surface et data-style
Deux attributs règlent le contexte d'une boîte au lieu de lui donner une couleur. Dans une vue, ils se posent à la racine du nœud ; dans un tree.json, dans props.
data-surfacevide : le niveau se compte par profondeur, trois au plus. Au premier,--surfacevaut--sf-1,--line--line-1et--shadow--shadow-sm; au deuxième,--sf-2,--line-2,--shadow-md; au troisième,--sf-3et--shadow-lg.data-surface="1","2"ou"3"impose un niveau absolu, pour une modale ou un menu flottant.data-stylepeint la boîte avec une matière : fond--surface, bordure--stroke-thinen--line, ombre--shadow.
| Valeur | Effet |
|---|---|
flat | Ni filet ni ombre. |
outlined | Filet --line-2, pas d'ombre. |
elevated | Pas de filet, ombre --shadow-lg. |
soft | Fond --surface-up, pas de filet, ombre --shadow-sm. |
glass | Fond --bg-secondary à l'opacité --glass-alpha sous un reflet --glass-sheen, bord --glass-edge, arête éclairée et ombre --glass-lift, flou --surface-filter. Se voit sur un fond uni ; le reflet suit le fond (blanc sur sombre, givre sur clair). |
"" | Valeur vide : la matière de effects.surface, flat par défaut. |
Le vocabulaire de l'inspecteur
@utils/dsVocabulary dérive de cette feuille les familles que l'inspecteur de l'Atelier lit et écrit. Une famille regroupe les classes qui posent les mêmes propriétés (deux classes d'une famille s'excluent) et porte un axe : écran, grille ou aucun. L'inspecteur tient une seule source par nœud et par propriété : poser une classe retire du styleJson les déclarations qui touchent ses propriétés (les états restent), et écrire une valeur hors du système retire la famille de classes correspondante.
À l'écriture d'un tree.json ou d'un structure.json, l'API compare chaque classe au vocabulaire. Sont reconnues les classes du design system, les classes propres du bloc (<id>-..., l'id étant le nom de son dossier), les références $..., btn*, badge*, form-control et under-nav. Toute autre classe est écrite quand même, et la réponse porte un avertissement node "<id>": <classes> outside the design system vocabulary.
styleJson dans un tree.json
Chaque nœud d'un tree.json peut porter, à côté de props, un styleJson : des blocs de déclarations en camelCase, la forme d'un objet style React. Les éditeurs y écrivent ce qu'aucune classe ne couvre : couleurs hors tons, dégradés, filets fins, transitions, états.
| Bloc | Compilé en |
|---|---|
base | La règle du nœud. |
hover, focus-visible, active | :hover, :focus-visible, :active, dans cet ordre. |
before, after | ::before, ::after. |
hover-before, hover-after | :hover::before, :hover::after. |
sm, md, lg, xl, xxl | @media (min-width: 576px) à 1400px, ou @container ds-cell (min-width: 20rem) à 60rem avec containerQuery. |
containerQuery | true, et rien d'autre, fait passer les blocs de palier sur l'axe cellule. |
Un bloc de palier ne contient que des déclarations, pas d'état imbriqué. Quand base pose display: grid sans piste intrinsèque (auto, min-content, max-content, fit-content()), la compilation ajoute une règle qui fait de chaque enfant un conteneur ds-cell.
Exemple réel
Le bandeau d'appel à l'action d'une démo : un conteneur au contexte outlined, un lien avec ses états, une liste qui passe en ligne à partir de 992 px.
{
"id": "banner",
"primitive": "wrapper",
"props": {
"className": "d-flex flex-row flex-wrap align-items-center justify-content-between gap-6 w-100",
"data-style": "outlined",
"data-surface": ""
},
"styleJson": {
"base": {
"padding": "var(--pad)",
"borderRadius": "var(--radius-sm)",
"backgroundColor": "var(--scrim)",
"color": "var(--scrim-ink)",
"boxShadow": "var(--shadow-lg)"
}
},
"children": [ ... ]
}
{
"id": "primary",
"primitive": "Link",
"props": {
"href": "/gateaux",
"className": "btn btn-primary btn-md d-inline-flex align-items-center justify-content-center gap-2 py-3 px-5"
},
"styleJson": {
"base": { "whiteSpace": "normal", "textAlign": "center" },
"hover": { "transform": "translateY(-1px)" },
"focus-visible": { "outlineOffset": "2px" }
},
"children": [ ... ]
}
{
"id": "milestones",
"primitive": "List",
"styleJson": {
"base": { "display": "flex", "flexDirection": "column", "margin": "0", "padding": "0", "listStyle": "none" },
"lg": { "flexDirection": "row" }
},
"props": { "ordered": true },
"children": [ ... ]
}Ce que la compilation écrit dans le style.css de chaque bloc :
/* forge:nodes:start */
[data-forge-id="homeCta--root"][data-forge-id="homeCta--root"] { position: relative; overflow: hidden; }
[data-forge-id="homeCta--banner"][data-forge-id="homeCta--banner"] { padding: var(--pad); border-radius: var(--radius-sm); background-color: var(--scrim); color: var(--scrim-ink); box-shadow: var(--shadow-lg); }
...
[data-forge-id="homeCta--primary"][data-forge-id="homeCta--primary"] { white-space: normal; text-align: center; }
[data-forge-id="homeCta--primary"][data-forge-id="homeCta--primary"]:hover { transform: translateY(-1px); }
[data-forge-id="homeCta--primary"][data-forge-id="homeCta--primary"]:focus-visible { outline-offset: 2px; }
...
/* forge:nodes:end */
/* forge:nodes:start */
...
[data-forge-id="aboutMilestones--milestones"][data-forge-id="aboutMilestones--milestones"] { display: flex; flex-direction: column; margin: 0; padding: 0; list-style: none; }
@media (min-width: 992px) { [data-forge-id="aboutMilestones--milestones"][data-forge-id="aboutMilestones--milestones"] { flex-direction: row; } }
...
/* forge:nodes:end */Propriétés permises
Une clé hors de cette liste est retirée sans erreur. Les noms sont en camelCase et en côtés physiques : paddingLeft, jamais paddingInlineStart.
| Groupe | Propriétés |
|---|---|
| Boîte | margin, marginTop, marginRight, marginBottom, marginLeft, padding, paddingTop, paddingRight, paddingBottom, paddingLeft, gap, rowGap, columnGap |
| Taille | width, minWidth, maxWidth, height, minHeight, maxHeight, aspectRatio, boxSizing, overflow, overflowX, overflowY, contain |
| Affichage et position | display, position, top, right, bottom, left, inset, zIndex, float, clear, visibility |
| Flex | flexDirection, flexWrap, flexFlow, flex, flexGrow, flexShrink, flexBasis, order, alignItems, alignContent, alignSelf, justifyContent, justifyItems, justifySelf, placeItems, placeContent, placeSelf |
| Grille | gridTemplateColumns, gridTemplateRows, gridTemplateAreas, gridTemplate, gridAutoColumns, gridAutoRows, gridAutoFlow, gridColumn, gridRow, gridArea, gridColumnStart, gridColumnEnd, gridRowStart, gridRowEnd |
| Typographie | color, fontFamily, fontSize, fontWeight, fontStyle, fontVariant, lineHeight, letterSpacing, wordSpacing, textAlign, textTransform, textDecoration, textDecorationLine, textDecorationColor, textDecorationStyle, textIndent, textOverflow, textShadow, whiteSpace, wordBreak, overflowWrap, verticalAlign, listStyle, listStyleType, listStylePosition, direction |
| Fond | background, backgroundColor, backgroundImage, backgroundPosition, backgroundSize, backgroundRepeat, backgroundClip, backgroundOrigin, backgroundAttachment, backgroundBlendMode |
| Bordure | border, borderWidth, borderStyle, borderColor, borderRadius, borderTop, borderRight, borderBottom, borderLeft, borderTopWidth, borderRightWidth, borderBottomWidth, borderLeftWidth, borderTopStyle, borderRightStyle, borderBottomStyle, borderLeftStyle, borderTopColor, borderRightColor, borderBottomColor, borderLeftColor, borderTopLeftRadius, borderTopRightRadius, borderBottomRightRadius, borderBottomLeftRadius |
| Contour | outline, outlineWidth, outlineStyle, outlineColor, outlineOffset |
| Effets | boxShadow, opacity, filter, backdropFilter, mixBlendMode, transition, transitionProperty, transitionDuration, transitionTimingFunction, transitionDelay, cursor, pointerEvents, userSelect, objectFit, objectPosition, resize, clipPath |
| Transformations | transform, transformOrigin, transformStyle, perspective, perspectiveOrigin, backfaceVisibility, rotate, scale, translate, willChange |
| Divers | textWrap, textUnderlineOffset, textDecorationThickness, fontFeatureSettings, fontVariationSettings, accentColor, caretColor, isolation, tabSize, scrollBehavior, scrollSnapType, scrollSnapAlign, scrollMargin, scrollPadding |
| Animation | animation, animationName, animationDuration, animationTimingFunction, animationDelay, animationIterationCount, animationDirection, animationFillMode, animationPlayState, animationTimeline, animationRange, animationRangeStart, animationRangeEnd |
| Contenu | content |
Les images clés d'une animation ne s'écrivent pas dans un styleJson : elles vivent dans le CSS du bloc, et le styleJson les nomme par animationName.
Valeurs permises
- Une chaîne ou un nombre, de 512 caractères au plus.
- Caractères permis : lettres, chiffres, espaces et
. , % # ( ) + * ' " / -. Tout autre caractère refuse la valeur, notamment;,{,},<,>,\,:,!(donc!important),@,&et_. url(,@import,expression(,/*et*/sont refusés, ainsi que les motsjavascript,vbscript,behavior,-moz-binding,image(doncimage-set()),cross-fade,srcetbrand-logo-url. Une image de fond passe par le choix d'un média, jamais par unstyleJson.var(): chaque référence doit nommer un jeton permis, c'est-à-dire une variable des tableaux précédents (sauf--accents-mutedet les cinq du logo) ou une variable de contexte. Une référence avec repli (var(--brand, red)) ou imbriquée est refusée. Plusieurs références dans une valeur sont permises (var(--space-2) var(--space-4)), et une référence peut vivre dans une fonction (color-mix(in srgb, var(--scrim-ink) 40%, transparent)).contentn'accepte quenone,normalou une chaîne entre guillemets de 64 caractères au plus.- Une déclaration refusée est retirée sans message ; le reste du bloc est compilé.
Reteinter un sous-arbre
Dix variables peuvent être redéfinies dans un styleJson pour reteinter un nœud et ses descendants : --brand, --brand-secondary, --accent, --accent-2, --accent-3 et leurs jumelles -rgb. La couleur prend un hexadécimal (3, 4, 6 ou 8 chiffres), un rgb() ou rgba() numérique, ou un jeton permis ; la jumelle -rgb prend un triplet r, g, b. Écrivez toujours la paire, sinon les transparences rgba(var(--brand-rgb), ...) gardent l'ancienne couleur. Aucune autre propriété personnalisée n'est acceptée.
Les variables calculées depuis la marque ne suivent pas : --tone-primary, --ink-text-on-brand ou --tone-primary-subtle gardent la valeur posée sur :root.
Raccourcis de grille
Un nœud peut aussi porter col, offset, order et g, sur le nœud ou dans props. Ils se développent en styleJson à la compilation : col: 6 donne gridColumn: "auto/span 6" ; col: "md-6" ou col: { "base": 12, "md": 6 } visent un palier d'écran ; offset donne gridColumnStart, order donne order, g donne gap: var(--space-N). Le styleJson écrit à la main l'emporte sur le raccourci. Les éditeurs n'écrivent plus ces raccourcis : préférez les classes g-col-*.
La compilation dans style.css
Les éditeurs de l'Atelier et la création d'un bloc compilent l'arbre (compileFragmentNodesCss) et réécrivent la zone du style.css du bloc comprise entre /* forge:nodes:start */ et /* forge:nodes:end */. Tout ce qui est hors de ces deux marques est conservé tel quel. À la première compilation, la zone est ajoutée en fin de fichier, après le CSS déjà écrit.
Chaque nœud stylé reçoit une règle sur son identifiant préfixé par l'id du bloc, <bloc>--<nœud>, écrit deux fois. Ce doublement porte la règle à une spécificité de (0,2,0), au niveau des styles de base des fragments du framework ([data-fragment='Nom'] .titre), que la création d'un bloc recopie au-dessus de la zone : écrite après eux, elle l'emporte. Toutes les instances d'un même bloc partagent l'identifiant, donc la règle.
Au rendu, le bloc est enveloppé dans <div style="display: contents" data-fid="<bloc>">. Seuls la racine de chaque instance et les nœuds qui portent un style effectif reçoivent l'attribut data-forge-id ; le canevas d'édition, lui, le pose sur tous les nœuds.
Le style.css est le seul porteur de ces règles : le rendu n'applique pas le styleJson en ligne. Un styleJson modifié à la main dans tree.json ne change rien tant que la zone n'est pas recompilée, ce que fait un enregistrement depuis un éditeur.
Le piège de la classe d'état
Une classe d'état écrite à la main, .x.is-on, pèse (0,2,0) : autant que la règle générée du nœud. À égalité, l'ordre dans le fichier décide. Écrite avant la zone générée, la règle d'état perd sur toute propriété que le styleJson du nœud pose aussi : si base pose background et que .x.is-on pose aussi background, l'état ne s'affiche jamais.
- Laissez hors du
styleJsonles propriétés que l'état change, et écrivez l'état de repos et l'état actif dans le CSS du bloc (.xet.x.is-on). - Ou écrivez la règle d'état après
/* forge:nodes:end */: la recompilation conserve ce qui suit la zone. - Ou préfixez par
[data-fid='<bloc>'], comme ci-dessous, pour passer à (0,3,0). Un tel sélecteur bat aussi les retouches de style posées sur le nœud depuis l'administration de contenu (même sélecteur doublé), et la vérification des lois du design system signale tout sélecteur de plus de deux composés : gardez-le pour les états.
[data-fid='tarifFlow'] .tarifFlow-choice.is-on {
border-color: var(--brand);
background: var(--brand);
color: var(--ink-text-on-brand);
}Exemple complet
La charte d'une démo de pâtisserie, raccourcie : un thème sombre complet, un thème clair qui ne change que les couleurs et les ombres, le clair par défaut.
{
"defaultTheme": "light",
"dark": {
"palette": {
"brand": "#d23a4f",
"brandSecondary": "#e3c49c",
"accent": "#e3c49c",
"accent2": "#d9a46b",
"accent3": "#a9b5ad",
"accentSoft": "#3a2328",
"success": "#7fd3a0",
"warning": "#f0c06a",
"error": "#ff8f85",
"info": "#9cc6e0"
},
"surface": {
"base": "#1a211c",
"panel": "#212a24",
"elevated": "#29332c",
"bg1": "#1d2520",
"bg2": "#344038",
"bgCard": "#202923",
"scrim": "#141a16"
},
"ink": {
"text": "#f3f1ea",
"muted": 0.76,
"dim": 0.64,
"faint": 0.38,
"line": 0.13,
"lineStrong": 0.22,
"lineFaint": 0.07
},
"fonts": {
"sans": {
"family": "Sofia Sans",
"import": "https://fonts.googleapis.com/css2?family=Sofia+Sans:wght@400;500;600;700;800&display=swap"
},
"mono": { "family": "Geist Mono", "import": "..." },
"display": { "family": "Sofia Sans Extra Condensed", "import": "..." }
},
"typography": {
"roles": {
"display": { "min": 56, "max": 96 },
"pageTitle": { "min": 48, "max": 80 },
"sectionTitle": { "min": 38, "max": 60 },
"lead": { "step": "xl" }
},
"sizes": { "3xs": 10, "2xs": 11, "xs": 12, "sm": 14, "base": 16, ... , "7xl": 72 },
"leading": { "3xs": 1.4, ... , "4xl": 1, "5xl": 0.95, "6xl": 0.95, "7xl": 0.95 },
"tracking": { "3xs": 0.02, ... , "7xl": -0.005 },
"trackingScale": { "tight": -0.02, "normal": 0, "wide": 0.03 },
"weights": { "regular": 400, "medium": 500, "semibold": 700, "bold": 800 },
"lineHeights": { "tight": 1.15, "base": 1.5 }
},
"radii": { "radius": 6, "pill": 9999 },
"shadows": {
"xs": "0 1px 2px rgba(0, 0, 0, 0.3)",
"soft": "0 2px 8px rgba(0, 0, 0, 0.3)",
"lg": "0 12px 28px rgba(0, 0, 0, 0.36), 0 4px 8px rgba(0, 0, 0, 0.3)",
"brand": "0 8px 20px rgba(196, 42, 63, 0.22)",
...
},
"motion": {
"easeOut": "cubic-bezier(0.16, 1, 0.3, 1)",
"easeSpring": "cubic-bezier(0.22, 1, 0.36, 1)",
"easeInOut": "cubic-bezier(0.4, 0, 0.2, 1)",
"fast": "120ms",
"base": "220ms",
"slow": "380ms"
},
"spacing": {
"0": 0, "1": 4, "2": 8, "3": 12, "4": 16, "5": 20, "6": 24, "8": 32,
"10": 40, "12": 48, "16": 64, "20": 80, "24": 96, "32": 128, "40": 160
},
"stroke": { "thin": "0.5px", "base": "1px" },
"grid": {
"containers": { "sm": 1180, "md": 1180, "lg": 1180, "xl": 1180, "xxl": 1180 },
"gutter": 24,
"columns": 12,
"narrow": 700
},
"layers": { "sticky": 100, "banner": 200, ... , "tooltip": 800 },
"effects": { "surface": "soft", "glassBlur": 14, "glassAlpha": 0.62, ... }
},
"light": {
"palette": {
"brand": "#c42a3f",
"brandSecondary": "#1f2a24",
"accent": "#1f2a24",
"accent2": "#8a5a2b",
"accent3": "#5f6b63",
"accentSoft": "#f6dfe2",
"success": "#2e6b45",
"warning": "#8a5a00",
"error": "#a3231c",
"info": "#1f4f6b"
},
"surface": {
"base": "#f2f4f5",
"panel": "#e6e9ec",
"elevated": "#ffffff",
"bg1": "#eef1f3",
"bg2": "#dde2e6",
"bgCard": "#ffffff"
},
"ink": {
"text": "#1b211d",
"muted": 0.76,
"dim": 0.66,
"faint": 0.42,
"line": 0.14,
"lineStrong": 0.26,
"lineFaint": 0.07
},
"shadows": { "xs": "0 1px 2px rgba(27, 33, 29, 0.07)", ... }
}
}Ce qui change entre les thèmes
| Variable | Thème sombre | Thème clair |
|---|---|---|
--brand | #d23a4f | #c42a3f |
--brand-rgb | 210, 58, 79 | 196, 42, 63 |
--tone-primary-on | #ffffff | #ffffff |
--tone-primary-subtle | rgba(210, 58, 79, 0.12) | rgba(196, 42, 63, 0.12) |
--tone-primary-on-subtle | #dd6b7b | #bb283d |
--tone-primary-emphasis | #d95869 | #cd4a5c |
--tone-secondary-on | #0b0b12 | #ffffff |
--ink-text-on-accent | #0b0b12 | #ffffff |
--bg-primary | #1a211c | #f2f4f5 |
--sf-2 | #29332c | #ffffff |
--text-primary | #f3f1ea | #1b211d |
--text-secondary | rgba(243, 241, 234, 0.76) | rgba(27, 33, 29, 0.76) |
--ui-line | rgba(243, 241, 234, 0.13) | rgba(27, 33, 29, 0.14) |
--scrim | #141a16 | #141a16 |
--scrim-brand | #d95869 | #d95869 |
dark)--brand#d23a4f--brand-secondary#e3c49c--accent#e3c49c--accent-2#d9a46b--accent-3#a9b5ad--accent-soft#3a2328--success#7fd3a0--warning#f0c06a--error#ff8f85--info#9cc6e0--bg-primary#1a211c--bg-secondary#212a24--bg-tertiary#29332c--text-primary#f3f1ealight, fusionné sur dark)--brand#c42a3f--brand-secondary#1f2a24--accent#1f2a24--accent-2#8a5a2b--accent-3#5f6b63--accent-soft#f6dfe2--success#2e6b45--warning#8a5a00--error#a3231c--info#1f4f6b--bg-primary#f2f4f5--bg-secondary#e6e9ec--bg-tertiary#ffffff--text-primary#1b211dCe qui est commun aux deux thèmes
| Variable | Valeur |
|---|---|
--font-display | 'Sofia Sans Extra Condensed', ui-sans-serif, -apple-system, system-ui, sans-serif |
--text-display | clamp(56px, 4.42vw + 39.43px, 96px) |
--leading-display | 0.95 |
--text-section-title | clamp(38px, 2.431vw + 28.88px, 60px) |
--text-lead, --leading-lead | 20px, 1.4 |
--radius-sm, --radius-md, --radius-lg, --radius-xl | 0px, 2px, 6px, 10px |
--radius-btn, --radius-input | 0px |
--space-4, --u | 16px, 4px |
--container-md | 1180px |
--weight-semibold | 700 |
radius: 6donne des coins presque droits :--radius-smtombe à 0, donc boutons et champs sont carrés.- Les rôles fluides font grandir les titres avec la page : un titre de section passe de 38 px à 375 px de large à 60 px à 1280 px.
- Les cinq largeurs de conteneur valent 1180 : à partir de 576 px,
.containerne dépasse jamais 1180 px. - Le voile est déclaré dans
darket reste le même en clair, comme--scrim-brand, calculé depuis la palette sombre. effects.surface: "soft"fait d'undata-stylevide une surface pleine sans filet.- Le bloc
lightn'a nifontsnitypography: la typographie est la même dans les deux thèmes. weights.semiboldvaut 700 :fw-semiboldet tout ce qui lit--weight-semiboldsont plus gras que d'habitude, sans toucher un bloc.
Règles et pièges
Règles d'usage
- Jamais de couleur en dur dans un bloc :
#c42a3fs'écritvar(--brand), une transparencergba(var(--brand-rgb), 0.2). Une couleur figée ne suit ni le thème clair ni les changements de charte. - Les variables plutôt que les valeurs, aussi pour les tailles, les rayons, les espacements et les durées :
var(--text-lg),var(--radius-md),var(--space-4),var(--duration-fast). - Les classes d'abord, le
styleJsonpour ce qu'elles n'expriment pas, le CSS écrit à la main pour le reste : sélecteurs d'enfants, pseudo-éléments complexes, états portés par une classe, images clés. - Un bloc ne fixe pas sa largeur de page : la largeur vient de
containerou d'une bandeframe-*. Un bloc se mesure à son conteneur (@container), pas à la fenêtre ; la vérification des lois du design system signale unmax-widthde 1000 px ou plus centré, et un@mediade largeur dans le CSS d'un bloc.
web/index.css perd contre les blocs
web/index.css n'est pas chargé tel quel : il est inséré, avec le CSS global du framework, dans @layer app-globals. Une règle en couche perd toujours contre une règle hors couche, quelle que soit sa spécificité. Une règle de web/index.css ne corrige donc jamais le style.css d'un bloc, d'un fragment ou d'une mise en page.
- Bon usage : le socle du document (
body, police,box-sizing) et les valeurs de variables propres au projet, comme--shell-clear. - Mauvais usage : surcharger un bloc, la règle est ignorée ; poser une remise à zéro large comme
*{margin:0;padding:0}, qui bat les couchesds-*déclarées avantapp-globalset casse le centrage de.containeret les retraits du système.
body {
background: var(--bg-primary);
color: var(--text-primary);
font-family: var(--font-sans);
line-height: var(--leading-base);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow-x: hidden;
}La mise en page dans layouts/<id>/style.css
Ce qui tient la page autour des blocs (barre collante, zones, filet sous l'en-tête) va dans le style.css de la mise en page, web/configurations/layouts/<id>/style.css. Ce CSS est hors couche et inséré sur chaque page qui monte cette mise en page. Pas dans web/index.css, où ces règles perdraient, ni dans un bloc, qui ne connaît pas la page.
.l-site-top {
position: sticky;
top: 0;
z-index: var(--z-sticky);
}Pièges
- Un
spacingincomplet donneundefinedpxpour les clés0,1,2,3,4,6,8,12,16: marges, retraits et gouttières disparaissent. Même chose pour une opacité deinkmanquante. - Une couleur écrite autrement qu'en
#rrggbbdonne un canal-rgbnoir (0, 0, 0) et une encre calculée blanche. - Sans
typography.lineHeights.base,--leading-basen'existe pas, même avecleading.base. radii.radiusécrit en chaîne est ignoré : le cadran revient à 14.- Une police sans
importenhttps://dansdarkne se charge pas : le site s'affiche dans la pile de repli. - Une variable inventée (
--border-color,--font-size-lg) n'existe pas : sans repli, toute la déclaration tombe. Dans unstyleJson, elle est refusée à la compilation. - Une classe construite à partir d'une donnée est élaguée en ligne alors qu'elle s'affiche dans l'Aperçu.
- Un sélecteur écrit à la main sur
[data-forge-id='<bloc>--<nœud>']ne trouve en ligne que la racine et les nœuds stylés ; le canevas d'édition marque tous les nœuds et cache l'écart. - Redéfinir
--brandsur un nœud ne change ni--tone-primaryni--ink-text-on-brand. "defaultTheme": "light"sans bloclightlaisse le site sombre.- Une valeur de charte qui contient
;, une accolade,<ou unurl(hors logo est retirée du<style>sans message.



