fold-ng 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,223 @@ All notable changes to **fold-ng** are documented here. The format follows
8
8
 
9
9
  _Nothing yet._
10
10
 
11
+ ## [0.17.0] - 2026-08-26
12
+
13
+ ### Added
14
+
15
+ - **`fold-fieldset`** — le groupe NOMMÉ de contrôles : la paire
16
+ `<fieldset>`/`<legend>`, son habillage navigateur neutralisé et son rythme sur
17
+ l'échelle des tokens.
18
+
19
+ Il existe parce que la paire native est inutilisable telle quelle : chaque
20
+ appelant réécrit les six mêmes lignes pour la défaire (`margin: 0; padding: 0;
21
+ border: 0`, puis flex + gap, puis `legend { padding: 0 }`). Ce reset avait été
22
+ écrit **cinq fois, au caractère près**, dans deux applications. Une répétition
23
+ aussi exacte n'est pas une coïncidence, c'est un composant manquant.
24
+
25
+ Ce n'est ni une carte ni une section : `fold-card` est une surface,
26
+ `fold-page-section` un chapitre de page ; ceci est le **groupement
27
+ accessible** des contrôles qui répondent à une seule question. C'est le
28
+ `<legend>` qui nomme le groupe à un lecteur d'écran — et c'est toute la raison
29
+ de prendre un `fieldset` plutôt qu'un `div` : un titre au-dessus d'un `div`
30
+ aurait exactement la même allure et n'annoncerait rien.
31
+
32
+ **Ce que l'élément natif achète, et que le composant garde** :
33
+
34
+ - `disabled` — le **super-pouvoir** du `fieldset`, que rien d'autre en HTML
35
+ n'a : il désactive TOUS les contrôles qu'il contient, en un attribut, sans
36
+ câblage par contrôle. (La première `<legend>` en est exemptée par la spec.)
37
+ - `hint` — l'instruction du groupe (« au moins un jour »), pointée par
38
+ `aria-describedby`. Un indice seulement peint est un indice que la moitié
39
+ des lecteurs n'aura jamais. `hintPosition` la place `under` (défaut, sa
40
+ propre ligne : de la place pour une phrase) ou `inline` (sur la ligne de la
41
+ légende, pour l'incise courte qui qualifie le nom au lieu d'instruire).
42
+ L'annonce ne dépend pas de la position — seul l'œil change de chemin. Sans
43
+ légende, `inline` retombe sur `under` : il n'y a pas de ligne où se poser.
44
+ - `ariaLabel` — nomme un groupe qui ne doit pas afficher de légende visible.
45
+ Ignoré si `legend` est renseigné : deux noms pour un groupe, c'est ainsi
46
+ qu'ils divergent.
47
+ - `legend` vide **et** `ariaLabel` vide = groupe volontairement sans nom, et
48
+ sans `aria-label` vide non plus — un groupe qui réclame un nom et n'en donne
49
+ aucun est pire qu'un groupe muet. C'est le cas imbriqué, déjà nommé par son
50
+ parent.
51
+
52
+ `legendVariant` porte les **deux registres** que les formulaires réels
53
+ emploient — les mêmes mots que le `titleVariant` d'un `fold-page-section`, et
54
+ délibérément : on rencontre UN vocabulaire pour « petit et au-dessus » contre
55
+ « se lit comme de la prose », pas un par composant.
56
+
57
+ - `eyebrow` (défaut) — petit, capitales, tracé, atténué : le groupe est une
58
+ **partie d'un formulaire**, son nom se pose au-dessus sans disputer
59
+ l'attention aux contrôles.
60
+ - `heading` — au poids du libellé d'un champ : le groupe **est** une seule
61
+ chose aux yeux du formulaire (« Heures de retrait »), son nom se lit donc
62
+ comme les libellés qui l'entourent.
63
+
64
+ Ce n'est pas un réglage cosmétique : un eyebrow au-dessus d'un groupe qui est
65
+ en réalité UN champ fait paraître au formulaire plus de sections qu'il n'a de
66
+ questions.
67
+
68
+ `optional` marque le groupe ENTIER — même parenthèse, même mot et même
69
+ fournisseur (`FOLD_COMMON_LABELS`) que le marqueur d'un libellé de champ. Il
70
+ existe parce qu'un groupe peut être facultatif quand aucun de ses membres ne
71
+ l'est : un point GPS, ce sont deux champs qu'on donne ensemble ou pas du
72
+ tout, et écrire « optionnel » sur chacun dirait autre chose — que l'un
73
+ pourrait manquer sans l'autre.
74
+
75
+ Plus `direction` (`vertical` par défaut, `horizontal`) et `appearance`
76
+ (`plain` par défaut, `border` pour le groupe encadré qui doit se distinguer de
77
+ ses pairs d'un coup d'œil). `--fold-fieldset-gap` thème l'écart entre membres. Il vaut par défaut le
78
+ **rythme d'un formulaire** (`--fold-space-md`) : un champ, c'est un libellé,
79
+ une boîte et parfois un indice — il lui faut l'air que trois cases à cocher
80
+ n'exigent pas. Un groupe qu'on n'a pas réglé empile des champs ; les groupes
81
+ compacts (cases, lignes horaires, boutons radio) resserrent.
82
+
83
+ ⚠️ Une finesse que les cinq versions manuscrites avaient toutes redécouverte à
84
+ leurs dépens : **la légende n'est pas un élément flex**. La boîte flex d'un
85
+ `fieldset` est sa boîte de contenu anonyme, et la légende rendue vit en
86
+ dehors — le `gap` ne l'atteint donc jamais, et son espace en dessous doit être
87
+ sa propre marge.
88
+
89
+ ⚠️ Deuxième finesse, trouvée en écrivant le test : **`input.disabled` ne dit
90
+ pas la vérité** dans un `fieldset` désactivé. La propriété IDL ne reflète que
91
+ l'attribut PROPRE du contrôle et reste `false` ; seule la pseudo-classe
92
+ `:disabled` connaît l'ancêtre. Un test écrit sur la propriété serait passé au
93
+ vert sur un composant qui ne désactivait plus rien.
94
+
95
+ - **`--fold-font-label`** — la face du registre **micro-libellé** : les libellés
96
+ 2xs / gras / capitales / tracés qui titrent une section, coiffent une colonne
97
+ de tableau ou servent d'eyebrow. Elle vaut `inherit` par défaut, donc rien ne
98
+ change tant qu'un hôte ne la nomme pas — la règle « un composant porte la face
99
+ de son hôte » tient toujours. Elle existe parce que c'est précisément le rôle
100
+ où un hôte veut souvent une AUTRE face que son texte courant (un libellé
101
+ monospace se lit comme une parole du système, pas comme de la prose), et que
102
+ le dire sans elle obligeait à entrer dans les entrailles de trois composants —
103
+ avec la dérive garantie le jour où un quatrième rejoint le registre.
104
+
105
+ - **`fold-page-section` gagne `collapsible` + `[(open)]`** — replier le CORPS
106
+ d'une section, et rien d'autre. Le titre, son sous-titre, sa description et
107
+ ses `[sectionActions]` restent en place, et c'est toute la différence entre
108
+ replier et **cacher** : un onglet cachait l'ÉTAT avec les champs, donc on ne
109
+ pouvait pas savoir ce qui manquait sans tout ouvrir. Replié, la section dit
110
+ encore ce qu'elle est et ce qu'elle fait — et ses actions restent cliquables,
111
+ donc elle s'enregistre sans se déplier.
112
+
113
+ Deux conséquences de cette règle, toutes deux voulues : le bouton est le
114
+ TITRE et non l'en-tête (un bouton autour de l'en-tête aurait imbriqué
115
+ « Enregistrer » dans un `<button>` — inerte, et invalide), et l'état par
116
+ défaut est **ouvert**, parce qu'une section qui démarre repliée est une
117
+ section qu'il faut découvrir.
118
+
119
+ - **`fold-page-section` gagne `[sectionSubtitle]`, `titleVariant` et
120
+ `separator`** — les pendants exacts de ce que `fold-page-layout` a reçu, pour
121
+ la même raison : une section d'écran dense est une petite page.
122
+
123
+ - **`fold-page-layout` sépare la PORTÉE de l'en-tête de son SOL** :
124
+ `headerBleed` (aller bord à bord) et `headerBand` (peindre la bande) sont deux
125
+ entrées, plus une seule. `headerBand` faisait les deux, donc un simple filet
126
+ pleine largeur sous l'en-tête — traitement courant et discret — obligeait à
127
+ peindre une bande. On ne pouvait pas demander la portée sans l'encre.
128
+
129
+ - **Les bandes montent d'un cran : `fold-page-layout[headerBand]` et
130
+ `fold-aside-layout[band]`.** Une en-tête de page et un rail collant peuvent
131
+ enfin se poser sur `--fold-color-surface-band` — le rôle qui existait déjà et
132
+ que `fold-card` consomme par `raisedBands` : « un pas À L'ÉCART de son
133
+ conteneur, dans le sens que la polarité du thème impose ». Clair il fonce,
134
+ sombre il éclaircit ; le composant nomme le rôle et n'a jamais à savoir dans
135
+ quel sens.
136
+
137
+ **Ce n'est pas une `surface`, et c'est le point.** L'axe `surface` est une
138
+ IDENTITÉ (`chrome`, `accent`) et il repointe l'encre sans jamais peindre de
139
+ fond (`docs/surfaces.md`) ; « l'en-tête est du mobilier, pas du contenu » n'est
140
+ ni l'un ni l'autre — c'est une élévation. Un `surface="raised"` aurait mélangé
141
+ les deux axes et aurait menti sur ce qu'il fait.
142
+
143
+ Deux détails qui ne sont pas de la décoration : la bande d'en-tête **annule
144
+ exactement** ce que la page marge (les deux mêmes tokens, moitié à l'étroit),
145
+ puis rembourse ce padding à l'intérieur — la colonne de texte ne bouge pas
146
+ quand on l'allume. Et un rail bandé **ferme la gouttière de colonne** au profit
147
+ d'un filet : un fond tenu à 28px du contenu qu'il accompagne ne se lit pas
148
+ comme une bande mais comme une carte flottante. L'espace passe à l'intérieur,
149
+ donc le contenu se lit à la même largeur dans les deux cas.
150
+
151
+ - **`fold-aside-layout[bleed]`** — sortir de la gouttière de page et atteindre
152
+ le bord, même mécanisme et même variable que `fold-page-section[bleed]`. C'est
153
+ le compagnon de `band` : un rail bandé tenu à distance du bord par une
154
+ gouttière est exactement la carte flottante que la bande sert à remplacer.
155
+
156
+ - **`fold-view-toggle` : un segment peut porter un point d'état** (`dot` +
157
+ `dotLabel` sur une option). Un point dit « regarde ici », jamais _quoi_ — il
158
+ est donc `aria-hidden`, et son sens rejoint le **nom accessible** du segment
159
+ au lieu de disparaître. Sans ça, un lecteur d'écran entendait « EN » là où un
160
+ œil voyait « EN, il manque quelque chose ».
161
+
162
+ ### Changed
163
+
164
+ - **BREAKING (visuel) — le segment choisi d'un `fold-view-toggle` est PLEIN**
165
+ (`activeStyle` passe de `raised` à `solid`, et gagne au passage la valeur
166
+ `accent`). Même raison que le bouton solide : un contrôle segmenté existe pour
167
+ montrer **lequel est choisi**, et le choisi doit être la chose la plus forte
168
+ du contrôle.
169
+
170
+ Trois registres, parce qu'aucun ne convient partout : `solid` (rempli),
171
+ `accent` (teinté — présent, plus discret, et il garde l'encre du segment
172
+ lisible au milieu de beaucoup de couleur), `raised` (puce neutre). La puce
173
+ confiait la distinction à une élévation qui disparaît entièrement sous
174
+ `forced-colors`. Rien ne verrouillait ce défaut non plus.
175
+
176
+ Un détail que le plein impose : sur un fond d'accent, un point d'état peint
177
+ dans sa propre teinte peut tomber à un cheveu du fond — un point ambre sur un
178
+ accent chaud s'efface. Sur `solid`, le point prend donc l'encre du segment,
179
+ la seule garantie d'y être lisible.
180
+
181
+ - **BREAKING (visuel) — un `foldButton` est SOLIDE par défaut** (`emphasis`
182
+ passe de `soft` à `solid` ; `intent` reste `primary`). Le défaut, c'est ce
183
+ qu'une app écrit quand elle n'écrit rien — et ce qu'elle écrit le plus, c'est
184
+ **l'action principale** de l'écran, celle qu'elle veut qu'on presse. Avec un
185
+ défaut teinté, le bouton le plus fort d'une page était celui que quelqu'un
186
+ avait pensé à baliser : l'emphase suivait l'effort de rédaction plutôt que
187
+ l'importance. `soft` et `outline` se demandent maintenant exprès, pour les
188
+ actions qui accompagnent celle-là.
189
+
190
+ Rien ne verrouillait ce défaut : les vingt tests du bouton passaient à
191
+ l'identique avant et après le basculement. Un test le tient désormais.
192
+
193
+ - **BREAKING (visuel) — un titre de `fold-page-section` porte le registre
194
+ MICRO-LIBELLÉ par défaut** : 2xs, gras, capitales, tracé, dans
195
+ `--fold-font-label`. Un titre de section est un **libellé du bloc qui le
196
+ suit** ; à l'échelle d'une page, une pile de titres en taille de corps entre en
197
+ concurrence avec le contenu même qu'elle est censée étiqueter. C'est le
198
+ registre que portent déjà une en-tête de colonne de tableau et un eyebrow de
199
+ `fold-element-title`, donc les trois s'accordent.
200
+
201
+ C'est une **peau**, jamais une sémantique : le titre reste le même `h2`, avec
202
+ le même `aria-level` et le même nom de région dans les deux registres — un
203
+ test le vérifie explicitement, parce qu'un titre qui cesserait d'être un titre
204
+ pour ressembler à un libellé coûterait son plan à la page sans que rien ne le
205
+ dise.
206
+
207
+ Pour retrouver l'ancien rendu : `titleVariant="heading"`.
208
+
209
+ ### Fixed
210
+
211
+ - **Un `bleed` annule désormais la gouttière que la page PAYE, pas celle qu'on
212
+ lui a demandée.** Sous 640px, `fold-page-layout` réduit son inset de moitié —
213
+ et `fold-page-section[bleed]`, épinglé au token écrit, continuait d'annuler la
214
+ valeur entière : la section débordait d'une demi-gouttière de chaque côté,
215
+ précisément là où il y avait le moins de place. La page publie maintenant
216
+ `--fold-page-gutter-effective` et tout ce qui annule lit celle-là. Le bug était
217
+ invisible partout : aucune requête média ne s'évalue en test unitaire, et le
218
+ symptôme est un débordement horizontal. Un test de contrat sur les sources
219
+ tient l'invariant.
220
+
221
+ - **`fold-page-layout` : les actions s'alignent sur la rangée du TITRE.** Elles
222
+ se calaient en haut de la colonne de texte ; depuis que `[pageEyebrow]`
223
+ existe, cette colonne commence par le fil d'Ariane — et les actions
224
+ remontaient se coller à lui. L'eyebrow sort donc de la colonne et coiffe
225
+ l'en-tête entier : c'est sa place logique (il désigne la page, pas le titre) et
226
+ la rangée titre + actions redevient une vraie rangée.
227
+
11
228
  ## [0.16.0] - 2026-08-24
