origam 2.18.5 → 2.18.7

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
@@ -18,6 +18,282 @@ This project follows [Semantic Versioning](https://semver.org).
18
18
 
19
19
  ## [Unreleased]
20
20
 
21
+ ### Fixed — #607 / #569 : 19 canaux de thème confisqués par le bloc du composant
22
+
23
+ Un composant qui redéclare un token que les feuilles déclarent déjà rend ce
24
+ canal **inatteignable par tout `IOrigamTheme` de marque**. Une custom property
25
+ est substituée sur l'élément **qui la déclare**, et l'héritage depuis
26
+ `[data-theme="brand-x"]` perd contre n'importe quelle déclaration directe — la
27
+ spécificité n'entre même pas en jeu.
28
+
29
+ **Le motif dominant, mesuré, n'est pas celui que les tickets décrivaient.** Sur
30
+ les 503 déclarations scopées visant un token de feuille, **434 sont portées par
31
+ un modificateur que le consommateur doit demander** (`&--rounded-large`,
32
+ `&--density-compact`) : `useRounded` / `useDensity` n'émettent rien quand la
33
+ prop est absente, donc ces règles ne confisquent rien — c'est la logique
34
+ PROPS-FIRST du DS en train de fonctionner. La vraie cause commune est plus
35
+ étroite : **un modificateur dont la valeur est aussi le défaut de la prop dans
36
+ `withDefaults`** (`&--size-default` sur `size: SIZES.DEFAULT`,
37
+ `&--density-default` sur `density: DENSITY.DEFAULT`). La classe est alors posée
38
+ sur **chaque** instance, et la règle épingle le token à vie.
39
+
40
+ Les 19 déclarations retirées résolvent toutes **exactement** la valeur de la
41
+ feuille, chaîne de `var()` déroulée — `12px` = `var(--origam-space---3)`,
42
+ `0.875rem` = `var(--origam-font__size---md)`, `48px` = `var(--origam-space---12)`.
43
+ Le rendu par défaut est donc inchangé ; seul le canal redevient atteignable.
44
+ (3 seulement sont littéralement identiques : une comparaison de chaînes ne voit
45
+ rien, d'où le résolveur.)
46
+
47
+ - `OrigamAlert`, `OrigamBtn`, `OrigamList`, `OrigamTable`, `OrigamTabs`,
48
+ `OrigamToolbar` — `&--density-default`
49
+ - `OrigamBtn`, `OrigamStepper` — `&--size-default`
50
+ - `OrigamKbd` — `&--variant-outlined` (`border-color`)
51
+ - `OrigamChip` — second bloc `<style scoped>` entier, qui épinglait `density`
52
+ - `OrigamTabs` — plus son bloc `<style>:root{}` (#569)
53
+
54
+ **Preuve navigateur, A/B contre le commit parent** (Chromium, Histoire statique,
55
+ thème de marque injecté par `addInitScript` avant le document). Même story, même
56
+ liste de classes, sur `--origam-btn---density` que `.origam-btn--density-default`
57
+ épinglait à `0px` :
58
+
59
+ | | avant | après |
60
+ |---|---|---|
61
+ | `--origam-btn---density` | `0px` | **`24px`** |
62
+ | hauteur calculée | 28px | **52px** |
63
+
64
+ **Changed** — `--origam-btn---height` et `--origam-toolbar---height` lisent
65
+ désormais leur échelon (`…---height-md` / `…---height-default`) depuis les
66
+ feuilles, dans les quatre fichiers miroirs css/scss. Sans ça l'échelon restait
67
+ sans lecteur après le retrait et `token-var-channels` rougissait ; les deux
68
+ canaux sont vivants.
69
+
70
+ **Added** — garde 29 `theme-channel-confiscation` : il ne lève que
71
+ l'intersection « règle toujours active » × « valeur identique après résolution
72
+ des `var()` », les deux discriminants pinnés par 13 témoins de rappel **et de
73
+ précision**. Baseline à **37** entrées : 14 blocs `<style>:root{}` restants
74
+ (#569) et 22 surcharges parent→enfant où le canal est **renommé** et non mort
75
+ (`OrigamSnackbarGroup` expose `--origam-snackbar-group__item---*` depuis #436,
76
+ la famille Media configure ses `.origam-btn` imbriqués). Livraison
77
+ **volontairement partielle** : chacune de ces deux familles demande une
78
+ migration de feuille composant par composant, avec risque de rendu — donc un
79
+ arbitrage, pas un correctif.
80
+
81
+ ### Fixed — #829 : `geek` ET `glass` déclaraient `surface.sunken` PLUS CLAIR que `surface.default` (mode clair)
82
+
83
+ `sunken` et `default` ne sont pas deux couleurs interchangeables, ce sont des
84
+ **rôles** : `sunken` promet une surface encaissée. En mode **clair**, `geek`
85
+ déclarait `#fbf5ff` — un ton plus clair que son `default` `#f6f0ff` — donc
86
+ tout consommateur demandant un creux recevait un ton **surélevé**.
87
+
88
+ Mesuré en navigateur réel (Chromium, `/installation`, `data-theme="geek"`,
89
+ valeur peinte sur une sonde, pas le littéral du fichier) :
90
+
91
+ | | avant | après |
92
+ |---|---|---|
93
+ | `surface---default` | `rgb(246, 240, 255)` L=0.891 | inchangé |
94
+ | `surface---sunken` | `rgb(251, 245, 255)` **L=0.930** | `rgb(239, 232, 252)` **L=0.831** |
95
+ | relation | **surélevé** ❌ | **encaissé** ✅ |
96
+
97
+ **Valeur choisie par la grammaire du dépôt, pas à l'œil.** Sur les 5 autres
98
+ thèmes clairs corrects, la « profondeur du creux » — contrast(default,
99
+ sunken) — va de 1.0487 (`material`) à 1.1001 (`editorial`). `#efe8fc` place
100
+ `geek` à **1.0686**, dans cette bande, conserve la teinte lavande et
101
+ respecte l'ordre `disabled < sunken < default`.
102
+
103
+ **Consommateurs** : 41 occurrences de `surface---sunken` dans 20 fichiers de
104
+ `packages/marketing/src` (le ticket annonçait « ~30 » ; recompté). La plus
105
+ large est `assets/css/themes/_shared.css:88`, qui alimente
106
+ `--origam-code---background-color` : vérifié sur les **13** blocs
107
+ `.origam-code` de `/installation`, qui passent de `rgb(251,245,255)` à
108
+ `rgb(239,232,252)`.
109
+
110
+ ⛔ **La moitié « mode sombre » du ticket n'est pas reproduite, et rien n'y a
111
+ été changé.** En mode sombre un creux se lit plus CLAIR que la page, et c'est
112
+ déjà le cas : `sunken` L=0.009 > `default` L=0.002. Le ticket la signalait au
113
+ motif que `sunken` y dépasse `raised` ; sur cette relation le dépôt n'est pas
114
+ uniforme (`apple`, `cartoon`, `editorial`, `material` et `geek` font ainsi,
115
+ seuls `ecom` et `glass` l'inversent), donc elle ne constitue pas une règle.
116
+
117
+ `overlay` n'a **pas** suivi `sunken` : c'est un rôle de remplissage de
118
+ composant (chip, avatar, kbd, btn tonal, lignes survolées — 58 occurrences),
119
+ pas une surface encaissée, et le bloc sombre de `geek` découplait déjà les
120
+ deux.
121
+
122
+ #### `glass` — même défaut, même mode
123
+
124
+ Trouvé en relevant les 7 marques pour choisir la valeur de `geek`. `glass`
125
+ déclarait `sunken: rgba(255, 255, 255, 0.85)` — **la même valeur que son
126
+ `overlay`**. Mesure des pixels **rendus** (capture relue via canvas, donc
127
+ compositing réel) :
128
+
129
+ | | avant | après |
130
+ |---|---|---|
131
+ | `default` | `rgb(233, 236, 255)` L=0.8453 | inchangé |
132
+ | `raised` | `rgb(247, 248, 255)` L=0.9413 | inchangé |
133
+ | `sunken` | `rgb(252, 252, 255)` **L=0.9754** | `rgb(226, 229, 249)` **L=0.7905** |
134
+
135
+ ⛔ **« Un verre dépoli éclaircit ce qu'il couvre » ne l'excusait pas, et c'est
136
+ le mode SOMBRE de `glass` lui-même qui le prouve.** En sombre, l'identité place
137
+ déjà `sunken` (blanc 4 %) ENTRE `default` et `raised` (blanc 5 %) — une
138
+ élévation plus faible. En clair elle le plaçait AU-DELÀ de `raised` (blanc 85 %
139
+ contre 65 %). La thèse prédirait le même sens dans les deux modes : l'identité
140
+ se contredisait elle-même. Ce n'est pas un idiome assumé.
141
+
142
+ Le correctif **garde la translucidité** sur laquelle l'identité est bâtie (les
143
+ consommateurs de `backdrop-filter` voient toujours au travers) et remplace le
144
+ blanc par l'encre de la palette à 3 % : `rgba(26, 21, 56, 0.03)`. Profondeur
145
+ rendue **1.0653**, contre **1.0591** pour le mode sombre de `glass` lui-même —
146
+ la valeur est celle que l'identité utilise déjà, et elle est dans la bande du
147
+ dépôt. `overlay` garde le blanc dépoli : c'est un rôle de remplissage de
148
+ composant, où « plus opaque qu'une carte » est la bonne lecture glassmorphism.
149
+
150
+ Non-régression : `audit:dark-contrast` reste à **0 / 1 664**.
151
+
152
+ Ajouté `packages/tests/TU/marketing/brand-surface-elevation.spec.ts` — **14 cas
153
+ (7 marques × 2 modes), tous arbitrés**, aucun sauté : les surfaces translucides
154
+ sont **compositées** sur le `default` de leur propre thème, avec l'opération
155
+ `over()` de `audit/dark-contrast.audit.mjs`. Accord avec le navigateur à 3
156
+ décimales (composite 0.9757 contre pixel rendu 0.9754). Vérifiée ROUGE sur les
157
+ valeurs du commit parent, une fois par marque : `geek/light` puis `glass/light`.
158
+
159
+ ### Fixed — #818 baseline shrink : les 8 `color-contrast` du DS (8 → 0)
160
+
161
+ ⚠️ **Ce lot porte sur le DS lui-même, pas sur une palette marketing** : il
162
+ part dans le paquet npm publié, un consommateur le reçoit.
163
+
164
+ Recompté sur `a11y-violations.baseline.json` (aucun chiffre repris
165
+ d'ailleurs) : **18 violations / 17 clés**, dont **8 `color-contrast`**.
166
+ Mesurées par axe-core en navigateur réel (Playwright + Histoire statique,
167
+ thème `origamTheme` — celui que `histoire.setup.ts` installe via
168
+ `createOrigam()`), elles valaient **15 nœuds** en défaut. Ce ne sont pas 8
169
+ défauts : ce sont **3 causes**.
170
+
171
+ **Cause 1 — une `opacity` décorative délave le TEXTE qu'elle contient**
172
+ (3 entrées : `OrigamNumberField`, `OrigamDatePickerField`,
173
+ `OrigamDataTableRows`). `opacity` crée un groupe de composition qui
174
+ s'applique à tout le sous-arbre, pas au seul trait qu'on visait.
175
+ - `OrigamField` : le label flottant est monté DANS
176
+ `.origam-field__outline--notch`, dont l'`opacity: .38` ne visait que la
177
+ bordure. Mesuré : `#6d28d9` à .38 sur blanc → `#c8adf1`, soit **1.96:1**.
178
+ L'alpha passe sur `border-color` (`color-mix`) — trait identique, label à
179
+ pleine opacité : **7.10:1**. Appliqué aux trois variantes (`outlined`,
180
+ `underlined`, `filled`).
181
+ - `OrigamDataTable` : `.origam-data-table--loading .origam-data-table-cell`
182
+ voilait aussi les en-têtes. Mesuré : `#6d28d9` à .5 → `#b694ec`,
183
+ **2.38:1**. Remonter l'opacité ne sauve pas la règle (il faudrait ≥ .82,
184
+ ce n'est plus un voile) : le voile épargne désormais les en-têtes, qui
185
+ sont la structure du tableau et non la donnée rafraîchie → **6.52:1**.
186
+
187
+ **Cause 2 — un premier plan choisi sans regarder la surface réellement
188
+ peinte** (4 entrées : `OrigamPicker`, `OrigamDatePicker`,
189
+ `OrigamUtilities` ×2). Même famille que #777 (Card) et #514.
190
+ - `--origam-picker-title---color` épinglait `text---secondary` (`#525252`)
191
+ quel que soit le fond ; sur un en-tête `bgColor="primary"` (`#7c3aed`)
192
+ cela donnait **1.37:1**. Le token vaut désormais `currentColor` : le
193
+ titre suit le premier plan de sa surface → **5.70:1** sur primary, et il
194
+ s'aligne visuellement sur le reste de l'en-tête.
195
+ - `.origam--color-{intent}` lisait `---fg`, le premier plan *sur* la
196
+ couleur (blanc) — donc `.origam--color-primary` peignait blanc sur
197
+ `#fafafa`, **1.04:1** : 5 des 7 pastilles étaient littéralement
198
+ invisibles. Ces classes miroitent désormais `tokenForegroundForIntent()`
199
+ (`---fgSubtle`), c'est-à-dire **la valeur que la déclaration inline du
200
+ même prop `color="{intent}"` applique déjà et qui l'emporte sur la
201
+ classe** : la correction fait converger les deux canaux (cf. #514, « 7
202
+ intents sur 8 rendaient une couleur différente »). 4.81:1 à 6.81:1 selon
203
+ l'intention.
204
+ - `.origam--bg-{intent}` porte désormais son premier plan apparié
205
+ (`---fg`), l'appariement que `useColor()` effectue déjà sur le canal
206
+ inline pour `bgColor`. Sans lui, du markup nu héritait de l'encre de la
207
+ page. Les 7 paires sont ≥ 4.5:1 (4.83 → 19.80).
208
+
209
+ **Cause 3 — un couple en dur réellement insuffisant** (1 entrée :
210
+ `OrigamChartRangeSelector`). Le bouton actif peignait blanc sur `#3b82f6`,
211
+ **3.68:1**. `#3b82f6` n'appartient à **aucune échelle du DS** (bleu
212
+ Tailwind) : c'était une valeur isolée, et c'est elle qui produisait la
213
+ violation. Corriger le contraste en gardant une valeur hors échelle aurait
214
+ réglé le symptôme en laissant la cause — l'état actif rejoint donc la
215
+ convention que `chip--selected`, `pagination__item---active` et `stepper`
216
+ appliquent déjà, **la paire complète** :
217
+ `--origam-color__action--primary---bg` + `---fg` (**5.70:1**), survol
218
+ `---bgHover` + `---fg` (**7.10:1**). Le premier plan passe du token brut
219
+ `--origam-color---white` au sémantique `---fg` : l'état sélectionné
220
+ devient **atteignable par le theming**, ce qu'un `#3b82f6` en dur ne serait
221
+ jamais. Mesuré rendu : `rgb(124, 58, 237)` sur `rgb(255, 255, 255)`.
222
+
223
+ Baseline : **18 → 10 violations, 17 → 10 clés, zéro `color-contrast`**.
224
+ Aucune autre entrée touchée. Formule de contraste reprise telle quelle de
225
+ `packages/tests/e2e/token-intent-contrast.spec.ts` (témoins rejoués :
226
+ noir/blanc **21.00**, `#767676` **4.54**, `#777777` **4.48**).
227
+
228
+ ⚠️ **Rupture visuelle assumée** (les ruptures sont gratuites sur ce dépôt,
229
+ mais celles-ci se voient) : `.origam--color-{intent}` passe du blanc à la
230
+ couleur de l'intention ; `.origam--bg-{intent}` impose son premier plan ;
231
+ le titre de Picker suit sa surface (sur un Picker neutre il passe du gris
232
+ `#525252` à l'encre héritée) ; le bouton actif de `OrigamChartRangeSelector`
233
+ passe du bleu au violet de l'identité (`action--primary`), comme tout autre
234
+ état sélectionné du DS.
235
+
236
+ ### Fixed — #818 baseline shrink : structure ARIA — `aria-required-parent`/`aria-required-children`/`listitem`/`aria-prohibited-attr` (4 entrées)
237
+
238
+ Recompté sur `a11y-violations.baseline.json` **après rebase sur `develop`**
239
+ (le lot `button-name`/`scrollable-region-focusable` ci-dessous, #892, est
240
+ passé pendant ce travail et a déjà fait bouger le compteur : 18/17 → 12/12
241
+ avant ce commit) : **12 violations / 12 clés → 8 violations / 8 clés** avec
242
+ ce lot. Les quatre entrées traitées ici sont la même famille de défaut vue
243
+ sous quatre angles : un rôle ARIA exigeant un parent ou des enfants d'un
244
+ rôle précis, et l'arbre rendu ne les fournissait pas.
245
+
246
+ - **`OrigamCalendar` — `aria-required-parent`** (`role="row"` sans parent
247
+ `grid`/`rowgroup`/`table`/`treegrid`). La ligne d'en-tête des jours
248
+ (`.origam-calendar__weekdays`, `role="row"`) était un SIBLING de
249
+ `.origam-calendar__month-grid` (qui seul portait `role="grid"`), jamais un
250
+ descendant. `role="grid"` + l'`aria-label` du mois sont montés d'un cran,
251
+ sur `.origam-calendar__body--month` (qui enveloppe déjà les deux) ; la
252
+ grille interne redevient un simple conteneur de mise en page. Les lignes du
253
+ mois (`role="row"`, déjà correctement rattachées) ne changent pas de statut.
254
+ - **`OrigamListGroup` — `aria-required-children`** (`<origam-list>` /
255
+ `role="list"` ou `"listbox"` sans AUCUN `listitem`/`option` descendant
256
+ lorsque le groupe est fermé — l'activateur porte délibérément aucun rôle,
257
+ ce n'est pas une ligne de la liste). Le groupe entier (activateur + sous-
258
+ liste) est désormais lui-même compté comme UNE ligne de la liste ambiante,
259
+ via un nouveau `groupRole` calculé sur `list.itemRole` : `listitem` en mode
260
+ `list` (seul rôle que `list` autorise comme enfant — vérifié empiriquement
261
+ contre `axe-core@4.13`, `role="group"` y est refusé), `group` en mode
262
+ `listbox` (rôle explicitement autorisé là). Sans liste ambiante (composant
263
+ autonome), aucun rôle n'est posé — un `listitem` hors de tout `list`
264
+ aurait simplement déplacé la violation vers `aria-required-parent`.
265
+ `OrigamListGroupActivator.story.vue` plaçait ses lignes hors du slot
266
+ `#items` (bug de story pré-existant, invisible tant que le wrapper ne
267
+ portait aucun rôle) ; corrigé dans le même commit — sans quoi le nouveau
268
+ rôle aurait cassé la ligne réelle imbriquée (`aria-required-parent` par
269
+ ricochet, capturé en re-testant la suite complète après le fix).
270
+ - **`OrigamFileFieldListItem` — `listitem`** (un `<li>` isolé sans `<ul>`
271
+ parent). Le composant réel est toujours monté par `OrigamFileField` dans
272
+ un vrai `<ul>` — la violation venait uniquement de la story qui démontre
273
+ le sous-composant seul, enveloppé dans un `<div>`. Les six Variants de
274
+ `OrigamFileFieldListItem.story.vue` enveloppent désormais le `<li>` dans un
275
+ `<ul role="list">` (le `role="list"` explicite est une marge de sécurité
276
+ contre le bug WebKit qui retire la sémantique de liste implicite quand
277
+ `list-style: none` est appliqué — présent aussi sur le vrai
278
+ `OrigamFileField.vue`, non corrigé ici, aucun outil de la suite ne le
279
+ détecte).
280
+ - **`OrigamChartMap` — `aria-prohibited-attr`** (`aria-label` sur un
281
+ `<path>` SVG sans rôle). `:role="country.hasData ? 'button' : undefined"`
282
+ gérait déjà le cas « pays sans donnée », mais `:aria-label` restait posé
283
+ sans condition — un `<path>` sans rôle a un rôle implicite `generic`, qui
284
+ interdit `aria-label`. `aria-label` suit désormais la même condition que
285
+ `role`.
286
+
287
+ **Preuve** : porte a11y ciblée (`OrigamCalendar`/`OrigamListGroup`/
288
+ `OrigamFileFieldListItem`/`OrigamChartMap`/`OrigamListGroupActivator`
289
+ Default Variant) verte, `axe-core` réel via Playwright + Histoire statique
290
+ (`E2E_STATIC=1`). Contrôle positif exécuté sur `OrigamListGroup` : rôle
291
+ retiré → `aria-required-children` NOUVELLE (non baselinée) → `exit 1` réel ;
292
+ rôle restauré → vert. Suite a11y complète (218 stories + 8 Variants
293
+ d'intention) rejouée après le lot : **226 passed, 0 failed**. Suite unitaire
294
+ complète : **7149 passed**. `pnpm -F origam guards` 28/28, `guards:self`
295
+ 15/15, `type-check` et `pnpm audit` propres.
296
+
21
297
  ### Fixed — #818 baseline shrink : `aria-allowed-attr`/`aria-prohibited-attr`/`aria-valid-attr-value` (12 des 25 entrées)
22
298
 
23
299
  Recompté sur `a11y-violations.baseline.json` (pas repris d'un chiffre cité