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.
- package/CHANGELOG.md +50 -0
- package/MIGRATING.md +228 -20
- package/agent/rules/basalt-mantine.md +2 -2
- package/agent/rules/basalt-state.md +2 -2
- package/agent/rules/basalt-tokens.md +1 -1
- package/configs/oxlint-plugin.js +160 -89
- package/configs/oxlint-plugin.test.ts +273 -10
- 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 +9 -2
- package/dist/data/data-table.d.ts.map +1 -1
- package/dist/data/data-table.js +2 -3
- package/dist/forms/form-layout.d.ts +6 -1
- package/dist/forms/form-layout.d.ts.map +1 -1
- package/dist/forms/form-layout.module.css +3 -0
- package/dist/guard/index.d.ts +18 -4
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +67 -31
- 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 +24 -0
- 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 +2 -0
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +10 -0
- 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 +11 -5
- package/src/forms/form-layout.module.css +3 -0
- package/src/forms/form-layout.tsx +6 -1
- package/src/guard/index.ts +151 -62
- 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 +25 -4
- package/src/surfaces.ts +2 -0
- package/src/tokens/index.ts +10 -0
- 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
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// The `basalt` config shape and helpers, shared by the dispatcher and `--audit-allows`. A leaf: it
|
|
2
|
+
// imports nothing from `./index`, so audit-allows → index → check-theme → audit-allows is no cycle.
|
|
3
|
+
import type { GuardKind, GuardSeverity } from '../guard'
|
|
4
|
+
|
|
5
|
+
/** Shape of the optional `"basalt"` key in a consumer's package.json. */
|
|
6
|
+
export type BasaltConfig = {
|
|
7
|
+
/** Source roots to scan. Default: `['src']`. Set explicitly for a monorepo layout. */
|
|
8
|
+
roots?: string[]
|
|
9
|
+
/** Files exempt from the scan (they ARE the palette source). Default: argo's exempt set. */
|
|
10
|
+
exempt?: string[]
|
|
11
|
+
/**
|
|
12
|
+
* Declares this package a tokens-only consumer: it uses the `--vx-*` layer and no Mantine. Turns
|
|
13
|
+
* off every guard kind whose remedy is a Mantine component, a Mantine prop or the React theme
|
|
14
|
+
* factory, leaving the color and typography kinds live. Default: `'mantine'`.
|
|
15
|
+
*
|
|
16
|
+
* Deliberately not inferred from a missing `@mantine/core`: silencing 17 rules (the live count is
|
|
17
|
+
* `TOKENS_ONLY_DISABLED_KINDS.size`, which the check-theme banner prints) is a decision, and
|
|
18
|
+
* a repo that keeps Mantine in a workspace package would otherwise switch off half its own guard
|
|
19
|
+
* without saying so. `basalt-ui doctor` detects the shape and tells you to set this.
|
|
20
|
+
*/
|
|
21
|
+
profile?: 'mantine' | 'tokens-only'
|
|
22
|
+
/**
|
|
23
|
+
* Extra files to scan, named individually and relative to this package — for a design surface
|
|
24
|
+
* that lives outside every root and outside the `index.html` / `public/` conventions the walker
|
|
25
|
+
* already derives. Also the ONLY way a `.json` is ever scanned: the guard understands JSON as
|
|
26
|
+
* markup, but a consumer repo's JSON is overwhelmingly config, fixtures and lockfiles, so
|
|
27
|
+
* blanket-scanning it is a false-positive generator rather than a guard.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* { include: ['app/manifest.json', 'emails/template.html'] }
|
|
31
|
+
*/
|
|
32
|
+
include?: string[]
|
|
33
|
+
/** Named spacing-scale steps (px) flagged when used as a raw spacing prop. Default: 10/12/16/20/32. */
|
|
34
|
+
spacingSteps?: number[]
|
|
35
|
+
/** Per-kind severity override — `'warn'` reports without failing, `'error'` fails the build.
|
|
36
|
+
* See `GuardSeverity` in ../guard/types for the grace-minor doctrine behind it. */
|
|
37
|
+
severity?: Partial<Record<GuardKind, GuardSeverity>>
|
|
38
|
+
/** Off-identity Mantine accent families forbidden as chrome accents. Default: argo's set. */
|
|
39
|
+
forbiddenAccents?: string[]
|
|
40
|
+
/** Earned accent hue recorded in DESIGN.md `{{ACCENT_HUE}}`. Default: `blue`. */
|
|
41
|
+
accentHue?: string
|
|
42
|
+
/**
|
|
43
|
+
* Flag any numeric radius prop literal (radius={6}) — use the radius token scale instead.
|
|
44
|
+
* Default: `true` (ON). Set `false` to disable the `raw-radius` check (e.g. a repo that DEFINES
|
|
45
|
+
* the radius primitives).
|
|
46
|
+
*/
|
|
47
|
+
rawRadius?: boolean
|
|
48
|
+
/**
|
|
49
|
+
* Flag ad-hoc inline surface styling (`border`/`borderRadius`/`boxShadow` literals in a `style={{}}`)
|
|
50
|
+
* that bypasses the radius-token + `VX.surface.*` + `VX.shadowCard` system. Default: `true` (ON).
|
|
51
|
+
* Set `false` to disable the `raw-surface` check.
|
|
52
|
+
*/
|
|
53
|
+
rawSurface?: boolean
|
|
54
|
+
/**
|
|
55
|
+
* Flag `withBorder` on a `Card` / `Paper`. Card depth is `--vx-shadow-card` — a whisper shadow
|
|
56
|
+
* with the 1px ring baked into the SAME value — so `withBorder` draws a SECOND, real border over
|
|
57
|
+
* that ring and the card reads heavy/boxed (docs/DESIGN-SPEC.md doctrine inversion #1).
|
|
58
|
+
* `withBorder={false}` and `<Card.Section withBorder>` (a section divider) both pass.
|
|
59
|
+
* Default: `true` (ON). Set `false` to disable the `card-with-border` check.
|
|
60
|
+
*/
|
|
61
|
+
cardWithBorder?: boolean
|
|
62
|
+
/**
|
|
63
|
+
* Flag references to a raw Mantine ramp step used for surface color
|
|
64
|
+
* (`var(--mantine-color-gray-N)` / `var(--mantine-color-dark-N)`) — these bypass the basalt
|
|
65
|
+
* surface tokens. Default: `true` (ON). Set `false` to disable the `off-system-surface-var` check.
|
|
66
|
+
*/
|
|
67
|
+
offSystemSurfaceVar?: boolean
|
|
68
|
+
/**
|
|
69
|
+
* Flag a SHADE-PINNED Mantine color — `c="yellow.7"`, `bg="blue.4"`,
|
|
70
|
+
* `var(--mantine-color-red-6)`. A pinned step is one fixed swatch in BOTH color schemes, so it
|
|
71
|
+
* cannot stay legible in either; route a verdict color through `VX.status.*` / `--vx-status-*`,
|
|
72
|
+
* or drop the index (`c="red"`) and let the theme resolve the shade per scheme. `gray-*`/`dark-*`
|
|
73
|
+
* in `var()` form belong to `off-system-surface-var` instead. Default: `true` (ON). Set `false`
|
|
74
|
+
* to disable the `mantine-shade-index` check.
|
|
75
|
+
*/
|
|
76
|
+
mantineShadeIndex?: boolean
|
|
77
|
+
/**
|
|
78
|
+
* Flag raw lowercase JSX layout/surface elements (`div`/`span`/`section`/…) carrying an inline
|
|
79
|
+
* `style={{}}` with a layout/surface property — steer to a Mantine layout primitive
|
|
80
|
+
* (`Box`/`Flex`/`Grid`/`Stack`/`Group`/`SimpleGrid`/`Paper`). Default: `true` (ON). Set `false`
|
|
81
|
+
* to disable the `raw-html-layout` check.
|
|
82
|
+
*/
|
|
83
|
+
rawHtmlLayout?: boolean
|
|
84
|
+
/**
|
|
85
|
+
* Flag spacing/sizing literals inside an inline `style={{}}` (`padding`/`margin`/`gap`/…) — the
|
|
86
|
+
* `raw-spacing` check only catches the Mantine prop syntax. Default: `true` (ON). Set `false` to
|
|
87
|
+
* disable the `inline-spacing` check.
|
|
88
|
+
*/
|
|
89
|
+
inlineSpacing?: boolean
|
|
90
|
+
/**
|
|
91
|
+
* Flag `display: 'flex' | 'grid' | 'inline-flex' | 'inline-grid'` in an inline `style={{}}` —
|
|
92
|
+
* steer to `<Flex>`/`<Grid>`/`<Group>`. Default: `true` (ON). Set `false` to disable the
|
|
93
|
+
* `inline-display` check.
|
|
94
|
+
*/
|
|
95
|
+
inlineDisplay?: boolean
|
|
96
|
+
/**
|
|
97
|
+
* Flag raw `<AxisLeft>` / `<AxisBottom>` / `<AxisRight>` visx JSX inside chart files (a path
|
|
98
|
+
* containing `/charts/`) — these bypass the `AxisLeftNumeric` / `AxisBottomDate` /
|
|
99
|
+
* `AxisRightNumeric` primitives that carry the theme tokens + smart ticks. The legitimate wrapper
|
|
100
|
+
* (`Axes.tsx`, which IS the primitive) is exempt. Default: `true` (ON). Set `false` to disable the
|
|
101
|
+
* `raw-visx-axis` check.
|
|
102
|
+
*/
|
|
103
|
+
rawVisxAxis?: boolean
|
|
104
|
+
/**
|
|
105
|
+
* Flag a hardcoded duration/spring/ease literal inline in a `transition={{...}}` prop — route it
|
|
106
|
+
* through `MOTION_DURATION` / `MOTION_SPRING` / `MOTION_EASE_STANDARD` instead. Default: `true`
|
|
107
|
+
* (ON). Set `false` to disable the `raw-motion-value` check.
|
|
108
|
+
*/
|
|
109
|
+
rawMotionValue?: boolean
|
|
110
|
+
/**
|
|
111
|
+
* Flag a chart entry-point JSX tag (`MultiLine`/`Bars`/`Donut`/`DualPanel`/`Heatmap`/`ZonedLine`/
|
|
112
|
+
* `StackedArea`/`LineSparkline`/`BarSparkline`) missing an `ariaLabel` prop. Default: `true`
|
|
113
|
+
* (ON). Set `false` to disable the `chart-missing-aria-label` check.
|
|
114
|
+
*/
|
|
115
|
+
chartMissingAriaLabel?: boolean
|
|
116
|
+
/**
|
|
117
|
+
* Flag a raw lowercase `<input>`/`<select>`/`<textarea>` JSX element — it bypasses the entire
|
|
118
|
+
* theme, not just the iOS font-size floor. Use the Mantine equivalents (`TextInput`,
|
|
119
|
+
* `NumberInput`, `Select`, `Textarea`, …) instead. Default: `true` (ON). Set `false` to disable
|
|
120
|
+
* the `raw-form-control` check.
|
|
121
|
+
*/
|
|
122
|
+
rawFormControl?: boolean
|
|
123
|
+
/**
|
|
124
|
+
* Flag a `fontSize`/`font-size` literal below 16 inside a `style={{…}}` on a raw form control,
|
|
125
|
+
* or a Mantine `styles={{ input: {…} }}` per-part style — the `styles.css` iOS floor is
|
|
126
|
+
* `!important`, so the override is dead code. Default: `true` (ON). Set `false` to disable the
|
|
127
|
+
* `sub-16-input-font` check.
|
|
128
|
+
*/
|
|
129
|
+
sub16InputFont?: boolean
|
|
130
|
+
/**
|
|
131
|
+
* Path of the consumer's guard-exempt series file, for DESIGN.md `{{SERIES_MODULE_PATH}}`.
|
|
132
|
+
* Default: `<first basalt.root>/lib/series.ts` — see `resolveSeriesModulePath`.
|
|
133
|
+
*/
|
|
134
|
+
seriesModulePath?: string
|
|
135
|
+
/**
|
|
136
|
+
* Per-rule, per-path exemptions — complements whole-file `exempt` (which skips ALL rules for a
|
|
137
|
+
* file) and the hardcoded per-kind `appliesTo` scoping (e.g. `raw-visx-axis` → chart files
|
|
138
|
+
* only). Each value is a list of patterns matched against a finding's relative path. A pattern
|
|
139
|
+
* may be a whole path segment (`'agent'` matches `src/agent/x.tsx`, not `src/agenting.ts`), a
|
|
140
|
+
* relative path (`'public/site.webmanifest'`), a directory prefix, or a glob — `*` stops at `/`,
|
|
141
|
+
* `**` does not, and a slash-free glob also matches the basename, so `'*.module.css'` works.
|
|
142
|
+
* A trailing `/` is stripped. An entry that suppresses nothing is reported, and
|
|
143
|
+
* `check-theme --audit-allows` exits 1 on it. Default: `{}` (no exemptions).
|
|
144
|
+
*
|
|
145
|
+
* Two forms per kind. The bare array is paths only. The object form adds the REASON, which is
|
|
146
|
+
* the half a `theme-allow` carries and this key could not: JSON has no comments, so a
|
|
147
|
+
* `.webmanifest` / `.json` finding's only escape is this key, and it was un-reviewable by
|
|
148
|
+
* construction — the rationale ended up in a CLAUDE.md paragraph nobody reads next to the diff.
|
|
149
|
+
* `check-theme --audit-allows` prints the reason (or names its absence) beside what the entry
|
|
150
|
+
* still suppresses. The array form stays supported and unchanged; nothing here is required.
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* { exemptRules: { 'inline-display': ['agent'] } } // inline-display never fires under src/agent/**
|
|
154
|
+
* @example
|
|
155
|
+
* { exemptRules: { 'raw-hex': { paths: ['site.webmanifest'],
|
|
156
|
+
* reason: 'a PWA manifest theme_color MUST be a literal hex — JSON cannot reference a CSS var' } } }
|
|
157
|
+
*/
|
|
158
|
+
exemptRules?: Partial<Record<GuardKind, ExemptRuleEntry>>
|
|
159
|
+
/**
|
|
160
|
+
* Declares an intentional `ai` package major-version skew across workspace packages, exempting
|
|
161
|
+
* `doctor`'s `ai-major-parity` hard check. The value IS the reason — a bare `true` is rejected,
|
|
162
|
+
* because the whole point of this key is that the pairing is written down, not just switched off.
|
|
163
|
+
* A written declaration is the pin `basalt/ai-sdk-major`'s own comment notes is otherwise missing.
|
|
164
|
+
*
|
|
165
|
+
* Semantics: absent (or present but not a non-empty string) → the skew still hard-fails exactly as
|
|
166
|
+
* without this key. A non-empty string with a skew present → `doctor` passes, echoing both the
|
|
167
|
+
* skew and this reason. A non-empty string with NO skew present → `doctor` warns that the
|
|
168
|
+
* exemption is stale and can be deleted (an exemption nobody re-checks is how a real, later skew
|
|
169
|
+
* slips through unnoticed).
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* // apps/api streams on ai@5; apps/dashboard parses on ai@7; a producer-side TransformStream
|
|
173
|
+
* // neutralizes the one enum value that differs between the two majors.
|
|
174
|
+
* { aiMajorSkewReason: 'apps/api pinned to ai@5, apps/dashboard on ai@7 — the skew is neutralized '
|
|
175
|
+
* + 'by a producer-side TransformStream in apps/api/src/stream-transform.ts' }
|
|
176
|
+
*/
|
|
177
|
+
aiMajorSkewReason?: string
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* One `basalt.exemptRules` value: the historical bare path list, or the same list with the reason
|
|
182
|
+
* it exists. See `BasaltConfig.exemptRules`.
|
|
183
|
+
*/
|
|
184
|
+
export type ExemptRuleEntry = string[] | { paths: string[]; reason: string }
|
|
185
|
+
|
|
186
|
+
/** The paths half of an exemption entry, whichever form it was written in. */
|
|
187
|
+
export function exemptRulePaths(entry: ExemptRuleEntry | undefined): string[] {
|
|
188
|
+
if (entry === undefined) return []
|
|
189
|
+
return Array.isArray(entry) ? entry : entry.paths
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** The recorded reason for an exemption entry, or null for the bare-array form. */
|
|
193
|
+
export function exemptRuleReason(entry: ExemptRuleEntry | undefined): string | null {
|
|
194
|
+
if (entry === undefined || Array.isArray(entry)) return null
|
|
195
|
+
return entry.reason.trim().length > 0 ? entry.reason.trim() : null
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export const DEFAULT_ROOT = 'src'
|
|
199
|
+
export const DEFAULT_ROOTS = [DEFAULT_ROOT]
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The configured source roots, or the built-in default. The ONE resolution every roots-derived seed
|
|
203
|
+
* reads — an empty `roots: []` falls back rather than resolving to nothing, because a bare `??`
|
|
204
|
+
* would let `[]` through and render an empty oxfmt glob into the seeded CI, reproducing the exact
|
|
205
|
+
* "matches zero files" break this derivation exists to prevent.
|
|
206
|
+
*/
|
|
207
|
+
export function resolveRoots(cfg: BasaltConfig): string[] {
|
|
208
|
+
return cfg.roots?.length ? cfg.roots : DEFAULT_ROOTS
|
|
209
|
+
}
|
package/src/cli/doctor.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { existsSync } from 'node:fs'
|
|
7
7
|
import { relative, resolve } from 'node:path'
|
|
8
8
|
|
|
9
|
-
import type { BasaltConfig } from './
|
|
9
|
+
import type { BasaltConfig } from './config'
|
|
10
10
|
import {
|
|
11
11
|
MANIFEST_PATH,
|
|
12
12
|
basaltBinCommand,
|
|
@@ -21,10 +21,10 @@ import {
|
|
|
21
21
|
readIfExists,
|
|
22
22
|
readManifest,
|
|
23
23
|
resolveProjectDir,
|
|
24
|
-
resolveRoots,
|
|
25
24
|
scannableFiles,
|
|
26
25
|
shippedAssetPath,
|
|
27
26
|
} from './index'
|
|
27
|
+
import { resolveRoots } from './config'
|
|
28
28
|
import { findManifestAbove, parentInstallAdvice } from './sync'
|
|
29
29
|
|
|
30
30
|
export type DoctorResult = {
|
package/src/cli/guard-hook.ts
CHANGED
|
@@ -8,14 +8,8 @@ import { isAbsolute, relative, resolve } from 'node:path'
|
|
|
8
8
|
import { DEFAULT_GUARD_CONFIG } from '../guard'
|
|
9
9
|
import type { GuardConfig } from '../guard'
|
|
10
10
|
import { evaluateGuardHook } from '../guard/guard-hook'
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
SKIP,
|
|
14
|
-
declaredProfile,
|
|
15
|
-
defaultExempt,
|
|
16
|
-
readBasaltConfig,
|
|
17
|
-
resolveExemptRules,
|
|
18
|
-
} from './index'
|
|
11
|
+
import { SKIP, declaredProfile, defaultExempt, readBasaltConfig, resolveExemptRules } from './index'
|
|
12
|
+
import { DEFAULT_ROOTS } from './config'
|
|
19
13
|
|
|
20
14
|
/**
|
|
21
15
|
* guard-hook — PreToolUse stdin adapter.
|
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
|
|