basalt-ui 1.19.1 → 1.20.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.
Files changed (78) hide show
  1. package/AGENTS.md +15 -1
  2. package/CHANGELOG.md +20 -0
  3. package/MIGRATING.md +228 -0
  4. package/README.md +53 -9
  5. package/agent/rules/basalt-app.md +43 -0
  6. package/agent/rules/basalt-charts.md +26 -14
  7. package/agent/rules/basalt-mantine.md +25 -10
  8. package/agent/rules/basalt-router.md +22 -10
  9. package/agent/rules/basalt-tokens.md +75 -6
  10. package/agent/skills/basalt-app/SKILL.md +42 -6
  11. package/configs/check.yml +11 -4
  12. package/configs/lefthook.yml +21 -2
  13. package/configs/oxlint-plugin.js +515 -25
  14. package/configs/oxlint-plugin.test.ts +383 -10
  15. package/configs/oxlint.json +10 -1
  16. package/dist/agent/stick-to-bottom.js +1 -1
  17. package/dist/agent-chat/thread-detail-panel.js +1 -1
  18. package/dist/agent-chat/thread-message.js +2 -2
  19. package/dist/agent-chat/tool-chip.js +1 -1
  20. package/dist/charts/kinds/Donut.d.ts.map +1 -1
  21. package/dist/charts/kinds/Donut.js +1 -1
  22. package/dist/charts/primitives/ChartLegend.js +2 -2
  23. package/dist/charts/primitives/ChartTooltip.js +1 -1
  24. package/dist/cli/index.d.ts +107 -15
  25. package/dist/cli/index.d.ts.map +1 -1
  26. package/dist/cli/index.js +610 -72
  27. package/dist/cli/toolchain-wiring.test.d.ts +2 -0
  28. package/dist/cli/toolchain-wiring.test.d.ts.map +1 -0
  29. package/dist/dashboard/delta-badge.js +2 -2
  30. package/dist/dashboard/stat-card.js +1 -1
  31. package/dist/data/virtual-list.js +5 -5
  32. package/dist/guard/index.d.ts +56 -2
  33. package/dist/guard/index.d.ts.map +1 -1
  34. package/dist/guard/index.js +326 -59
  35. package/dist/guard/types.d.ts +25 -5
  36. package/dist/guard/types.d.ts.map +1 -1
  37. package/dist/register.d.ts +25 -0
  38. package/dist/register.d.ts.map +1 -1
  39. package/dist/shell/app-mobile-nav.module.css +11 -3
  40. package/dist/surfaces.d.ts +1 -1
  41. package/dist/surfaces.js +1 -1
  42. package/dist/tokens/index.d.ts +3 -3
  43. package/dist/tokens/index.d.ts.map +1 -1
  44. package/dist/tokens/index.js +2 -0
  45. package/dist/tokens/palette.d.ts +35 -27
  46. package/dist/tokens/palette.d.ts.map +1 -1
  47. package/dist/tokens/palette.js +24 -18
  48. package/dist/tokens.css +19 -17
  49. package/dist/vite-color-mix.js +2 -2
  50. package/llms.txt +3 -3
  51. package/package.json +4 -3
  52. package/src/agent/stick-to-bottom.tsx +1 -1
  53. package/src/agent-chat/thread-detail-panel.tsx +1 -1
  54. package/src/agent-chat/thread-message.tsx +3 -3
  55. package/src/agent-chat/tool-chip.tsx +1 -1
  56. package/src/charts/kinds/Donut.tsx +2 -1
  57. package/src/charts/primitives/ChartLegend.tsx +2 -2
  58. package/src/charts/primitives/ChartTooltip.tsx +1 -1
  59. package/src/cli/index.ts +1070 -99
  60. package/src/cli/init-sync.test.ts +19 -3
  61. package/src/cli/tokens-css.test.ts +27 -14
  62. package/src/cli/toolchain-wiring.test.ts +694 -0
  63. package/src/dashboard/delta-badge.tsx +3 -3
  64. package/src/dashboard/stat-card.tsx +1 -1
  65. package/src/data/virtual-list.tsx +8 -8
  66. package/src/guard/check-source.test.ts +663 -11
  67. package/src/guard/index.ts +727 -94
  68. package/src/guard/types.ts +29 -4
  69. package/src/register.ts +32 -0
  70. package/src/shell/app-mobile-nav.module.css +11 -3
  71. package/src/shell/app-mobile-nav.tsx +1 -1
  72. package/src/surfaces.ts +1 -1
  73. package/src/theme/density-relations.test.ts +8 -5
  74. package/src/theme/spacing.test.ts +2 -0
  75. package/src/theme-toggle/index.tsx +2 -2
  76. package/src/tokens/index.ts +5 -3
  77. package/src/tokens/palette.ts +24 -18
  78. package/src/vite-color-mix.ts +2 -2
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * Guard types — Mantine-free, dependency-free (zero imports beyond TS types).
3
3
  *
