@helixui/tokens 3.2.0 → 3.3.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/dist/tokens.json CHANGED
@@ -113,11 +113,11 @@
113
113
  },
114
114
  "secondary": {
115
115
  "value": "var(--hx-color-neutral-700)",
116
- "description": "Secondary body text. Bumped from neutral-600 to neutral-700 in 3.2.0 alongside text.muted moving from neutral-500 to neutral-600 — preserves the primary > strong > secondary > muted hierarchy now that muted occupies the slot secondary used to live in. neutral-700 (#313E4B) on surface.default (#FFFFFF) = 10.84:1, on surface.raised (#F5F8F3) = 10.10:1, on surface.sunken (#EBEEE9) = 9.01:1 — all AAA."
116
+ "description": "Secondary body text. Bumped from neutral-600 to neutral-700 in 3.2.0 alongside text.muted moving from neutral-500 to neutral-600 — preserves the primary > strong > secondary > muted hierarchy now that muted occupies the slot secondary used to live in. neutral-700 (#313E4B) on surface.default (#FFFFFF) = 10.93:1, on surface.raised (#F5F8F3) = 10.20:1, on surface.sunken (#EBEEE9) = 9.34:1 — all AAA."
117
117
  },
118
118
  "muted": {
119
119
  "value": "var(--hx-color-neutral-600)",
120
- "description": "Muted body text. Bumped from neutral-500 to neutral-600 in 3.2.0. The new precision-cool neutral-500 (#66787B) on surface.raised (#F5F8F3) = 4.32:1 — fails the 4.5:1 body-text floor. neutral-600 (#4A5362) on surface.raised = 7.36:1, on surface.default = 7.88:1, on surface.sunken = 6.55:1 — AA pass everywhere. Pre-existing palette bug from commit 2 caught by the new contrast regression matrix."
120
+ "description": "Muted body text. Bumped from neutral-500 to neutral-600 in 3.2.0. The new precision-cool neutral-500 (#66787B) on surface.raised (#F5F8F3) = 4.32:1 — fails the 4.5:1 body-text floor. neutral-600 (#4A5362) on surface.raised = 7.25:1, on surface.default = 7.76:1, on surface.sunken = 6.63:1 — AA pass everywhere. Pre-existing palette bug from commit 2 caught by the new contrast regression matrix."
121
121
  },
122
122
  "placeholder": {
123
123
  "value": "var(--hx-color-neutral-500)",
@@ -139,13 +139,25 @@
139
139
  },
140
140
  "on-success": {
141
141
  "value": "var(--hx-color-neutral-900)",
142
- "description": "Dark text on success surface. neutral-0 (white) on success-500 (#16A34A) was 2.8:1 — WCAG AA fail. neutral-900 on success-500 = 11.2:1 (AAA). Matches the on-warning pattern."
142
+ "description": "Dark text on success surface. neutral-0 (white) on the precision-cool success-500 (#3B9E58) is 3.38:1 — WCAG AA fail for body text. neutral-900 on success-500 = 5.29:1 (AA pass). Matches the on-warning pattern."
143
143
  },
144
144
  "on-warning": { "value": "var(--hx-color-neutral-900)" },
145
145
  "on-info": {
146
146
  "value": "var(--hx-color-neutral-900)",
147
147
  "description": "Dark text on info surface. neutral-0 on the precision-cool info-500 (#0C8BEB) was 3.55:1 — WCAG AA fail. neutral-900 on info-500 = 5.03:1 (AA pass)."
148
148
  },
149
+ "on-primary-strong": {
150
+ "value": "var(--hx-color-neutral-0)",
151
+ "description": "White text override for primary surfaces darker than primary-500 (i.e. primary-600 hover, primary-700 active). The AA-tuned text.on-primary (neutral-900) only meets AA against primary-500; on primary-600 (#0F7078) it drops to 3.07:1 and on primary-700 (#0F6363) to 2.54:1 — both AA fails. neutral-0 on primary-600 = 5.82:1 (AA pass) and on primary-700 = 7.03:1 (AA pass). Added in 3.2.1 to remediate the token-cascade campaign findings where component variants were pinning to neutral-0 directly to escape this exact AA gap; routes the white-on-darker-primary pin through the semantic tier instead of the raw primitive."
152
+ },
153
+ "on-success-strong": {
154
+ "value": "var(--hx-color-neutral-0)",
155
+ "description": "White text override for success surfaces darker than success-500 (i.e. the success-700 #146831 fill that hx-toast--success and other emphasis-success surfaces paint). The AA-tuned text.on-success (neutral-900) only meets AA against success-500 (#3B9E58 = 5.29:1); on success-700 it drops to 2.60:1 — AA fail. neutral-0 on success-700 = 6.88:1 AA pass. Sister token to text.on-primary-strong / text.on-error-strong; same 3.2.1 token-cascade rationale (route the white-on-darker-success pin through the semantic tier instead of letting components consume neutral-0 directly)."
156
+ },
157
+ "on-error-strong": {
158
+ "value": "var(--hx-color-neutral-0)",
159
+ "description": "White text override for error surfaces darker than error-500 (i.e. error-600 hover, error-700 active). The AA-tuned text.on-error (neutral-900) only meets AA against error-500; on error-600 (#C92A2A) it drops to 3.28:1 and on error-700 (#A21312) to 2.25:1 — both AA fails. neutral-0 on error-600 = 5.46:1 (AA pass) and on error-700 = 7.96:1 (AA pass). Sister token to text.on-primary-strong; same 3.2.1 token-cascade remediation rationale."
160
+ },
149
161
  "link": { "value": "var(--hx-color-primary-600)" },
