basalt-ui 1.32.0 → 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 (145) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/MIGRATING.md +228 -20
  3. package/agent/rules/basalt-mantine.md +2 -2
  4. package/agent/rules/basalt-state.md +2 -2
  5. package/agent/rules/basalt-tokens.md +1 -1
  6. package/configs/oxlint-plugin.js +160 -89
  7. package/configs/oxlint-plugin.test.ts +273 -10
  8. package/configs/oxlint.json +2 -2
  9. package/dist/agent/coalesce.d.ts.map +1 -1
  10. package/dist/agent/coalesce.js +1 -2
  11. package/dist/agent/merge.d.ts.map +1 -1
  12. package/dist/agent/merge.js +1 -2
  13. package/dist/agent-chat/relative-time.d.ts +9 -0
  14. package/dist/agent-chat/relative-time.d.ts.map +1 -1
  15. package/dist/agent-chat/relative-time.js +1 -2
  16. package/dist/agent-chat/thread-message.d.ts.map +1 -1
  17. package/dist/agent-chat/thread-message.js +1 -2
  18. package/dist/charts/kinds/BandStrip.d.ts.map +1 -1
  19. package/dist/charts/kinds/BandStrip.js +3 -3
  20. package/dist/charts/kinds/Bars.d.ts.map +1 -1
  21. package/dist/charts/kinds/Bars.js +1 -2
  22. package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
  23. package/dist/charts/kinds/StackedArea.js +1 -2
  24. package/dist/charts/primitives/legend-disclosure.d.ts.map +1 -1
  25. package/dist/cli/audit-allows.d.ts +1 -1
  26. package/dist/cli/audit-allows.d.ts.map +1 -1
  27. package/dist/cli/audit-allows.js +4 -4
  28. package/dist/cli/check-theme.d.ts.map +1 -1
  29. package/dist/cli/check-theme.js +1 -2
  30. package/dist/cli/config.d.ts +197 -0
  31. package/dist/cli/config.d.ts.map +1 -0
  32. package/dist/cli/config.js +20 -0
  33. package/dist/cli/doctor.js +1 -1
  34. package/dist/cli/guard-hook.d.ts.map +1 -1
  35. package/dist/cli/guard-hook.js +2 -8
  36. package/dist/cli/index.d.ts +2 -196
  37. package/dist/cli/index.d.ts.map +1 -1
  38. package/dist/cli/index.js +1 -18
  39. package/dist/commands/define-commands.d.ts.map +1 -1
  40. package/dist/commands/define-commands.js +2 -2
  41. package/dist/commands/define-overlays.d.ts.map +1 -1
  42. package/dist/commands/define-overlays.js +1 -2
  43. package/dist/common/index.d.ts +0 -1
  44. package/dist/common/index.d.ts.map +1 -1
  45. package/dist/common/index.js +0 -2
  46. package/dist/common/validate.d.ts +2 -2
  47. package/dist/common/validate.d.ts.map +1 -1
  48. package/dist/common/validate.js +2 -3
  49. package/dist/content/article-card.d.ts.map +1 -1
  50. package/dist/content/article-card.js +4 -1
  51. package/dist/controls/enum-filter.d.ts.map +1 -1
  52. package/dist/controls/enum-filter.js +3 -13
  53. package/dist/controls/filter-pill.d.ts +8 -0
  54. package/dist/controls/filter-pill.d.ts.map +1 -1
  55. package/dist/controls/filter-pill.js +15 -0
  56. package/dist/controls/multi-select-filter.d.ts.map +1 -1
  57. package/dist/controls/multi-select-filter.js +3 -13
  58. package/dist/controls/number-filter.d.ts +3 -0
  59. package/dist/controls/number-filter.d.ts.map +1 -1
  60. package/dist/controls/number-filter.js +12 -4
  61. package/dist/controls/range-filter.d.ts.map +1 -1
  62. package/dist/controls/range-filter.js +1 -2
  63. package/dist/data/data-table.d.ts +9 -2
  64. package/dist/data/data-table.d.ts.map +1 -1
  65. package/dist/data/data-table.js +2 -3
  66. package/dist/forms/form-layout.d.ts +6 -1
  67. package/dist/forms/form-layout.d.ts.map +1 -1
  68. package/dist/forms/form-layout.module.css +3 -0
  69. package/dist/guard/index.d.ts +18 -4
  70. package/dist/guard/index.d.ts.map +1 -1
  71. package/dist/guard/index.js +67 -31
  72. package/dist/guard/types.d.ts +3 -3
  73. package/dist/guard/types.d.ts.map +1 -1
  74. package/dist/guard/vx-names.d.ts +3 -0
  75. package/dist/guard/vx-names.d.ts.map +1 -0
  76. package/dist/guard/vx-names.js +257 -0
  77. package/dist/notifications/mount-guard.d.ts.map +1 -1
  78. package/dist/notifications/mount-guard.js +1 -2
  79. package/dist/provider/index.d.ts.map +1 -1
  80. package/dist/provider/index.js +2 -3
  81. package/dist/provider/lab-theme.d.ts +5 -4
  82. package/dist/provider/lab-theme.d.ts.map +1 -1
  83. package/dist/provider/lab-theme.js +1 -3
  84. package/dist/router-tanstack/search-store.d.ts +18 -0
  85. package/dist/router-tanstack/search-store.d.ts.map +1 -1
  86. package/dist/router-tanstack/search-store.js +33 -1
  87. package/dist/shell/mobile-nav-model.d.ts.map +1 -1
  88. package/dist/shell/mobile-nav-model.js +3 -4
  89. package/dist/shell/page-bar.d.ts +24 -0
  90. package/dist/shell/page-bar.d.ts.map +1 -1
  91. package/dist/shell/page-bar.js +1 -2
  92. package/dist/surfaces.d.ts +1 -1
  93. package/dist/surfaces.d.ts.map +1 -1
  94. package/dist/surfaces.js +2 -0
  95. package/dist/tokens/index.d.ts +2 -0
  96. package/dist/tokens/index.d.ts.map +1 -1
  97. package/dist/tokens/index.js +10 -0
  98. package/dist/tokens/size-classes.d.ts +13 -0
  99. package/dist/tokens/size-classes.d.ts.map +1 -1
  100. package/dist/tokens/size-classes.js +8 -0
  101. package/llms.txt +2 -2
  102. package/package.json +1 -1
  103. package/src/agent/coalesce.ts +1 -2
  104. package/src/agent/merge.ts +1 -2
  105. package/src/agent-chat/relative-time.ts +1 -2
  106. package/src/agent-chat/thread-message.tsx +2 -3
  107. package/src/charts/kinds/BandStrip.tsx +7 -3
  108. package/src/charts/kinds/Bars.tsx +1 -2
  109. package/src/charts/kinds/StackedArea.tsx +4 -2
  110. package/src/charts/primitives/legend-disclosure.tsx +1 -0
  111. package/src/cli/audit-allows.ts +10 -9
  112. package/src/cli/check-theme.ts +1 -2
  113. package/src/cli/config.ts +209 -0
  114. package/src/cli/doctor.ts +2 -2
  115. package/src/cli/guard-hook.ts +2 -8
  116. package/src/cli/index.ts +2 -206
  117. package/src/commands/define-commands.ts +2 -2
  118. package/src/commands/define-overlays.ts +1 -2
  119. package/src/common/index.ts +0 -1
  120. package/src/common/validate.ts +4 -5
  121. package/src/content/article-card.tsx +1 -0
  122. package/src/controls/enum-filter.tsx +3 -13
  123. package/src/controls/filter-pill.tsx +20 -0
  124. package/src/controls/multi-select-filter.tsx +3 -13
  125. package/src/controls/number-filter.tsx +24 -3
  126. package/src/controls/range-filter.tsx +1 -2
  127. package/src/data/data-table.tsx +11 -5
  128. package/src/forms/form-layout.module.css +3 -0
  129. package/src/forms/form-layout.tsx +6 -1
  130. package/src/guard/index.ts +151 -62
  131. package/src/guard/types.ts +3 -2
  132. package/src/guard/vx-names.ts +257 -0
  133. package/src/notifications/mount-guard.ts +1 -2
  134. package/src/provider/index.tsx +2 -3
  135. package/src/provider/lab-theme.ts +28 -21
  136. package/src/router-tanstack/search-store.ts +73 -1
  137. package/src/shell/mobile-nav-model.ts +3 -5
  138. package/src/shell/page-bar.tsx +25 -4
  139. package/src/surfaces.ts +2 -0
  140. package/src/tokens/index.ts +10 -0
  141. package/src/tokens/size-classes.ts +14 -0
  142. package/dist/common/is-dev.d.ts +0 -3
  143. package/dist/common/is-dev.d.ts.map +0 -1
  144. package/dist/common/is-dev.js +0 -6
  145. package/src/common/is-dev.ts +0 -4
