origam 2.18.2 → 2.18.4

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,71 @@ This project follows [Semantic Versioning](https://semver.org).
18
18
 
19
19
  ## [Unreleased]
20
20
 
21
+ ### Fixed — #818 baseline shrink : `aria-allowed-attr`/`aria-prohibited-attr`/`aria-valid-attr-value` (12 des 25 entrées)
22
+
23
+ Recompté sur `a11y-violations.baseline.json` (pas repris d'un chiffre cité
24
+ ailleurs) : la baseline #818 portait **25 clés / 33 violations** sur 24
25
+ composants, dont **12 `aria-allowed-attr`** concentrées sur la famille
26
+ `OrigamDataTable*`. Cause commune, pas 12 défauts distincts :
27
+ `OrigamTextField` ne posait pas `inheritAttrs: false` alors qu'il
28
+ redistribue déjà explicitement chaque attr de fall-through
29
+ (`filterInputAttrs` → `rootAttrs` sur `<origam-input>`, `inputAttrs` sur le
30
+ vrai `<input>`). Sans ce flag, Vue appliquait EN PLUS le `$attrs` brut sur
31
+ la racine `<origam-input>`, qui ne s'y soustrayait pas non plus — doublant
32
+ `aria-haspopup`/`aria-expanded`/`aria-controls`/`aria-valuenow`/… sur le
33
+ `<div>` wrapper, qui ne porte aucun rôle les autorisant. `role` (prop
34
+ déclarée sur `OrigamTextField`) n'atteignait par ailleurs que
35
+ `<origam-field>`, jamais le vrai `<input>` — c'est ce défaut exact que le
36
+ commentaire `KNOWN_FAILURES` documentait déjà pour `OrigamSelect` (test
37
+ `fixme`).
38
+
39
+ **Correctif à la source** (`packages/ds/src/components/TextField/OrigamTextField.vue`) :
40
+ `defineOptions({ inheritAttrs: false })` + `role` désormais aussi lié sur le
41
+ `<input>` réel. Élimine, mesuré axe-core en navigateur réel (Playwright +
42
+ Histoire statique) :
43
+ - `OrigamSelect` — sort de `KNOWN_FAILURES` (`components.spec.ts`), test
44
+ `fixme` → vert.
45
+ - `OrigamDataTable`, `OrigamDataTableFooter`, `OrigamDataTableHeaderCell`,
46
+ `OrigamDataTableHeaders`, `OrigamDataTableHeadersCell`,
47
+ `OrigamDataTableRow` — `aria-allowed-attr` disparaît, entrée de baseline
48
+ retirée entièrement.
49
+ - `OrigamDataTableGroupHeaderRow`, `OrigamDataTableRows`, `OrigamNumberField`
50
+ — `aria-allowed-attr` disparaît, l'entrée reste pour leur AUTRE violation
51
+ (`button-name` / `color-contrast`), non touchée par ce lot.
52
+ - `OrigamDataTableHeadersCellMobile` — `aria-allowed-attr` ET
53
+ `aria-prohibited-attr` disparaissent (même fuite, deux règles axe
54
+ différentes), entrée retirée entièrement.
55
+
56
+ **Deuxième cause, distincte, deux composants** —
57
+ `packages/ds/src/components/ColorPickerField/OrigamColorPickerField.vue` et
58
+ `.../DatePickerField/OrigamDatePickerField.vue` posaient un
59
+ `aria-haspopup` avec une valeur inventée (`'colorpickerbox'` /
60
+ `'datepickerbox'`, jamais un token ARIA valide) sur l'élément
61
+ `.origam-field` (via `activator="parent"` d'`OrigamMenu`), sans `role` le
62
+ justifiant. Remplacé par `aria-haspopup: 'dialog'` (motif WAI-ARIA « Date
63
+ Picker Dialog ») + `role: 'combobox'` sur le même élément — même principe
64
+ que `comboboxAriaAttrs` d'`OrigamSelect`. `aria-allowed-attr` ET
65
+ `aria-valid-attr-value` disparaissent pour les deux ; `OrigamColorPickerField`
66
+ est intégralement retiré de la baseline, `OrigamDatePickerField` y reste
67
+ pour son `color-contrast` (non touché).
68
+
69
+ **Bilan mesuré** : baseline #818 25 clés/33 violations → **17 clés/18
70
+ violations**. Suite `pnpm -F @origam/tests test:a11y` complète (226 tests,
71
+ catalogue 218/218) : **226 passed**, aucune régression sur les 36
72
+ composants à baseline vide. Contrôle positif exécuté : réintroduire l'ancien
73
+ comportement fait échouer `a11y — OrigamSelect Default Variant` avec
74
+ `aria-allowed-attr`/`aria-prohibited-attr` NON baselinés — la porte capte
75
+ la régression, pas seulement l'édition du JSON.
76
+
77
+ **Laissé de côté, délibérément** (17 clés / 18 violations restent dans
78
+ `a11y-violations.baseline.json`, cf. #818) : `color-contrast` (8 instances —
79
+ plusieurs agents dédiés au contraste travaillent déjà sur ce périmètre),
80
+ `button-name` (3, boutons icône sans libellé — famille distincte),
81
+ `scrollable-region-focusable` (3), `aria-required-parent`/`aria-required-children`/
82
+ `listitem`/`aria-prohibited-attr` (`OrigamChartMap`, sans lien avec ce
83
+ correctif) — un défaut chacun, cause non partagée avec ce lot, non
84
+ instruit ici.
85
+
21
86
  Récolte de deux dépréciations posées "pour la prochaine majeure" avant que le
22
87
  `CLAUDE.md` n'acte que `3.0.0` est réservé à la séparation en modules et que
23
88
  les ruptures, elles, ne le sont pas — voir "Work priorities and versioning"
@@ -156,6 +221,105 @@ conclure qu'elle ne l'est plus par défaut.
156
221
 
157
222
  ### Fixed
158
223
 
224
+ - **#871 (clôture) — les 7 dernières violations AA étaient UN défaut de
225
+ palette répété, pas sept défauts de composant.** Mesuré avec
226
+ `packages/tests/audit/dark-contrast.audit.mjs` : **7 / 1 664 → 0 / 1 664**,
227
+ les 8 identités × 2 modes × 2 portées, quatre contrôles verts des deux
228
+ côtés. Exactement **7 lignes changent, aucune ne se dégrade**, et le ratio
229
+ minimum de toute la surface passe de 3.65 à **4.70**.
230
+
231
+ Les 7 se répartissaient en deux causes, toutes deux dans
232
+ `packages/marketing/src/themes` — **aucune dans le DS**, qui était déjà à
233
+ zéro :
234
+
235
+ | cause | marque · mode | couple | avant | après |
236
+ |---|---|---|---|---|
237
+ | encre héritée du mode clair | `apple` sombre | `#ffffff` → `#000000` sur `#0a84ff` | 3.65 | **5.76** |
238
+ | encre héritée du mode clair | `ecom` sombre | `#ffffff` → `#1a0f0a` sur `#f43f5e` | 3.67 | **5.12** |
239
+ | jeton de rôle inversé | `ecom` clair | `action.primary.bg` → `.fgSubtle` sur la crème | 4.43 | **5.93** |
240
+
241
+ **La cause commune.** Le mode sombre ÉCLAIRCIT l'accent de marque — c'est
242
+ la bonne pratique pour une grande surface (`#0071e3` → `#0a84ff` chez
243
+ `apple`, rose-600 → rose-500 chez `ecom`) — mais les deux palettes gardaient
244
+ l'encre `#ffffff` de leur jumeau clair. Or chacune applique **déjà** la règle
245
+ inverse à ses intentions `feedback` : les 4 d'`ecom` et 3 des 4 d'`apple`
246
+ encrent leur accent éclairci avec le `text.inverse` de la palette.
247
+ `action.primary` était la dernière exception. Le badge n'était pas en cause :
248
+ `--origam-badge__badge---{background-color,color}` lit exactement ce couple —
249
+ tout comme `--origam-btn--primary---*`, qui n'est pas dans la sonde et était
250
+ donc en défaut sans être mesuré.
251
+
252
+ ⚠️ **On n'a PAS foncé l'accent**, bien que ce soit l'autre levier : chez
253
+ `apple`, `action.primary.bg` sert aussi d'ENCRE (le bloc `components` passe
254
+ ce `var()` en `color` sur `origam-breadcrumb-item`). Le ramener à `#0071e3`
255
+ donnait 4.70 sous du blanc mais faisait tomber le fil d'Ariane à **4.47** sur
256
+ le fond noir — un défaut échangé contre un autre. Vérifié par calcul avant
257
+ d'écrire la moindre ligne.
258
+
259
+ La 3ᵉ cause est une erreur de RÔLE : `bg` est un jeton de surface, `fgSubtle`
260
+ la teinte prévue pour être LUE sur une surface neutre. `ecomDarkTheme` posait
261
+ déjà `fgSubtle` ; seul le bloc clair se trompait de jeton. Les deux modes sont
262
+ désormais symétriques.
263
+
264
+ - **#871 — une 8ᵉ violation, que la sonde ne pouvait pas voir** :
265
+ `material` sombre, `feedback.success`, `#1b5e20` sur `#81c784` = **3.91**
266
+ → `#0d3b10` = **6.32**. La sonde de `dark-contrast.audit.mjs` ne rend qu'UNE
267
+ intention par famille de composant, donc elle ne peint jamais
268
+ `feedback.success`. Trouvée par la spec statique ajoutée ci-dessous. C'est
269
+ l'encre qui était l'intruse, pas le conteneur : les trois autres intentions
270
+ sombres de `material` tiennent 5.79 / 7.70 / 7.71 avec des encres bespoke
271
+ très profondes, `success` était la seule à se contenter du green-900 de la
272
+ rampe.
273
+
274
+ - **#871 — l'audit lui-même ne démarrait plus.** Son `npx vite build` résolvait
275
+ vite par le PATH, via un lien `node_modules/.bin` qu'un peer auto-installé
276
+ ne garantit pas : sur un `pnpm install --frozen-lockfile` propre,
277
+ `packages/tests/node_modules/.bin/` ne contient que `playwright` et `vitest`,
278
+ et la commande mourait en `127 — sh: vite: command not found`. **L'audit ne
279
+ rendait plus aucun chiffre**, et rien ne le signalait puisque rien ne le
280
+ rejoue en CI. `resolveViteBin()` passe désormais par le realpath de
281
+ `@vitejs/plugin-vue`, ce qui ne dépend ni du PATH ni du hoisting et
282
+ sélectionne la copie de vite que le plugin utilise réellement (deux majeures
283
+ coexistent dans le magasin). Les commentaires qui affirmaient le contraire
284
+ dans les deux fichiers ont été corrigés.
285
+
286
+ - **#871 — l'état SURVOL était en défaut plus grave encore, et le correctif
287
+ d'encre l'a réglé sans qu'on y touche.** Le survol n'a pas d'encre à lui :
288
+ `--origam-btn--primary---background-color-hover` bascule la surface, mais il
289
+ n'existe **aucun** `--origam-btn--primary---color-hover` (ni de
290
+ `--origam-badge__badge---color-hover`) — l'encre reste `action.primary.fg`
291
+ dans les deux états. Sous l'encre blanche, `apple` sombre y tombait à
292
+ **2.19:1** et `ecom` sombre à **2.69:1** : *pires que n'importe laquelle des
293
+ 7 violations comptées*, et invisibles parce que la sonde de
294
+ `dark-contrast.audit.mjs` ne rend que l'état au repos.
295
+
296
+ **Aucun `bgHover` n'a été modifié.** La direction du survol n'était pas le
297
+ problème — l'encre l'était. Mesuré sur les deux marques :
298
+
299
+ | mode · encre | direction du survol | ratio repos → survol |
300
+ |---|---|---|
301
+ | clair · blanche | fonce | 4.70 → **6.95** / **6.29** ↑ |
302
+ | sombre · blanche *(avant)* | éclaircit | 3.65 → **2.19** / 3.67 → **2.69** ↓ |
303
+ | sombre · foncée *(après)* | éclaircit | 5.76 → **9.60** / 5.12 → **6.99** ↑ |
304
+
305
+ La règle n'est donc pas *« le survol doit foncer »* mais **« le survol doit
306
+ déplacer l'accent à l'opposé de la clarté de son encre »**. Les palettes la
307
+ respectaient déjà dans 3 quadrants sur 4 ; le seul cassé l'était par son
308
+ encre. L'idiome « le survol éclaircit », qui est le bon en mode sombre, est
309
+ conservé dans les deux marques.
310
+
311
+ - **#871 — non-régression : `packages/tests/TU/marketing/brand-palette-contrast.spec.ts`.**
312
+ Les couples `{bg, fg}` littéraux des 7 palettes de marque (14 thèmes) doivent
313
+ tenir AA. L'audit de #871 est un script qu'on lance à la main : rien ne le
314
+ rejoue, donc rien n'empêchait une retouche de palette de ramener ces couples
315
+ en silence. La spec lit les hex **dans l'objet `IOrigamTheme`**, avant toute
316
+ indirection CSS — la règle jsdom/`var()` du `CLAUDE.md` ne s'y applique pas,
317
+ et le navigateur reste l'arbitre de ce qui est peint. Elle embarque ses
318
+ propres garde-fous : les 3 témoins de la formule, un contrôle négatif sur les
319
+ 3 couples corrigés, et un plancher de couverture qui rend bruyante une dérive
320
+ du filtre. **Vérifiée ROUGE sur `origin/develop`** : 3 échecs / 14 succès.
321
+ C'est elle qui a trouvé la 8ᵉ violation ci-dessus.
322
+
159
323
  - **#871 — le mode sombre ne peignait qu'à moitié dans un sous-arbre thémé.**