150
162
  "link-hover": { "value": "var(--hx-color-primary-700)" },
151
163
  "link-visited": { "value": "var(--hx-color-secondary-600)" },
@@ -167,18 +179,119 @@
167
179
  "value": "var(--hx-color-neutral-900)",
168
180
  "description": "Inverse surface for dark-always components (tooltips, inverse nav). Flips to a light surface in dark mode; uses system Canvas in forced-colors."
169
181
  },
170
- "overlay": { "value": "rgba(0, 0, 0, 0.75)" }
182
+ "overlay": { "value": "rgba(0, 0, 0, 0.75)" },
183
+ "success-strong": {
184
+ "value": "var(--hx-color-success-700)",
185
+ "description": "Emphasis success surface for high-prominence components (e.g. hx-toast--success). Pairs with text.on-success-strong (neutral-0, no dark-mode flip) for AA — neutral-0 on success-700 (#146831) = 6.88:1. Do NOT pair with text.inverse: surface.success-strong itself does not dark-mode-flip (the value stays success-700 in light and dark), but text.inverse does flip to neutral-900 in dark mode — pairing the two produces dark text on a dark fill (sub-AA). Added in 3.2.1 to route the toast variant fills through the semantic tier instead of consuming success-700 directly."
186
+ },
187
+ "warning-strong": {
188
+ "value": "var(--hx-color-warning-500)",
189
+ "description": "Emphasis warning surface for high-prominence components (e.g. hx-toast--warning). Pairs with text.on-warning (neutral-900) for AA — neutral-900 on warning-500 (#C2711C) = 4.83:1. Stays on the lighter -500 stop because warning's on-token contract is tuned for it; see toast variant rationale."
190
+ },
191
+ "danger-strong": {
192
+ "value": "var(--hx-color-error-600)",
193
+ "description": "Emphasis danger surface for high-prominence components (e.g. hx-toast--danger). Pairs with text.on-error-strong (neutral-0, no dark-mode flip) for AA — neutral-0 on error-600 (#C92A2A) = 5.46:1. Do NOT pair with text.inverse: surface.danger-strong itself does not dark-mode-flip (the value stays error-600 in light and dark), but text.inverse does flip to neutral-900 in dark mode — pairing the two produces dark text on a dark fill (sub-AA). Tracks action.danger.bg-hover by value (also error-600) but is exposed as a surface semantic so non-interactive consumers (toasts, banners) don't reach into the action.* hover state to find it."
194
+ },
195
+ "info-strong": {
196
+ "value": "var(--hx-color-primary-600)",
197
+ "description": "Emphasis info surface for high-prominence components (e.g. hx-toast--info). Pairs with text.on-primary-strong (neutral-0, no dark-mode flip) for AA — neutral-0 on primary-600 (#0F7078) = 5.82:1. Do NOT pair with text.inverse: surface.info-strong itself does not dark-mode-flip (the value stays primary-600 in light and dark), but text.inverse does flip to neutral-900 in dark mode — pairing the two produces dark text on a dark fill (sub-AA). Tracks action.primary.bg-hover by value but exposed under surface.* so toasts/banners consume a surface semantic, not an action-state semantic."
198
+ },
199
+ "on-dark-overlay-default": {
200
+ "value": "var(--hx-overlay-white-30)",
201
+ "description": "Translucent fill on dark surfaces — the resting/hover background for inverted secondary, ghost, and tertiary buttons painted on surface.inverse (which is dark in light mode, light in dark mode). Renamed from border.on-dark-default in 3.2.2: it was always used as a fill, never a border, and its 30% alpha cannot honour WCAG 1.4.11 3:1 against either neutral-900 (2.07:1) or neutral-0 (1.30:1) — borders need higher alpha. As a fill, contrast is irrelevant: it tints, it doesn't delimit."
202
+ },
203
+ "on-dark-overlay-subtle": {
204
+ "value": "var(--hx-overlay-white-10)",
205
+ "description": "Lowest-emphasis translucent fill on dark surfaces — the resting background for inverted-tertiary buttons and inverted hover affordances on dark side-nav. Renamed from border.on-dark-subtle in 3.2.2 for the same reason as on-dark-overlay-default: it is a fill, not a border."
206
+ }
171
207
  },