@@ -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
@@ -43,6 +43,24 @@ export type CreateSearchStoreOptions<S extends Record<string, AnyField>> = {
43
43
  fields: S
44
44
  /** Envelope version — bump when the field set changes to discard stale localStorage. */
45
45
  version?: number
46
+ /**
47
+ * Search params this store does NOT own that a URL-lane write drops, in the SAME navigate —
48
+ * `['page']` on a paginated list, so narrowing the set on page 5 lands on page 1 and Back restores
49
+ * both together. Dropped, not written: the route's own `validateSearch` resolves the default
50
+ * (`page` → 1), so the store never has to know it. Applies to a URL-lane field's setter and
51
+ * `clear()` when the value moves, and to `useReset()` when any field moves; an explicit `patch`
52
+ * naming the same key wins.
53
+ *
54
+ * **URL-lane writes only.** A `url: false` field (mirror-only or memory) makes no navigate, so it
55
+ * has none to ride — and a `useReset()` on a store with no URL-lane field keeps the page too.
56
+ *
57
+ * Store-wide by design: every filter of a list narrows the same list. A param the store owns is
58
+ * refused at definition — reset a field with `useReset()` or its own `clear()`.
59
+ *
60
+ * @example
61
+ * createSearchStore({ key: 'shares', fields: { q: field.string({}), sort: … }, resets: ['page'] })
62
+ */
63
+ resets?: readonly string[]
46
64
  }