12
229
 
13
230
  ### Added
@@ -1955,7 +2172,8 @@ design-token stylesheet.
1955
2172
  `currentColor`; `prefers-reduced-motion` + `forced-colors` are respected;
1956
2173
  strings localise via inputs / providers (`provideFoldPanelLabels`).
1957
2174
 
1958
- [unreleased]: https://github.com/hugoheynard/fold-ng/compare/v0.16.0...HEAD
2175
+ [unreleased]: https://github.com/hugoheynard/fold-ng/compare/v0.17.0...HEAD
2176
+ [0.17.0]: https://github.com/hugoheynard/fold-ng/releases/tag/v0.17.0
1959
2177
  [0.16.0]: https://github.com/hugoheynard/fold-ng/releases/tag/v0.16.0
1960
2178
  [0.15.0]: https://github.com/hugoheynard/fold-ng/releases/tag/v0.15.0
1961
2179
  [0.14.0]: https://github.com/hugoheynard/fold-ng/releases/tag/v0.14.0
package/README.md CHANGED
@@ -350,6 +350,7 @@ ships it.
350
350
  | `FoldDateComponent` | `fold-date` | Native calendar-date wrapper (`type` = date · datetime-local · month · week) — keeps the OS picker, hands back a typed `[(value)]` string (`YYYY-MM-DD`). `min`/`max`/`step` pass through. Time-of-day → `fold-time`. |