172
208
  "border": {
173
209
  "default": { "value": "var(--hx-color-neutral-200)" },
174
210
  "subtle": { "value": "var(--hx-color-neutral-100)" },
175
- "strong": { "value": "var(--hx-color-neutral-400)" },
176
- "focus": { "value": "var(--hx-color-primary-500)" }
211
+ "strong": {
212
+ "value": "var(--hx-color-neutral-500)",
213
+ "description": "Strong border for state-conveying form-control borders (text input, select, checkbox, radio, switch track, file upload dropzone, side-nav dividers, etc.). Light-mode pairs with surface.default (white) at 4.63:1 — clears the WCAG 1.4.11 3:1 floor for non-text UI components and the 3:1 floor for state-conveying borders. Pre-3.2.2 this was neutral-400 (#8E9C98) which lands at 2.85:1 on white — a sub-3:1 fail systemic to every form-control border across the library. Dark mode flips to neutral-400 (#8E9C98 on #0D1825 = 6.27:1) via the dark.color.border.strong override — the flip both preserves mode differentiation (outline button parity) and keeps the dark border bright enough to remain crisp on the dark page."
214
+ },
215
+ "focus": { "value": "var(--hx-color-primary-500)" },
216
+ "on-dark-strong": {
217
+ "value": "var(--hx-overlay-white-70)",
218
+ "description": "Strong border treatment on dark surfaces (e.g. inverted button outlines on a dark page). overlay-white-70 on neutral-900 ≈ 5.0:1 — clears WCAG 1.4.11 3:1 floor for non-text UI. Added in 3.2.1 alongside the action.* layer for the token-cascade remediation; only the strong tier survived the 3.2.2 audit because the lower-alpha siblings (overlay-white-30/10) cannot honour a border-contract — they were renamed to surface.on-dark-overlay-* (which is what they always were: translucent fills). DEPRECATED ALIAS NOTE: --hx-color-border-on-dark-{default,subtle} shipped in 3.2.0/3.2.1 and were renamed to surface.on-dark-overlay-{default,subtle} in 3.2.2. The deprecated names are NOT emitted at :root in dist/tokens.css. Two `:root`-alias variants were considered and rejected for the same root cause — both shadow host-scoped canonical-name overrides at every component consume site. Variant A (`var()` alias): `:root { --hx-color-border-on-dark-default: var(--hx-color-surface-on-dark-overlay-default); }` — the inner var() resolves at :root's computed-value time (CSS Custom Properties §3) and inherits as an opaque resolved value to every descendant. Variant B (concrete-value alias, light + dark): `:root { --hx-color-border-on-dark-default: rgba(255,255,255,0.30); } .dark { --hx-color-border-on-dark-default: <dark-mode value>; }` — no inner var() to substitute, but inheritance still delivers a non-empty value to every descendant. Both variants cause the same failure at the consume site: components read the deprecated name FIRST (`var(--hx-color-border-on-dark-*, var(--hx-color-surface-on-dark-overlay-*, …))`), so as long as the deprecated name resolves to ANY value above the host, the var() chain never falls through to the host's canonical override. Variant B has additional cost: it bakes a literal value into :root, breaking the primitive-chain (a consumer who overrides --hx-overlay-white-30 at :root would no longer see that change reflected in --hx-color-border-on-dark-default). Backwards compatibility is preserved at the consume sites instead: every component rule that paints with surface.on-dark-overlay-* reads the both-name fallback chain so a consumer override on either name reaches paint. The dark-mode-resolution.test.ts override-path tests pin both directions (line 209-227 covers deprecated-name + canonical-name overrides); both alias variants would fail the canonical-override test. The deliberate trade-off: downstream code that reads the deprecated names DIRECTLY (app-level CSS using `var(--hx-color-border-on-dark-default)` outside an hx-* component, or `getComputedStyle(el).getPropertyValue('--hx-color-border-on-dark-default')`) will resolve to an empty/invalid value in 3.2.2 — those readers must migrate to the canonical `--hx-color-surface-on-dark-overlay-{default,subtle}` (or set the deprecated name explicitly on their own scope). The chosen design breaks an undocumented direct-reader path explicitly, rather than breaking a documented host-override path silently. Removal scheduled for 4.0.0."
219
+ }
220
+ },
221
+ "focus-ring": {
222
+ "value": "var(--hx-color-primary-600)",
223
+ "description": "Light-mode focus-ring color. Pairs with surface.default (white) at 5.82:1 — clears the WCAG 1.4.11 3:1 floor for non-text UI components with comfortable headroom. Pre-3.2.2 this resolved to primary-400 (#6AB1B1) which lands at 2.45:1 on white — a sub-3:1 fail for any focus indicator drawn at full opacity (outline-style focus on hx-link, border-flip on hx-text-input, etc.). The dark.color.focus-ring override keeps primary-400 because primary-400 on dark surface.default = 7.27:1 (AA pass) while primary-600 on dark = 3.07:1 (barely clears the 3:1 UI floor with no headroom). Color-mix box-shadow ring treatments (hx-text-input wrapper) layer their alpha on top of this base; the change only sharpens, never weakens, the ring against light surfaces."
177
224
  },