47
65
 
48
66
  export type SearchStore<S extends Record<string, AnyField>> = {
@@ -205,6 +223,28 @@ function assertNoLazyUrlFallback(key: string, fields: Record<string, AnyField>):
205
223
  }
206
224
  }
207
225
 
226
+ /**
227
+ * `resets` exists for keys the store does NOT own; one it owns would be dropped from the URL while
228
+ * the mirror kept its value — the same URL/mirror split `patch` refuses. Static, so thrown at
229
+ * definition.
230
+ */
231
+ function assertResetsNotOwned(input: {
232
+ key: string
233
+ resets: readonly string[]
234
+ paramOwners: ReadonlyMap<string, string>
235
+ }): void {
236
+ const { key, resets, paramOwners } = input
237
+ for (const param of resets) {
238
+ const owner = paramOwners.get(param)
239
+ if (owner === undefined) continue
240
+ throw new Error(
241
+ `basalt-ui: createSearchStore('${key}'): \`resets\` names \`${param}\`, which this store's ` +
242
+ `field '${owner}' owns. \`resets\` is for sibling params the store does not own (a ` +
243
+ '`page`); a field of the store is reset through `useReset()` or its own `clear()`.',
244
+ )
245
+ }
246
+ }
247
+
208
248
  /** The memory-only lane: neither the URL nor the mirror, so the value lives in the store's own
209
249
  * session-scoped external store (`StoreCore.memoryUse`) — the lane `createLocalStore` also uses. */
