fold-ng 0.16.0 → 0.17.1

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,255 @@ All notable changes to **fold-ng** are documented here. The format follows
8
8
 
9
9
  _Nothing yet._
10
10
 
11
+ ## [0.17.1] - 2026-08-26
12
+
13
+ ### Fixed
14
+
15
+ - **`fold-data-table` : le mode cartes ne rendait RIEN avec l'API documentée.**
16
+ `narrowLayout="cards"` plus un `foldRowCard` projeté — la forme que la 0.13.0
17
+ a introduite et que le JSDoc recommande — laissait un espace blanc à la place
18
+ du tableau.
19
+
20
+ Le gabarit basculait bien : `@if (!cardMode())` retirait le `<table>` et
21
+ construisait la liste, cartes remplies du contenu du consommateur. La feuille
22
+ de style, elle, gardait une **seconde porte** — `.folddt--custom.folddt--narrow`
23
+ — dont les classes venaient de l'entrée **dépréciée** `mobileLayout`. Sur la
24
+ nouvelle API cette paire ne se posait jamais, et la liste restait
25
+ `display: none`. Le tableau était parti, les cartes étaient cachées.
26
+
27
+ `auto-cards` était atteint pour la même raison : `.folddt--cards` n'avait
28
+ aucune règle en face. Seul `mobileLayout="custom"`, le chemin déprécié,
29
+ fonctionnait.
30
+
31
+ La correction retire la porte plutôt que de la réparer : la liste n'est dans
32
+ l'arbre QUE en mode cartes, donc sa présence suffit. Ce fichier disait déjà
33
+ « two gates that had to agree were one gate too many » — et en gardait deux.
34
+ Les classes `folddt--cards` et `folddt--custom`, qui ne stylaient plus rien,
35
+ disparaissent avec.
36
+
37
+ Aucun cas ne l'avait vu : les huit cas « mobile layout » pilotaient tous
38
+ `mobileLayout`. Un cas pilote désormais `narrowLayout`, et un autre lit la
39
+ **feuille de style** — le DOM n'a jamais été le problème, et jsdom n'applique
40
+ pas les styles d'un composant, si bien qu'un `getComputedStyle` y passe au
41
+ vert sur une liste cachée.
42
+
43
+ ## [0.17.0] - 2026-08-26
44
+
45
+ ### Added
46
+
47
+ - **`fold-fieldset`** — le groupe NOMMÉ de contrôles : la paire
48
+ `<fieldset>`/`<legend>`, son habillage navigateur neutralisé et son rythme sur
49
+ l'échelle des tokens.
50
+
51
+ Il existe parce que la paire native est inutilisable telle quelle : chaque
52
+ appelant réécrit les six mêmes lignes pour la défaire (`margin: 0; padding: 0;
53
+ border: 0`, puis flex + gap, puis `legend { padding: 0 }`). Ce reset avait été
54
+ écrit **cinq fois, au caractère près**, dans deux applications. Une répétition
55
+ aussi exacte n'est pas une coïncidence, c'est un composant manquant.
56
+
57
+ Ce n'est ni une carte ni une section : `fold-card` est une surface,
58
+ `fold-page-section` un chapitre de page ; ceci est le **groupement
59
+ accessible** des contrôles qui répondent à une seule question. C'est le
60
+ `<legend>` qui nomme le groupe à un lecteur d'écran — et c'est toute la raison
61
+ de prendre un `fieldset` plutôt qu'un `div` : un titre au-dessus d'un `div`
62
+ aurait exactement la même allure et n'annoncerait rien.
63
+
64
+ **Ce que l'élément natif achète, et que le composant garde** :
65
+
66
+ - `disabled` — le **super-pouvoir** du `fieldset`, que rien d'autre en HTML
67
+ n'a : il désactive TOUS les contrôles qu'il contient, en un attribut, sans
68
+ câblage par contrôle. (La première `<legend>` en est exemptée par la spec.)
69
+ - `hint` — l'instruction du groupe (« au moins un jour »), pointée par
70
+ `aria-describedby`. Un indice seulement peint est un indice que la moitié
71
+ des lecteurs n'aura jamais. `hintPosition` la place `under` (défaut, sa
72
+ propre ligne : de la place pour une phrase) ou `inline` (sur la ligne de la
73
+ légende, pour l'incise courte qui qualifie le nom au lieu d'instruire).
74
+ L'annonce ne dépend pas de la position — seul l'œil change de chemin. Sans
75
+ légende, `inline` retombe sur `under` : il n'y a pas de ligne où se poser.
76
+ - `ariaLabel` — nomme un groupe qui ne doit pas afficher de légende visible.
77
+ Ignoré si `legend` est renseigné : deux noms pour un groupe, c'est ainsi
78
+ qu'ils divergent.
79
+ - `legend` vide **et** `ariaLabel` vide = groupe volontairement sans nom, et
80
+ sans `aria-label` vide non plus — un groupe qui réclame un nom et n'en donne
81
+ aucun est pire qu'un groupe muet. C'est le cas imbriqué, déjà nommé par son
82
+ parent.
83
+
84
+ `legendVariant` porte les **deux registres** que les formulaires réels
85
+ emploient — les mêmes mots que le `titleVariant` d'un `fold-page-section`, et
86
+ délibérément : on rencontre UN vocabulaire pour « petit et au-dessus » contre
87
+ « se lit comme de la prose », pas un par composant.
88
+
89
+ - `eyebrow` (défaut) — petit, capitales, tracé, atténué : le groupe est une
90
+ **partie d'un formulaire**, son nom se pose au-dessus sans disputer
91
+ l'attention aux contrôles.
92
+ - `heading` — au poids du libellé d'un champ : le groupe **est** une seule
93
+ chose aux yeux du formulaire (« Heures de retrait »), son nom se lit donc
94
+ comme les libellés qui l'entourent.
95
+
96
+ Ce n'est pas un réglage cosmétique : un eyebrow au-dessus d'un groupe qui est
97
+ en réalité UN champ fait paraître au formulaire plus de sections qu'il n'a de
98
+ questions.
99
+
100
+ `optional` marque le groupe ENTIER — même parenthèse, même mot et même
101
+ fournisseur (`FOLD_COMMON_LABELS`) que le marqueur d'un libellé de champ. Il
102
+ existe parce qu'un groupe peut être facultatif quand aucun de ses membres ne
103
+ l'est : un point GPS, ce sont deux champs qu'on donne ensemble ou pas du
104
+ tout, et écrire « optionnel » sur chacun dirait autre chose — que l'un
105
+ pourrait manquer sans l'autre.
106
+
107
+ Plus `direction` (`vertical` par défaut, `horizontal`) et `appearance`
108
+ (`plain` par défaut, `border` pour le groupe encadré qui doit se distinguer de
109
+ ses pairs d'un coup d'œil). `--fold-fieldset-gap` thème l'écart entre membres. Il vaut par défaut le
110
+ **rythme d'un formulaire** (`--fold-space-md`) : un champ, c'est un libellé,
111
+ une boîte et parfois un indice — il lui faut l'air que trois cases à cocher
112
+ n'exigent pas. Un groupe qu'on n'a pas réglé empile des champs ; les groupes
113
+ compacts (cases, lignes horaires, boutons radio) resserrent.
114
+
115
+ ⚠️ Une finesse que les cinq versions manuscrites avaient toutes redécouverte à
116
+ leurs dépens : **la légende n'est pas un élément flex**. La boîte flex d'un
117
+ `fieldset` est sa boîte de contenu anonyme, et la légende rendue vit en
118
+ dehors — le `gap` ne l'atteint donc jamais, et son espace en dessous doit être
119
+ sa propre marge.
120
+
121
+ ⚠️ Deuxième finesse, trouvée en écrivant le test : **`input.disabled` ne dit
122
+ pas la vérité** dans un `fieldset` désactivé. La propriété IDL ne reflète que
123
+ l'attribut PROPRE du contrôle et reste `false` ; seule la pseudo-classe
124
+ `:disabled` connaît l'ancêtre. Un test écrit sur la propriété serait passé au
125
+ vert sur un composant qui ne désactivait plus rien.
126
+
127
+ - **`--fold-font-label`** — la face du registre **micro-libellé** : les libellés
128
+ 2xs / gras / capitales / tracés qui titrent une section, coiffent une colonne
129
+ de tableau ou servent d'eyebrow. Elle vaut `inherit` par défaut, donc rien ne
130
+ change tant qu'un hôte ne la nomme pas — la règle « un composant porte la face
131
+ de son hôte » tient toujours. Elle existe parce que c'est précisément le rôle
132
+ où un hôte veut souvent une AUTRE face que son texte courant (un libellé
133
+ monospace se lit comme une parole du système, pas comme de la prose), et que
134
+ le dire sans elle obligeait à entrer dans les entrailles de trois composants —
135
+ avec la dérive garantie le jour où un quatrième rejoint le registre.
136
+
137
+ - **`fold-page-section` gagne `collapsible` + `[(open)]`** — replier le CORPS
138
+ d'une section, et rien d'autre. Le titre, son sous-titre, sa description et
139
+ ses `[sectionActions]` restent en place, et c'est toute la différence entre
140
+ replier et **cacher** : un onglet cachait l'ÉTAT avec les champs, donc on ne
141
+ pouvait pas savoir ce qui manquait sans tout ouvrir. Replié, la section dit
142
+ encore ce qu'elle est et ce qu'elle fait — et ses actions restent cliquables,
143
+ donc elle s'enregistre sans se déplier.
144
+
145
+ Deux conséquences de cette règle, toutes deux voulues : le bouton est le
146
+ TITRE et non l'en-tête (un bouton autour de l'en-tête aurait imbriqué
147
+ « Enregistrer » dans un `<button>` — inerte, et invalide), et l'état par
148
+ défaut est **ouvert**, parce qu'une section qui démarre repliée est une
149
+ section qu'il faut découvrir.
150
+
151
+ - **`fold-page-section` gagne `[sectionSubtitle]`, `titleVariant` et
152
+ `separator`** — les pendants exacts de ce que `fold-page-layout` a reçu, pour
153
+ la même raison : une section d'écran dense est une petite page.
154
+
155
+ - **`fold-page-layout` sépare la PORTÉE de l'en-tête de son SOL** :
156
+ `headerBleed` (aller bord à bord) et `headerBand` (peindre la bande) sont deux
157
+ entrées, plus une seule. `headerBand` faisait les deux, donc un simple filet
158
+ pleine largeur sous l'en-tête — traitement courant et discret — obligeait à
159
+ peindre une bande. On ne pouvait pas demander la portée sans l'encre.
160
+
161
+ - **Les bandes montent d'un cran : `fold-page-layout[headerBand]` et
162
+ `fold-aside-layout[band]`.** Une en-tête de page et un rail collant peuvent
163
+ enfin se poser sur `--fold-color-surface-band` — le rôle qui existait déjà et
164
+ que `fold-card` consomme par `raisedBands` : « un pas À L'ÉCART de son
165
+ conteneur, dans le sens que la polarité du thème impose ». Clair il fonce,
166
+ sombre il éclaircit ; le composant nomme le rôle et n'a jamais à savoir dans
167
+ quel sens.
168
+
169
+ **Ce n'est pas une `surface`, et c'est le point.** L'axe `surface` est une
170
+ IDENTITÉ (`chrome`, `accent`) et il repointe l'encre sans jamais peindre de
171
+ fond (`docs/surfaces.md`) ; « l'en-tête est du mobilier, pas du contenu » n'est
172
+ ni l'un ni l'autre — c'est une élévation. Un `surface="raised"` aurait mélangé
173
+ les deux axes et aurait menti sur ce qu'il fait.
174
+
175
+ Deux détails qui ne sont pas de la décoration : la bande d'en-tête **annule
176
+ exactement** ce que la page marge (les deux mêmes tokens, moitié à l'étroit),
177
+ puis rembourse ce padding à l'intérieur — la colonne de texte ne bouge pas
178
+ quand on l'allume. Et un rail bandé **ferme la gouttière de colonne** au profit
179
+ d'un filet : un fond tenu à 28px du contenu qu'il accompagne ne se lit pas
180
+ comme une bande mais comme une carte flottante. L'espace passe à l'intérieur,
181
+ donc le contenu se lit à la même largeur dans les deux cas.
182
+
183
+ - **`fold-aside-layout[bleed]`** — sortir de la gouttière de page et atteindre
184
+ le bord, même mécanisme et même variable que `fold-page-section[bleed]`. C'est
185
+ le compagnon de `band` : un rail bandé tenu à distance du bord par une
186
+ gouttière est exactement la carte flottante que la bande sert à remplacer.
187
+
188
+ - **`fold-view-toggle` : un segment peut porter un point d'état** (`dot` +
189
+ `dotLabel` sur une option). Un point dit « regarde ici », jamais _quoi_ — il
190
+ est donc `aria-hidden`, et son sens rejoint le **nom accessible** du segment
191
+ au lieu de disparaître. Sans ça, un lecteur d'écran entendait « EN » là où un
192
+ œil voyait « EN, il manque quelque chose ».
193
+
194
+ ### Changed
195
+
196
+ - **BREAKING (visuel) — le segment choisi d'un `fold-view-toggle` est PLEIN**
197
+ (`activeStyle` passe de `raised` à `solid`, et gagne au passage la valeur
198
+ `accent`). Même raison que le bouton solide : un contrôle segmenté existe pour
199
+ montrer **lequel est choisi**, et le choisi doit être la chose la plus forte
200
+ du contrôle.
201
+
202
+ Trois registres, parce qu'aucun ne convient partout : `solid` (rempli),
203
+ `accent` (teinté — présent, plus discret, et il garde l'encre du segment
204
+ lisible au milieu de beaucoup de couleur), `raised` (puce neutre). La puce
205
+ confiait la distinction à une élévation qui disparaît entièrement sous
206
+ `forced-colors`. Rien ne verrouillait ce défaut non plus.
207
+
208
+ Un détail que le plein impose : sur un fond d'accent, un point d'état peint
209
+ dans sa propre teinte peut tomber à un cheveu du fond — un point ambre sur un
210
+ accent chaud s'efface. Sur `solid`, le point prend donc l'encre du segment,
211
+ la seule garantie d'y être lisible.
212
+
213
+ - **BREAKING (visuel) — un `foldButton` est SOLIDE par défaut** (`emphasis`
214
+ passe de `soft` à `solid` ; `intent` reste `primary`). Le défaut, c'est ce
215
+ qu'une app écrit quand elle n'écrit rien — et ce qu'elle écrit le plus, c'est
216
+ **l'action principale** de l'écran, celle qu'elle veut qu'on presse. Avec un
217
+ défaut teinté, le bouton le plus fort d'une page était celui que quelqu'un
218
+ avait pensé à baliser : l'emphase suivait l'effort de rédaction plutôt que
219
+ l'importance. `soft` et `outline` se demandent maintenant exprès, pour les
220
+ actions qui accompagnent celle-là.
221
+
222
+ Rien ne verrouillait ce défaut : les vingt tests du bouton passaient à
223
+ l'identique avant et après le basculement. Un test le tient désormais.
224
+
225
+ - **BREAKING (visuel) — un titre de `fold-page-section` porte le registre
226
+ MICRO-LIBELLÉ par défaut** : 2xs, gras, capitales, tracé, dans
227
+ `--fold-font-label`. Un titre de section est un **libellé du bloc qui le
228
+ suit** ; à l'échelle d'une page, une pile de titres en taille de corps entre en
229
+ concurrence avec le contenu même qu'elle est censée étiqueter. C'est le
230
+ registre que portent déjà une en-tête de colonne de tableau et un eyebrow de
231
+ `fold-element-title`, donc les trois s'accordent.
232
+
233
+ C'est une **peau**, jamais une sémantique : le titre reste le même `h2`, avec
234
+ le même `aria-level` et le même nom de région dans les deux registres — un
235
+ test le vérifie explicitement, parce qu'un titre qui cesserait d'être un titre
236
+ pour ressembler à un libellé coûterait son plan à la page sans que rien ne le
237
+ dise.
238
+
239
+ Pour retrouver l'ancien rendu : `titleVariant="heading"`.
240
+
241
+ ### Fixed
242
+
243
+ - **Un `bleed` annule désormais la gouttière que la page PAYE, pas celle qu'on
244
+ lui a demandée.** Sous 640px, `fold-page-layout` réduit son inset de moitié —
245
+ et `fold-page-section[bleed]`, épinglé au token écrit, continuait d'annuler la
246
+ valeur entière : la section débordait d'une demi-gouttière de chaque côté,
247
+ précisément là où il y avait le moins de place. La page publie maintenant
248
+ `--fold-page-gutter-effective` et tout ce qui annule lit celle-là. Le bug était
249
+ invisible partout : aucune requête média ne s'évalue en test unitaire, et le
250
+ symptôme est un débordement horizontal. Un test de contrat sur les sources
251
+ tient l'invariant.
252
+
253
+ - **`fold-page-layout` : les actions s'alignent sur la rangée du TITRE.** Elles
254
+ se calaient en haut de la colonne de texte ; depuis que `[pageEyebrow]`
255
+ existe, cette colonne commence par le fil d'Ariane — et les actions
256
+ remontaient se coller à lui. L'eyebrow sort donc de la colonne et coiffe
257
+ l'en-tête entier : c'est sa place logique (il désigne la page, pas le titre) et
258
+ la rangée titre + actions redevient une vraie rangée.
259
+
11
260
  ## [0.16.0] - 2026-08-24
12
261
 
13
262
  ### Added
@@ -1955,7 +2204,9 @@ design-token stylesheet.
1955
2204
  `currentColor`; `prefers-reduced-motion` + `forced-colors` are respected;
1956
2205
  strings localise via inputs / providers (`provideFoldPanelLabels`).
1957
2206
 
1958
- [unreleased]: https://github.com/hugoheynard/fold-ng/compare/v0.16.0...HEAD
2207
+ [unreleased]: https://github.com/hugoheynard/fold-ng/compare/v0.17.1...HEAD
2208
+ [0.17.1]: https://github.com/hugoheynard/fold-ng/releases/tag/v0.17.1
2209
+ [0.17.0]: https://github.com/hugoheynard/fold-ng/releases/tag/v0.17.0
1959
2210
  [0.16.0]: https://github.com/hugoheynard/fold-ng/releases/tag/v0.16.0
1960
2211
  [0.15.0]: https://github.com/hugoheynard/fold-ng/releases/tag/v0.15.0
1961
2212
  [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. |