351
351
  | `FoldTimeComponent` | `fold-time` | Native time-of-day wrapper (`<input type="time">`) — typed `[(value)]` string (`HH:mm`), `min`/`max`/`step`. The sibling of `fold-date`. |
352
352
  | `FoldCheckboxComponent` | `fold-checkbox` | Boolean control — a native `<input type="checkbox">` (keyboard, `indeterminate`, forms) restyled to tokens. Signal Forms (`[formField]`, a `FormCheckboxControl`) or standalone `[(checked)]`; `indeterminate`, `label`/`ariaLabel`, `hint`/`errors`, `size`. |
353
+ | `FoldFieldsetComponent` | `fold-fieldset` | Named group of controls — a real `<fieldset>`/`<legend>` with the browser's margin/padding/3D border undone and the rhythm on tokens. `disabled` disables every control inside (the element's unique power); `hint` wired through `aria-describedby` (`hintPosition` `under`/`inline`); `legend` (empty renders none) or `ariaLabel`; `optional` marks the whole group (not each member); `legendVariant` (`eyebrow`/`heading` — the same two registers as a page-section's title), `direction` (`vertical`/`horizontal`), `appearance` (`plain`/`border`); `--fold-fieldset-gap`. |
353
354
  | `FoldPasswordFieldComponent` | `fold-password-field` | Password input + a live requirements checklist (a dot/tick per rule). Rules injected via `FoldPasswordRule` (`{ label, test }` — regex/zod/anything); `revealable` eye (a `fold-input` capability); `marker` dot/check; `[rules]` slot to redesign the list; `validChange`; Signal Forms. |
354
355
  | `FoldViewToggleComponent` | `fold-view-toggle` | Segmented single-select (Cards/Table, density, chart-mode…). Generic `options` (`{ value, icon?, label?, ariaLabel?, disabled? }`) + `[(value)]`; a real `role="radiogroup"` — roving tabindex, arrow keys, Home/End, disabled-skip; `size`, `iconOnly`, `activeStyle` (raised / accent). |
355
356
  | `FoldSearchComponent` | `fold-search` | Debounced search box — an `fold-input` that emits `searchChange` once typing settles (`delayMs`), trimmed + de-duplicated. |