210
250
  function isMemoryLane(entry: StoreEntry): boolean {
@@ -240,6 +280,25 @@ export function buildSearchStore<const S extends Record<string, AnyField>>(
240
280
  for (const entry of core.urlEntries) {
241
281
  for (const param of entry.codec.params) paramOwners.set(param, entry.name)
242
282
  }
283
+ const resets = o.resets ?? []
284
+ assertResetsNotOwned({ key: o.key, resets, paramOwners })
285
+ /** Spread right after `prev` in every store navigate, so a `patch` or the field itself wins. */
286
+ const resetPatch: Record<string, undefined> = Object.fromEntries(
287
+ resets.map((param) => [param, undefined]),
288
+ )
289
+ /**
290
+ * `resetPatch` when `next` moves the field off what the URL holds NOW — read from the navigate's
291
+ * own `prev`, never the render-captured value, which a second `set()` in the same tick would
292
+ * misjudge against a value the first one already replaced.
293
+ */
294
+ const resetsFor = (
295
+ entry: StoreEntry,
296
+ prev: Record<string, unknown>,
297
+ next: unknown,
298
+ ): Record<string, undefined> | undefined => {
299
+ const current = core.resolve(entry, prev, core.readRecord())
300
+ return entry.codec.equals(next, current) ? undefined : resetPatch
301
+ }
243
302
  const persistedNames = new Set(
244
303
  core.entries.filter((entry) => entry.codec.lane.persist).map((entry) => entry.name),
245
304
  )
@@ -326,8 +385,10 @@ export function buildSearchStore<const S extends Record<string, AnyField>>(
326
385
  if (!validated) return
327
386
  navigate({
328
387
  to: '.',
388
+ // Gated like the setter: clearing a field already at its fallback keeps the page.
329
389
  search: (prev: Record<string, unknown>) => ({
330
390
  ...prev,
391
+ ...resetsFor(entry, prev, entry.codec.fallback),
331
392
  ...entry.codec.toSearch(entry.codec.fallback),
332
393
  }),
333
394
  replace: true,
@@ -356,8 +417,11 @@ export function buildSearchStore<const S extends Record<string, AnyField>>(
356
417
  // `patch` FIRST, the field's own params last: a patch is for keys the store does not
357
418
  // own (clearing a sibling `detailDate` with `undefined`), so it must never be able to
358
419
  // overwrite the value this very call is setting.
420
+ // `resets` only when the value moves: a no-op write (a debounced box settling back on
421
+ // what it held) must not throw the reader off the page they are on.
359
422
  search: (prev: Record<string, unknown>) => ({
360
423
  ...prev,
424
+ ...resetsFor(entry, prev, next),
361
425
  ...opts?.patch,
362
426
  ...entry.codec.toSearch(next),
363
427
  }),
@@ -464,7 +528,15 @@ export function buildSearchStore<const S extends Record<string, AnyField>>(
464
528
  }
465
529
  navigate({
466
530
  to: '.',
467
- search: (prev: Record<string, unknown>) => ({ ...prev, ...patch }),
531
+ // `resets` only when some field actually moves — the same gate as the setter and
532
+ // `clear()`. A `Reset all` with everything already at its fallback must keep the page:
533
+ // this navigate REPLACES, so a page dropped here could not be got back with Back.
534
+ search: (prev: Record<string, unknown>) => {
535
+ const moved = validatedEntries.some(
536
+ (entry) => resetsFor(entry, prev, entry.codec.fallback) !== undefined,
537
+ )
538
+ return { ...prev, ...(moved ? resetPatch : undefined), ...patch }
539
+ },
468
540
  replace: true,
469
541
  // Same reason as a single field's write above — a `Reset all` pressed from the mobile
470
542
  // sheet must not also scroll the page it was pressed on.
@@ -87,8 +87,6 @@ export function blockRowCount(blocks: readonly SidebarBlock[] | undefined): numb
87
87
  }).length
88
88
  }
89
89
 
90
- const DEV = process.env['NODE_ENV'] !== 'production'
91
-
92
90
  /**
93
91
  * §2.3 rule 1. A section's `mobile: false` wins over every item inside it; otherwise the item's own
94
92
  * field decides, with the shorthand booleans normalized and `undefined` meaning "reachable, but not
@@ -351,7 +349,7 @@ function buildCandidates(
351
349
  /** Rule 11 — `mobile: 'tab'` on a disabled destination is a configuration mistake, not a slot. */
352
350
  const tabbable = (item: SidebarItem): boolean => {
353
351
  if (!item.disabled) return true
354
- if (DEV) {
352
+ if (process.env.NODE_ENV !== 'production') {
355
353
  console.warn(
356
354
  `[basalt] MobileNav: "${item.key}" asks for a bar slot but is disabled — a disabled destination never becomes a tab. It still renders, disabled, in the More surface.`,
357
355
  )
@@ -373,7 +371,7 @@ function buildCandidates(
373
371
  candidates.push(sectionCandidate(section, menuMax))
374
372
  continue
375
373
  }
376
- if (DEV) {
374
+ if (process.env.NODE_ENV !== 'production') {
377
375
  console.warn(
378
376
  `[basalt] MobileNav: mobileNav.tabs names "${id}", which is neither a destination key nor a visible section label — ignoring it.`,
379
377
  )
@@ -464,7 +462,7 @@ export function projectMobileNav(
464
462
  const limit = needsMore ? maxTabs - 1 : maxTabs
465
463
  const kept = candidates.slice(0, limit)
466
464
  const dropped = candidates.slice(limit)
467
- if (DEV && dropped.length > 0) {
465
+ if (process.env.NODE_ENV !== 'production' && dropped.length > 0) {
468
466
  // Rule 13 wants the NAMES, not a tally: "2 candidates dropped" tells a consumer their bar is
469
467
  // wrong, while the keys tell them which `mobile: 'tab'` to move or which section to reorder.
470
468
  console.warn(
@@ -46,9 +46,6 @@ import asideClasses from './page-aside.module.css'
46
46
  /** The custom property row 2's measured height is published on. */
47
47
  export const PAGE_BAR_HEIGHT_VAR = '--basalt-page-bar-h'
48
48
 
49
- /** Dev-only diagnostics, constant-folded out of a production bundle — see {@link useAsidePanelSlot}. */
50
- const DEV = process.env['NODE_ENV'] !== 'production'
51
-
52
49
  /**
53
50
  * What a `PageAside` publishes when it projects itself into row 2 below `sm`
54
51
  * (`docs/ASIDE-SPEC.md` §0 "Desktop and mobile are one declaration"). Metadata only — the aside's
@@ -236,7 +233,7 @@ export function useAsidePanelSlot(): {
236
233
  setSettled(true)
237
234
  }, [])
238
235
  useEffect(() => {
239
- if (!DEV || !settled || !inShell || panelHost) return
236
+ if (process.env.NODE_ENV === 'production' || !settled || !inShell || panelHost) return
240
237
  console.warn(
241
238
  "[basalt] PageAside: this page's PageBar renders no row 2 (no `tabs`, no `filters`, no `filtersEnd`), so below `sm` there is no pill to open the aside — it renders in flow at the bottom of the page instead. Give the bar a row 2, or expect the in-flow form on a phone.",
242
239
  )
@@ -346,6 +343,30 @@ function useMeasuredHeightVar(active: boolean): (el: HTMLDivElement | null) => v
346
343
  return attach
347
344
  }
348
345
 
346
+ /**
347
+ * The page's control bar — row 1 (title/icon, `actions`, `sync`) and row 2 (`tabs`, `filters`,
348
+ * `filtersEnd`). Inside `BasaltShell` both rows portal into shell chrome; with no shell it renders
349
+ * ONE sticky bar in flow, `title` leading, and needs no `PageBarProvider`.
350
+ *
351
+ * @example
352
+ * // Shell-less (linewatch-shaped): the store is local, the bar is the page's first child.
353
+ * const filters = createLocalStore({ key: 'outages', fields: { q: field.string({ max: 60 }) } })
354
+ *
355
+ * function OutagesPage() {
356
+ * const [q] = filters.field.q.use()
357
+ * return (
358
+ * <>
359
+ * <PageBar
360
+ * title="Outages"
361
+ * actions={{ primary: { key: 'export', label: 'Export', onClick: exportCsv } }}
362
+ * filters={<FilterSet><SearchFilter field={filters.field.q} placeholder="Find a line" /></FilterSet>}
363
+ * classNames={{ root: classes.bleed }} // your gutter bleed — see `PageBarSlot`
364
+ * />
365
+ * <OutageTable query={q} />
366
+ * </>
367
+ * )
368
+ * }
369
+ */
349
370
  export function PageBar({
350
371
  title,
351
372
  icon,
package/src/surfaces.ts CHANGED
@@ -355,6 +355,8 @@ export const SURFACES = {
355
355
  'inline-font-size',
356
356
  'css-raw-surface',
357
357
  'surface-shadow-override',
358
+ // A `var(--vx-*)` naming a token basalt no longer (or never) emits.
359
+ 'unknown-vx-token',
358
360
  // Not a token kind — the kind that judges the ANNOTATION rather than the code. It lands here
359
361
  // because `agent/rules/basalt-tokens.md` is where the `theme-allow` grammar is documented
360
362
  // (said once, on purpose), so this is the header that must name its enforcement.
@@ -52,6 +52,16 @@ export type { RadiusValues } from './palette'
52
52
  export { deriveSpacing } from './palette'
53
53
  export type { SpaceValues } from './palette'
54
54
 
55
+ // The two responsive tables — MIGRATING recipes and the raw-breakpoint guard messages send
56
+ // consumers here for them; `migrating-imports.test.ts` pins that every documented import resolves.
57
+ export {
58
+ CONTAINER_CLASSES,
59
+ CONTAINER_GRID_BREAKPOINTS,
60
+ resolveContainerClass,
61
+ SIZE_CLASSES,
62
+ } from './size-classes'
63
+ export type { ContainerClass } from './size-classes'
64
+
55
65
  /** A per-theme color pair: a hue keeps its identity but shifts shade across schemes. */
56
66
  export type ColorPair = { light: string; dark: string }
57
67
 
@@ -14,6 +14,20 @@ export const CONTAINER_CLASSES = { micro: 0, compact: 240, regular: 480, wide: 8
14
14
 
15
15
  export type ContainerClass = keyof typeof CONTAINER_CLASSES
16
16
 
17
+ /**
18
+ * `CONTAINER_CLASSES` as the five-key map `<Grid type="container" breakpoints={…}>` takes (Mantine
19
+ * types it `Record<MantineSize, string>`), so `Grid.Col` keys resolve onto the grid's own width:
20
+ * `xs` opens compact, `sm` regular, `md`/`lg`/`xl` wide. `basalt/raw-breakpoint` trusts this import
21
+ * by name (from `basalt-ui/tokens`) — any other imported map is unreadable to it and flagged.
22
+ */
23
+ export const CONTAINER_GRID_BREAKPOINTS = {
24
+ xs: `${CONTAINER_CLASSES.compact}px`,
25
+ sm: `${CONTAINER_CLASSES.regular}px`,
26
+ md: `${CONTAINER_CLASSES.wide}px`,
27
+ lg: `${CONTAINER_CLASSES.wide}px`,
28
+ xl: `${CONTAINER_CLASSES.wide}px`,
29
+ } as const
30
+
17
31
  /** The class a MEASURED inline size falls in (`px` is a min-width boundary, so 240 is `compact`). */
18
32
  export function resolveContainerClass(px: number): ContainerClass {
19
33
  if (px >= CONTAINER_CLASSES.wide) return 'wide'
@@ -1,3 +0,0 @@
1
- /** Dev-mode gate. Bundlers constant-fold `process.env.NODE_ENV` so the branch disappears in production builds. */
2
- export declare function isDev(): boolean;
3
- //# sourceMappingURL=is-dev.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"is-dev.d.ts","sourceRoot":"","sources":["../../src/common/is-dev.ts"],"names":[],"mappings":"AAAA,kHAAkH;AAClH,wBAAgB,KAAK,IAAI,OAAO,CAE/B"}
@@ -1,6 +0,0 @@
1
- function isDev() {
2
- return process.env["NODE_ENV"] !== "production";
3
- }
4
- export {
5
- isDev
6
- };
@@ -1,4 +0,0 @@
1
- /** Dev-mode gate. Bundlers constant-fold `process.env.NODE_ENV` so the branch disappears in production builds. */
2
- export function isDev(): boolean {
3
- return process.env['NODE_ENV'] !== 'production'
4
- }