4
- * GuardKind is the closed set of 20 violation kinds the theme guard can emit.
4
+ * GuardKind is the closed set of 25 violation kinds the theme guard can emit.
5
5
  * Finding is the structured result per violation, replacing the old `Violation` shape.
6
6
  * GuardConfig is the per-run configuration that drives checkSource.
7
7
  */
8
8
 
9
- /** The 20 theme-guard violation kinds. */
9
+ /** The 25 theme-guard violation kinds. */
10
10
  export type GuardKind =
11
11
  | 'raw-hex'
12
12
  | 'raw-color-fn'
@@ -28,6 +28,11 @@ export type GuardKind =
28
28
  | 'raw-form-control'
29
29
  | 'sub-16-input-font'
30
30
  | 'raw-font-family'
31
+ | 'theme-allow-unscoped'
32
+ | 'surface-shadow-override'
33
+ | 'css-raw-surface'
34
+ | 'inline-font-size'
35
+ | 'hidden-inline-style'
31
36
 
32
37
  /**
33
38
  * How hard a finding lands. `error` fails the build; `warn` reports and passes.
@@ -151,10 +156,30 @@ export type GuardConfig = {
151
156
  */
152
157
  readonly sub16InputFont: boolean
153
158
  /**
154
- * Allow-comment policy: a line containing this substring is skipped entirely.
155
- * Default 'theme-allow'. Pure-comment lines (// * /\*) are always skipped.
159
+ * Allow-comment policy. The token that opens an exception annotation inside a COMMENT, on the
160
+ * finding's own line or the line directly above it (matching the oxlint plugin's placement, and
161
+ * surviving an `oxfmt` reflow that moves the comment off the offending line). Default
162
+ * `'theme-allow'`.
163
+ *
164
+ * The accountable form is `theme-allow <rule-id>[, <rule-id>] — <reason>`: the rule ids scope the
165
+ * exception to those kinds only, and the reason says why. A `theme-allow` missing either half
166
+ * still suppresses (so an upgrade never breaks a build over it) but reports
167
+ * {@link GuardKind} `theme-allow-unscoped` on its own line.
156
168
  */
157
169
  readonly allowComment: string