178
- "focus-ring": { "value": "var(--hx-color-primary-400)" },
179
225
  "selection": {
180
226
  "bg": { "value": "var(--hx-color-primary-200)" },
181
227
  "color": { "value": "var(--hx-color-neutral-900)" }
228
+ },
229
+ "action": {
230
+ "_comment": "Semantic action surfaces — the interactive-state layer between the primitive ramp (color.{role}.{stop}) and component-tier overrides (--hx-{component}-bg, --hx-{component}-hover-bg, etc). Pre-3.2.1, components reached straight to ramp stops (e.g. --hx-button-bg: var(--hx-color-primary-500)) which violated the three-tier cascade contract — consumers had no themable hook between the primitive and the component. The token-cascade campaign found 21 high-severity bare-primitive consumptions across hx-button, hx-toast, and hx-side-nav/hx-nav-item; this block is the systemic fix. Components rebind their --hx-{component}-bg etc. through these semantics so theme overrides at the action.* tier propagate everywhere.",
231
+ "primary": {
232
+ "bg": {
233
+ "value": "var(--hx-color-primary-500)",
234
+ "description": "Resting interactive primary surface (button bg, active nav-item bg, primary toast bg). Resolves to primary-500 in light/dark and the HC primary-500 override in HC."
235
+ },
236
+ "bg-inverted-rest": {
237
+ "value": "var(--hx-color-primary-500)",
238
+ "description": "Resting state for primary surfaces that live on the inverse surface (e.g. inverted-mode primary buttons). Tracks action.primary.bg (primary-500) in light mode where surface.inverse is dark (#0D1825) and primary-500 lands at 5.20:1. Dark-mode override flips to primary-600 because surface.inverse becomes light (#EBEEE9) in dark mode and primary-500 on light = 2.94:1 (sub-3:1 fail for the WCAG 1.4.11 UI-component boundary). Decoupled from action.primary.bg so the standard non-inverted button keeps its primary-500 fill against the dark page in dark mode (the documented architectural intent — see dark.action._comment)."
239
+ },
240
+ "bg-hover": {
241
+ "value": "var(--hx-color-primary-600)",
242
+ "description": "Hover state for interactive primary surfaces. Pairs with text.on-primary-strong (neutral-0) for AA — primary-600 on neutral-0 = 5.82:1."
243
+ },
244
+ "bg-active": {
245
+ "value": "var(--hx-color-primary-700)",
246
+ "description": "Active/pressed state for interactive primary surfaces. Pairs with text.on-primary-strong (neutral-0) — primary-700 on neutral-0 = 7.03:1 AA."
247
+ },
248
+ "bg-inverted-hover": {
249
+ "value": "var(--hx-color-primary-400)",
250
+ "description": "Hover state for primary surfaces that live on a dark/inverted background — flips lighter rather than darker so the affordance reads against an already-dark page. Matches the precision-cool inverted-button hover pattern in hx-button."
251
+ }
252
+ },
253
+ "secondary": {
254
+ "fg": {
255
+ "value": "var(--hx-color-primary-600)",
256
+ "description": "Foreground (text + icon) color for resting secondary/outline interactive treatments. primary-600 on surface.default = 5.82:1 AA."
257
+ },
258
+ "border": {
259
+ "value": "var(--hx-color-primary-600)",
260
+ "description": "Border color for resting secondary/outline interactive treatments. Matches action.secondary.fg so the affordance reads as a single chromatic outline."
261
+ },
262
+ "bg-hover": {
263
+ "value": "var(--hx-color-primary-50)",
264
+ "description": "Hover surface fill for secondary/outline interactive treatments — the lightest primary tint (#EBF8F8) so the hover affordance is felt without flipping to a full primary fill. Foreground stays bound to action.secondary.fg in the consuming component."
265
+ }
266
+ },
267
+ "ghost": {
268
+ "fg": {
269
+ "value": "var(--hx-color-primary-600)",
270
+ "description": "Foreground color for resting ghost (text-only, no border) interactive treatments. Shares its value with action.secondary.fg by design — ghost is secondary minus the outline — but kept as a separate semantic so theming can diverge if a brand wants ghost ≠ secondary."
271
+ },
272
+ "bg-hover": {
273
+ "value": "var(--hx-color-primary-50)",
274
+ "description": "Hover surface fill for ghost interactive treatments. Mirrors action.secondary.bg-hover; same divergence rationale."
275
+ }
276
+ },
277
+ "danger": {
278
+ "bg": {
279
+ "value": "var(--hx-color-error-500)",
280
+ "description": "Resting interactive danger surface (destructive button bg, danger toast bg). Pairs with text.on-error (neutral-900, AA-tuned) on error-500."
281
+ },
282
+ "bg-hover": {
283
+ "value": "var(--hx-color-error-600)",
284
+ "description": "Hover state for danger surfaces. Pairs with text.on-error-strong (neutral-0) for AA — error-600 on neutral-0 = 5.46:1."
285
+ },
286
+ "bg-active": {
287
+ "value": "var(--hx-color-error-700)",
288
+ "description": "Active/pressed state for danger surfaces. Pairs with text.on-error-strong (neutral-0) — error-700 on neutral-0 = 7.96:1 AA."
289
+ },
290
+ "bg-inverted-hover": {
291
+ "value": "var(--hx-color-error-400)",
292
+ "description": "Hover/pressed state for danger surfaces that live on a dark/inverted background — flips lighter rather than darker so the affordance reads against an already-dark page. error-400 (#FC7264) on surface.inverse (neutral-900, #0D1825) = 6.58:1, clears the WCAG 1.4.11 3:1 UI floor. Sister token to action.primary.bg-inverted-hover; added in 3.2.1 round-7 to close the inverted-mode :active regression where base error-700 (#A21312) on dark surface dropped to ~3:1."
293
+ }
294
+ }
182
295
  }
183
296
  },
184
297
  "body": {
@@ -332,7 +445,10 @@
332
445
  }
333
446
  },
