basalt-ui 1.19.1 → 1.21.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 (129) hide show
  1. package/AGENTS.md +19 -1
  2. package/CHANGELOG.md +39 -0
  3. package/MIGRATING.md +326 -0
  4. package/README.md +87 -9
  5. package/agent/rules/basalt-app.md +84 -0
  6. package/agent/rules/basalt-charts.md +32 -17
  7. package/agent/rules/basalt-content.md +4 -0
  8. package/agent/rules/basalt-mantine.md +25 -10
  9. package/agent/rules/basalt-router.md +79 -15
  10. package/agent/rules/basalt-state.md +21 -3
  11. package/agent/rules/basalt-tokens.md +132 -6
  12. package/agent/skills/basalt-app/SKILL.md +45 -6
  13. package/agent/skills/basalt-charts/SKILL.md +5 -3
  14. package/agent/templates/CLAUDE-block.md.tpl +11 -5
  15. package/configs/check.yml +11 -4
  16. package/configs/lefthook.yml +59 -7
  17. package/configs/oxlint-plugin.js +633 -30
  18. package/configs/oxlint-plugin.test.ts +594 -15
  19. package/configs/oxlint.json +10 -1
  20. package/dist/agent/stick-to-bottom.js +1 -1
  21. package/dist/agent-chat/thread-detail-panel.js +1 -1
  22. package/dist/agent-chat/thread-message.js +2 -2
  23. package/dist/agent-chat/tool-chip.js +1 -1
  24. package/dist/charts/kinds/Bars.d.ts +1 -1
  25. package/dist/charts/kinds/Bars.d.ts.map +1 -1
  26. package/dist/charts/kinds/Donut.d.ts +1 -1
  27. package/dist/charts/kinds/Donut.d.ts.map +1 -1
  28. package/dist/charts/kinds/Donut.js +1 -1
  29. package/dist/charts/kinds/DualPanel.d.ts +1 -1
  30. package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
  31. package/dist/charts/kinds/Heatmap.d.ts +1 -1
  32. package/dist/charts/kinds/Heatmap.d.ts.map +1 -1
  33. package/dist/charts/kinds/MultiLine.d.ts +1 -1
  34. package/dist/charts/kinds/MultiLine.d.ts.map +1 -1
  35. package/dist/charts/kinds/StackedArea.d.ts +1 -1
  36. package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
  37. package/dist/charts/kinds/ZonedLine.d.ts +1 -1
  38. package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
  39. package/dist/charts/primitives/ChartLegend.js +2 -2
  40. package/dist/charts/primitives/ChartTooltip.js +1 -1
  41. package/dist/cli/index.d.ts +146 -20
  42. package/dist/cli/index.d.ts.map +1 -1
  43. package/dist/cli/index.js +901 -101
  44. package/dist/cli/toolchain-wiring.test.d.ts +2 -0
  45. package/dist/cli/toolchain-wiring.test.d.ts.map +1 -0
  46. package/dist/cli/waiver-audit.test.d.ts +2 -0
  47. package/dist/cli/waiver-audit.test.d.ts.map +1 -0
  48. package/dist/dashboard/delta-badge.js +2 -2
  49. package/dist/dashboard/stat-card.js +1 -1
  50. package/dist/data/virtual-list.js +5 -5
  51. package/dist/guard/index.d.ts +101 -2
  52. package/dist/guard/index.d.ts.map +1 -1
  53. package/dist/guard/index.js +474 -62
  54. package/dist/guard/types.d.ts +25 -5
  55. package/dist/guard/types.d.ts.map +1 -1
  56. package/dist/register.d.ts +25 -0
  57. package/dist/register.d.ts.map +1 -1
  58. package/dist/router-tanstack/multi-search-param-store.d.ts +13 -0
  59. package/dist/router-tanstack/multi-search-param-store.d.ts.map +1 -1
  60. package/dist/router-tanstack/multi-search-param-store.js +4 -1
  61. package/dist/router-tanstack/search-param-store.d.ts +29 -5
  62. package/dist/router-tanstack/search-param-store.d.ts.map +1 -1
  63. package/dist/router-tanstack/search-param-store.js +31 -3
  64. package/dist/router-tanstack/search-param-store.test.d.ts +2 -0
  65. package/dist/router-tanstack/search-param-store.test.d.ts.map +1 -0
  66. package/dist/shell/app-mobile-nav.module.css +11 -3
  67. package/dist/shell/index.d.ts +12 -5
  68. package/dist/shell/index.d.ts.map +1 -1
  69. package/dist/shell/index.js +24 -6
  70. package/dist/surfaces.d.ts +1 -1
  71. package/dist/surfaces.js +1 -1
  72. package/dist/tokens/index.d.ts +5 -3
  73. package/dist/tokens/index.d.ts.map +1 -1
  74. package/dist/tokens/index.js +7 -1
  75. package/dist/tokens/palette.d.ts +35 -27
  76. package/dist/tokens/palette.d.ts.map +1 -1
  77. package/dist/tokens/palette.js +24 -18
  78. package/dist/tokens/text-scale.test.d.ts +2 -0
  79. package/dist/tokens/text-scale.test.d.ts.map +1 -0
  80. package/dist/tokens.css +21 -17
  81. package/dist/vite-color-mix.js +2 -2
  82. package/dist/vite.d.ts +24 -2
  83. package/dist/vite.d.ts.map +1 -1
  84. package/dist/vite.js +54 -13
  85. package/llms.txt +3 -3
  86. package/package.json +4 -3
  87. package/src/agent/stick-to-bottom.tsx +1 -1
  88. package/src/agent-chat/thread-detail-panel.tsx +1 -1
  89. package/src/agent-chat/thread-message.tsx +3 -3
  90. package/src/agent-chat/tool-chip.tsx +1 -1
  91. package/src/charts/kinds/Bars.tsx +1 -1
  92. package/src/charts/kinds/Donut.tsx +3 -2
  93. package/src/charts/kinds/DualPanel.tsx +2 -2
  94. package/src/charts/kinds/Heatmap.tsx +1 -1
  95. package/src/charts/kinds/MultiLine.tsx +1 -1
  96. package/src/charts/kinds/StackedArea.tsx +1 -1
  97. package/src/charts/kinds/ZonedLine.tsx +1 -1
  98. package/src/charts/primitives/ChartLegend.tsx +2 -2
  99. package/src/charts/primitives/ChartTooltip.tsx +1 -1
  100. package/src/cli/index.ts +1614 -164
  101. package/src/cli/init-sync.test.ts +21 -3
  102. package/src/cli/template-ledger.test.ts +1 -1
  103. package/src/cli/tokens-css.test.ts +27 -14
  104. package/src/cli/toolchain-wiring.test.ts +856 -0
  105. package/src/cli/waiver-audit.test.ts +131 -0
  106. package/src/dashboard/delta-badge.tsx +3 -3
  107. package/src/dashboard/stat-card.tsx +1 -1
  108. package/src/data/virtual-list.tsx +8 -8
  109. package/src/guard/check-source.test.ts +1104 -12
  110. package/src/guard/index.ts +1106 -103
  111. package/src/guard/types.ts +29 -4
  112. package/src/register.ts +32 -0
  113. package/src/router-tanstack/multi-search-param-store.ts +16 -1
  114. package/src/router-tanstack/search-param-store.test.ts +135 -0
  115. package/src/router-tanstack/search-param-store.ts +90 -8
  116. package/src/shell/app-mobile-nav.module.css +11 -3
  117. package/src/shell/app-mobile-nav.tsx +1 -1
  118. package/src/shell/index.test.tsx +52 -0
  119. package/src/shell/index.tsx +64 -13
  120. package/src/surfaces.ts +1 -1
  121. package/src/theme/density-relations.test.ts +8 -5
  122. package/src/theme/spacing.test.ts +2 -0
  123. package/src/theme-toggle/index.tsx +2 -2
  124. package/src/tokens/index.ts +28 -3
  125. package/src/tokens/palette.ts +24 -18
  126. package/src/tokens/text-scale.test.ts +51 -0
  127. package/src/vite-color-mix.ts +2 -2
  128. package/src/vite.test.ts +125 -6
  129. package/src/vite.ts +148 -20