160
324
  Les feuilles de tokens portent ~1 761 déclarations **dérivées**
161
325
  (`--origam-title---color: var(--origam-color__text---primary)`). Une custom
@@ -301,6 +465,84 @@ conclure qu'elle ne l'est plus par défaut.
301
465
 
302
466
  ---
303
467
 
468
+ #### `v-contrast` était silencieusement inerte sur fond opaque (#869)
469
+
470
+ La directive `v-contrast`, câblée sur 30 composants, n'émettait **jamais**
471
+ rien — ni classe, ni `color: !important`, ni `console.warn` — dès que fond
472
+ **et** texte étaient tous deux OPAQUES, c'est-à-dire la configuration par
473
+ défaut du DS. Deux ruptures indépendantes dans le code partagé, corrigeant
474
+ l'une ne suffisait pas sans l'autre :
475
+
476
+ 1. **`toRgb()`** faisait **toujours** passer la couleur par un aller-retour
477
+ `canvas.fillStyle`, même quand `getComputedStyle` avait déjà rendu une
478
+ chaîne `rgb()/rgba()` exploitable. Le getter `fillStyle` de Chromium
479
+ sérialise toute couleur OPAQUE en hexadécimal `#rrggbb` (seul l'alpha < 1
480
+ ressort en `rgba(…)`), ce qui transformait une valeur déjà utilisable en
481
+ une valeur morte pour `rgbaParts`/`channelsOf`.
482
+ 2. **`channelsOf()`** extrayait les canaux via `match(/[\d.]+/g)`, taillé
483
+ pour `rgb(r, g, b)` — une suite de chiffres hexadécimaux SANS séparateur
484
+ (ex. `#777777`, six chiffres) s'agrège en UN seul grand nombre au lieu de
485
+ trois, donc `channelsOf` rend `null` sur exactement la forme hex que la
486
+ rupture 1 pouvait encore produire (couleur nommée, `hsl()`, …
487
+ nécessitant légitimement le détour par le canvas).
488
+
489
+ **Correctif** : `toRgb()` court-circuite désormais le canvas quand l'entrée
490
+ est déjà `rgb()/rgba()` (le cas de la quasi-totalité des appels, puisqu'ils
491
+ lisent `getComputedStyle`), et le chemin canvas restant convertit son
492
+ résultat hexadécimal en `rgb()/rgba()` via un nouveau `hexToRgb()` avant de
493
+ le rendre — `toRgb()` ne rend plus jamais de hex. La mécanique de correction
494
+ elle-même (forcer noir/blanc, poser `data-origam-contrast-fixed`, journaliser
495
+ le ratio) n'est pas modifiée : elle fonctionnait déjà, elle n'était
496
+ simplement jamais atteinte.
497
+
498
+ **Preuve** — `packages/tests/e2e/contrast-directive.spec.ts` (Playwright,
499
+ Chromium réel — jsdom n'a pas de `canvas.getContext('2d')` fonctionnel dans
500
+ ce dépôt, donc ne peut ni reproduire ni vérifier ce bug) : contrôle positif
501
+ (`#777777` sur blanc, 4.48:1, opaque des deux côtés) déclenche désormais la
502
+ correction + le `console.warn` ; contrôle négatif (noir sur blanc, 21:1) ne
503
+ déclenche rien ; non-régression du chemin translucide (fond composé via
504
+ `resolvePaintedBackground`) inchangée. **A/B contre le commit parent** : 4
505
+ des 5 tests rougissent sur le code d'avant (le contrôle négatif reste vert
506
+ des deux côtés, comme attendu).
507
+
508
+ ⚠️ **Rayon mesuré séparément, pas hérité de #871 — et le chiffre publié par
509
+ #871 contenait un artefact, corrigé depuis par #883.** Le chiffrage initial
510
+ du ticket (346 violations en mode sombre) datait d'avant #876. #876 avait
511
+ d'abord publié **189 → 11** ; en rejouant `audit/dark-contrast.audit.mjs`
512
+ avec la VRAIE directive (redirect du stub désactivé) au lieu de sa
513
+ reformulation mathématique, seuls **7 `console.warn` réels** se
514
+ déclenchaient sur les 32 configurations (30 composants × 8 identités × 2
515
+ modes, portées racine + sous-arbre) — écart signalé à l'auteur de #876, qui
516
+ l'a reproduit, creusé et corrigé sous #883 : **les bornes AVANT et APRÈS
517
+ étaient toutes les deux fausses (185 → 7, pas 189 → 11)**, la même erreur de
518
+ mesure existant déjà dans le comptage "avant". Le delta réel (178) n'a
519
+ jamais changé.
520
+
521
+ L'écart venait d'`origam-tooltip__content` (identité `apple`, root + subtree,
522
+ light + dark) et, découvert par #883, également d'`origam-bottom-nav` sur la
523
+ même identité. Fond réel : `color(srgb 0.898039 0.898039 0.905882 / 0.94)`
524
+ (translucide) ; `toRgb()` le résout correctement en `rgb(215, 215, 217)` une
525
+ fois composé sur son ancêtre opaque, contre du texte noir : **ratio 14.61:1,
526
+ conforme** — vérifié en appelant directement `toRgb()`/
527
+ `resolvePaintedBackground()` de la vraie directive sur cette page. La
528
+ fonction `parse()` de `dark-contrast.audit.mjs` (une réimplémentation
529
+ distincte, pas la directive) ne reconnaissait que `rgba?\(…\)` — elle
530
+ ignorait silencieusement cette couche `color(srgb …)`, sautait jusqu'à
531
+ l'ancêtre opaque suivant et rapportait à tort un faux "noir sur noir"
532
+ (ratio 1.00). **Ce n'était pas un défaut du DS ni de `v-contrast`** — la
533
+ même classe de lacune que ce ticket corrige, mais dans un autre fichier. Les
534
+ 7 restantes (4 `origam-badge__badge`, 3 `origam-breadcrumb-item`) sont
535
+ confirmées identiques entre les deux mesures, et `sous 2:1` passe à **0** une
536
+ fois l'artefact retiré : plus rien de ce qui reste n'est proche d'invisible.
537
+
538
+ Non corrigé ici : `dark-contrast.audit.mjs` appartenait à #871/#876, pas à ce
539
+ ticket — signalé, reproduit et corrigé séparément sous #883 (ne pas citer
540
+ 189 → 11, c'était faux des deux côtés).
541
+
542
+ **Rupture d'API** : aucune — `toRgb`/`channelsOf`/`hexToRgb` restent des
543
+ fonctions privées du module, non exportées ; la surface publique
544
+ (`setContrastConfig`, `v-contrast` par défaut) est inchangée.
545
+
304
546
  ## [2.18.0] - 2026-09-19