170
+ /**
171
+ * Which kinds apply to this consumer. `'mantine'` (default) is the full framework guard.
172
+ *
173
+ * `'tokens-only'` is for a consumer that installs the TOKEN LAYER and nothing else — no
174
+ * `@mantine/core`, often no React (`basalt-ui tokens:css` + `basalt-ui/tokens`). Every kind whose
175
+ * remedy is a Mantine component or prop is disabled there, because "use `TextInput` from
176
+ * `@mantine/core`" is not advice, it is a dependency the consumer deliberately does not have.
177
+ * The color/typography kinds stay on — those are the tokens, which is the whole surface they DO
178
+ * consume. See `TOKENS_ONLY_DISABLED_KINDS` in `./index` for the exact set.
179
+ *
180
+ * Optional so a caller built before this field existed keeps compiling; absent means `'mantine'`.
181
+ */
182
+ readonly profile?: 'mantine' | 'tokens-only'
158
183
  /**
159
184
  * Per-rule, per-path exemptions — complements whole-file `exempt` (BasaltConfig, which skips
160
185
  * ALL rules for a file) and code-level `appliesTo` (hardcoded per-kind path scoping, e.g.
package/src/register.ts CHANGED
@@ -156,3 +156,35 @@ export namespace StandardSchemaV1 {
156
156
  T['~standard']['types']
157
157
  >['output']
158
158
  }
159
+
160
+ // ── Ambient app globals ───────────────────────────────────────────────────────────────────────────
161
+
162
+ declare global {
163
+ /**
164
+ * The app's version string, injected as a build-time constant by basalt's own Vite preset —
165
+ * `basaltViteConfig` defines it from `BUILD_VERSION`, falling back to the app package.json
166
+ * `version` (see `../vite`, the `define` block).
167
+ *
168
+ * Declared HERE rather than beside the define because a consumer's `src/**` never loads
169
+ * `basalt-ui/vite`'s types (that subpath is imported from `vite.config.ts`, which most apps keep
170
+ * in a separate tsconfig project). This file is re-exported from the root barrel, so an app that
171
+ * imports from `'basalt-ui'` at all has it in scope with no `/// <reference>` and no
172
+ * `src/vite-env.d.ts` entry — which is the point: basalt shipped the define and no declaration,
173
+ * so every consumer hand-wrote this block, one of them needing an `oxlint-disable` in a file that
174
+ * existed only because basalt asked for it.
175
+ *
176
+ * A consumer importing ONLY a subpath (`basalt-ui/tokens`, `basalt-ui/charts`) does not load this
177
+ * file and does not get the global. That gap is the same set as the applicability gap: reaching
178
+ * for the root barrel is what `BasaltProvider` requires, and an app that never does is not on
179
+ * basalt's Vite preset either, so there is no `__APP_VERSION__` to declare.
180
+ *
181
+ * Precondition: only defined when your Vite config actually goes through `basaltViteConfig` (or
182
+ * you define it yourself). An app that hand-rolls its Vite config and reads this gets a
183
+ * ReferenceError at runtime — TypeScript is trusting the preset, not proving it.
184
+ */
185
+ // The dangle IS the API: this declares the build-time constant basalt's own Vite preset defines,
186
+ // so the name is not ours to change. The SHIPPED preset allows it (configs/oxlint.json); this
187
+ // repo's own root .oxlintrc.json does not, hence the local suppression.
188
+ // oxlint-disable-next-line no-underscore-dangle
189
+ const __APP_VERSION__: string
190
+ }
@@ -46,9 +46,17 @@
46
46
  display: inline-flex;
47
47
  align-items: center;
48
48
  justify-content: center;
49
- /* Deliberately sub-scale: the active pill hugs a 24px glyph, so it is a shape inset rather than
50
- * layout spacing — the xs..xl scale has no step this tight. */
51
- padding: 2px 12px; /* theme-allow */
49
+ /* This span IS the active pill, so its box may not depend on what it contains: shipping no icon
50
+ * dependency is a supported configuration, and an empty span collapsed the pill to a 24x4px
51
+ * dash. The floor is the icon box PLUS its inset — everything is border-box under Mantine's
52
+ * reset — so the pill renders identically with and without a glyph, at every density. */
53
+ min-width: calc(
54
+ var(--vx-space-mobile-nav-icon-size) + 2 * var(--vx-space-mobile-nav-tab-inset-x)
55
+ );
56
+ min-height: calc(
57
+ var(--vx-space-mobile-nav-icon-size) + 2 * var(--vx-space-mobile-nav-tab-inset-y)
58
+ );
59
+ padding: var(--vx-space-mobile-nav-tab-inset-y) var(--vx-space-mobile-nav-tab-inset-x);
52
60
  border-radius: var(--vx-radius-ctrl);
53
61
  transition: transform 140ms cubic-bezier(0.32, 0.72, 0, 1);
54
62
  }
