Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ node_modules
# Generated Tokens (npm run tokens:build)
projects/ui-kit/styles/generated

# Copie servie par Storybook pour le téléchargement (regénérée par docs:config)
storybook/public/component-vars.scss

# Compodoc (generated for Storybook)
documentation.json

Expand Down
56 changes: 56 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,62 @@ $card-radius: var(--radius-md); /// Rayon des coins.
override, otherwise the doc shows the wrong default value.
- Every config variable must have its `///`: `npm run docs:config` counts the missing ones.

#### Every structural value is read through a `--ui-*` hook

In **package mode** the kit's SCSS is already compiled, so `_ui-config.scss` and a component's
local variables are out of reach. Each structural value is therefore read *through* a custom
property whose fallback is the shipped default — the hook goes on the **config variable**, never
on the usage sites:

```scss
// --- Config ---
$radius: var(--ui-button-radius, var(--radius-sm)); /// Rayon des coins.
$stroke-width: var(--ui-button-stroke-width, #{utils.$control-stroke-width}); /// Épaisseur de la bordure.

.ui-button { border-radius: $radius; border: $stroke-width solid transparent; }
```

- **Naming**: `--ui-{family}[-{part}]-{property}[-{modifier}]`, modifier **last** (same rule as
the tokens: `actions-high-surface-hover`). `{family}` is the entry-point folder minus `ui-`, so
a sub-component uses its family (`ui-tab-list.scss` → `--ui-tabs-*`). The property vocabulary
and the modifier list live in `scripts/component-vars.build.mjs`; `npm run docs:config` **fails**
on a hook that doesn't parse, so extend the vocabulary there rather than inventing a name.
- **Inside a map**, every value carries its own hook (`height: var(--ui-button-height-small, …)`).
- **What gets one**: dimensions, spacings, gaps, radii, stroke/focus-ring widths, font sizes,
durations, offsets, z-index — anything structural, including raw `px`/`rem` literals.
- **What doesn't**: SCSS lists/maps that generate classes (`$levels`, `$variants` — build-time,
no custom property can carry them), and **colours**, which stay on the semantic tokens (theme ×
3 brands × WCAG). Exception: a colour whose per-instance repaint is a real use case (mask
backdrop, loading marker, skeleton shine, rating fill).
- **A component must never declare a name it also exposes**: a custom property cannot reference
itself, and a declaration on the element beats an inherited override. When a value must live on
a variable (read by a sub-component, re-read by the consumer), declare a **private mirror** and
read the public hook in its fallback:
```scss
:host { --_width: var(--ui-sidebar-width, #{$width-default}); } /// Largeur du panneau déployé.
```
The `///` stays on the mirror: that is what publishes the public hook's role.
- Never a value hardcoded inline in a rule when it is structural: promote it to a config variable
with its hook and its `///`.
- `npm run docs:config` regenerates `projects/ui-kit/styles/component-vars.scss` (the consumer's copy-me
theme) and `figma/component-vars.json` from these hooks. Both are committed;
`docs:config:check` fails when they are stale, when a hook doesn't parse, or when a hook has
no `///`. Values in both files are read from the **compiled CSS**, not from the SCSS text — so
`rem-calc(44px - 2 * $gutter)` lands as `2.25rem`, not as an unevaluated expression.
- A hook whose default **differs per variant** (`--ui-button-radius` is `--radius-sm`, or
`--radius-full` when `_rounded`) is excluded from the theme file: declaring one value there
would flatten the others. So is a hook the component declares itself. The generator classifies
this on its own — nothing to annotate.
- **`_ui-config.scss` carries hooks too**, named `--ui-<scss name>` (`$form-control-size` →
`--ui-form-control-size`). They sit between the token and the component hook, and reach every
consumer for free since components *interpolate* them:
`var(--ui-checkbox-box-size, var(--ui-form-control-size, var(--size-components-2xs)))`. Use
this layer when a value is shared by a category and the token it points at is used elsewhere
too (`--size-components-2xs` also feeds `ui-tag`, so retuning the token would overshoot).
⚠️ A constant that **references another constant** (`$form-focus-ring-width: $focus-ring-width`)
takes **no hook of its own**: it inherits the referenced one, and hooking both makes
`docs.config.mjs`'s binding resolver recurse forever.

### Shared structural constants — `ui-config`

