basalt-ui 1.32.1 → 1.33.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 (137) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/MIGRATING.md +124 -24
  3. package/agent/rules/basalt-state.md +2 -2
  4. package/agent/rules/basalt-tokens.md +1 -1
  5. package/configs/oxlint-plugin.js +68 -61
  6. package/configs/oxlint-plugin.test.ts +126 -9
  7. package/configs/oxlint.json +2 -2
  8. package/dist/agent/coalesce.d.ts.map +1 -1
  9. package/dist/agent/coalesce.js +1 -2
  10. package/dist/agent/merge.d.ts.map +1 -1
  11. package/dist/agent/merge.js +1 -2
  12. package/dist/agent-chat/relative-time.d.ts +9 -0
  13. package/dist/agent-chat/relative-time.d.ts.map +1 -1
  14. package/dist/agent-chat/relative-time.js +1 -2
  15. package/dist/agent-chat/thread-message.d.ts.map +1 -1
  16. package/dist/agent-chat/thread-message.js +1 -2
  17. package/dist/charts/kinds/BandStrip.d.ts.map +1 -1
  18. package/dist/charts/kinds/BandStrip.js +3 -3
  19. package/dist/charts/kinds/Bars.d.ts.map +1 -1
  20. package/dist/charts/kinds/Bars.js +1 -2
  21. package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
  22. package/dist/charts/kinds/StackedArea.js +1 -2
  23. package/dist/charts/primitives/legend-disclosure.d.ts.map +1 -1
  24. package/dist/cli/audit-allows.d.ts +1 -1
  25. package/dist/cli/audit-allows.d.ts.map +1 -1
  26. package/dist/cli/audit-allows.js +4 -4
  27. package/dist/cli/check-theme.d.ts.map +1 -1
  28. package/dist/cli/check-theme.js +1 -2
  29. package/dist/cli/config.d.ts +197 -0
  30. package/dist/cli/config.d.ts.map +1 -0
  31. package/dist/cli/config.js +20 -0
  32. package/dist/cli/doctor.js +1 -1
  33. package/dist/cli/guard-hook.d.ts.map +1 -1
  34. package/dist/cli/guard-hook.js +2 -8
  35. package/dist/cli/index.d.ts +2 -196
  36. package/dist/cli/index.d.ts.map +1 -1
  37. package/dist/cli/index.js +1 -18
  38. package/dist/commands/define-commands.d.ts.map +1 -1
  39. package/dist/commands/define-commands.js +2 -2
  40. package/dist/commands/define-overlays.d.ts.map +1 -1
  41. package/dist/commands/define-overlays.js +1 -2
  42. package/dist/common/index.d.ts +0 -1
  43. package/dist/common/index.d.ts.map +1 -1
  44. package/dist/common/index.js +0 -2
  45. package/dist/common/validate.d.ts +2 -2
  46. package/dist/common/validate.d.ts.map +1 -1
  47. package/dist/common/validate.js +2 -3
  48. package/dist/content/article-card.d.ts.map +1 -1
  49. package/dist/content/article-card.js +4 -1
  50. package/dist/controls/enum-filter.d.ts.map +1 -1
  51. package/dist/controls/enum-filter.js +3 -13
  52. package/dist/controls/filter-pill.d.ts +8 -0
  53. package/dist/controls/filter-pill.d.ts.map +1 -1
  54. package/dist/controls/filter-pill.js +15 -0
  55. package/dist/controls/multi-select-filter.d.ts.map +1 -1
  56. package/dist/controls/multi-select-filter.js +3 -13
  57. package/dist/controls/number-filter.d.ts +3 -0
  58. package/dist/controls/number-filter.d.ts.map +1 -1
  59. package/dist/controls/number-filter.js +12 -4
  60. package/dist/controls/range-filter.d.ts.map +1 -1
  61. package/dist/controls/range-filter.js +1 -2
  62. package/dist/data/data-table.d.ts.map +1 -1
  63. package/dist/data/data-table.js +2 -3
  64. package/dist/guard/index.d.ts +16 -2
  65. package/dist/guard/index.d.ts.map +1 -1
  66. package/dist/guard/index.js +63 -22
  67. package/dist/guard/types.d.ts +3 -3
  68. package/dist/guard/types.d.ts.map +1 -1
  69. package/dist/guard/vx-names.d.ts +3 -0
  70. package/dist/guard/vx-names.d.ts.map +1 -0
  71. package/dist/guard/vx-names.js +257 -0
  72. package/dist/notifications/mount-guard.d.ts.map +1 -1
  73. package/dist/notifications/mount-guard.js +1 -2
  74. package/dist/provider/index.d.ts.map +1 -1
  75. package/dist/provider/index.js +2 -3
  76. package/dist/provider/lab-theme.d.ts +5 -4
  77. package/dist/provider/lab-theme.d.ts.map +1 -1
  78. package/dist/provider/lab-theme.js +1 -3
  79. package/dist/router-tanstack/search-store.d.ts +18 -0
  80. package/dist/router-tanstack/search-store.d.ts.map +1 -1
  81. package/dist/router-tanstack/search-store.js +33 -1
  82. package/dist/shell/mobile-nav-model.d.ts.map +1 -1
  83. package/dist/shell/mobile-nav-model.js +3 -4
  84. package/dist/shell/page-bar.d.ts.map +1 -1
  85. package/dist/shell/page-bar.js +1 -2
  86. package/dist/surfaces.d.ts +1 -1
  87. package/dist/surfaces.d.ts.map +1 -1
  88. package/dist/surfaces.js +2 -0
  89. package/dist/tokens/index.d.ts +1 -1
  90. package/dist/tokens/index.d.ts.map +1 -1
  91. package/dist/tokens/index.js +7 -1
  92. package/dist/tokens/size-classes.d.ts +13 -0
  93. package/dist/tokens/size-classes.d.ts.map +1 -1
  94. package/dist/tokens/size-classes.js +8 -0
  95. package/llms.txt +2 -2
  96. package/package.json +1 -1
  97. package/src/agent/coalesce.ts +1 -2
  98. package/src/agent/merge.ts +1 -2
  99. package/src/agent-chat/relative-time.ts +1 -2
  100. package/src/agent-chat/thread-message.tsx +2 -3
  101. package/src/charts/kinds/BandStrip.tsx +7 -3
  102. package/src/charts/kinds/Bars.tsx +1 -2
  103. package/src/charts/kinds/StackedArea.tsx +4 -2
  104. package/src/charts/primitives/legend-disclosure.tsx +1 -0
  105. package/src/cli/audit-allows.ts +10 -9
  106. package/src/cli/check-theme.ts +1 -2
  107. package/src/cli/config.ts +209 -0
  108. package/src/cli/doctor.ts +2 -2
  109. package/src/cli/guard-hook.ts +2 -8
  110. package/src/cli/index.ts +2 -206
  111. package/src/commands/define-commands.ts +2 -2
  112. package/src/commands/define-overlays.ts +1 -2
  113. package/src/common/index.ts +0 -1
  114. package/src/common/validate.ts +4 -5
  115. package/src/content/article-card.tsx +1 -0
  116. package/src/controls/enum-filter.tsx +3 -13
  117. package/src/controls/filter-pill.tsx +20 -0
  118. package/src/controls/multi-select-filter.tsx +3 -13
  119. package/src/controls/number-filter.tsx +24 -3
  120. package/src/controls/range-filter.tsx +1 -2
  121. package/src/data/data-table.tsx +2 -3
  122. package/src/guard/index.ts +136 -46
  123. package/src/guard/types.ts +3 -2
  124. package/src/guard/vx-names.ts +257 -0
  125. package/src/notifications/mount-guard.ts +1 -2
  126. package/src/provider/index.tsx +2 -3
  127. package/src/provider/lab-theme.ts +28 -21
  128. package/src/router-tanstack/search-store.ts +73 -1
  129. package/src/shell/mobile-nav-model.ts +3 -5
  130. package/src/shell/page-bar.tsx +1 -4
  131. package/src/surfaces.ts +2 -0
  132. package/src/tokens/index.ts +6 -1
  133. package/src/tokens/size-classes.ts +14 -0
  134. package/dist/common/is-dev.d.ts +0 -3
  135. package/dist/common/is-dev.d.ts.map +0 -1
  136. package/dist/common/is-dev.js +0 -6
  137. package/src/common/is-dev.ts +0 -4
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * ./guard — headless policy core. Mantine-free, dependency-free.
3
3
  *