334
447
  "focus": {
335
- "ring-color": { "value": "var(--hx-color-primary-400)" },
448
+ "ring-color": {
449
+ "value": "var(--hx-color-primary-600)",
450
+ "description": "Light-mode focus ring color. Mirrors color.focus-ring (primary-600 = #0F7078, 5.82:1 on white surface — AA pass for the 3:1 UI-component floor). Pre-3.2.2 this was primary-400 (#6AB1B1, 2.45:1 on white — sub-3:1 fail). Dark mode keeps primary-400 via dark.focus.ring-color override (7.27:1 on dark surface)."
451
+ },
336
452
  "ring-width": { "value": "2px" },
337
453
  "ring-offset": { "value": "2px" },
338
454
  "ring-style": { "value": "solid" },
@@ -484,11 +600,11 @@
484
600
  "color": {
485
601
  "error-text": {
486
602
  "value": "var(--hx-color-error-400)",
487
- "description": "WCAG AA compliant error text for dark backgrounds. #F87171 = 6.45:1 on neutral-900 dark surface."
603
+ "description": "WCAG AA compliant error text for dark backgrounds. #F87171 = 6.46:1 on neutral-900 dark surface."
488
604
  },
489
605
  "success-text": {
490
606
  "value": "var(--hx-color-success-400)",
491
- "description": "WCAG AA compliant success text for dark backgrounds. #4ADE80 = 10.25:1 on neutral-900 dark surface."
607
+ "description": "WCAG AA compliant success text for dark backgrounds. #4ADE80 = 10.26:1 on neutral-900 dark surface."
492
608
  },
493
609
  "text": {
494
610
  "primary": { "value": "var(--hx-color-neutral-100)" },
@@ -510,28 +626,83 @@
510
626
  "link-active": { "value": "var(--hx-color-primary-200)" }
511
627
  },
512
628
  "surface": {
629
+ "_comment": "Dark-mode surface.{role}-strong tokens intentionally do NOT flip — emphasis brand fills remain brand-colored against the dark page, matching the action.{primary,danger}.bg pattern (see action._comment above). The base light values resolve through the same primitives in dark mode.",
513
630
  "default": { "value": "var(--hx-color-neutral-900)" },
514
631
  "raised": { "value": "var(--hx-color-neutral-800)" },
515
632
  "sunken": { "value": "var(--hx-color-neutral-950)" },
516
633
  "inverse": { "value": "var(--hx-color-neutral-100)" },
517
- "overlay": { "value": "rgba(0, 0, 0, 0.85)" }
634
+ "overlay": { "value": "rgba(0, 0, 0, 0.85)" },
635
+ "on-dark-overlay-default": {
636
+ "value": "var(--hx-overlay-black-30)",
637
+ "description": "Dark-mode override. surface.inverse flips light (#EBEEE9) in dark mode, so the base overlay-white-30 binding tints white-on-light (~1.05:1) and disappears. overlay-black-30 tints dark-on-light against the now-light inverse surface and reads correctly."
638
+ },
639
+ "on-dark-overlay-subtle": {
640
+ "value": "var(--hx-overlay-black-10)",
641
+ "description": "Dark-mode override mirroring on-dark-overlay-default's flip rationale. Lowest-emphasis tint on the (now-light in dark mode) inverse surface."
642
+ }
518
643
  },
519
644
  "border": {
520
645
  "default": { "value": "var(--hx-color-neutral-700)" },
521
646
  "subtle": { "value": "var(--hx-color-neutral-800)" },
522
- "strong": { "value": "var(--hx-color-neutral-500)" },
523
- "focus": { "value": "var(--hx-color-primary-400)" }
647
+ "strong": {
648
+ "value": "var(--hx-color-neutral-400)",
649
+ "description": "Dark-mode override. Light mode binds border.strong to neutral-500 (#66787B); dark flips to neutral-400 (#8E9C98) so (a) the border resolves to a different value across modes — preserving the dark-mode-resolution outline-button parity contract — and (b) on dark surface.default (#0D1825) neutral-400 lands at 6.27:1, clearing the WCAG 1.4.11 3:1 floor with comfortable headroom (vs neutral-500's 3.86:1 dark-mode floor)."
650
+ },
651
+ "focus": { "value": "var(--hx-color-primary-400)" },
652
+ "on-dark-strong": {
653
+ "value": "var(--hx-overlay-black-50)",
654
+ "description": "Dark-mode override. surface.inverse flips light (#EBEEE9) in dark mode, so the original overlay-white-70 binding paints white-ish on light = 1.12:1 (invisible). overlay-black-50 over #EBEEE9 = 3.84:1, clears the WCAG 1.4.11 3:1 floor for inverted-mode button outlines and focus rings drawn on the (now-light) inverse surface. Symmetrical with the light-mode 70% intent: dark text on light reaches the equivalent perceptual strength at a lower alpha than light text on dark."
655
+ }
524
656
  },
525
657
  "focus-ring": { "value": "var(--hx-color-primary-400)" },
526
658
  "selection": {
527
659
  "bg": { "value": "var(--hx-color-primary-800)" },
528
660
  "color": { "value": "var(--hx-color-neutral-100)" }
661
+ },
662
+ "action": {
663
+ "_comment": "Dark-mode overrides for the action.* semantic layer. Filled-action surfaces (primary.bg/bg-hover/bg-active, danger.*) intentionally do NOT flip in dark mode for the standard non-inverted button — the brand fill remains brand-colored against the dark page, which is the standard dark-theme treatment for primary/destructive buttons. Outline/ghost treatments DO flip: the resting fg primary-600 fails AA (3.07:1) on dark surface.default, and primary-50 hover-bg is far too bright on a dark page. Mirrors the dark.text.link cascade (light primary-600 link → dark primary-400 link). Inverted-rest filled surfaces (action.primary.bg-inverted-rest) ALSO flip — they paint on surface.inverse, which becomes light (#EBEEE9) in dark mode, and the brand-resting fill needs to clear the WCAG 1.4.11 3:1 UI-component floor against the now-light inverse surface. The paired bg-inverted-hover does NOT have a dark-mode override — flipping it in isolation breaks body-text AA (the foreground binding in hx-button.styles.ts pins text.on-primary = neutral-900, which fails 4.5:1 on any primary stop ≥ 600 against neutral-900). The coordinated bg+fg dark-inverted-hover fix (mode-aware fill stop + mode-aware foreground via text.on-primary-strong) is tracked as a 3.3.x follow-up; see contrast.test.ts:540-546.",
664
+ "primary": {
665
+ "bg-inverted-rest": {
666
+ "value": "var(--hx-color-primary-600)",
667
+ "description": "Dark-mode override. surface.inverse becomes light (#EBEEE9) in dark mode, and primary-500 on #EBEEE9 = 2.94:1 — sub-3:1 fail for the inverted-mode primary button boundary. primary-600 on #EBEEE9 = 4.97:1, clears the WCAG 1.4.11 floor with headroom. Sister to action.secondary.fg's dark flip (primary-600 → primary-400 — opposite direction because secondary.fg paints on surface.default whereas inverted-rest paints on surface.inverse)."
668
+ }
669
+ },
670
+ "secondary": {
671
+ "fg": {
672
+ "value": "var(--hx-color-primary-400)",
673
+ "description": "Dark-mode action.secondary.fg flips to primary-400 (#6AB1B1) — primary-600 on dark surface.default = 3.07:1 (AA fail). primary-400 = 7.27:1 (AA pass). Mirrors dark.text.link's primary-600→primary-400 flip."
674
+ },
675
+ "border": {
676
+ "value": "var(--hx-color-primary-400)",
677
+ "description": "Dark-mode action.secondary.border tracks the fg flip so the outline reads as a single chromatic affordance."
678
+ },
679
+ "bg-hover": {
680
+ "value": "var(--hx-color-primary-900)",
681
+ "description": "Dark-mode hover surface — primary-50 (light fill) on a dark page would be too bright. primary-900 (#0B3232) gives action.secondary.fg (primary-400 = #6AB1B1) on primary-900 = 5.63:1, AA pass. primary-800 was tested first but only reaches 4.14:1, just shy of the body-text floor."
682
+ }
683
+ },
684
+ "ghost": {
685
+ "fg": {
686
+ "value": "var(--hx-color-primary-400)",
687
+ "description": "Dark-mode action.ghost.fg flips to primary-400 alongside action.secondary.fg (same AA gap, same fix)."
688
+ },
689
+ "bg-hover": {
690
+ "value": "var(--hx-color-primary-900)",
691
+ "description": "Dark-mode action.ghost.bg-hover mirrors action.secondary.bg-hover (primary-900 for AA headroom over primary-800)."
692
+ }
693
+ }
529
694
  }
530
695
  },
531
696
  "body": {
532
697
  "bg": { "value": "var(--hx-color-surface-default)" },
533
698
  "color": { "value": "var(--hx-color-text-primary)" }
534
699
  },
700
+ "focus": {
701
+ "ring-color": {
702
+ "value": "var(--hx-color-primary-400)",
703
+ "description": "Dark-mode focus ring flips to primary-400 (#6AB1B1) — primary-600 on dark surface.default = 3.07:1, razor above the 3:1 UI floor (1.4.11) with no headroom. primary-400 on dark surface = 7.27:1, comfortable headroom. Mirrors the dark.color.focus-ring override and the dark.text.link cascade (primary-600 → primary-400)."
704
+ }
705
+ },
535
706
  "shadow": {
536
707
  "sm": { "value": "0 1px 2px 0 rgb(0 0 0 / 0.3)" },
537
708
  "md": { "value": "0 4px 6px -1px rgb(0 0 0 / 0.4), 0 2px 4px -2px rgb(0 0 0 / 0.3)" },
@@ -543,40 +714,40 @@
543
714
  "high-contrast": {
544
715
  "color": {
545
716
  "primary": {
546
- "500": { "value": "#3B82F6", "description": "High-contrast primary — 8.59:1 on #000" },
547
- "600": { "value": "#60A5FA", "description": "High-contrast primary-600 — 10.09:1 on #000" },
548
- "700": { "value": "#93C5FD", "description": "High-contrast primary-700 — 13.02:1 on #000" }
717
+ "500": { "value": "#3B82F6", "description": "High-contrast primary — 5.71:1 on #000" },
718
+ "600": { "value": "#60A5FA", "description": "High-contrast primary-600 — 8.26:1 on #000" },
719
+ "700": { "value": "#93C5FD", "description": "High-contrast primary-700 — 11.65:1 on #000" }
549
720
  },
550
721
  "secondary": {
551
- "500": { "value": "#22D3EE", "description": "High-contrast secondary — 12.40:1 on #000" },
722
+ "500": { "value": "#22D3EE", "description": "High-contrast secondary — 11.62:1 on #000" },
552
723
  "600": {
553
724
  "value": "#67E8F9",
554
- "description": "High-contrast secondary-600 — 15.48:1 on #000"
725
+ "description": "High-contrast secondary-600 — 14.49:1 on #000"
555
726
  }
556
727
  },
557
728
  "error": {
558
- "500": { "value": "#F87171", "description": "High-contrast error — 6.45:1 on #000" },
559
- "600": { "value": "#FCA5A5", "description": "High-contrast error-600 — 10.07:1 on #000" }
729
+ "500": { "value": "#F87171", "description": "High-contrast error — 7.59:1 on #000" },
730
+ "600": { "value": "#FCA5A5", "description": "High-contrast error-600 — 11.06:1 on #000" }
560
731
  },
561
732
  "error-text": {
562
733
  "value": "#FCA5A5",
563
- "description": "WCAG AAA compliant error text on #000 surface — 10.07:1 contrast"
734
+ "description": "WCAG AAA compliant error text on #000 surface — 11.06:1 contrast"
564
735
  },
565
736
  "warning": {
566
- "500": { "value": "#FBBF24", "description": "High-contrast warning — 13.73:1 on #000" },
567
- "600": { "value": "#FCD34D", "description": "High-contrast warning-600 — 15.97:1 on #000" }
737
+ "500": { "value": "#FBBF24", "description": "High-contrast warning — 12.58:1 on #000" },
738
+ "600": { "value": "#FCD34D", "description": "High-contrast warning-600 — 14.56:1 on #000" }
568
739
  },
569
740
  "success": {
570
- "500": { "value": "#4ADE80", "description": "High-contrast success — 10.25:1 on #000" },
571
- "600": { "value": "#86EFAC", "description": "High-contrast success-600 — 14.04:1 on #000" }
741
+ "500": { "value": "#4ADE80", "description": "High-contrast success — 12.05:1 on #000" },
742
+ "600": { "value": "#86EFAC", "description": "High-contrast success-600 — 14.96:1 on #000" }
572
743
  },
573
744
  "success-text": {
574
745
  "value": "#86EFAC",
575
- "description": "WCAG AAA compliant success text on #000 surface — 14.04:1 contrast"
746
+ "description": "WCAG AAA compliant success text on #000 surface — 14.96:1 contrast"
576
747
  },
577
748
  "info": {
578
- "500": { "value": "#38BDF8", "description": "High-contrast info — 10.55:1 on #000" },
579
- "600": { "value": "#7DD3FC", "description": "High-contrast info-600 — 13.47:1 on #000" }
749
+ "500": { "value": "#38BDF8", "description": "High-contrast info — 9.80:1 on #000" },
750
+ "600": { "value": "#7DD3FC", "description": "High-contrast info-600 — 12.60:1 on #000" }
580
751
  },
581
752
  "text": {
582
753
  "primary": { "value": "#FFFFFF", "description": "21:1 contrast on #000 — WCAG AAA" },
@@ -598,6 +769,18 @@
598
769
  "on-success": { "value": "#000000", "description": "Black text on bright HC success" },
599
770
  "on-warning": { "value": "#000000", "description": "Black text on bright HC warning" },
600
771
  "on-info": { "value": "#000000", "description": "Black text on bright HC info" },
772
+ "on-primary-strong": {
773
+ "value": "#000000",
774
+ "description": "HC override for text.on-primary-strong. The light/dark value (neutral-0 / white) is unreadable on HC primary-600 (#60A5FA) and primary-700 (#93C5FD), which are bright fills. Black on HC primary-600 = 8.26:1, on HC primary-700 = 11.65:1 — both AAA. Mirrors the existing on-primary HC override pattern."
775
+ },
776
+ "on-error-strong": {
777
+ "value": "#000000",
778
+ "description": "HC override for text.on-error-strong. White is unreadable on HC error-600 (#FCA5A5 — light red). Black on HC error-600 = 11.06:1, AAA. action.danger.bg-active also has its own HC override (flips to HC error-500) so the base error-700 stop is never paired with this token at runtime; both pairings are asserted in the regression matrix."
779
+ },
780
+ "on-success-strong": {
781
+ "value": "#000000",
782
+ "description": "HC override for text.on-success-strong. The light/dark value (neutral-0 / white) is unreadable on the HC bright-fill success surfaces. Black on HC success-500 (#4ADE80) = 12.05:1 AAA. Sister token to on-primary-strong / on-error-strong HC overrides."
783
+ },
601
784
  "link": { "value": "#FFFF00", "description": "19.56:1 contrast on #000 — high visibility" },
602
785
  "link-hover": { "value": "#FFFF99", "description": "High-contrast hover link" },
603
786
  "link-visited": { "value": "#FF80FF", "description": "High-contrast visited link" },
@@ -611,18 +794,55 @@
611
794
  "value": "#FFFFFF",
612
795
  "description": "Inverse of HC canvas — white surface for dark-always components in HC mode"
613
796
  },
614
- "overlay": { "value": "rgba(0, 0, 0, 0.95)" }
797
+ "overlay": { "value": "rgba(0, 0, 0, 0.95)" },
798
+ "success-strong": {
799
+ "value": "var(--hx-color-success-500)",
800
+ "description": "HC override for surface.success-strong. Light/dark resolves to success-700 (#146831) which is too dark to read against the HC #000 canvas as a fill. Flip to HC success-500 (#4ADE80, 12.05:1 on #000) so emphasis success surfaces remain visible. Pairs with text.inverse (HC = #000000) for 12.05:1 AAA."
801
+ },
802
+ "warning-strong": {
803
+ "value": "var(--hx-color-warning-500)",
804
+ "description": "HC override for surface.warning-strong. Resolves to HC warning-500 (#FBBF24) — bright yellow visible against the HC canvas. Pairs with text.on-warning (HC = #000000) for 12.58:1 AAA."
805
+ },
806
+ "danger-strong": {
807
+ "value": "var(--hx-color-error-500)",
808
+ "description": "HC override for surface.danger-strong. Light/dark resolves to error-600 which has no HC override (palette gap); flip to HC error-500 (#FCA5A5 — bright red) for visibility on the HC canvas. Pairs with text.inverse (HC = #000000) for AA."
809
+ },
810
+ "info-strong": {
811
+ "value": "var(--hx-color-primary-500)",
812
+ "description": "HC override for surface.info-strong. Light/dark resolves to primary-600. HC primary-600 (#60A5FA) is defined but pinning to HC primary-500 (#3B82F6, 5.71:1 on #000) keeps the info-strong surface visually distinct from the primary-strong action layer. Pairs with text.inverse (HC = #000000) for 5.71:1 AA pass."
813
+ },
814
+ "on-dark-overlay-default": {
815
+ "value": "#FFFFFF",
816
+ "description": "HC override for surface.on-dark-overlay-default. Translucent overlays disappear on the HC canvas; pin to solid white so inverted-secondary/ghost hover fills remain visible. Pairs with text.inverse (HC = #000000) for 21:1 AAA when inverted controls are in their hover state."
817
+ },
818
+ "on-dark-overlay-subtle": {
819
+ "value": "#C0C0C0",
820
+ "description": "HC override for surface.on-dark-overlay-subtle. Pinned to neutral silver so the subtle inverted hover affordance remains distinguishable from the strong tier in HC."
821
+ }
615
822
  },
616
823
  "border": {
617
824
  "default": { "value": "#FFFFFF", "description": "21:1 contrast on #000" },
618
825
  "subtle": { "value": "#C0C0C0" },
619
826
  "strong": { "value": "#FFFFFF" },
620
- "focus": { "value": "#FFFF00", "description": "High-visibility yellow focus — WCAG 2.4.7" }
827
+ "focus": { "value": "#FFFF00", "description": "High-visibility yellow focus — WCAG 2.4.7" },
828
+ "on-dark-strong": {
829
+ "value": "#FFFFFF",
830
+ "description": "HC override for border.on-dark-strong. The base overlay-white-70 reads softly against translucent darks but is invisible against the HC #000 canvas — pin to solid #FFFFFF (21:1 on #000) so inverted button outlines remain visible in HC."
831
+ }
621
832
  },
622
833
  "focus-ring": { "value": "#FFFF00", "description": "High-visibility yellow focus ring" },
623
834
  "selection": {
624
835
  "bg": { "value": "#1AEBFF", "description": "High-contrast cyan selection" },
625
836
  "color": { "value": "#000000" }
837
+ },
838
+ "action": {
839
+ "_comment": "HC overrides for the action.* layer. Filled action surfaces in HC must paint a bright system-aligned fill so text.on-{role}-strong (HC = #000000) reads at AA. Without these overrides the base error-700 / primary-700 stops resolve unchanged in HC and pair with #000 at sub-AA ratios (Codex token-cascade finding: text.on-error-strong × action.danger.bg-active = 2.56:1 on base error-700).",
840
+ "danger": {
841
+ "bg-active": {
842
+ "value": "var(--hx-color-error-500)",
843
+ "description": "HC override for action.danger.bg-active. Base error-700 (#A21312) has no HC override, so HC consumers paint black on #A21312 = 2.64:1 — AA fail. Flip to HC error-500 (#F87171, 7.59:1 on #000) so danger pressed states pair with text.on-error-strong (HC = #000000) at AA. Mirrors surface.danger-strong HC pattern (also flips to error-500)."
844
+ }
845
+ }
626
846
  }
627
847
  },
628
848
  "body": {
@@ -766,6 +986,7 @@
766
986
  "--hx-breadcrumb-text-color": null
767
987
  },
768
988
  "hx-button": {
989
+ "--hx-button-active-bg": null,
769
990
  "--hx-button-bg": null,
770
991
  "--hx-button-border-color": null,
771
992
  "--hx-button-border-radius": null,
@@ -775,8 +996,10 @@
775
996
  "--hx-button-font-weight": null,
776
997
  "--hx-button-hover-bg": null,
777
998
  "--hx-button-inverted-color": null,
999
+ "--hx-button-inverted-danger-interactive-color": null,
778
1000
  "--hx-button-inverted-focus-ring-color": null,
779
- "--hx-button-inverted-ghost-hover-bg": null
1001
+ "--hx-button-inverted-ghost-hover-bg": null,
1002
+ "--hx-button-inverted-primary-interactive-color": null
780
1003
  },
781
1004
  "hx-button-group": {
782
1005
  "--hx-button-group-border-radius": null,
@@ -1130,6 +1353,8 @@
1130
1353
  "--hx-nav-item-hover-bg": null,
1131
1354
  "--hx-nav-item-hover-color": null,
1132
1355
  "--hx-nav-item-padding": null,
1356
+ "--hx-nav-item-tooltip-bg": null,
1357
+ "--hx-nav-item-tooltip-color": null,
1133
1358
  "--hx-nav-link-active-bg": null,
1134
1359
  "--hx-nav-link-active-color": null,
1135
1360
  "--hx-nav-link-color": null,
@@ -1287,6 +1512,7 @@
1287
1512
  "--hx-side-nav-footer-padding": null,
1288
1513
  "--hx-side-nav-header-padding": null,
1289
1514
  "--hx-side-nav-toggle-color": null,
1515
+ "--hx-side-nav-toggle-hover-color": null,
1290
1516
  "--hx-side-nav-width": null
1291
1517
  },
1292
1518
  "hx-skeleton": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helixui/tokens",
3
- "version": "3.2.0",
3
+ "version": "3.3.0",
4
4
  "description": "Design tokens for the HELiX enterprise healthcare web component library",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",