Structural values common to components (focus ring width, form control size,
Expand Down
39 changes: 39 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,30 @@ Le format suit [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/) et le pr

Désactivé par défaut : sans Storybook, story et MDX sont deux fichiers qui importent des packages absents. Le choix est retenu dans `ui-kit.json` et `update` s'y tient.
- **Les README du kit (EN + FR) présentent les deux modes de consommation** (FSHSP-116) : section « Deux modes de consommation » avant l'installation — tableau comparatif *dépendance* / *starter* (ce qui arrive dans le dépôt, imports, styles, documentation, personnalisation, mise à jour), qui renvoie au package compagnon pour la voie starter. FSHSP-109 avait livré le mode starter sans qu'il apparaisse nulle part sur la page npm, où le README est la seule documentation.
- **Les composants sont personnalisables en mode package, via des custom properties `--ui-*` exposées.** Jusqu'ici le seul levier de personnalisation était le rebinding SCSS (`_ui-config.scss`, variables locales) : inaccessible quand le kit est consommé en dépendance, puisque son SCSS est déjà compilé. Un projet ne pouvait retoucher que les design tokens — donc rien de propre à un composant, ni à une seule de ses tailles. Chaque valeur **structurelle** des 54 composants est maintenant lue à travers un hook dont le défaut est le réglage livré :

```scss
// avant : valeur compilée, hors d'atteinte
height: var(--size-components-sm);
// après : surchargeable, même défaut
height: var(--ui-button-height-small, var(--size-components-sm));
```

**579 hooks** exposés, nommés `--ui-{famille}[-{partie}]-{propriété}[-{modifieur}]` (modifieur en dernier, comme les tokens). Le composant ne déclare jamais ces noms : les poser sur `:root` (tout le projet), sur un sélecteur (une zone) ou sur l'élément suffit à gagner.

```scss
:root { --ui-button-height-small: 24px; }
.toolbar ui-button { --ui-button-height-small: 24px; }
```

Les **couleurs** restent volontairement sur les tokens sémantiques (clair/sombre × 3 marques × contraste WCAG) : seuls quelques hooks de couleur existent, là où repeindre **un exemplaire** est un cas d'usage réel (voile de masque, marqueur de chargement, reflet de squelette, remplissage de notation).

Aucun défaut ne change : la migration a été vérifiée fichier par fichier en comparant le CSS compilé avant/après, hook réduit à son défaut.
- **`@4sh/ui-kit/styles/component-vars.scss`** — livré avec le package : un fichier prêt à copier où les **581 hooks** réglables sont déclarés à la valeur livrée par le starter, groupés par catégorie → composant → type de propriété (dimensions, espacements, bordures, typographie, couleurs), valeurs alignées sur une seule colonne. On le copie dans le projet, on le charge après `styles.css`, on change les valeurs voulues ; tel quel il ne change rien. Également téléchargeable depuis Storybook (*Spécifications → Thème & Système de Tokens*).

Généré par `npm run docs:config`, **valeurs lues dans le CSS réellement compilé par Sass** : une expression comme `rem-calc(44px - 2 * $gutter)` y figure résolue (`2.25rem`), jamais sous sa forme SCSS. Les 17 hooks pour lesquels un réglage global n'a pas de sens (valeur dépendante de la variante rendue, propriété que le composant se pose lui-même) en sont volontairement absents — la colonne « Hook exposé » de chaque composant les donne.

Le mot **« thème »** reste réservé au couple marque + clair/sombre (`ThemeService`, `BrandService`) : cette couche-ci s'appelle **hooks**, du nom des custom properties qu'elle règle, pour éviter trois sens différents du même mot.
- **`@4sh/ui-kit-schematics` embarque son `LICENSE` et ses README** (EN + FR). Le package déclarait `Apache-2.0` sans en fournir le texte, alors que la licence (§4a) demande qu'une redistribution en joigne une copie — d'autant plus ici que ce package existe pour que son contenu soit recopié ailleurs. Sa page npmjs, jusque-là vide, redirige maintenant vers `@4sh/ui-kit` : le compagnon ne s'installe jamais directement.

### Fixed
Expand Down Expand Up @@ -62,6 +86,21 @@ Le format suit [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/) et le pr
Les bases partagées atterrissaient dans `components/ui/{catégorie}/_shared/`, c'est-à-dire des services (`theme.service`) et des utilitaires (`mask-engine`) rangés sous `components/`. Elles vivent maintenant dans `ui-core/`, en conservant leur regroupement par domaine. Les fichiers propres à un composant (`date-utils`, `ui-alert.types`, `ui-toast.service`…) restent à côté de lui : ils ne servent qu'à lui.

**Mise à jour non triviale** pour un projet déjà installé en `0.2.0` : les chemins des fichiers copiés changent tous, donc `update` ne reconnaîtra pas les anciens. Déplacer les fichiers existants avant de lancer la commande, ou repartir d'un `add` propre.
- **Les constantes partagées de `_ui-config.scss` sont retouchables sans recompiler.** Ma migration n'avait hookifié que les variables *locales* de chaque composant : il n'existait aucun levier de **catégorie** en mode package. Changer la taille de tous les contrôles de formulaire demandait soit de poser un hook par composant, soit de retoucher `--size-components-2xs`, qui déborde (il alimente aussi `ui-tag`, `ui-select`, `ui-autocomplete`, `ui-datepicker`).

**16 constantes** exposées, nommées d'après leur variable SCSS (`$form-control-size` → `--ui-form-control-size`). Les composants les *interpolant*, la cascade se construit d'elle-même :

```css
var(--ui-checkbox-box-size, var(--ui-form-control-size, var(--size-components-2xs)))
/* ↑ composant ↑ catégorie ↑ token */
```

Une constante qui **référence une autre constante** (`$form-focus-ring-width: $focus-ring-width`) ne prend pas de nom propre : elle hérite de celui qu'elle vise, si bien que `--ui-focus-ring-width` couvre à lui seul les anneaux de focus du kit. Les quatre tailles d'avatar restent des nombres bruts (elles passent par `rem-calc()`), déjà atteignables par `--ui-avatar-size*`.
- **`ui-icon` : l'échelle et la surcharge d'exemplaire sont deux niveaux distincts.** `--ui-icon-size` était répété dans les cinq entrées de l'échelle ; il est désormais lu une seule fois, au-dessus d'une variable interne que chaque taille alimente. Comportement inchangé : `--ui-icon-size-{sm,md,default,lg,xl}` retouche un **pas de l'échelle** (donc toutes les icônes de cette taille), `--ui-icon-size` force **un exemplaire** quel que soit son `size` — c'est par là qu'un composant dimensionne l'icône qu'il projette.
- **`ui-checkbox` et `ui-select` exposent la taille de leur marqueur** (`--ui-checkbox-icon-size`, `--ui-select-checkbox-icon-size`). Rétrécir la case (`--ui-checkbox-box-size`) laissait la coche à la taille de l'échelle d'icônes, donc débordante. Le défaut de ces deux hooks pointe sur le pas d'échelle utilisé, si bien que retoucher l'échelle continue de traverser.
- **`--ui-card-padding` ne se relit plus sous ce nom : le miroir relisible est `--ui-card-gutter`.** Une custom property ne pouvant pas se référencer elle-même, le nom déclaré par la carte et le nom surchargeable devaient être distincts. `--ui-card-padding` devient le **point de surcharge** (gouttière de la carte), `--ui-card-gutter` la valeur à **relire** pour regouttiérer un contenu `contentFlush` : `padding-inline: var(--ui-card-gutter)`.
- **`--ui-sidebar-width`, `--ui-sidebar-rail-width`, `--ui-empty-state-media-size`, `--ui-empty-state-media-color`, `--ui-input-group-radius`, `--ui-input-group-overlap` et `--ui-skeleton-shine` sont désormais réellement surchargeables.** Ces sept custom properties étaient documentées comme des points de surcharge, mais le composant les **déclarait** sur son propre élément : une valeur posée par le consommateur sur `:root` était systématiquement écrasée (et, pour `--ui-skeleton-shine`, écrasée en mode sombre uniquement). Les déclarations passent sur un miroir privé, le nom public reste lu en amont. Mêmes valeurs par défaut.
- **`ui-tab-list` : `--ui-tabs-active-bar-color` / `--ui-tabs-active-bar-size` deviennent `--ui-tabs-indicator-color` / `--ui-tabs-indicator-thickness`** (le mot `active` se lisait comme un état alors que la convention met le modifieur en dernier). Les anciens noms restent honorés une version, en repli.
- **L'en-tête de traçabilité des fichiers copiés par `ng generate @4sh/ui-kit:add` porte la mention de licence** (`Apache-2.0 — Copyright 2026 4SH.`) en plus de son origine et de sa version. Une fois copié, le fichier vit dans le dépôt du consommateur, où plus rien n'indiquait sous quels termes il est fourni. Conséquence attendue : le premier `ng generate @4sh/ui-kit:update` suivant cette version affiche un diff d'une ligne en tête de chaque fichier installé.

## [0.2.0] - 2026-08-14
Expand Down
14 changes: 11 additions & 3 deletions docs/PUBLISHING.md
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,11 @@ Contenu et résolution des imports strictement identiques à une vraie
publication. Pour du développement en parallèle :
`npm install ../starter-angular/dist/ui-kit`.

Côté branchement dans le projet consommateur (feuille à charger, `data-theme` /
`data-brand`, surcharge des tokens et des variables de composant), tout est dans
[`projects/ui-kit/README.md`](../projects/ui-kit/README.md#theme-brand-and-overrides) —
c'est ce fichier que voit l'utilisateur sur npmjs.

Pour éprouver le **starter**, le compagnon suffit — il porte les sources *et* les
schematics, et le parcours n'installe pas le kit :

Expand All @@ -340,13 +345,16 @@ npm run schematics:pack # → 4sh-ui-kit-schematics-<version>.tgz

# dans le projet consommateur
npm install -D /chemin/vers/4sh-ui-kit-schematics-<version>.tgz
npx ng generate @4sh/ui-kit-schematics:ng-add --components ui-button ui-checkbox
npx ng generate @4sh/ui-kit-schematics:ng-add --all
```

On passe par `ng generate …:ng-add` plutôt que `ng add` : la commande publiée
`ng add @4sh/ui-kit-schematics` irait chercher le package sur le registre, pas le
tarball local. La règle exécutée est exactement la même. Et `--components` évite
le prompt interactif, ce qui rend l'essai scriptable — l'omettre le rétablit.
tarball local. La règle exécutée est exactement la même. Et `--all` évite le
prompt interactif, ce qui rend l'essai scriptable — l'omettre le rétablit, et
c'est là qu'on coche à la barre d'espace. Pour n'essayer que quelques
composants sans prompt : `ng generate @4sh/ui-kit-schematics:add --components
ui-button --components ui-checkbox`, l'option ne vivant plus que sur `add`.

Vérifications qui valent la peine, une fois la commande passée :

Expand Down
59 changes: 59 additions & 0 deletions figma/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# `component-vars.json`

The components' `--ui-*` variables, in the same DTCG shape as
`src/design-tokens/*.json` — so the plugin that imports your token files imports this
one too. Generated by `npm run docs:config`; `docs:config:check` fails if it is stale.

**These are not design tokens.** A token belongs to the system (`--units-sm`); a
component variable belongs to one component, and 439 of the 560 entries merely *alias* a
token. Hence a **dedicated collection** (`$extensions.com.4sh.ui-kit.figmaCollection`)
rather than adding them to `semantics` or `metrics`.

```json
"button": {
"height": {
"$value": "{responsive.size.components.default}",
"$type": "dimension",
"$description": "Hauteur de `ui-button`.",
"$extensions": {
"com.figma": {
"resolvedType": "FLOAT",
"scopes": ["WIDTH_HEIGHT"],
"codeSyntax": { "WEB": "var(--ui-button-height)" }
},
"com.4sh.ui-kit": { "cssVar": "--ui-button-height", "component": "ui-button" }
}
}
}
```

## Importing

- Write to the file that **owns** the collections — `targetFileKey`. In the UI Kit the
variables are `remote`, hence read-only (see `CLAUDE.md`).
- Keep the aliases as aliases: that is what preserves brand, light/dark and responsive.
- Literal values are in `px` (converted from `rem`, base 16), like the token files.
- `$extensions.com.4sh.ui-kit.skipped` lists 38 entries with nothing to create: `calc()`
expressions, multi-value CSS shorthands, relative units (`em`, `ch`, `%`), duration /
easing / cursor / z-index, and internal plumbing.

## Two flags worth reading

- `perInstance: true` — the variable has no single value (the component picks a different
default per rendered variant). Creating it is fine, but it does not represent *the*
value of the component.
- `descriptionSource: "derived"` — the description was composed from the variable name,
because the value lives in a SCSS map whose `///` comment covers all its entries. Worth
a read before publishing.

## `global/*` aliases

Three aliases point at the `global/*` family: `global.text.default`, `global.text.muted`,
`global.background.muted`. Their names are the ones in `src/design-tokens/` — **nothing
to change there**.

But an alias only resolves if the target variable carries exactly that name. If your
Figma file still uses the pre-rename names (`global/high/content/default`), those three
links resolve to nothing. Search for `global/text/default` in your Figma variables: if it
exists, ignore this; if not, run `docs/figma-migration-global.md` first — its step order
is constrained and step 3 is irreversible.
Loading
Loading