@@ -38,7 +38,7 @@ import classes from './app-mobile-nav.module.css'
38
38
  * bottom-anchored menu puts it UNDER the footer, off-screen — `menuMax` is what guarantees it fits
39
39
  * above the bar (§2.2). `shift.padding` is a viewport inset in px, not a layout spacing token.
40
40
  */
41
- const MENU_MIDDLEWARES = { flip: false, shift: { padding: 8 } } // theme-allow
41
+ const MENU_MIDDLEWARES = { flip: false, shift: { padding: 8 } } // theme-allow inline-spacing — a floating-ui viewport inset, not layout spacing
42
42
 
43
43
  /** A slot that navigates on tap. */
44
44
  type LinkSlot = Extract<MobileNavSlot, { kind: 'link' }>
package/src/surfaces.ts CHANGED
@@ -467,7 +467,7 @@ export const SURFACES = {
467
467
  kind: 'tooling',
468
468
  layer: 'non-js-asset',
469
469
  description:
470
- 'Prebuilt --vx-* token stylesheet — the default buildPaletteCss() output as a plain file, for a consumer with no bundler, no React and no Mantine; retune the selector or trim the spacing set with `basalt-ui tokens:css`',
470
+ 'Prebuilt --vx-* token stylesheet — the default buildPaletteCss() output as a plain file, for a consumer that has a bundler to resolve the subpath but no React and no Mantine; with no bundler, emit a file you own via `basalt-ui tokens:css`',
471
471
  forbiddenImports: [],
472
472
  },