@@ -0,0 +1,131 @@
1
+ /**
2
+ * `check-theme --audit-allows` — the accountability half of the escape hatch.
3
+ *
4
+ * 1.20.0 shipped `theme-allow-unscoped`, which reports a waiver that names no rule. It cannot see
5
+ * the other failure: a perfectly well-written waiver whose finding no longer exists. Two consumers
6
+ * found five of those by hand — editing the token out and re-running the guard — and that manual
7
+ * method is what this command automates, which is why every assertion below is about a VERDICT
8
+ * derived from re-running the scan, never from parsing the annotation's text.
9
+ *
10
+ * Run: bun test packages/basalt-ui/src/cli/waiver-audit.test.ts
11
+ */
12
+ import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
13
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
14
+ import { tmpdir } from 'node:os'
15
+ import { join, resolve } from 'node:path'
16
+
17
+ import { checkTheme } from './index.ts'
18
+
19
+ let dir: string
20
+
21
+ function write(relPath: string, content: string): void {
22
+ const abs = join(dir, relPath)
23
+ mkdirSync(resolve(abs, '..'), { recursive: true })
24
+ writeFileSync(abs, content, 'utf8')
25
+ }
26
+
27
+ function audit(): { code: number; log: string } {
28
+ const originalLog = console.log
29
+ const originalError = console.error
30
+ let log = ''
31
+ const sink = (...args: unknown[]) => {
32
+ log += `${args.join(' ')}\n`
33
+ }
34
+ console.log = sink
35
+ console.error = sink
36
+ try {
37
+ return { code: checkTheme(dir, ['--audit-allows']), log }
38
+ } finally {
39
+ console.log = originalLog
40
+ console.error = originalError
41
+ }
42
+ }
43
+
44
+ /** A repo whose only source file is the one under test. */
45
+ function fixture(source: string, basalt: Record<string, unknown> = {}): void {
46
+ write('package.json', JSON.stringify({ name: 'fixture', basalt: { roots: ['src'], ...basalt } }))
47
+ write('src/app.tsx', source)
48
+ }
49
+
50
+ beforeEach(() => {
51
+ dir = mkdtempSync(resolve(tmpdir(), 'basalt-audit-'))
52
+ })
53
+
54
+ afterEach(() => {
55
+ rmSync(dir, { recursive: true, force: true })
56
+ })
57
+
58
+ describe('check-theme --audit-allows — theme-allow annotations', () => {
59
+ it('names what a live waiver still suppresses, and exits 0', () => {
60
+ fixture("export const c = '#ff0000' // theme-allow raw-hex — a deliberate brand literal\n")
61
+ const { code, log } = audit()
62
+ expect(log).toContain('suppresses raw-hex@1')
63
+ expect(code).toBe(0)
64
+ })
65
+
66
+ it('calls a waiver over a line with no finding DEAD, and exits 1', () => {
67
+ fixture('export const c = 1 // theme-allow raw-hex — nothing here fires any more\n')
68
+ const { code, log } = audit()
69
+ expect(log).toContain('SUPPRESSES NOTHING')
70
+ expect(code).toBe(1)
71
+ })
72
+
73
+ it('does not report prose that merely mentions the token', () => {
74
+ // The guard stopped treating a mid-comment mention as an annotation for exactly this reason;
75
+ // an audit that listed them as dead waivers would send someone editing documentation.
76
+ fixture('// Waive it with a `theme-allow raw-hex — why` comment.\nexport const c = 1\n')
77
+ const { code, log } = audit()
78
+ expect(log).toContain('annotations (0)')
79
+ expect(code).toBe(0)
80
+ })
81
+
82
+ it('refuses to judge a waiver scoped to an oxlint plugin rule instead of calling it dead', () => {
83
+ // `hand-rolled-plot` lives in the plugin, so `checkSource` can never see what it suppresses.
84
+ // Reporting "delete it" here would be the command telling someone to remove a live waiver.
85
+ fixture(
86
+ '// theme-allow hand-rolled-plot — DualPanel is not a single cartesian plot\nexport const c = 1\n',
87
+ )
88
+ const { code, log } = audit()
89
+ expect(log).toContain('not a check-theme kind')
90
+ expect(log).not.toContain('SUPPRESSES NOTHING')
91
+ expect(code).toBe(0)
92
+ })
93
+
94
+ it('marks an unaccountable waiver while still reporting what it covers', () => {
95
+ fixture("export const c = '#ff0000' // theme-allow\n")
96
+ const { log } = audit()
97
+ expect(log).toContain('suppresses raw-hex@1')
98
+ expect(log).toContain('no rule id')
99
+ expect(log).toContain('1 unaccountable')
100
+ })
101
+ })
102
+
103
+ describe('check-theme --audit-allows — basalt.exemptRules', () => {
104
+ it('prints the reason from the object form, and says so when there is none', () => {
105
+ fixture("export const c = '#ff0000'\n", {
106
+ exemptRules: { 'raw-hex': { paths: ['src'], reason: 'a vendor asset basalt does not own' } },
107
+ })
108
+ const { log } = audit()
109
+ expect(log).toContain('a vendor asset basalt does not own')
110
+ expect(log).toContain('suppresses findings in src/app.tsx')
111
+
112
+ fixture("export const c = '#ff0000'\n", { exemptRules: { 'raw-hex': ['src'] } })
113
+ expect(audit().log).toContain('[no reason recorded]')
114
+ })
115
+
116
+ it('the object form really does exempt — the reason is accountability, not a second config', () => {
117
+ fixture("export const c = '#ff0000'\n", {
118
+ exemptRules: { 'raw-hex': { paths: ['src'], reason: 'a vendor asset basalt does not own' } },
119
+ })
120
+ expect(checkTheme(dir, [])).toBe(0)
121
+ })
122
+
123
+ it('reports a pattern that matches no scanned file as dead, and exits 1', () => {
124
+ // rollhook wrote a real, correct relative path, it matched nothing, and nothing said so — the
125
+ // finding kept failing the build with the config sitting right there looking like the answer.
126
+ fixture("export const c = '#ff0000'\n", { exemptRules: { 'raw-hex': ['does-not-exist'] } })
127
+ const { code, log } = audit()
128
+ expect(log).toContain('matches no scanned file')
129
+ expect(code).toBe(1)
130
+ })
131
+ })
@@ -49,9 +49,9 @@ export function DeltaBadge({
49
49
  height: 'auto',
50
50
  },
51
51
  label: {
52
- display: 'inline-flex', // theme-allow: Mantine Badge styles.label part — CSS-in-JS, not a layout node
52
+ display: 'inline-flex', // theme-allow inline-display — a Badge `styles.label` part, not a layout node Flex could replace
53
53
  alignItems: 'center',
54
- gap: 3, // theme-allow: sub-scale badge glyph-to-label gap, below the token scale
54
+ gap: 3, // theme-allow inline-spacing — sub-scale glyph-to-label gap, below the smallest scale stop
55
55
  fontFamily: 'var(--basalt-font-mono)',
56
56
  fontSize: VX.text.xs,
57
57
  fontWeight: 600,
@@ -66,7 +66,7 @@ export function DeltaBadge({
66
66
  {format(value)}
67
67
  {period && (
68
68
  <span
69
- style={{ marginLeft: 4, fontWeight: 500, opacity: 0.6 }} // theme-allow: sub-scale optical period offset, below the token scale
69
+ style={{ marginLeft: 4, fontWeight: 500, opacity: 0.6 }} // theme-allow inline-spacing — sub-scale optical period offset, below the smallest scale stop
70
70
  >
71
71
  {period}
72
72
  </span>
@@ -110,7 +110,7 @@ export function StatCard({
110
110
  // explicit for intent.
111
111
  padding: 'var(--mantine-spacing-xs) var(--mantine-spacing-sm)',
112
112
  minHeight: 118,
113
- display: 'flex', // theme-allow: Card root as a flex column — Mantine Card takes no layout props
113
+ display: 'flex', // theme-allow inline-display — Card root as a flex column; Mantine Card takes no layout props
114
114
  flexDirection: 'column',
115
115
  gap: 'var(--vx-space-stat-card-gap)',
116
116
  overflow: 'hidden',
@@ -126,10 +126,10 @@ export function BasaltVirtualList<T>({
126
126
 
127
127
  if (isLoading) {
128
128
  return (
129
- // theme-allow — matches the virtualizer's own scroll box below, so the skeleton doesn't reflow.
129
+ // theme-allow raw-scroll-container — matches the virtualizer's own scroll box below, so the skeleton doesn't reflow.
130
130
  <Box style={{ height, overflow: 'auto' }}>
131
131
  {Array.from({ length: skeletonRows }, (_, i) => (
132
- <div
132
+ <Box
133
133
  key={`skeleton-${i}`}
134
134
  style={{
135
135
  height: estimateSize,
@@ -144,7 +144,7 @@ export function BasaltVirtualList<T>({
144
144
  height={`calc(${estimateSize}px - 2 * var(--vx-space-virtual-row-inset-y))`}
145
145
  radius="sm"
146
146
  />
147
- </div>
147
+ </Box>
148
148
  ))}
149
149
  </Box>
150
150
  )
@@ -155,12 +155,12 @@ export function BasaltVirtualList<T>({
155
155
  ref={parentRef}
156
156
  style={{
157
157
  height,
158
- // theme-allow — TanStack Virtual measures this element as the scroll container.
158
+ // theme-allow raw-scroll-container — TanStack Virtual measures this element as the scroll container.
159
159
  overflow: 'auto',
160
160
  }}
161
161
  >
162
162
  {/* Inner sizer — defines total scroll height for the virtualizer */}
163
- <div
163
+ <Box
164
164
  style={{
165
165
  height: rowVirtualizer.getTotalSize(),
166
166
  position: 'relative',
@@ -170,7 +170,7 @@ export function BasaltVirtualList<T>({
170
170
  const item = items[virtualItem.index]
171
171
  if (item === undefined) return null
172
172
  return (
173
- <div
173
+ <Box
174
174
  key={virtualItem.key}
175
175
  style={{
176
176
  position: 'absolute',
@@ -182,10 +182,10 @@ export function BasaltVirtualList<T>({
182
182
  }}
183
183
  >
184
184
  {renderItem(item, virtualItem.index)}
185
- </div>
185
+ </Box>
186
186
  )
187
187
  })}
188
- </div>
188
+ </Box>
189
189
  </Box>
190
190
  )
191
191
  }