305
547
 
306
548
  80 commits depuis `v2.17.1`. L'essentiel est du **correctif** : des tokens qui
@@ -331,9 +331,31 @@ const menuProps = computed(() => {
331
331
  return {
332
332
  ...props.menuProps,
333
333
  activatorProps: {
334
+ /*********************************************************
335
+ * role + aria-haspopup — #818
336
+ *
337
+ * @description
338
+ * `'colorpickerbox'` was never a valid `aria-haspopup` token
339
+ * (spec allows `false|true|menu|listbox|tree|grid|dialog`) —
340
+ * axe-core's `aria-valid-attr-value` flagged it, measured
341
+ * against a real browser. The popup is a free-form panel
342
+ * (a color canvas + swatches), not a list of options, so
343
+ * `'dialog'` is the correct token — same family as the
344
+ * WAI-ARIA "Date Picker Dialog" pattern `OrigamDatePickerField`
345
+ * uses right below.
346
+ *
347
+ * @description
348
+ * `role: 'combobox'` is ALSO required here: `activator="parent"`
349
+ * (below) lands these ARIA attrs on the `.origam-field` wrapper
350
+ * `<div>`, which carries no role by default — `aria-allowed-attr`
351
+ * rejects `aria-expanded` on an element whose role doesn't
352
+ * support it. Same fix, same rationale as
353
+ * `OrigamSelect.comboboxAriaAttrs` (`role` MUST sit on the same
354
+ * element as `aria-haspopup`/`aria-expanded`).
355
+ ********************************************************/
356
+ role: "combobox",
334
357
  ...props.menuProps?.activatorProps || {},
335
- "aria-haspopup": "colorpickerbox"
336
- // Set aria-haspopup to 'listbox'
358
+ "aria-haspopup": "dialog"
337
359
  },
338
360
  contentProps: {
339
361
  ...consumerContentProps,
@@ -405,9 +405,29 @@ const menuProps = computed(() => {
405
405
  return {
406
406
  ...props.menuProps,
407
407
  activatorProps: {
408
+ /*********************************************************
409
+ * role + aria-haspopup — #818
410
+ *
411
+ * @description
412
+ * `'datepickerbox'` was never a valid `aria-haspopup` token
413
+ * (spec allows `false|true|menu|listbox|tree|grid|dialog`) —
414
+ * axe-core's `aria-valid-attr-value` flagged it, measured
415
+ * against a real browser. `'dialog'` matches the WAI-ARIA
416
+ * "Date Picker Dialog" pattern: a button/combobox opening a
417
+ * calendar dialog.
418
+ *
419
+ * @description
420
+ * `role: 'combobox'` is ALSO required here: `activator="parent"`
421
+ * (below) lands these ARIA attrs on the `.origam-field` wrapper
422
+ * `<div>`, which carries no role by default — `aria-allowed-attr`
423
+ * rejects `aria-expanded` on an element whose role doesn't
424
+ * support it. Same fix, same rationale as
425
+ * `OrigamSelect.comboboxAriaAttrs` (`role` MUST sit on the same
426
+ * element as `aria-haspopup`/`aria-expanded`).
427
+ ********************************************************/
428
+ role: "combobox",
408
429
  ...props.menuProps?.activatorProps || {},
409
- "aria-haspopup": "datepickerbox"
410
- // Set aria-haspopup to 'listbox'
430
+ "aria-haspopup": "dialog"
411
431
  },
412
432
  contentProps: {
413
433
  ...consumerContentProps,
@@ -91,6 +91,7 @@
91
91
  :readonly="isReadonly"
92
92
  :required="required || undefined"
93
93
  :aria-required="required ? 'true' : undefined"
94
+ :role="role"
94
95
  :size="1"
95
96
  :type="resolvedInputType"
96
97
  :value="displayValue"
@@ -208,6 +209,7 @@ import { applyMask } from "../../utils/Commons/apply-mask.util";
208
209
  import { filterInputAttrs } from "../../utils/Input/input.util";
209
210
  import { forwardRefs } from "../../utils/Commons/forwardRefs.util";
210
211
  import { resolveMaskConfig } from "../../utils/Commons/resolve-mask-config.util";
212
+ defineOptions({ inheritAttrs: false });
211
213
  const props = defineProps({
212
214
  autofocus: { type: Boolean, required: false },
213
215
  counter: { type: [Boolean, Number, String], required: false },
@@ -34,10 +34,24 @@ function srgbToRgb(color) {
34
34
  const a = m[4] !== void 0 ? parseFloat(m[4]) : 1;
35
35
  return a < 1 ? `rgba(${r}, ${g}, ${b}, ${a})` : `rgb(${r}, ${g}, ${b})`;
36
36
  }
37
+ function hexToRgb(hex) {
38
+ const m = hex.match(/^#([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i);
39
+ if (!m) return null;
40
+ const h = m[1].length <= 4 ? [...m[1]].map(c => c + c).join("") : m[1];
41
+ const r = parseInt(h.slice(0, 2), 16);
42
+ const g = parseInt(h.slice(2, 4), 16);
43
+ const b = parseInt(h.slice(4, 6), 16);
44
+ if (h.length === 8) {
45
+ const a = parseInt(h.slice(6, 8), 16) / 255;
46
+ return a < 1 ? `rgba(${r}, ${g}, ${b}, ${a})` : `rgb(${r}, ${g}, ${b})`;
47
+ }
48
+ return `rgb(${r}, ${g}, ${b})`;
49
+ }
37
50
  function toRgb(color) {
38
51
  if (!color) return null;
39
52
  const direct = srgbToRgb(color);
40
53
  if (direct) return direct;
54
+ if (/^rgba?\(/i.test(color)) return color;
41
55
  if (typeof document === "undefined") return color;
42
56
  if (normCtx === void 0) normCtx = document.createElement("canvas").getContext("2d");
43
57
  if (!normCtx) return color;
@@ -48,7 +62,7 @@ function toRgb(color) {
48
62
  normCtx.fillStyle = color;
49
63
  const onWhite = normCtx.fillStyle;
50
64
  if (onBlack !== onWhite) return null;
51
- return srgbToRgb(onBlack) ?? onBlack;
65
+ return srgbToRgb(onBlack) ?? hexToRgb(onBlack) ?? onBlack;
52
66
  }
53
67
  function rgbaParts(rgb) {
54
68
  if (!rgb) return null;
@@ -22,10 +22,24 @@ function srgbToRgb(color) {
22
22
  const a = m[4] !== void 0 ? parseFloat(m[4]) : 1;
23
23
  return a < 1 ? `rgba(${r}, ${g}, ${b}, ${a})` : `rgb(${r}, ${g}, ${b})`;
24
24
  }
25
+ function hexToRgb(hex) {
26
+ const m = hex.match(/^#([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i);
27
+ if (!m) return null;
28
+ const h = m[1].length <= 4 ? [...m[1]].map((c) => c + c).join("") : m[1];
29
+ const r = parseInt(h.slice(0, 2), 16);
30
+ const g = parseInt(h.slice(2, 4), 16);
31
+ const b = parseInt(h.slice(4, 6), 16);
32
+ if (h.length === 8) {
33
+ const a = parseInt(h.slice(6, 8), 16) / 255;
34
+ return a < 1 ? `rgba(${r}, ${g}, ${b}, ${a})` : `rgb(${r}, ${g}, ${b})`;
35
+ }
36
+ return `rgb(${r}, ${g}, ${b})`;
37
+ }
25
38
  function toRgb(color) {
26
39
  if (!color) return null;
27
40
  const direct = srgbToRgb(color);
28
41
  if (direct) return direct;
42
+ if (/^rgba?\(/i.test(color)) return color;
29
43
  if (typeof document === "undefined") return color;
30
44
  if (normCtx === void 0) normCtx = document.createElement("canvas").getContext("2d");
31
45
  if (!normCtx) return color;
@@ -36,7 +50,7 @@ function toRgb(color) {
36
50
  normCtx.fillStyle = color;
37
51
  const onWhite = normCtx.fillStyle;
38
52
  if (onBlack !== onWhite) return null;
39
- return srgbToRgb(onBlack) ?? onBlack;
53
+ return srgbToRgb(onBlack) ?? hexToRgb(onBlack) ?? onBlack;
40
54
  }
41
55
  function rgbaParts(rgb) {
42
56
  if (!rgb) return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "origam",
3
- "version": "2.18.2",
3
+ "version": "2.18.4",
4
4
  "type": "module",
5
5
  "description": "origam — a Vue 3 design system with multi-theme design tokens, CSS-first components and TypeScript types.",
6
6
  "keywords": [