473
473
  './configs/*': {
@@ -266,17 +266,20 @@ describe("Fix 7 — the knob's resolution is honest at every notch", () => {
266
266
  expect(minRatio).toBeLessThan(0.7)
267
267
  })
268
268
 
269
- test('frozen-vs-baseline ceiling: fewer than 25 of the ~108 values equal their level-0 value at any non-zero level', () => {
270
- // Measured under the 0.1/±3 law: worst case is 22 (at level ±1) — down from 43 under the old
271
- // 0.06/±5 law. The ceiling of 25 leaves margin while still catching a future step reduction
272
- // that reintroduces that dead zone.
269
+ test('frozen-vs-baseline ceiling: fewer than 27 of the ~129 values equal their level-0 value at any non-zero level', () => {
270
+ // Measured under the 0.1/±3 law: worst case is 25 (at level −1) — down from 43 under the old
271
+ // 0.06/±5 law. It is 25 rather than 24 because `mobileNavTabInsetY` (base 2) joined the set:
272
+ // a base that small rounds back to itself at ±1 and ±2 under any multiplier this law can
273
+ // produce, exactly as `mobileNavTabGap` (base 3) already did. That is the rounding law, not a
274
+ // resolution regression — the ceiling of 27 keeps the same margin over the measured worst case
275
+ // while still catching a future step reduction that reintroduces the real dead zone.
273
276
  const baseline = flattenSpacing(0)
274
277
  const keys = Object.keys(baseline)
275
278
  for (const level of ALL_LEVELS) {
276
279
  if (level === 0) continue
277
280
  const values = flattenSpacing(level)
278
281
  const frozen = keys.filter((k) => values[k] === baseline[k]).length
279
- expect(frozen).toBeLessThan(25)
282
+ expect(frozen).toBeLessThan(27)
280
283
  }
281
284
  })
282
285
 
@@ -357,6 +357,8 @@ const SPACE_STEP_SWEEP: ReadonlyArray<
357
357
  // `app-mobile-nav.tsx` hands `mobileNavMenuWidth` to `<Menu width={…}>`.
358
358
  ['mobileNavBarHeight', 56, null],
359
359
  ['mobileNavIconSize', 24, 'space-mobile-nav-icon-size'],
360
+ ['mobileNavTabInsetY', 2, 'space-mobile-nav-tab-inset-y'],
361
+ ['mobileNavTabInsetX', 12, 'space-mobile-nav-tab-inset-x'],
360
362
  ['mobileNavRowHeight', 44, 'space-mobile-nav-row-height'],
361
363
  ['mobileNavMenuWidth', 232, null],
362
364
  ['agentRailInsetX', 10, 'space-agent-rail-inset-x'],
@@ -84,7 +84,7 @@ function SchemeGlyph({ dark, reduceMotion }: { dark: boolean; reduceMotion: bool
84
84
  {dark ? (
85
85
  <motion.span
86
86
  key="moon"
87
- style={{ display: 'inline-flex' }} // theme-allow: framer-motion element — inline-flex glyph wrapper, not a Mantine node
87
+ style={{ display: 'inline-flex' }} // theme-allow inline-display — a framer-motion glyph wrapper, not a Mantine layout node
88
88
  initial={{ opacity: 0, scale: 0.6, rotate: 30 }}
89
89
  animate={{ opacity: 1, scale: 1, rotate: 0 }}
90
90
  exit={{ opacity: 0, scale: 0.6, rotate: -30 }}
@@ -95,7 +95,7 @@ function SchemeGlyph({ dark, reduceMotion }: { dark: boolean; reduceMotion: bool
95
95
  ) : (
96
96
  <motion.span
97
97
  key="sun"
98
- style={{ display: 'inline-flex' }} // theme-allow: framer-motion element — inline-flex glyph wrapper, not a Mantine node
98
+ style={{ display: 'inline-flex' }} // theme-allow inline-display — a framer-motion glyph wrapper, not a Mantine layout node
99
99
  initial={{ opacity: 0, scale: 0.6, rotate: -30 }}
100
100
  animate={{ opacity: 1, scale: 1, rotate: 0 }}
101
101
  exit={{ opacity: 0, scale: 0.6, rotate: 30 }}
@@ -533,6 +533,8 @@ function spaceDecls(space: SpaceValues): string[] {
533
533
  decl('space-app-header-mobile-actions-height', `${space.step.appHeaderMobileActionsHeight}px`),
534
534
  decl('space-mobile-nav-tab-gap', `${space.step.mobileNavTabGap}px`),
535
535
  decl('space-mobile-nav-icon-size', `${space.step.mobileNavIconSize}px`),
536
+ decl('space-mobile-nav-tab-inset-y', `${space.step.mobileNavTabInsetY}px`),
537
+ decl('space-mobile-nav-tab-inset-x', `${space.step.mobileNavTabInsetX}px`),
536
538
  decl('space-mobile-nav-row-height', `${space.step.mobileNavRowHeight}px`),
537
539
  decl('space-agent-rail-inset-x', `${space.step.agentRailInsetX}px`),
538
540
  decl('space-agent-part-gap-top', `${space.step.agentPartGapTop}px`),
@@ -758,13 +760,13 @@ export type BuildPaletteOpts = {
758
760
  */
759
761
  mediaFallback?: boolean
760
762
  /**
761
- * How much of the SPACING set to emit. Default: `'all'` (every one of the 104 `--vx-space-*`
763
+ * How much of the SPACING set to emit. Default: `'all'` (every one of the 108 `--vx-space-*`
762
764
  * declarations the framework's own components read).
763
765
  *
764
766
  * `'core'` keeps the 9 generic anchors — the stack rhythm, the two control heights, the row
765
- * insets — and drops the 95 named for a specific basalt component
767
+ * insets — and drops the 99 named for a specific basalt component
766
768
  * (`--vx-space-agent-transcript-inset`, `--vx-space-toc-sub-indent`, …), taking the emitted set
767
- * from 197 variables to 102. Nothing outside basalt's own React components reads those, so a
769
+ * from 202 variables to 103. Nothing outside basalt's own React components reads those, so a
768
770
  * framework-free consumer is carrying them for nothing.
769
771
  *
770
772
  * This is a SPACING filter only: color, radius, shadow, type and status variables are identical
@@ -97,12 +97,12 @@ export const p = (fam: readonly string[], light = 2, dark = 3): ColorPair => ({
97
97
  */
98
98
  export const SHADOW = {
99
99
  card: {
100
- light: '0 1px 2px rgba(28,25,23,0.05), 0 0 0 1px var(--vx-surface-hairline)',
101
- dark: '0 1px 3px rgba(0,0,0,0.4), inset 0 0 0 1px color-mix(in srgb, #ffffff 4%, transparent)',
100
+ light: '0 1px 2px rgba(28, 25, 23, 0.05), 0 0 0 1px var(--vx-surface-hairline)',
101
+ dark: '0 1px 3px rgba(0, 0, 0, 0.4), inset 0 0 0 1px color-mix(in srgb, #ffffff 4%, transparent)',
102
102
  },
103
103
  ctrl: {
104
- light: '0 1px 2px rgba(28,25,23,0.12)',
105
- dark: '0 1px 2px rgba(0,0,0,0.35)',
104
+ light: '0 1px 2px rgba(28, 25, 23, 0.12)',
105
+ dark: '0 1px 2px rgba(0, 0, 0, 0.35)',
106
106
  },
107
107
  /**
108
108
  * Interactive-control depth — the ONE value every raised Button/ActionIcon/Input variant shares
@@ -132,16 +132,16 @@ export const SHADOW = {
132
132
  * ever needs more, raise the DROP. Do not reintroduce an edge; both shapes have been tried.
133
133
  */
134
134
  raised: {
135
- light: '0 1px 2px rgba(28,25,23,0.10)',
136
- dark: '0 1px 2px rgba(0,0,0,0.35), inset 0 0 0 1px color-mix(in srgb, #ffffff 8%, transparent)',
135
+ light: '0 1px 2px rgba(28, 25, 23, 0.10)',
136
+ dark: '0 1px 2px rgba(0, 0, 0, 0.35), inset 0 0 0 1px color-mix(in srgb, #ffffff 8%, transparent)',
137
137
  },
138
138
  // Floating-layer elevation (menus, popovers, tooltips, modals, drawers) — a REAL drop shadow, a
139
139
  // step above `card`, so detached surfaces read as lifted off the page (`docs/DESIGN-SPEC.md` §5).
140
140
  // Unlike `card`, no embedded ring: floating surfaces carry a real 1px `--vx-surface-border` so
141
141
  // Mantine's rotated-square arrow inherits a proper edge.
142
142
  overlay: {
143
- light: '0 8px 24px rgba(28,25,23,0.10)',
144
- dark: '0 8px 24px rgba(0,0,0,0.5)',
143
+ light: '0 8px 24px rgba(28, 25, 23, 0.10)',
144
+ dark: '0 8px 24px rgba(0, 0, 0, 0.5)',
145
145
  },
146
146
  } as const
147
147
 
@@ -613,8 +613,14 @@ const SPACE_STEP_BASE = {
613
613
  * `env(safe-area-inset-bottom)` to it. JS-consumed (AppShell `footer.height`), no `--vx-*` var,
614
614
  * same shape as `appShellHeaderHeight`. Floored at 48px in `deriveSpacing`. */
615
615
  mobileNavBarHeight: 56,
616
- /** Slot icon box. */
616
+ /** Slot icon box. Also the MIN box of the icon slot itself, so the active pill keeps its shape
617
+ * when a destination ships no icon (an icon-less consumer is a supported configuration). */
617
618
  mobileNavIconSize: 24,
619
+ /** Active-pill inset around the slot icon, vertical. Sub-scale on its own, but it is one half of
620
+ * a pair with `mobileNavTabInsetX` — a step keeps the pill's shape tracking density. */
621
+ mobileNavTabInsetY: 2,
622
+ /** Active-pill inset around the slot icon, horizontal. */
623
+ mobileNavTabInsetX: 12,
618
624
  /** Menu/sheet row height. Floored at 44px in `deriveSpacing` (Apple HIG 44pt / WCAG 2.5.5 AAA). */
619
625
  mobileNavRowHeight: 44,
620
626
  /** More/group `<Menu width={…}>`. JS-consumed, no var — same rationale as
@@ -1059,22 +1065,22 @@ function buildPaletteDataUncached(config: DeriveConfig) {
1059
1065
  const NEUTRAL = {
1060
1066
  line: { light: BP.gray[0], dark: BP.gray[4] }, // primary line
1061
1067
  line2: { light: BP.gray[1], dark: BP.gray[2] }, // secondary line
1062
- axis: { light: inkRgba(inkLight, 0.6), dark: 'rgba(255,255,255,0.6)' },
1063
- axisStroke: { light: inkRgba(inkLight, 0.12), dark: 'rgba(255,255,255,0.12)' },
1064
- grid: { light: inkRgba(inkLight, 0.07), dark: 'rgba(255,255,255,0.06)' },
1065
- crosshair: { light: inkRgba(inkLight, 0.32), dark: 'rgba(255,255,255,0.42)' },
1068
+ axis: { light: inkRgba(inkLight, 0.6), dark: 'rgba(255, 255, 255, 0.6)' },
1069
+ axisStroke: { light: inkRgba(inkLight, 0.12), dark: 'rgba(255, 255, 255, 0.12)' },
1070
+ grid: { light: inkRgba(inkLight, 0.07), dark: 'rgba(255, 255, 255, 0.06)' },
1071
+ crosshair: { light: inkRgba(inkLight, 0.32), dark: 'rgba(255, 255, 255, 0.42)' },
1066
1072
  dotStroke: { light: BP.white, dark: BP.darkGray[1] }, // matches chart-area bg
1067
- tooltipBg: { light: BP.white, dark: 'rgba(39,39,42,0.96)' },
1068
- tooltipText: { light: inkRgba(inkLight, 0.88), dark: 'rgba(255,255,255,0.88)' },
1069
- tooltipMuted: { light: inkRgba(inkLight, 0.5), dark: 'rgba(255,255,255,0.5)' },
1073
+ tooltipBg: { light: BP.white, dark: 'rgba(39, 39, 42, 0.96)' },
1074
+ tooltipText: { light: inkRgba(inkLight, 0.88), dark: 'rgba(255, 255, 255, 0.88)' },
1075
+ tooltipMuted: { light: inkRgba(inkLight, 0.5), dark: 'rgba(255, 255, 255, 0.5)' },
1070
1076
  tooltipBorder: { light: `1px solid ${BP.lightGray[1]}`, dark: 'none' },
1071
1077
  tooltipShadow: {
1072
1078
  light: `0 2px 8px ${inkRgba(inkLight, 0.1)}`,
1073
- dark: '0 2px 8px rgba(0,0,0,0.35)',
1079
+ dark: '0 2px 8px rgba(0, 0, 0, 0.35)',
1074
1080
  },
1075
1081
  // Chart legend text — same DERIVED-ink treatment as axis/grid/tooltip* above (light keys off
1076
1082
  // `inkLight`; dark stays flat white like every other neutral chart-chrome pair here).
1077
- legendText: { light: inkRgba(inkLight, 0.82), dark: 'rgba(255,255,255,0.92)' },
1083
+ legendText: { light: inkRgba(inkLight, 0.82), dark: 'rgba(255, 255, 255, 0.92)' },
1078
1084
  // Base neutral for hairlines / muted / DIMMED text / overlays — apply opacity via alpha().
1079
1085
  // Binds to --mantine-color-dimmed, so it is THE dimmed-text lever. Equal to `INK.muted` (the
1080
1086
  // spec's "muted (secondary text)" row) — kept as its own pair for the pre-existing `--vx-neutral`
@@ -28,8 +28,8 @@
28
28
  * what a color parser must never do.
29
29
  */
30
30
  const NAMED_COLORS: Record<string, string> = {
31
- white: '#ffffff', // theme-allow — CSS keyword definition, not a design color
32
- black: '#000000', // theme-allow — CSS keyword definition, not a design color
31
+ white: '#ffffff', // theme-allow raw-hex — the CSS keyword's definition, not a design color
32
+ black: '#000000', // theme-allow raw-hex — the CSS keyword's definition, not a design color
33
33
  }
34
34
 
35
35
  function normalizeHex(hex: string): string {