4
- * GUARD_RULES: the closed registry of all 27 violation kinds.
4
+ * GUARD_RULES: the closed registry of all 28 violation kinds.
5
5
  * checkSource: pure (text, relPath, cfg) → Finding[]. No FS, no walk, no console.
6
6
  */
7
7
  import type { Finding, GuardConfig, GuardKind, GuardSeverity } from './types'
8
+ import { BASALT_VX_NAMES } from './vx-names'
8
9
 
9
10
  export type { Finding, GuardConfig, GuardKind, GuardSeverity }
10
11
 
@@ -286,6 +287,62 @@ const INLINE_FONT_SIZE =
286
287
  const CARD_SURFACE_TAG = /<(?:Card|Paper)(?![\w.])(?:=>|[^>])*?>/g
287
288
  const WITH_BORDER_PROP = /\bwithBorder\b(?!\s*=\s*\{\s*false\s*\})/
288
289
 
290
+ /**
291
+ * A `var(--vx-*)` REFERENCE with a literal name. The name must end on an alphanumeric and must not
292
+ * run into a `${…}` interpolation, so `var(--vx-fill-${hue})` — a name computed at runtime — never
293
+ * matches a truncated `--vx-fill`.
294
+ */
295
+ const VX_VAR_REF = /var\(\s*(--vx-[A-Za-z0-9-]*[A-Za-z0-9])(?![A-Za-z0-9-]|\$\{)/g
296
+
297
+ /**
298
+ * A `--vx-*` name the file DECLARES — a consumer's own custom property, never unknown. Only a real
299
+ * declaration position counts, because the set is file-wide and one false hit silences every
300
+ * finding for that name in the file:
301
+ *
302
+ * - a CSS declaration (`--vx-x: …`, in a stylesheet or in CSS text inside a template literal),
303
+ * directly after a line start, `{` or `;`;
304
+ * - a quoted style-object key (`{ '--vx-x': … }`), directly after `{` or `,` — but not a TS
305
+ * member key (`'--vx-x'?: …`, or one typed `string`/`number`);
306
+ * - `setProperty('--vx-x', …)`.
307
+ *
308
+ * A quoted name used as an EXPRESSION — a ternary arm, a `case` label, an argument — is a reference,
309
+ * not a declaration, and never matches.
310
+ */
311
+ const VX_LOCAL_DECLARATION =
312
+ /(?:^|[{;])\s*(--vx-[A-Za-z0-9-]*[A-Za-z0-9])\s*:|[{,]\s*(['"])(--vx-[A-Za-z0-9-]*[A-Za-z0-9])\2\s*:(?!\s*(?:string|number)\b)|\bsetProperty\(\s*['"](--vx-[A-Za-z0-9-]*[A-Za-z0-9])['"]/gm
313
+
314
+ /**
315
+ * `--vx-*` names basalt once emitted and no longer does, with the replacement MIGRATING names
316
+ * (`null`: deleted with no replacement). Hand-kept on purpose: it is history, it only grows, and
317
+ * `vx-names.test.ts` pins that none of these is live and every replacement is.
318
+ */
319
+ const REMOVED_VX_NAMES: Readonly<Record<string, string | null>> = {
320
+ '--vx-space-touch-target': '--vx-hit',
321
+ '--vx-space-touch-control-height': '--vx-hit',
322
+ '--vx-space-app-header-mobile-actions-height': '--vx-space-sticky-header-clearance',
323
+ '--vx-space-sticky-header-clearance-mobile': '--vx-space-sticky-header-clearance',
324
+ '--vx-space-sidebar-brand-inset-top': null,
325
+ }
326
+
327
+ const KNOWN_VX_NAMES: ReadonlySet<string> = new Set(BASALT_VX_NAMES)
328
+
329
+ /**
330
+ * The bare family roots basalt never emits — `--vx-space`, `--vx-surface`, `--vx-status`, … — derived
331
+ * from the `--vx-<family>-` prefixes of the emitted names, minus any root that is itself a token
332
+ * (`--vx-hit`, `--vx-accent`). Nobody emits one: basalt only ever writes `<root>-<key>`, and so does
333
+ * `groupTokens`. It is the one shape of unknown name basalt can judge WITHOUT seeing the consumer's
334
+ * palette.
335
+ *
336
+ * An unknown name INSIDE a family (`--vx-surface-2`) is deliberately not judged. `groupTokens(name,
337
+ * map)` takes any `name`, so `--vx-surface-custom` may be a consumer group declared in a file this
338
+ * scan cannot see, and a per-file guard has no honest way to tell that from a typo.
339
+ */
340
+ const BASALT_VX_FAMILY_ROOTS: ReadonlySet<string> = new Set(
341
+ BASALT_VX_NAMES.map((n) => /^(--vx-[A-Za-z0-9]+)-/.exec(n)?.[1]).filter(
342
+ (root): root is string => root !== undefined && !KNOWN_VX_NAMES.has(root),
343
+ ),
344
+ )
345
+
289
346
  // Raw Mantine ramp step used for surface color — gray/dark + a step digit.
290
347
  const OFF_SYSTEM_SURFACE_VAR = /var\(--mantine-color-(gray|dark)-\d/g
291
348
 
@@ -861,38 +918,14 @@ export type GraceEntry = { since: string; promote: string; why: string }
861
918
  * }
862
919
  */
863
920
  export const GRACE_PERIOD_KINDS: Partial<Record<GuardKind, GraceEntry>> = {
864
- 'raw-selection-control': {
865
- since: '1.26.0',
866
- promote: '1.33.0',
921
+ 'unknown-vx-token': {
922
+ since: '1.33.0',
923
+ promote: '1.34.0',
867
924
  why:
868
- "Extended 1.32.0 -> 1.33.0 at the 1.32.0 release, the last extension: basalt's own tree is at 0 (consumer-loop r1 dogfood) and argo is at 2, but RE-MEASURED 2026-09-30 the fleet carries ~33 (image-gen 11, rb 7, image-share 6, weatherorb 5, email-gateway 4, argo 2). The 1.32.0 fleet upgrade clears them while they warn, together with raw-breakpoint, so 1.33.0 promotes all four ids against a measured-clean fleet. " +
869
- 'Extended 1.31.0 -> 1.32.0 at the 1.31.0 release: the basalt tree still carries incumbents (data-table, notifications center, theme-lab) and the argo sweep has not run. ' +
870
- 'new in the wave-6 control guards (docs/CONTROLS-SPEC.md §6, law C1). The text lane cannot ' +
871
- 'see ancestry, so "no home" is approximated by a 12-line host-tag window — the loosest ' +
872
- 'reading in the guard, and the reason this one lands warn rather than error even though its ' +
873
- 'law is settled. ONE LAW, TWO LANES, ONE PROMOTION: it moves with its AST twin, ' +
874
- '`basalt/control-outside-home`, whose PLUGIN_RULE_GRACE entry carries the full argument and ' +
875
- 'the measurement, and it must never be promoted or extended alone — a consumer waiving one ' +
876
- 'spelling and failing the other is the exact confusion `WAIVER_ID_ALIASES` exists to close. ' +
877
- 'RE-MEASURED 2026-09-09 across the whole fleet on 1.29.2: 29 AST-lane incumbents in five ' +
878
- "repos, plus three of basalt's own (data-table, notifications/center, theme-lab). Extended " +
879
- 'to 1.31.0 because 1.30.0 is the FIRST release in which either lane names a home a ' +
880
- 'provider-only consumer can reach — every home both messages listed was a shell slot, and ' +
881
- 'the shell-less `PageBar` sentence ships in this minor. Promoting in the same minor that ' +
882
- 'makes a rule satisfiable gives nobody a window to act. 1.31.0 is when the 29 are ' +
883
- 're-measured against that number.',
884
- },
885
- 'raw-media-query': {
886
- since: '1.31.0',
887
- promote: '1.33.0',
888
- why:
889
- "The CSS-text twin of the oxlint plugin's `basalt/raw-breakpoint` — ONE LAW, TWO LANES, ONE " +
890
- 'PROMOTION, so it moves with that rule and never alone (`configs/oxlint-plugin.test.ts` › twin lanes share one promotion pins the pair). New in ' +
891
- 'the wave-11 responsive/touch guards; reaching CSS is a NEW file type for this check, same ' +
892
- "reasoning as `css-raw-surface`, with incumbents in basalt's own tree at ship time, so it " +
893
- 'shipped warn. Extended 1.32.0 -> 1.33.0 with its twin: 1.31.0 named ' +
894
- 'no reachable container primitive, and 1.32.0 is the first minor that documents one ' +
895
- '(`CONTAINER_CLASSES` + the declared `@container` names, MIGRATING § `basalt/raw-breakpoint`).',
925
+ 'New kind (consumer-loop r2 B3): obsidian lost its 44px touch floor to a removed ' +
926
+ '`var(--vx-space-touch-target)` that nothing reported. A new kind rejects code every previous ' +
927
+ 'release accepted, so it lands warn for one minor. Measured at ship time: argo, linewatch, rb, ' +
928
+ 'rollhook, obsidian, weatherorb, image-share, email-gateway, image-gen 0.',
896
929
  },
897
930
  }
898
931
 
@@ -1132,6 +1165,13 @@ export const WAIVER_ID_ALIASES: Readonly<Record<string, readonly string[]>> = {
1132
1165
  'raw-selection-control': ['control-outside-home'],
1133
1166
  }
1134
1167
 
1168
+ /**
1169
+ * The ids that are a guard kind AND an oxlint plugin rule — one law, two lanes, one annotation.
1170
+ * {@link PLUGIN_RULE_IDS} leaves them out by design, so `--audit-allows` would otherwise never ask
1171
+ * oxlint about them. `waiver-audit.test.ts` pins this against the plugin's own rule table.
1172
+ */
1173
+ const DUAL_LANE_IDS: ReadonlySet<string> = new Set(['in-body-page-title'])
1174
+
1135
1175
  /** The shortest string accepted as a written reason — enough to exclude a stray separator. */
1136
1176
  const MIN_ALLOW_REASON_LENGTH = 4
1137
1177
 
@@ -1973,7 +2013,7 @@ type GuardRule = {
1973
2013
  }
1974
2014
 
1975
2015
  /**
1976
- * The closed registry of all 27 guard kinds. The triad test asserts
2016
+ * The closed registry of all 28 guard kinds. The triad test asserts
1977
2017
  * `surface.guardKinds ⊆ keyof GUARD_RULES` at runtime.
1978
2018
  *
1979
2019
  * raw-surface, raw-html-layout, and sub-16-input-font are handled inline in checkSource
@@ -2215,6 +2255,17 @@ export const GUARD_RULES = {
2215
2255
  appliesTo: (relPath) => relPath.endsWith('.css'),
2216
2256
  message: `Raw viewport/container breakpoint in CSS — a width @media belongs to shell/** against SIZE_CLASSES (useSizeClass() is the JS seam); an @container name must be one of ${[...DECLARED_CONTAINER_NAMES].join(' / ')}, and a basalt-card or unnamed @container query must sit at a boundary in ${[...CONTAINER_CLASS_LITERALS_PX].map((px) => `${px}px`).join(' / ')} (the other declared names have no numeric table yet, so their literals are not judged); and a pointer query other than (pointer: coarse) or (pointer: fine) has no framework meaning. Same law the oxlint plugin enforces as basalt/raw-breakpoint over JSX; this is the CSS half.`,
2217
2257
  },
2258
+ 'unknown-vx-token': {
2259
+ kind: 'unknown-vx-token',
2260
+ pattern: VX_VAR_REF, // handled inline (removed map + family roots); entry keeps the registry complete
2261
+ message: `var() names a --vx-* token basalt does not emit, so it resolves to its fallback (or to nothing) with no error anywhere. Two shapes are judged, the only two basalt can judge without seeing your palette: a REMOVED name — ${Object.entries(
2262
+ REMOVED_VX_NAMES,
2263
+ )
2264
+ .map(([from, to]) => `${from} → ${to ?? '(deleted, no replacement)'}`)
2265
+ .join(
2266
+ '; ',
2267
+ )} — and a bare family root nothing emits (${[...BASALT_VX_FAMILY_ROOTS].join(', ')}): name the token under it from basalt-ui/tokens.css. Any other unknown name is never judged, because defineSeries/groupTokens may declare it in a file this scan cannot see.`,
2268
+ },
2218
2269
  } as const satisfies Record<GuardKind, GuardRule>
2219
2270
 
2220
2271
  /**
@@ -2389,6 +2440,22 @@ export function checkSource(text: string, relPath: string, cfg: GuardConfig): Fi
2389
2440
  [...codeText.matchAll(STYLE_BOUND_IDENTIFIER)].map((m) => m[1] ?? ''),
2390
2441
  )
2391
2442
 
2443
+ /** `--vx-*` names this file declares — its own custom properties, which `unknown-vx-token` skips. */
2444
+ const locallyDeclaredVx = new Set(
2445
+ [...codeText.matchAll(VX_LOCAL_DECLARATION)].map((m) => m[1] ?? m[3] ?? m[4] ?? ''),
2446
+ )
2447
+
2448
+ // unknown-vx-token: a removed name, or a bare family root nothing emits — see the roots' doc.
2449
+ // Scanned over the whole comment-stripped text rather than per line, so a formatter-wrapped
2450
+ // `var(\n --vx-x\n)` is seen; the finding lands on the line the NAME is on.
2451
+ for (const m of codeText.matchAll(VX_VAR_REF)) {
2452
+ const name = m[1] ?? ''
2453
+ if (locallyDeclaredVx.has(name)) continue
2454
+ if (!Object.hasOwn(REMOVED_VX_NAMES, name) && !BASALT_VX_FAMILY_ROOTS.has(name)) continue
2455
+ const at = (m.index ?? 0) + m[0].length - name.length
2456
+ push('unknown-vx-token', lineStarts.findLastIndex((start) => start <= at) + 1, name)
2457
+ }
2458
+
2392
2459
  for (let i = 0; i < lines.length; i++) {
2393
2460
  const line = codeLines[i] ?? ''
2394
2461
 
@@ -2793,6 +2860,15 @@ export type AllowAnnotationSite = {
2793
2860
  readonly guardKinds: readonly GuardKind[]
2794
2861
  /** The subset of `rules` the oxlint plugin owns — outside `checkSource`'s reach. */
2795
2862
  readonly pluginRules: readonly string[]
2863
+ /**
2864
+ * Every id an `oxlint` run can prove live: `pluginRules`, plus a guard kind that is ALSO a plugin
2865
+ * rule (`in-body-page-title`) or aliases one ({@link WAIVER_ID_ALIASES}). A waiver is live if
2866
+ * EITHER lane still needs it — judging only `pluginRules` called rb's live `in-body-page-title`
2867
+ * waivers dead, and deleting them made oxlint exit 1. A bare LINE-scope annotation covers every
2868
+ * plugin rule (the plugin's `allowCovers`), so it lists all of them; a bare file-scope one waives
2869
+ * nothing in either lane and lists none.
2870
+ */
2871
+ readonly oxlintRules: readonly string[]
2796
2872
  /** Words that occupied the id slot but name no rule. Their presence makes the parse fail closed. */
2797
2873
  readonly unknownRules: readonly string[]
2798
2874
  /** A written reason follows the ids. */
@@ -2831,19 +2907,33 @@ export function findAllowAnnotations(
2831
2907
  cfg.allowComment,
2832
2908
  syntax,
2833
2909
  )
2834
- return declared.map(({ line, annotation }) => ({
2835
- line,
2836
- scope: annotation.scope,
2837
- rules: [...annotation.rules],
2838
- guardKinds: annotation.rules.filter((id): id is GuardKind => Object.hasOwn(GUARD_RULES, id)),
2839
- pluginRules: annotation.rules.filter(
2840
- (id) => !Object.hasOwn(GUARD_RULES, id) && PLUGIN_RULE_IDS.has(id),
2841
- ),
2842
- unknownRules: [...annotation.unknownRules],
2843
- hasReason: annotation.hasReason,
2844
- bare: annotation.rules.length === 0 && annotation.unknownRules.length === 0,
2845
- text: lines[line - 1] ?? '',
2846
- }))
2910
+ const everyOxlintRule = [...PLUGIN_RULE_IDS, ...DUAL_LANE_IDS]
2911
+ return declared.map(({ line, annotation }) => {
2912
+ const bare = annotation.rules.length === 0 && annotation.unknownRules.length === 0
2913
+ return {
2914
+ line,
2915
+ scope: annotation.scope,
2916
+ rules: [...annotation.rules],
2917
+ guardKinds: annotation.rules.filter((id): id is GuardKind => Object.hasOwn(GUARD_RULES, id)),
2918
+ pluginRules: annotation.rules.filter(
2919
+ (id) => !Object.hasOwn(GUARD_RULES, id) && PLUGIN_RULE_IDS.has(id),
2920
+ ),
2921
+ oxlintRules:
2922
+ bare && annotation.scope === 'line'
2923
+ ? everyOxlintRule
2924
+ : [
2925
+ ...new Set(
2926
+ annotation.rules
2927
+ .flatMap((id) => [id, ...(WAIVER_ID_ALIASES[id] ?? [])])
2928
+ .filter((id) => PLUGIN_RULE_IDS.has(id) || DUAL_LANE_IDS.has(id)),
2929
+ ),
2930
+ ],
2931
+ unknownRules: [...annotation.unknownRules],
2932
+ hasReason: annotation.hasReason,
2933
+ bare,
2934
+ text: lines[line - 1] ?? '',
2935
+ }
2936
+ })
2847
2937
  }
2848
2938
 
2849
2939
  /** The token a neutralized annotation is rewritten to. Shares no substring with `theme-allow`. */
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Guard types — Mantine-free, dependency-free (zero imports beyond TS types).
3
3
  *
4
- * GuardKind is the closed set of 27 violation kinds the theme guard can emit — `unframed-chart`
4
+ * GuardKind is the closed set of 28 violation kinds the theme guard can emit — `unframed-chart`
5
5
  * retired at the C2 consolidation wave: its only case (a hand-authored `<ChartLegend items={[...]}>`
6
6
  * array literal) is fully subsumed by the oxlint plugin's `basalt/chart-legend-literal`, which also
7
7
  * catches the `.map()`-over-a-non-series form the regex never could. `'unframed-chart'` stays a
@@ -11,7 +11,7 @@
11
11
  * GuardConfig is the per-run configuration that drives checkSource.
12
12
  */
13
13
 
14
- /** The 27 theme-guard violation kinds. */
14
+ /** The 28 theme-guard violation kinds. */
15
15
  export type GuardKind =
16
16
  | 'raw-hex'
17
17
  | 'raw-color-fn'
@@ -40,6 +40,7 @@ export type GuardKind =
40
40
  | 'in-body-page-title'
41
41
  | 'raw-selection-control'
42
42
  | 'raw-media-query'
43
+ | 'unknown-vx-token'
43
44
 
44
45
  /**
45
46
  * How hard a finding lands. `error` fails the build; `warn` reports and passes.
@@ -0,0 +1,257 @@
1
+ // GENERATED by scripts/gen-vx-names.ts — do not edit. `vx-names.test.ts` fails on drift.
2
+
3
+ /** Every `--vx-*` name basalt emits: `dist/tokens.css` plus its component-scoped names. */
4
+ export const BASALT_VX_NAMES: readonly string[] = [
5
+ '--vx-accent',
6
+ '--vx-accent-fill',
7
+ '--vx-accent-fill-hover',
8
+ '--vx-accent-hover',
9
+ '--vx-accentFill',
10
+ '--vx-accentFillHover',
11
+ '--vx-accentHover',
12
+ '--vx-area-bottom',
13
+ '--vx-area-top',
14
+ '--vx-axis',
15
+ '--vx-axis-stroke',
16
+ '--vx-axisStroke',
17
+ '--vx-bad',
18
+ '--vx-bad-ref',
19
+ '--vx-bad-solid',
20
+ '--vx-badRef',
21
+ '--vx-badSolid',
22
+ '--vx-crosshair',
23
+ '--vx-divider',
24
+ '--vx-dot-stroke',
25
+ '--vx-dotStroke',
26
+ '--vx-faint',
27
+ '--vx-fill-cyan',
28
+ '--vx-fill-grape',
29
+ '--vx-fill-gray',
30
+ '--vx-fill-green',
31
+ '--vx-fill-hover-cyan',
32
+ '--vx-fill-hover-grape',
33
+ '--vx-fill-hover-gray',
34
+ '--vx-fill-hover-green',
35
+ '--vx-fill-hover-indigo',
36
+ '--vx-fill-hover-lime',
37
+ '--vx-fill-hover-orange',
38
+ '--vx-fill-hover-pink',
39
+ '--vx-fill-hover-red',
40
+ '--vx-fill-hover-teal',
41
+ '--vx-fill-hover-violet',
42
+ '--vx-fill-hover-yellow',
43
+ '--vx-fill-indigo',
44
+ '--vx-fill-lime',
45
+ '--vx-fill-orange',
46
+ '--vx-fill-pink',
47
+ '--vx-fill-red',
48
+ '--vx-fill-teal',
49
+ '--vx-fill-violet',
50
+ '--vx-fill-yellow',
51
+ '--vx-fillHover-cyan',
52
+ '--vx-fillHover-grape',
53
+ '--vx-fillHover-gray',
54
+ '--vx-fillHover-green',
55
+ '--vx-fillHover-indigo',
56
+ '--vx-fillHover-lime',
57
+ '--vx-fillHover-orange',
58
+ '--vx-fillHover-pink',
59
+ '--vx-fillHover-red',
60
+ '--vx-fillHover-teal',
61
+ '--vx-fillHover-violet',
62
+ '--vx-fillHover-yellow',
63
+ '--vx-good',
64
+ '--vx-good-ref',
65
+ '--vx-good-soft',
66
+ '--vx-good-solid',
67
+ '--vx-goodRef',
68
+ '--vx-goodSoft',
69
+ '--vx-goodSolid',
70
+ '--vx-grid',
71
+ '--vx-hit',
72
+ '--vx-hit-gap',
73
+ '--vx-ink',
74
+ '--vx-ink2',
75
+ '--vx-keyboard-inset',
76
+ '--vx-legend-text',
77
+ '--vx-legendText',
78
+ '--vx-line',
79
+ '--vx-line2',
80
+ '--vx-muted',
81
+ '--vx-neutral',
82
+ '--vx-on-accent',
83
+ '--vx-onAccent',
84
+ '--vx-prose-measure',
85
+ '--vx-radius-card',
86
+ '--vx-radius-ctrl',
87
+ '--vx-radius-fine',
88
+ '--vx-radius-floating',
89
+ '--vx-radius-pill',
90
+ '--vx-radius-tight',
91
+ '--vx-shadow-card',
92
+ '--vx-shadow-ctrl',
93
+ '--vx-shadow-overlay',
94
+ '--vx-shadow-raised',
95
+ '--vx-space-agent-code-inset',
96
+ '--vx-space-agent-error-inset-x',
97
+ '--vx-space-agent-error-inset-y',
98
+ '--vx-space-agent-message-inset-x',
99
+ '--vx-space-agent-message-inset-y',
100
+ '--vx-space-agent-part-gap-top',
101
+ '--vx-space-agent-rail-inset-x',
102
+ '--vx-space-agent-scroll-button-gap-top',
103
+ '--vx-space-agent-transcript-inset',
104
+ '--vx-space-app-shell-header-lead-min',
105
+ '--vx-space-app-shell-inset',
106
+ '--vx-space-app-shell-inset-mobile',
107
+ '--vx-space-article-card-meta-gap-top',
108
+ '--vx-space-article-card-tag-inset-x',
109
+ '--vx-space-article-card-tag-inset-y',
110
+ '--vx-space-article-card-tags-gap',
111
+ '--vx-space-article-column-gap',
112
+ '--vx-space-article-footer-gap',
113
+ '--vx-space-article-footer-margin-top',
114
+ '--vx-space-article-footer-padding-top',
115
+ '--vx-space-article-header-gap',
116
+ '--vx-space-article-header-margin-bottom',
117
+ '--vx-space-article-header-padding-bottom',
118
+ '--vx-space-article-meta-row-gap',
119
+ '--vx-space-article-nav-target-gap',
120
+ '--vx-space-article-toc-rail-width',
121
+ '--vx-space-badge-inset-x',
122
+ '--vx-space-badge-inset-y',
123
+ '--vx-space-callout-inset-x',
124
+ '--vx-space-callout-inset-y',
125
+ '--vx-space-callout-title-row-gap',
126
+ '--vx-space-card-inset-x',
127
+ '--vx-space-card-inset-y',
128
+ '--vx-space-code-block-body-inset-y',
129
+ '--vx-space-code-block-content-inset-x',
130
+ '--vx-space-code-block-floating-copy-offset',
131
+ '--vx-space-code-block-header-gap',
132
+ '--vx-space-code-block-header-inset-right',
133
+ '--vx-space-code-block-header-inset-y',
134
+ '--vx-space-code-block-header-right-gap',
135
+ '--vx-space-control-gap',
136
+ '--vx-space-control-height',
137
+ '--vx-space-control-height-ctl',
138
+ '--vx-space-control-height-tag',
139
+ '--vx-space-control-height-widget',
140
+ '--vx-space-guide-body-gap-bottom',
141
+ '--vx-space-guide-footer-gap-top',
142
+ '--vx-space-guide-footer-inset-top',
143
+ '--vx-space-icon-size',
144
+ '--vx-space-input-height',
145
+ '--vx-space-mermaid-container-inset',
146
+ '--vx-space-mobile-nav-icon-size',
147
+ '--vx-space-mobile-nav-row-height',
148
+ '--vx-space-mobile-nav-tab-gap',
149
+ '--vx-space-mobile-nav-tab-inset-x',
150
+ '--vx-space-mobile-nav-tab-inset-y',
151
+ '--vx-space-nav-icon-gap',
152
+ '--vx-space-page-bar-row-height',
153
+ '--vx-space-prose-article-block-gap',
154
+ '--vx-space-prose-article-h1-gap-bottom',
155
+ '--vx-space-prose-article-h1-gap-top',
156
+ '--vx-space-prose-article-h2-rule-gap',
157
+ '--vx-space-prose-article-heading-gap-bottom',
158
+ '--vx-space-prose-article-heading-gap-top',
159
+ '--vx-space-prose-article-list-gap-bottom',
160
+ '--vx-space-prose-article-list-gap-top',
161
+ '--vx-space-prose-article-list-indent',
162
+ '--vx-space-prose-article-paragraph-gap',
163
+ '--vx-space-prose-chat-heading-gap-bottom',
164
+ '--vx-space-prose-chat-heading-gap-top',
165
+ '--vx-space-prose-chat-list-gap-bottom',
166
+ '--vx-space-prose-chat-list-indent',
167
+ '--vx-space-prose-chat-list-item-gap',
168
+ '--vx-space-prose-chat-nested-list-gap',
169
+ '--vx-space-prose-code-block-inset-x',
170
+ '--vx-space-prose-code-block-inset-y',
171
+ '--vx-space-prose-heading-anchor-gap',
172
+ '--vx-space-prose-inline-code-inset-x',
173
+ '--vx-space-prose-inline-code-inset-y',
174
+ '--vx-space-prose-quote-indent',
175
+ '--vx-space-prose-quote-inset-y',
176
+ '--vx-space-prose-table-cell-inset-x',
177
+ '--vx-space-prose-table-cell-inset-y',
178
+ '--vx-space-row-inset-x',
179
+ '--vx-space-row-inset-y',
180
+ '--vx-space-row-line-height',
181
+ '--vx-space-section-gap',
182
+ '--vx-space-section-header-height',
183
+ '--vx-space-settings-row-gap',
184
+ '--vx-space-settings-row-inset-y',
185
+ '--vx-space-sheet-row-height',
186
+ '--vx-space-sidebar-account-inset-bottom',
187
+ '--vx-space-sidebar-account-inset-top',
188
+ '--vx-space-sidebar-account-inset-x',
189
+ '--vx-space-sidebar-avatar-size',
190
+ '--vx-space-sidebar-block-row-height',
191
+ '--vx-space-sidebar-brand-inset-x',
192
+ '--vx-space-sidebar-child-list-gap-bottom',
193
+ '--vx-space-sidebar-child-list-gap-top',
194
+ '--vx-space-sidebar-child-list-indent',
195
+ '--vx-space-sidebar-child-row-indent',
196
+ '--vx-space-sidebar-child-row-inset-y',
197
+ '--vx-space-sidebar-region-gap',
198
+ '--vx-space-sidebar-search-gap',
199
+ '--vx-space-sidebar-search-trigger-height',
200
+ '--vx-space-sidebar-section-gap',
201
+ '--vx-space-sidebar-section-label-gap',
202
+ '--vx-space-stack-lg',
203
+ '--vx-space-stack-md',
204
+ '--vx-space-stack-sm',
205
+ '--vx-space-stack-xl',
206
+ '--vx-space-stack-xs',
207
+ '--vx-space-stat-card-gap',
208
+ '--vx-space-sticky-header-clearance',
209
+ '--vx-space-toc-link-indent',
210
+ '--vx-space-toc-link-inset-y',
211
+ '--vx-space-toc-root-gap',
212
+ '--vx-space-toc-sub-indent',
213
+ '--vx-space-virtual-row-inset-x',
214
+ '--vx-space-virtual-row-inset-y',
215
+ '--vx-space-widget-header-height',
216
+ '--vx-status-bad',
217
+ '--vx-status-excellent',
218
+ '--vx-status-good',
219
+ '--vx-status-neutral',
220
+ '--vx-status-warn',
221
+ '--vx-surface-bg',
222
+ '--vx-surface-border',
223
+ '--vx-surface-elevated',
224
+ '--vx-surface-field',
225
+ '--vx-surface-hairline',
226
+ '--vx-surface-overlay',
227
+ '--vx-surface-panel',
228
+ '--vx-surface-panel-hover',
229
+ '--vx-surface-panelHover',
230
+ '--vx-surface-subtle',
231
+ '--vx-text-display',
232
+ '--vx-text-h1',
233
+ '--vx-text-h2',
234
+ '--vx-text-kpi',
235
+ '--vx-text-lg',
236
+ '--vx-text-md',
237
+ '--vx-text-micro',
238
+ '--vx-text-nano',
239
+ '--vx-text-sm',
240
+ '--vx-text-xl',
241
+ '--vx-text-xs',
242
+ '--vx-tooltip-bg',
243
+ '--vx-tooltip-border',
244
+ '--vx-tooltip-muted',
245
+ '--vx-tooltip-shadow',
246
+ '--vx-tooltip-text',
247
+ '--vx-tooltipBg',
248
+ '--vx-tooltipBorder',
249
+ '--vx-tooltipMuted',
250
+ '--vx-tooltipShadow',
251
+ '--vx-tooltipText',
252
+ '--vx-warn',
253
+ '--vx-warn-ref',
254
+ '--vx-warn-solid',
255
+ '--vx-warnRef',
256
+ '--vx-warnSolid',
257
+ ]
@@ -12,7 +12,6 @@
12
12
  * as `provider/lab-theme.ts`.
13
13
  */
14
14
  import { useEffect } from 'react'
15
- import { isDev } from '../common/is-dev'
16
15
 
17
16
  let mountedNotificationsCount = 0
18
17
 
@@ -25,7 +24,7 @@ let mountedNotificationsCount = 0
25
24
  export function useNotificationsMountGuard(): void {
26
25
  useEffect(() => {
27
26
  mountedNotificationsCount++
28
- if (mountedNotificationsCount > 1 && isDev()) {
27
+ if (mountedNotificationsCount > 1 && process.env.NODE_ENV !== 'production') {
29
28
  console.warn(
30
29
  '[basalt] more than one Notifications overlay is mounted at once — mount exactly ONE ' +
31
30
  "<BasaltOverlays notifications /> (they all render Mantine's <Notifications />, so " +
@@ -34,7 +34,6 @@ import { buildDensityCss, buildFontsCss, buildPaletteCss, buildRadiusCss } from
34
34
  import type { BuildPaletteOpts } from '../tokens'
35
35
  import { isDefaultDeriveConfig } from '../tokens/derive'
36
36
  import { buildPaletteData } from '../tokens/palette'
37
- import { isDev } from '../common/is-dev'
38
37
  import { useMediaQuery } from '../common/use-media-query'
39
38
  import { registerColorSchemeSetter } from '../commands/shell-bridge'
40
39
  import { useIsomorphicLayoutEffect } from '../common/isomorphic-layout-effect'
@@ -125,7 +124,7 @@ export type BasaltProviderProps = {
125
124
  // ── Default error handler ─────────────────────────────────────────────────────────────────────────
126
125
 
127
126
  function defaultOnError(error: unknown, ctx: BasaltErrorContext): void {
128
- if (isDev()) {
127
+ if (process.env.NODE_ENV !== 'production') {
129
128
  // eslint-disable-next-line no-console -- intentional dev-only diagnostic when no onError is supplied
130
129
  console.error('[BasaltProvider] unhandled error', ctx.kind, error)
131
130
  }
@@ -382,7 +381,7 @@ const providerMounts = createMountCounter()
382
381
 
383
382
  function useDuplicateProviderGuard(): void {
384
383
  useEffect(() => {
385
- if (providerMounts.mount() > 1 && isDev()) {
384
+ if (providerMounts.mount() > 1 && process.env.NODE_ENV !== 'production') {
386
385
  console.warn(
387
386
  '[basalt] BasaltProvider: more than one instance is mounted at once — nested/duplicate ' +
388
387
  'BasaltProviders double-mount MantineProvider, the palette <style> and ' +
@@ -33,9 +33,9 @@
33
33
  * The delta also carries `other.basaltDerive`/`basaltRadius`/`basaltDensity`, so `BasaltBridge`'s
34
34
  * existing injection emits the matching `--vx-*` CSS off the running theme — one path, both halves.
35
35
  *
36
- * DEV-tool path only, twice over: it is compiled out of a production build entirely (see
37
- * {@link LAB_ENABLED}), and even in a dev build, with the "Apply" switch off — or in any app that
38
- * never mounts the panel, since the persisted key is written by nothing else —
36
+ * DEV-tool path only, twice over: it is compiled out of a production build entirely (see the
37
+ * DEV-build gate on `useLabTheme`), and even in a dev build, with the "Apply" switch off — or in
38
+ * any app that never mounts the panel, since the persisted key is written by nothing else —
39
39
  * {@link applyLabOverride} returns the consumer theme verbatim.
40
40
  */
41
41
  import { mergeThemeOverrides } from '@mantine/core'
@@ -44,7 +44,6 @@ import { useMemo } from 'react'
44
44
  import { useDeriveControlsState, toDeriveOverride } from '../theme-lab/derive-state'
45
45
  import type { DeriveOverride } from '../theme-lab/derive-state'
46
46
  import { baseTheme, createBasaltTheme } from '../theme'
47
- import { isDev } from '../common/is-dev'
48
47
 
49
48
  type Plain = Record<string, unknown>
50
49
 
@@ -124,20 +123,27 @@ export function applyLabOverride(
124
123
  return mergeThemeOverrides(theme, themeOverrideDelta(baseTheme, labTheme))
125
124
  }
126
125
 
127
- /**
128
- * DEV-build gate. `BasaltProvider` is the mandatory `.` entry, so an unconditional subscription to
129
- * the lab store would make EVERY production app pay a localStorage read + schema validation on first
130
- * render and keep a `window` 'storage' listener registered for its whole lifetime — to answer a
131
- * question ("is the dev lab active?") that is always "no" in an app that never mounts
132
- * `<DeriveControls>`. Resolved ONCE at module scope, so it also lets a bundler drop
133
- * `useLabThemeDev` (and with it the `theme-lab/derive-state` import) from a production build.
126
+ /*
127
+ * DEV-build gate (on `useLabTheme` below). `BasaltProvider` is the mandatory `.` entry, so an
128
+ * unconditional subscription to the lab store would make EVERY production app pay a localStorage
129
+ * read + schema validation on first render and keep a `window` 'storage' listener registered for
130
+ * its whole lifetime — to answer a question ("is the dev lab active?") that is always "no" in an
131
+ * app that never mounts `<DeriveControls>`.
132
+ *
133
+ * Resolved ONCE at module scope, so a bundler can drop `useLabThemeDev` (and with it the
134
+ * `theme-lab/derive-state` + `state/persisted` imports) from a production build — but only
135
+ * because the gate is a literal, dotted `process.env.NODE_ENV` read inside the ternary itself.
136
+ * Measured (minified prod builds, dev branch behind an import): the literal read folds in
137
+ * Vite/Rolldown, esbuild, Bun and webpack + terser alike, in both the dotted and the bracketed
138
+ * form; dotted is kept as the form every `define` implementation matches. A cross-module
139
+ * `isDev()` call and a module-level `const` both LEAK the dev branch in esbuild and Bun (and
140
+ * `isDev()` in Rolldown too), which is how 1.32.1 shipped the whole lab store in every production
141
+ * app's first-paint chunk. `check-budgets.ts`'s provider-only budget pins it.
134
142
  *
135
- * The cost: the lab does nothing in a PRODUCTION BUILD of a playground/dev app. It is a dev tool run
136
- * through a dev server (`bun run dev`), so that is the right side of the trade — but if you ever
137
- * preview the playground via `vite build && vite preview`, the sliders will be inert there.
143
+ * The cost: the lab does nothing in a PRODUCTION BUILD of a playground/dev app. It is a dev tool
144
+ * run through a dev server (`bun run dev`), so that is the right side of the trade — but if you
145
+ * ever preview the playground via `vite build && vite preview`, the sliders will be inert there.
138
146
  */
139
- const LAB_ENABLED = isDev()
140
-
141
147
  /**
142
148
  * Dev build: subscribed to the lab store.
143
149
  *
@@ -159,9 +165,10 @@ function useLabThemeProd(overrides: MantineThemeOverride | undefined): MantineTh
159
165
  }
160
166
 
161
167
  /**
162
- * `BasaltProvider`'s theme resolution. Picked at module scope, not per render — both implementations
163
- * call their hooks unconditionally at their own top level, and which one is bound never changes for
164
- * the app's lifetime, so the hook order is as stable as a plain function's (a `LAB_ENABLED ? use…()`
165
- * inline in one function body would be a conditional hook call and a `react/rules-of-hooks` error).
168
+ * `BasaltProvider`'s theme resolution. Picked at module scope, not per render — both
169
+ * implementations call their hooks unconditionally at their own top level, and which one is bound
170
+ * never changes for the app's lifetime, so the hook order is as stable as a plain function's (a
171
+ * `dev ? use…()` inline in one function body would be a conditional hook call and a
172
+ * `react/rules-of-hooks` error).
166
173
  */
167
- export const useLabTheme = LAB_ENABLED ? useLabThemeDev : useLabThemeProd
174
+ export const useLabTheme = process.env.NODE_ENV !== 'production' ? useLabThemeDev : useLabThemeProd