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.
- package/CHANGELOG.md +39 -0
- package/MIGRATING.md +124 -24
- package/agent/rules/basalt-state.md +2 -2
- package/agent/rules/basalt-tokens.md +1 -1
- package/configs/oxlint-plugin.js +68 -61
- package/configs/oxlint-plugin.test.ts +126 -9
- package/configs/oxlint.json +2 -2
- package/dist/agent/coalesce.d.ts.map +1 -1
- package/dist/agent/coalesce.js +1 -2
- package/dist/agent/merge.d.ts.map +1 -1
- package/dist/agent/merge.js +1 -2
- package/dist/agent-chat/relative-time.d.ts +9 -0
- package/dist/agent-chat/relative-time.d.ts.map +1 -1
- package/dist/agent-chat/relative-time.js +1 -2
- package/dist/agent-chat/thread-message.d.ts.map +1 -1
- package/dist/agent-chat/thread-message.js +1 -2
- package/dist/charts/kinds/BandStrip.d.ts.map +1 -1
- package/dist/charts/kinds/BandStrip.js +3 -3
- package/dist/charts/kinds/Bars.d.ts.map +1 -1
- package/dist/charts/kinds/Bars.js +1 -2
- package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
- package/dist/charts/kinds/StackedArea.js +1 -2
- package/dist/charts/primitives/legend-disclosure.d.ts.map +1 -1
- package/dist/cli/audit-allows.d.ts +1 -1
- package/dist/cli/audit-allows.d.ts.map +1 -1
- package/dist/cli/audit-allows.js +4 -4
- package/dist/cli/check-theme.d.ts.map +1 -1
- package/dist/cli/check-theme.js +1 -2
- package/dist/cli/config.d.ts +197 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +20 -0
- package/dist/cli/doctor.js +1 -1
- package/dist/cli/guard-hook.d.ts.map +1 -1
- package/dist/cli/guard-hook.js +2 -8
- package/dist/cli/index.d.ts +2 -196
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +1 -18
- package/dist/commands/define-commands.d.ts.map +1 -1
- package/dist/commands/define-commands.js +2 -2
- package/dist/commands/define-overlays.d.ts.map +1 -1
- package/dist/commands/define-overlays.js +1 -2
- package/dist/common/index.d.ts +0 -1
- package/dist/common/index.d.ts.map +1 -1
- package/dist/common/index.js +0 -2
- package/dist/common/validate.d.ts +2 -2
- package/dist/common/validate.d.ts.map +1 -1
- package/dist/common/validate.js +2 -3
- package/dist/content/article-card.d.ts.map +1 -1
- package/dist/content/article-card.js +4 -1
- package/dist/controls/enum-filter.d.ts.map +1 -1
- package/dist/controls/enum-filter.js +3 -13
- package/dist/controls/filter-pill.d.ts +8 -0
- package/dist/controls/filter-pill.d.ts.map +1 -1
- package/dist/controls/filter-pill.js +15 -0
- package/dist/controls/multi-select-filter.d.ts.map +1 -1
- package/dist/controls/multi-select-filter.js +3 -13
- package/dist/controls/number-filter.d.ts +3 -0
- package/dist/controls/number-filter.d.ts.map +1 -1
- package/dist/controls/number-filter.js +12 -4
- package/dist/controls/range-filter.d.ts.map +1 -1
- package/dist/controls/range-filter.js +1 -2
- package/dist/data/data-table.d.ts.map +1 -1
- package/dist/data/data-table.js +2 -3
- package/dist/guard/index.d.ts +16 -2
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +63 -22
- package/dist/guard/types.d.ts +3 -3
- package/dist/guard/types.d.ts.map +1 -1
- package/dist/guard/vx-names.d.ts +3 -0
- package/dist/guard/vx-names.d.ts.map +1 -0
- package/dist/guard/vx-names.js +257 -0
- package/dist/notifications/mount-guard.d.ts.map +1 -1
- package/dist/notifications/mount-guard.js +1 -2
- package/dist/provider/index.d.ts.map +1 -1
- package/dist/provider/index.js +2 -3
- package/dist/provider/lab-theme.d.ts +5 -4
- package/dist/provider/lab-theme.d.ts.map +1 -1
- package/dist/provider/lab-theme.js +1 -3
- package/dist/router-tanstack/search-store.d.ts +18 -0
- package/dist/router-tanstack/search-store.d.ts.map +1 -1
- package/dist/router-tanstack/search-store.js +33 -1
- package/dist/shell/mobile-nav-model.d.ts.map +1 -1
- package/dist/shell/mobile-nav-model.js +3 -4
- package/dist/shell/page-bar.d.ts.map +1 -1
- package/dist/shell/page-bar.js +1 -2
- package/dist/surfaces.d.ts +1 -1
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +2 -0
- package/dist/tokens/index.d.ts +1 -1
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +7 -1
- package/dist/tokens/size-classes.d.ts +13 -0
- package/dist/tokens/size-classes.d.ts.map +1 -1
- package/dist/tokens/size-classes.js +8 -0
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/src/agent/coalesce.ts +1 -2
- package/src/agent/merge.ts +1 -2
- package/src/agent-chat/relative-time.ts +1 -2
- package/src/agent-chat/thread-message.tsx +2 -3
- package/src/charts/kinds/BandStrip.tsx +7 -3
- package/src/charts/kinds/Bars.tsx +1 -2
- package/src/charts/kinds/StackedArea.tsx +4 -2
- package/src/charts/primitives/legend-disclosure.tsx +1 -0
- package/src/cli/audit-allows.ts +10 -9
- package/src/cli/check-theme.ts +1 -2
- package/src/cli/config.ts +209 -0
- package/src/cli/doctor.ts +2 -2
- package/src/cli/guard-hook.ts +2 -8
- package/src/cli/index.ts +2 -206
- package/src/commands/define-commands.ts +2 -2
- package/src/commands/define-overlays.ts +1 -2
- package/src/common/index.ts +0 -1
- package/src/common/validate.ts +4 -5
- package/src/content/article-card.tsx +1 -0
- package/src/controls/enum-filter.tsx +3 -13
- package/src/controls/filter-pill.tsx +20 -0
- package/src/controls/multi-select-filter.tsx +3 -13
- package/src/controls/number-filter.tsx +24 -3
- package/src/controls/range-filter.tsx +1 -2
- package/src/data/data-table.tsx +2 -3
- package/src/guard/index.ts +136 -46
- package/src/guard/types.ts +3 -2
- package/src/guard/vx-names.ts +257 -0
- package/src/notifications/mount-guard.ts +1 -2
- package/src/provider/index.tsx +2 -3
- package/src/provider/lab-theme.ts +28 -21
- package/src/router-tanstack/search-store.ts +73 -1
- package/src/shell/mobile-nav-model.ts +3 -5
- package/src/shell/page-bar.tsx +1 -4
- package/src/surfaces.ts +2 -0
- package/src/tokens/index.ts +6 -1
- package/src/tokens/size-classes.ts +14 -0
- package/dist/common/is-dev.d.ts +0 -3
- package/dist/common/is-dev.d.ts.map +0 -1
- package/dist/common/is-dev.js +0 -6
- package/src/common/is-dev.ts +0 -4
package/src/cli/index.ts
CHANGED
|
@@ -39,199 +39,8 @@ import { COMMAND_OPTIONS, USAGE, unknownFlag } from './help'
|
|
|
39
39
|
import { init } from './init'
|
|
40
40
|
import { sync } from './sync'
|
|
41
41
|
import { fontsCss, normalizeColorFunctions, tokensCss } from './tokens-css'
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
export type BasaltConfig = {
|
|
45
|
-
/** Source roots to scan. Default: `['src']`. Set explicitly for a monorepo layout. */
|
|
46
|
-
roots?: string[]
|
|
47
|
-
/** Files exempt from the scan (they ARE the palette source). Default: argo's exempt set. */
|
|
48
|
-
exempt?: string[]
|
|
49
|
-
/**
|
|
50
|
-
* Declares this package a tokens-only consumer: it uses the `--vx-*` layer and no Mantine. Turns
|
|
51
|
-
* off every guard kind whose remedy is a Mantine component, a Mantine prop or the React theme
|
|
52
|
-
* factory, leaving the color and typography kinds live. Default: `'mantine'`.
|
|
53
|
-
*
|
|
54
|
-
* Deliberately not inferred from a missing `@mantine/core`: silencing 17 rules (the live count is
|
|
55
|
-
* `TOKENS_ONLY_DISABLED_KINDS.size`, which the check-theme banner prints) is a decision, and
|
|
56
|
-
* a repo that keeps Mantine in a workspace package would otherwise switch off half its own guard
|
|
57
|
-
* without saying so. `basalt-ui doctor` detects the shape and tells you to set this.
|
|
58
|
-
*/
|
|
59
|
-
profile?: 'mantine' | 'tokens-only'
|
|
60
|
-
/**
|
|
61
|
-
* Extra files to scan, named individually and relative to this package — for a design surface
|
|
62
|
-
* that lives outside every root and outside the `index.html` / `public/` conventions the walker
|
|
63
|
-
* already derives. Also the ONLY way a `.json` is ever scanned: the guard understands JSON as
|
|
64
|
-
* markup, but a consumer repo's JSON is overwhelmingly config, fixtures and lockfiles, so
|
|
65
|
-
* blanket-scanning it is a false-positive generator rather than a guard.
|
|
66
|
-
*
|
|
67
|
-
* @example
|
|
68
|
-
* { include: ['app/manifest.json', 'emails/template.html'] }
|
|
69
|
-
*/
|
|
70
|
-
include?: string[]
|
|
71
|
-
/** Named spacing-scale steps (px) flagged when used as a raw spacing prop. Default: 10/12/16/20/32. */
|
|
72
|
-
spacingSteps?: number[]
|
|
73
|
-
/** Per-kind severity override — `'warn'` reports without failing, `'error'` fails the build.
|
|
74
|
-
* See `GuardSeverity` in ../guard/types for the grace-minor doctrine behind it. */
|
|
75
|
-
severity?: Partial<Record<GuardKind, GuardSeverity>>
|
|
76
|
-
/** Off-identity Mantine accent families forbidden as chrome accents. Default: argo's set. */
|
|
77
|
-
forbiddenAccents?: string[]
|
|
78
|
-
/** Earned accent hue recorded in DESIGN.md `{{ACCENT_HUE}}`. Default: `blue`. */
|
|
79
|
-
accentHue?: string
|
|
80
|
-
/**
|
|
81
|
-
* Flag any numeric radius prop literal (radius={6}) — use the radius token scale instead.
|
|
82
|
-
* Default: `true` (ON). Set `false` to disable the `raw-radius` check (e.g. a repo that DEFINES
|
|
83
|
-
* the radius primitives).
|
|
84
|
-
*/
|
|
85
|
-
rawRadius?: boolean
|
|
86
|
-
/**
|
|
87
|
-
* Flag ad-hoc inline surface styling (`border`/`borderRadius`/`boxShadow` literals in a `style={{}}`)
|
|
88
|
-
* that bypasses the radius-token + `VX.surface.*` + `VX.shadowCard` system. Default: `true` (ON).
|
|
89
|
-
* Set `false` to disable the `raw-surface` check.
|
|
90
|
-
*/
|
|
91
|
-
rawSurface?: boolean
|
|
92
|
-
/**
|
|
93
|
-
* Flag `withBorder` on a `Card` / `Paper`. Card depth is `--vx-shadow-card` — a whisper shadow
|
|
94
|
-
* with the 1px ring baked into the SAME value — so `withBorder` draws a SECOND, real border over
|
|
95
|
-
* that ring and the card reads heavy/boxed (docs/DESIGN-SPEC.md doctrine inversion #1).
|
|
96
|
-
* `withBorder={false}` and `<Card.Section withBorder>` (a section divider) both pass.
|
|
97
|
-
* Default: `true` (ON). Set `false` to disable the `card-with-border` check.
|
|
98
|
-
*/
|
|
99
|
-
cardWithBorder?: boolean
|
|
100
|
-
/**
|
|
101
|
-
* Flag references to a raw Mantine ramp step used for surface color
|
|
102
|
-
* (`var(--mantine-color-gray-N)` / `var(--mantine-color-dark-N)`) — these bypass the basalt
|
|
103
|
-
* surface tokens. Default: `true` (ON). Set `false` to disable the `off-system-surface-var` check.
|
|
104
|
-
*/
|
|
105
|
-
offSystemSurfaceVar?: boolean
|
|
106
|
-
/**
|
|
107
|
-
* Flag a SHADE-PINNED Mantine color — `c="yellow.7"`, `bg="blue.4"`,
|
|
108
|
-
* `var(--mantine-color-red-6)`. A pinned step is one fixed swatch in BOTH color schemes, so it
|
|
109
|
-
* cannot stay legible in either; route a verdict color through `VX.status.*` / `--vx-status-*`,
|
|
110
|
-
* or drop the index (`c="red"`) and let the theme resolve the shade per scheme. `gray-*`/`dark-*`
|
|
111
|
-
* in `var()` form belong to `off-system-surface-var` instead. Default: `true` (ON). Set `false`
|
|
112
|
-
* to disable the `mantine-shade-index` check.
|
|
113
|
-
*/
|
|
114
|
-
mantineShadeIndex?: boolean
|
|
115
|
-
/**
|
|
116
|
-
* Flag raw lowercase JSX layout/surface elements (`div`/`span`/`section`/…) carrying an inline
|
|
117
|
-
* `style={{}}` with a layout/surface property — steer to a Mantine layout primitive
|
|
118
|
-
* (`Box`/`Flex`/`Grid`/`Stack`/`Group`/`SimpleGrid`/`Paper`). Default: `true` (ON). Set `false`
|
|
119
|
-
* to disable the `raw-html-layout` check.
|
|
120
|
-
*/
|
|
121
|
-
rawHtmlLayout?: boolean
|
|
122
|
-
/**
|
|
123
|
-
* Flag spacing/sizing literals inside an inline `style={{}}` (`padding`/`margin`/`gap`/…) — the
|
|
124
|
-
* `raw-spacing` check only catches the Mantine prop syntax. Default: `true` (ON). Set `false` to
|
|
125
|
-
* disable the `inline-spacing` check.
|
|
126
|
-
*/
|
|
127
|
-
inlineSpacing?: boolean
|
|
128
|
-
/**
|
|
129
|
-
* Flag `display: 'flex' | 'grid' | 'inline-flex' | 'inline-grid'` in an inline `style={{}}` —
|
|
130
|
-
* steer to `<Flex>`/`<Grid>`/`<Group>`. Default: `true` (ON). Set `false` to disable the
|
|
131
|
-
* `inline-display` check.
|
|
132
|
-
*/
|
|
133
|
-
inlineDisplay?: boolean
|
|
134
|
-
/**
|
|
135
|
-
* Flag raw `<AxisLeft>` / `<AxisBottom>` / `<AxisRight>` visx JSX inside chart files (a path
|
|
136
|
-
* containing `/charts/`) — these bypass the `AxisLeftNumeric` / `AxisBottomDate` /
|
|
137
|
-
* `AxisRightNumeric` primitives that carry the theme tokens + smart ticks. The legitimate wrapper
|
|
138
|
-
* (`Axes.tsx`, which IS the primitive) is exempt. Default: `true` (ON). Set `false` to disable the
|
|
139
|
-
* `raw-visx-axis` check.
|
|
140
|
-
*/
|
|
141
|
-
rawVisxAxis?: boolean
|
|
142
|
-
/**
|
|
143
|
-
* Flag a hardcoded duration/spring/ease literal inline in a `transition={{...}}` prop — route it
|
|
144
|
-
* through `MOTION_DURATION` / `MOTION_SPRING` / `MOTION_EASE_STANDARD` instead. Default: `true`
|
|
145
|
-
* (ON). Set `false` to disable the `raw-motion-value` check.
|
|
146
|
-
*/
|
|
147
|
-
rawMotionValue?: boolean
|
|
148
|
-
/**
|
|
149
|
-
* Flag a chart entry-point JSX tag (`MultiLine`/`Bars`/`Donut`/`DualPanel`/`Heatmap`/`ZonedLine`/
|
|
150
|
-
* `StackedArea`/`LineSparkline`/`BarSparkline`) missing an `ariaLabel` prop. Default: `true`
|
|
151
|
-
* (ON). Set `false` to disable the `chart-missing-aria-label` check.
|
|
152
|
-
*/
|
|
153
|
-
chartMissingAriaLabel?: boolean
|
|
154
|
-
/**
|
|
155
|
-
* Flag a raw lowercase `<input>`/`<select>`/`<textarea>` JSX element — it bypasses the entire
|
|
156
|
-
* theme, not just the iOS font-size floor. Use the Mantine equivalents (`TextInput`,
|
|
157
|
-
* `NumberInput`, `Select`, `Textarea`, …) instead. Default: `true` (ON). Set `false` to disable
|
|
158
|
-
* the `raw-form-control` check.
|
|
159
|
-
*/
|
|
160
|
-
rawFormControl?: boolean
|
|
161
|
-
/**
|
|
162
|
-
* Flag a `fontSize`/`font-size` literal below 16 inside a `style={{…}}` on a raw form control,
|
|
163
|
-
* or a Mantine `styles={{ input: {…} }}` per-part style — the `styles.css` iOS floor is
|
|
164
|
-
* `!important`, so the override is dead code. Default: `true` (ON). Set `false` to disable the
|
|
165
|
-
* `sub-16-input-font` check.
|
|
166
|
-
*/
|
|
167
|
-
sub16InputFont?: boolean
|
|
168
|
-
/**
|
|
169
|
-
* Path of the consumer's guard-exempt series file, for DESIGN.md `{{SERIES_MODULE_PATH}}`.
|
|
170
|
-
* Default: `<first basalt.root>/lib/series.ts` — see `resolveSeriesModulePath`.
|
|
171
|
-
*/
|
|
172
|
-
seriesModulePath?: string
|
|
173
|
-
/**
|
|
174
|
-
* Per-rule, per-path exemptions — complements whole-file `exempt` (which skips ALL rules for a
|
|
175
|
-
* file) and the hardcoded per-kind `appliesTo` scoping (e.g. `raw-visx-axis` → chart files
|
|
176
|
-
* only). Each value is a list of patterns matched against a finding's relative path. A pattern
|
|
177
|
-
* may be a whole path segment (`'agent'` matches `src/agent/x.tsx`, not `src/agenting.ts`), a
|
|
178
|
-
* relative path (`'public/site.webmanifest'`), a directory prefix, or a glob — `*` stops at `/`,
|
|
179
|
-
* `**` does not, and a slash-free glob also matches the basename, so `'*.module.css'` works.
|
|
180
|
-
* A trailing `/` is stripped. An entry that suppresses nothing is reported, and
|
|
181
|
-
* `check-theme --audit-allows` exits 1 on it. Default: `{}` (no exemptions).
|
|
182
|
-
*
|
|
183
|
-
* Two forms per kind. The bare array is paths only. The object form adds the REASON, which is
|
|
184
|
-
* the half a `theme-allow` carries and this key could not: JSON has no comments, so a
|
|
185
|
-
* `.webmanifest` / `.json` finding's only escape is this key, and it was un-reviewable by
|
|
186
|
-
* construction — the rationale ended up in a CLAUDE.md paragraph nobody reads next to the diff.
|
|
187
|
-
* `check-theme --audit-allows` prints the reason (or names its absence) beside what the entry
|
|
188
|
-
* still suppresses. The array form stays supported and unchanged; nothing here is required.
|
|
189
|
-
*
|
|
190
|
-
* @example
|
|
191
|
-
* { exemptRules: { 'inline-display': ['agent'] } } // inline-display never fires under src/agent/**
|
|
192
|
-
* @example
|
|
193
|
-
* { exemptRules: { 'raw-hex': { paths: ['site.webmanifest'],
|
|
194
|
-
* reason: 'a PWA manifest theme_color MUST be a literal hex — JSON cannot reference a CSS var' } } }
|
|
195
|
-
*/
|
|
196
|
-
exemptRules?: Partial<Record<GuardKind, ExemptRuleEntry>>
|
|
197
|
-
/**
|
|
198
|
-
* Declares an intentional `ai` package major-version skew across workspace packages, exempting
|
|
199
|
-
* `doctor`'s `ai-major-parity` hard check. The value IS the reason — a bare `true` is rejected,
|
|
200
|
-
* because the whole point of this key is that the pairing is written down, not just switched off.
|
|
201
|
-
* A written declaration is the pin `basalt/ai-sdk-major`'s own comment notes is otherwise missing.
|
|
202
|
-
*
|
|
203
|
-
* Semantics: absent (or present but not a non-empty string) → the skew still hard-fails exactly as
|
|
204
|
-
* without this key. A non-empty string with a skew present → `doctor` passes, echoing both the
|
|
205
|
-
* skew and this reason. A non-empty string with NO skew present → `doctor` warns that the
|
|
206
|
-
* exemption is stale and can be deleted (an exemption nobody re-checks is how a real, later skew
|
|
207
|
-
* slips through unnoticed).
|
|
208
|
-
*
|
|
209
|
-
* @example
|
|
210
|
-
* // apps/api streams on ai@5; apps/dashboard parses on ai@7; a producer-side TransformStream
|
|
211
|
-
* // neutralizes the one enum value that differs between the two majors.
|
|
212
|
-
* { aiMajorSkewReason: 'apps/api pinned to ai@5, apps/dashboard on ai@7 — the skew is neutralized '
|
|
213
|
-
* + 'by a producer-side TransformStream in apps/api/src/stream-transform.ts' }
|
|
214
|
-
*/
|
|
215
|
-
aiMajorSkewReason?: string
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* One `basalt.exemptRules` value: the historical bare path list, or the same list with the reason
|
|
220
|
-
* it exists. See `BasaltConfig.exemptRules`.
|
|
221
|
-
*/
|
|
222
|
-
export type ExemptRuleEntry = string[] | { paths: string[]; reason: string }
|
|
223
|
-
|
|
224
|
-
/** The paths half of an exemption entry, whichever form it was written in. */
|
|
225
|
-
export function exemptRulePaths(entry: ExemptRuleEntry | undefined): string[] {
|
|
226
|
-
if (entry === undefined) return []
|
|
227
|
-
return Array.isArray(entry) ? entry : entry.paths
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/** The recorded reason for an exemption entry, or null for the bare-array form. */
|
|
231
|
-
export function exemptRuleReason(entry: ExemptRuleEntry | undefined): string | null {
|
|
232
|
-
if (entry === undefined || Array.isArray(entry)) return null
|
|
233
|
-
return entry.reason.trim().length > 0 ? entry.reason.trim() : null
|
|
234
|
-
}
|
|
42
|
+
import { DEFAULT_ROOT, exemptRulePaths, resolveRoots } from './config'
|
|
43
|
+
import type { BasaltConfig } from './config'
|
|
235
44
|
|
|
236
45
|
/**
|
|
237
46
|
* The guard-shaped `exemptRules` — paths only. The reason the object form carries never reaches
|
|
@@ -247,19 +56,6 @@ export function resolveExemptRules(cfg: BasaltConfig): GuardConfig['exemptRules'
|
|
|
247
56
|
return out
|
|
248
57
|
}
|
|
249
58
|
|
|
250
|
-
export const DEFAULT_ROOT = 'src'
|
|
251
|
-
export const DEFAULT_ROOTS = [DEFAULT_ROOT]
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* The configured source roots, or the built-in default. The ONE resolution every roots-derived seed
|
|
255
|
-
* reads — an empty `roots: []` falls back rather than resolving to nothing, because a bare `??`
|
|
256
|
-
* would let `[]` through and render an empty oxfmt glob into the seeded CI, reproducing the exact
|
|
257
|
-
* "matches zero files" break this derivation exists to prevent.
|
|
258
|
-
*/
|
|
259
|
-
export function resolveRoots(cfg: BasaltConfig): string[] {
|
|
260
|
-
return cfg.roots?.length ? cfg.roots : DEFAULT_ROOTS
|
|
261
|
-
}
|
|
262
|
-
|
|
263
59
|
/**
|
|
264
60
|
* The one consumer series path — the DESIGN.md template's `{{SERIES_MODULE_PATH}}`, the default
|
|
265
61
|
* scan exemption, and the skills' prose all resolve through this single function.
|
|
@@ -24,7 +24,6 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import type { ReactNode } from 'react'
|
|
26
26
|
import type { Slot } from '../register'
|
|
27
|
-
import { isDev } from '../common/is-dev'
|
|
28
27
|
|
|
29
28
|
// ── Context ───────────────────────────────────────────────────────────────────
|
|
30
29
|
|
|
@@ -172,7 +171,8 @@ export function defineCommand(cmd: Command): Command {
|
|
|
172
171
|
export function runCommand(id: CommandId, ctx?: CommandRunContext): void | Promise<void> {
|
|
173
172
|
const cmd = activeCommands[id as string]
|
|
174
173
|
if (cmd === undefined) {
|
|
175
|
-
if (
|
|
174
|
+
if (process.env.NODE_ENV !== 'production')
|
|
175
|
+
console.warn(`[basalt] runCommand: no command registered for "${id}"`)
|
|
176
176
|
return
|
|
177
177
|
}
|
|
178
178
|
return cmd.run(ctx)
|
|
@@ -29,7 +29,6 @@ import type { ReactNode } from 'react'
|
|
|
29
29
|
import { useEffect } from 'react'
|
|
30
30
|
import { missingLayer } from '../common/errors'
|
|
31
31
|
import type { Slot } from '../register'
|
|
32
|
-
import { isDev } from '../common/is-dev'
|
|
33
32
|
|
|
34
33
|
// ── Lazy @mantine/modals resolution ───────────────────────────────────────────
|
|
35
34
|
// @mantine/modals is an OPTIONAL peer — never imported at module evaluation time, so importing
|
|
@@ -372,7 +371,7 @@ export const overlays = {
|
|
|
372
371
|
open<K extends OverlayKey>(key: K, props: OverlayProps<K>): void {
|
|
373
372
|
const spec = activeOverlays[key as string]
|
|
374
373
|
if (spec === undefined) {
|
|
375
|
-
if (
|
|
374
|
+
if (process.env.NODE_ENV !== 'production')
|
|
376
375
|
console.warn(`[basalt] overlays.open: no overlay registered for "${String(key)}"`)
|
|
377
376
|
return
|
|
378
377
|
}
|
package/src/common/index.ts
CHANGED
|
@@ -19,5 +19,4 @@ export {
|
|
|
19
19
|
} from './errors'
|
|
20
20
|
export { useValidateProps, assertRequiredProps, resetValidatedProps } from './validate'
|
|
21
21
|
export { scrollParentOf, SCROLLPORT_ATTRIBUTE } from './scroll-parent'
|
|
22
|
-
export { isDev } from './is-dev'
|
|
23
22
|
export { MOTION_DURATION, MOTION_EASE_STANDARD, MOTION_SPRING } from './motion'
|
package/src/common/validate.ts
CHANGED
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
* mean warns once in dev and renders on (`useValidateProps`).
|
|
8
8
|
*/
|
|
9
9
|
import { useEffect } from 'react'
|
|
10
|
-
import { isDev } from './is-dev'
|
|
11
10
|
import { requiredProp } from './errors'
|
|
12
11
|
|
|
13
12
|
/** A check returns the message(s) it has, or nothing. `null`/`undefined` entries are ignored. */
|
|
@@ -23,8 +22,8 @@ const reported = new Set<string>()
|
|
|
23
22
|
|
|
24
23
|
/**
|
|
25
24
|
* Dev-only prop validation. `console.error`s each message ONCE per `(component, message)` pair, and
|
|
26
|
-
* is a no-op in production:
|
|
27
|
-
* constant-folds, so the whole body disappears from a production build.
|
|
25
|
+
* is a no-op in production: the gate is a literal `process.env.NODE_ENV` read, which every bundler
|
|
26
|
+
* constant-folds, so the whole body disappears from a production build (see `dev-gate.test.ts`).
|
|
28
27
|
*
|
|
29
28
|
* **The dedup key is `${component} ${message}`, and nothing else** — not the instance, not the
|
|
30
29
|
* props. Two instances of one component producing the SAME message say it once between them, which
|
|
@@ -46,7 +45,7 @@ export function useValidateProps(
|
|
|
46
45
|
deps: unknown[],
|
|
47
46
|
): void {
|
|
48
47
|
useEffect(() => {
|
|
49
|
-
if (
|
|
48
|
+
if (process.env.NODE_ENV === 'production') return
|
|
50
49
|
const result = check()
|
|
51
50
|
const messages = Array.isArray(result) ? result : [result]
|
|
52
51
|
for (const raw of messages) {
|
|
@@ -67,7 +66,7 @@ export function useValidateProps(
|
|
|
67
66
|
* are about to win in the very next render.
|
|
68
67
|
*/
|
|
69
68
|
export function reportOnce(component: string, message: string): void {
|
|
70
|
-
if (
|
|
69
|
+
if (process.env.NODE_ENV === 'production') return
|
|
71
70
|
const key = `${component} ${message}`
|
|
72
71
|
if (reported.has(key)) return
|
|
73
72
|
reported.add(key)
|
|
@@ -166,6 +166,7 @@ export type ArticleGridProps = {
|
|
|
166
166
|
|
|
167
167
|
export function ArticleGrid({ children, cols = 3 }: ArticleGridProps) {
|
|
168
168
|
return (
|
|
169
|
+
// theme-allow raw-breakpoint — `cols` is documented per viewport `lg`; container keys would re-flow every consumer's grid, so that move ships opt-in
|
|
169
170
|
<SimpleGrid cols={{ base: 1, sm: 2, lg: cols }} spacing="sm">
|
|
170
171
|
{children}
|
|
171
172
|
</SimpleGrid>
|
|
@@ -26,13 +26,13 @@
|
|
|
26
26
|
* its doc). Both public wrappers still type `field` exactly, so a wrong field kind is still a type
|
|
27
27
|
* error at the call site; the widening lives here, where nothing consumer-facing points at it.
|
|
28
28
|
*/
|
|
29
|
-
import {
|
|
29
|
+
import { Radio, Stack } from '@mantine/core'
|
|
30
30
|
import type { ReactNode } from 'react'
|
|
31
31
|
import type { BasaltProps } from '../common/props'
|
|
32
32
|
import type { FieldOption } from '../state'
|
|
33
33
|
import classes from './controls.module.css'
|
|
34
34
|
import { POPOVER_ROW_HIT_GAP, useFilterRegistration, useFilterSurface } from './filter-context'
|
|
35
|
-
import { FilterPill } from './filter-pill'
|
|
35
|
+
import { FilterClearButton, FilterPill } from './filter-pill'
|
|
36
36
|
import { useControlName } from './filter-sheet'
|
|
37
37
|
import { PanelChoice, PanelRow } from './panel-row'
|
|
38
38
|
import type { FilterOption } from './select-filter'
|
|
@@ -157,17 +157,7 @@ export function EnumFilter<T extends string>({
|
|
|
157
157
|
/>
|
|
158
158
|
))}
|
|
159
159
|
</Stack>
|
|
160
|
-
{clearable === true && !isDefault && (
|
|
161
|
-
<Button
|
|
162
|
-
variant="subtle"
|
|
163
|
-
size="ctl"
|
|
164
|
-
onClick={() => {
|
|
165
|
-
field.clear()
|
|
166
|
-
}}
|
|
167
|
-
>
|
|
168
|
-
Clear
|
|
169
|
-
</Button>
|
|
170
|
-
)}
|
|
160
|
+
{clearable === true && !isDefault && <FilterClearButton onClear={() => field.clear()} />}
|
|
171
161
|
</Radio.Group>
|
|
172
162
|
)
|
|
173
163
|
|
|
@@ -122,3 +122,23 @@ export function FilterPill({
|
|
|
122
122
|
</Popover>
|
|
123
123
|
)
|
|
124
124
|
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The popover's `Clear` — one button for `EnumFilter`, `NumberFilter` and `MultiSelectFilter`. Its
|
|
128
|
+
* mousedown keeps focus where it is: a stepper commits its draft on blur, so a press that blurred
|
|
129
|
+
* first would land the typed value in the URL a tick before the clear replaced it.
|
|
130
|
+
*/
|
|
131
|
+
export function FilterClearButton({ onClear }: { readonly onClear: () => void }): ReactNode {
|
|
132
|
+
return (
|
|
133
|
+
<Button
|
|
134
|
+
variant="subtle"
|
|
135
|
+
size="ctl"
|
|
136
|
+
onMouseDown={(event) => {
|
|
137
|
+
event.preventDefault()
|
|
138
|
+
}}
|
|
139
|
+
onClick={onClear}
|
|
140
|
+
>
|
|
141
|
+
Clear
|
|
142
|
+
</Button>
|
|
143
|
+
)
|
|
144
|
+
}
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* // arrive as a map rather than baked into the labels the way the runtime catalogue does it.
|
|
23
23
|
* <MultiSelectFilter field={shipments.field.origin} label="Origin" counts={countsByOrigin} />
|
|
24
24
|
*/
|
|
25
|
-
import {
|
|
25
|
+
import { Checkbox, Stack } from '@mantine/core'
|
|
26
26
|
import { useState } from 'react'
|
|
27
27
|
import type { CSSProperties, ReactNode } from 'react'
|
|
28
28
|
import type { BasaltProps } from '../common/props'
|
|
@@ -31,7 +31,7 @@ import type { FieldHandle, MultiField } from '../state'
|
|
|
31
31
|
import type { FilterOption } from './select-filter'
|
|
32
32
|
import classes from './controls.module.css'
|
|
33
33
|
import { POPOVER_ROW_HIT_GAP, useFilterRegistration, useFilterSurface } from './filter-context'
|
|
34
|
-
import { FilterPill } from './filter-pill'
|
|
34
|
+
import { FilterClearButton, FilterPill } from './filter-pill'
|
|
35
35
|
import { CheckGlyph, useControlName } from './filter-sheet'
|
|
36
36
|
import { PanelRow } from './panel-row'
|
|
37
37
|
|
|
@@ -153,17 +153,7 @@ export function MultiSelectFilter<T extends string>(props: MultiSelectFilterProp
|
|
|
153
153
|
/>
|
|
154
154
|
))}
|
|
155
155
|
</Stack>
|
|
156
|
-
{!isDefault && (
|
|
157
|
-
<Button
|
|
158
|
-
variant="subtle"
|
|
159
|
-
size="ctl"
|
|
160
|
-
onClick={() => {
|
|
161
|
-
field.clear()
|
|
162
|
-
}}
|
|
163
|
-
>
|
|
164
|
-
Clear
|
|
165
|
-
</Button>
|
|
166
|
-
)}
|
|
156
|
+
{!isDefault && <FilterClearButton onClear={() => field.clear()} />}
|
|
167
157
|
</Checkbox.Group>
|
|
168
158
|
)
|
|
169
159
|
|
|
@@ -51,6 +51,12 @@
|
|
|
51
51
|
* // An open threshold — a pill holding a stepper, applied on blur or Enter.
|
|
52
52
|
* // minDuration: field.number({ fallback: 0, min: 0, max: 600 })
|
|
53
53
|
* <NumberFilter field={lines.field.minDuration} label="Min duration" step={30} />
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* // "No filter" is the fallback, not a row: `clearable` writes it back, and the pill reads the
|
|
57
|
+
* // filter's name while no preset is picked.
|
|
58
|
+
* // difficulty: field.number({ fallback: 0, min: 0, max: 5, int: true })
|
|
59
|
+
* <NumberFilter field={library.field.difficulty} label="Difficulty" options={LEVELS} clearable />
|
|
54
60
|
*/
|
|
55
61
|
import { NumberInput } from '@mantine/core'
|
|
56
62
|
import { useEffect, useRef, useState } from 'react'
|
|
@@ -62,7 +68,7 @@ import classes from './controls.module.css'
|
|
|
62
68
|
import { EnumFilter } from './enum-filter'
|
|
63
69
|
import type { ChoiceHandle } from './enum-filter'
|
|
64
70
|
import { useFilterRegistration, useFilterSurface } from './filter-context'
|
|
65
|
-
import { FilterPill } from './filter-pill'
|
|
71
|
+
import { FilterClearButton, FilterPill } from './filter-pill'
|
|
66
72
|
import { useControlName } from './filter-sheet'
|
|
67
73
|
import { PanelRow } from './panel-row'
|
|
68
74
|
|
|
@@ -89,6 +95,9 @@ export type NumberFilterProps = BasaltProps & {
|
|
|
89
95
|
* declared none. Absent on both, the input keeps Mantine's own default of 1.
|
|
90
96
|
*/
|
|
91
97
|
readonly step?: number
|
|
98
|
+
/** Adds a `Clear` action that clears the field — the same `field.clear()` `Reset all` calls.
|
|
99
|
+
* Parity with `SelectFilter`/`EnumFilter`, in both forms. */
|
|
100
|
+
readonly clearable?: boolean
|
|
92
101
|
}
|
|
93
102
|
|
|
94
103
|
export function NumberFilter(props: NumberFilterProps): ReactNode {
|
|
@@ -97,7 +106,7 @@ export function NumberFilter(props: NumberFilterProps): ReactNode {
|
|
|
97
106
|
assertRequiredProps('NumberFilter', props, ['field'], {
|
|
98
107
|
field: 'bind it to a store field (`store.field.<name>`), never a value/onChange pair.',
|
|
99
108
|
})
|
|
100
|
-
const { field, label, icon, options, step, className, style } = props
|
|
109
|
+
const { field, label, icon, options, step, clearable, className, style } = props
|
|
101
110
|
// Two BRANCHES, two components, and never a conditional hook: the radio form's state lives in
|
|
102
111
|
// `EnumFilter` and the stepper's in `NumberStepper`, so which one mounts is decided once by a prop
|
|
103
112
|
// that does not change across a mount's life.
|
|
@@ -107,6 +116,7 @@ export function NumberFilter(props: NumberFilterProps): ReactNode {
|
|
|
107
116
|
field={field}
|
|
108
117
|
label={label}
|
|
109
118
|
options={options}
|
|
119
|
+
{...(clearable === true && { clearable: true })}
|
|
110
120
|
{...(icon !== undefined && { icon })}
|
|
111
121
|
{...(className !== undefined && { className })}
|
|
112
122
|
{...(style !== undefined && { style })}
|
|
@@ -120,6 +130,7 @@ export function NumberFilter(props: NumberFilterProps): ReactNode {
|
|
|
120
130
|
<NumberStepper
|
|
121
131
|
field={field}
|
|
122
132
|
label={label}
|
|
133
|
+
{...(clearable === true && { clearable: true })}
|
|
123
134
|
{...(icon !== undefined && { icon })}
|
|
124
135
|
{...(grain !== undefined && { step: grain })}
|
|
125
136
|
{...(className !== undefined && { className })}
|
|
@@ -147,6 +158,7 @@ function NumberChoice({
|
|
|
147
158
|
label,
|
|
148
159
|
icon,
|
|
149
160
|
options,
|
|
161
|
+
clearable,
|
|
150
162
|
className,
|
|
151
163
|
style,
|
|
152
164
|
}: BasaltProps & {
|
|
@@ -154,6 +166,7 @@ function NumberChoice({
|
|
|
154
166
|
readonly label: string
|
|
155
167
|
readonly icon?: ReactNode
|
|
156
168
|
readonly options: readonly NumberFilterOption[]
|
|
169
|
+
readonly clearable?: boolean
|
|
157
170
|
}): ReactNode {
|
|
158
171
|
const [value, setValue] = field.use()
|
|
159
172
|
const rows = options.map((option) => ({ value: String(option.value), label: option.label }))
|
|
@@ -182,6 +195,7 @@ function NumberChoice({
|
|
|
182
195
|
label={label}
|
|
183
196
|
numeric
|
|
184
197
|
options={rows}
|
|
198
|
+
{...(clearable === true && { clearable: true })}
|
|
185
199
|
{...(icon !== undefined && { icon })}
|
|
186
200
|
{...(className !== undefined && { className })}
|
|
187
201
|
{...(style !== undefined && { style })}
|
|
@@ -195,6 +209,7 @@ function NumberStepper({
|
|
|
195
209
|
label,
|
|
196
210
|
icon,
|
|
197
211
|
step,
|
|
212
|
+
clearable,
|
|
198
213
|
className,
|
|
199
214
|
style,
|
|
200
215
|
}: BasaltProps & {
|
|
@@ -202,6 +217,7 @@ function NumberStepper({
|
|
|
202
217
|
readonly label: string
|
|
203
218
|
readonly icon?: ReactNode
|
|
204
219
|
readonly step?: number
|
|
220
|
+
readonly clearable?: boolean
|
|
205
221
|
}): ReactNode {
|
|
206
222
|
const [value, setValue] = field.use()
|
|
207
223
|
const surface = useFilterSurface()
|
|
@@ -265,7 +281,12 @@ function NumberStepper({
|
|
|
265
281
|
{...(className !== undefined && { className })}
|
|
266
282
|
{...(style !== undefined && { style })}
|
|
267
283
|
>
|
|
268
|
-
<div className={classes.optionList}>
|
|
284
|
+
<div className={classes.optionList}>
|
|
285
|
+
{input}
|
|
286
|
+
{/* Same placement as `EnumFilter`'s: popover only — the sheet and panel rows reset through
|
|
287
|
+
the `FilterSet` registration above. The draft follows the clear via its effect. */}
|
|
288
|
+
{clearable === true && !isDefault && <FilterClearButton onClear={() => field.clear()} />}
|
|
289
|
+
</div>
|
|
269
290
|
</FilterPill>
|
|
270
291
|
)
|
|
271
292
|
}
|
|
@@ -32,7 +32,6 @@ import { useEffect, useRef, useState } from 'react'
|
|
|
32
32
|
import type { ComponentType, ReactNode } from 'react'
|
|
33
33
|
import type { BasaltProps } from '../common/props'
|
|
34
34
|
import { assertRequiredProps } from '../common/validate'
|
|
35
|
-
import { isDev } from '../common/is-dev'
|
|
36
35
|
import type { FieldHandle, RangeField, RangeValue } from '../state'
|
|
37
36
|
import classes from './controls.module.css'
|
|
38
37
|
import { useFilterRegistration, useFilterSurface } from './filter-context'
|
|
@@ -60,7 +59,7 @@ const noPickerWarned = new Set<string>()
|
|
|
60
59
|
* the type cannot tell "deliberately preset-only" from "forgot the import".
|
|
61
60
|
*/
|
|
62
61
|
function warnCustomWithoutPicker(label: string, unreachable: boolean): void {
|
|
63
|
-
if (!unreachable ||
|
|
62
|
+
if (!unreachable || process.env.NODE_ENV === 'production' || noPickerWarned.has(label)) return
|
|
64
63
|
noPickerWarned.add(label)
|
|
65
64
|
// oxlint-disable-next-line no-console -- a dev-time wiring warning has no other channel
|
|
66
65
|
console.warn(
|
package/src/data/data-table.tsx
CHANGED
|
@@ -98,7 +98,6 @@ import {
|
|
|
98
98
|
useRowDisclosure,
|
|
99
99
|
} from './data-table-state'
|
|
100
100
|
import { dataQueryBranch } from './query-branch'
|
|
101
|
-
import { isDev } from '../common/is-dev'
|
|
102
101
|
import classes from './data-table.module.css'
|
|
103
102
|
|
|
104
103
|
// ── Column alignment ──────────────────────────────────────────────────────────
|
|
@@ -303,7 +302,7 @@ function resolveFacetColumn<T>(
|
|
|
303
302
|
): Column<T, unknown> | undefined {
|
|
304
303
|
const column = table.getColumn(facet.columnId)
|
|
305
304
|
if (column) return column
|
|
306
|
-
if (
|
|
305
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
307
306
|
const knownIds = table
|
|
308
307
|
.getAllColumns()
|
|
309
308
|
.map((known) => known.id)
|
|
@@ -1231,7 +1230,7 @@ const reportedBreaches = new Set<string>()
|
|
|
1231
1230
|
function enforceManualPaginationContract(breaches: ManualPaginationBreach[]): void {
|
|
1232
1231
|
if (breaches.length === 0) return
|
|
1233
1232
|
const message = manualPaginationMessage(breaches)
|
|
1234
|
-
if (
|
|
1233
|
+
if (process.env.NODE_ENV !== 'production') throw new Error(message)
|
|
1235
1234
|
if (reportedBreaches.has(message)) return
|
|
1236
1235
|
reportedBreaches.add(message)
|
|
1237
1236
|
console.error(message)
|