basalt-ui 1.29.2 → 1.30.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 (116) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/MIGRATING.md +693 -368
  3. package/README.md +23 -2
  4. package/agent/rules/basalt-controls.md +34 -34
  5. package/agent/skills/basalt-app/SKILL.md +4 -2
  6. package/agent/templates/query-client.ts.tpl +18 -4
  7. package/configs/lefthook.yml +51 -19
  8. package/configs/oxlint-plugin.js +210 -84
  9. package/configs/oxlint-plugin.test.ts +182 -5
  10. package/configs/oxlint.json +4 -4
  11. package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
  12. package/dist/charts/primitives/ChartCard.js +2 -2
  13. package/dist/charts/primitives/ChartFrame.d.ts +23 -3
  14. package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
  15. package/dist/charts/primitives/ChartFrame.js +4 -4
  16. package/dist/charts/primitives/chart-frame-layout.d.ts +55 -0
  17. package/dist/charts/primitives/chart-frame-layout.d.ts.map +1 -1
  18. package/dist/charts/primitives/chart-frame-layout.js +16 -0
  19. package/dist/cli/check-theme.d.ts.map +1 -1
  20. package/dist/cli/check-theme.js +6 -0
  21. package/dist/cli/doctor.d.ts +10 -4
  22. package/dist/cli/doctor.d.ts.map +1 -1
  23. package/dist/cli/doctor.js +28 -1
  24. package/dist/cli/help.d.ts.map +1 -1
  25. package/dist/cli/help.js +2 -1
  26. package/dist/cli/index.d.ts +12 -0
  27. package/dist/cli/index.d.ts.map +1 -1
  28. package/dist/cli/index.js +29 -0
  29. package/dist/cli/init.d.ts.map +1 -1
  30. package/dist/cli/init.js +4 -15
  31. package/dist/cli/sync.d.ts.map +1 -1
  32. package/dist/cli/sync.js +8 -14
  33. package/dist/controls/actions.d.ts.map +1 -1
  34. package/dist/controls/actions.js +12 -2
  35. package/dist/dashboard/section.module.css +15 -7
  36. package/dist/dashboard/stat-card.d.ts.map +1 -1
  37. package/dist/dashboard/stat-card.js +7 -2
  38. package/dist/dashboard/stat-group.d.ts +8 -2
  39. package/dist/dashboard/stat-group.d.ts.map +1 -1
  40. package/dist/dashboard/stat-group.js +23 -15
  41. package/dist/dashboard/stat-group.module.css +85 -9
  42. package/dist/dashboard/stat-group.module.css.d.ts +1 -0
  43. package/dist/dashboard/widget-grid.d.ts.map +1 -1
  44. package/dist/dashboard/widget-grid.js +4 -5
  45. package/dist/dashboard/widget-grid.module.css +64 -21
  46. package/dist/dashboard/widget-grid.module.css.d.ts +1 -0
  47. package/dist/guard/index.d.ts +19 -2
  48. package/dist/guard/index.d.ts.map +1 -1
  49. package/dist/guard/index.js +27 -4
  50. package/dist/index.d.ts +9 -0
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +2 -0
  53. package/dist/shell/app-brand.module.css +12 -3
  54. package/dist/shell/app-header.module.css +49 -9
  55. package/dist/shell/app-main.module.css +24 -4
  56. package/dist/shell/app-mobile-nav.d.ts.map +1 -1
  57. package/dist/shell/app-mobile-nav.js +98 -51
  58. package/dist/shell/app-mobile-nav.module.css +99 -11
  59. package/dist/shell/app-mobile-nav.module.css.d.ts +1 -0
  60. package/dist/shell/app-sidebar.module.css +55 -2
  61. package/dist/shell/index.d.ts.map +1 -1
  62. package/dist/shell/index.js +3 -3
  63. package/dist/shell/mobile-nav-model.d.ts +26 -7
  64. package/dist/shell/mobile-nav-model.d.ts.map +1 -1
  65. package/dist/shell/mobile-nav-model.js +12 -3
  66. package/dist/shell/page-aside.module.css +8 -6
  67. package/dist/shell/page-bar.d.ts +13 -0
  68. package/dist/shell/page-bar.d.ts.map +1 -1
  69. package/dist/shell/page-bar.js +13 -3
  70. package/dist/shell/page-bar.module.css +20 -12
  71. package/dist/shell/page-title.module.css +3 -1
  72. package/dist/theme/use-basalt-spacing.d.ts.map +1 -1
  73. package/dist/tokens/index.d.ts.map +1 -1
  74. package/dist/tokens/index.js +51 -7
  75. package/dist/tokens/palette.d.ts +98 -17
  76. package/dist/tokens/palette.d.ts.map +1 -1
  77. package/dist/tokens/palette.js +110 -18
  78. package/dist/tokens.css +17 -5
  79. package/llms.txt +2 -2
  80. package/package.json +1 -1
  81. package/src/charts/primitives/ChartCard.tsx +12 -7
  82. package/src/charts/primitives/ChartFrame.tsx +30 -15
  83. package/src/charts/primitives/chart-frame-layout.ts +86 -0
  84. package/src/cli/check-theme.ts +13 -0
  85. package/src/cli/doctor.ts +49 -8
  86. package/src/cli/help.ts +5 -3
  87. package/src/cli/index.ts +49 -0
  88. package/src/cli/init.ts +6 -27
  89. package/src/cli/sync.ts +11 -22
  90. package/src/controls/actions.tsx +51 -0
  91. package/src/dashboard/section.module.css +15 -7
  92. package/src/dashboard/stat-card.tsx +7 -2
  93. package/src/dashboard/stat-group.module.css +85 -9
  94. package/src/dashboard/stat-group.module.css.d.ts +1 -0
  95. package/src/dashboard/stat-group.tsx +40 -20
  96. package/src/dashboard/widget-grid.module.css +64 -21
  97. package/src/dashboard/widget-grid.module.css.d.ts +1 -0
  98. package/src/dashboard/widget-grid.tsx +30 -12
  99. package/src/guard/index.ts +79 -11
  100. package/src/index.ts +9 -0
  101. package/src/shell/app-brand.module.css +12 -3
  102. package/src/shell/app-header.module.css +49 -9
  103. package/src/shell/app-main.module.css +24 -4
  104. package/src/shell/app-mobile-nav.module.css +99 -11
  105. package/src/shell/app-mobile-nav.module.css.d.ts +1 -0
  106. package/src/shell/app-mobile-nav.tsx +225 -83
  107. package/src/shell/app-sidebar.module.css +55 -2
  108. package/src/shell/index.tsx +26 -6
  109. package/src/shell/mobile-nav-model.ts +37 -9
  110. package/src/shell/page-aside.module.css +8 -6
  111. package/src/shell/page-bar.module.css +20 -12
  112. package/src/shell/page-bar.tsx +39 -5
  113. package/src/shell/page-title.module.css +3 -1
  114. package/src/theme/use-basalt-spacing.ts +8 -6
  115. package/src/tokens/index.ts +90 -6
  116. package/src/tokens/palette.ts +111 -18
package/README.md CHANGED
@@ -241,7 +241,12 @@ exemption:
241
241
  ```
242
242
 
243
243
  `check-theme` fails loudly (instead of a false `✓`) when the configured roots resolve to zero
244
- scanned files, and `doctor`'s `guard-scan` check agrees with it. Each root's **parent** also
244
+ scanned files, and `doctor`'s `guard-scan` check agrees with it — it shares `scannableFiles` with
245
+ `check-theme`, so the two cannot disagree about what would be scanned. (`guard-scan` and
246
+ `install-parity` were absent from `doctor` for four minors up to 1.29.2, with no MIGRATING row, and
247
+ are back in 1.30.0. While they were gone a consumer CI step that ran `doctor` to catch an
248
+ install/manifest version mismatch caught nothing, zero-file roots could ship as a permanently green
249
+ guard, and this sentence described a check that did not exist.) Each root's **parent** also
245
250
  contributes its `index.html` and its `public/` tree — the Vite layout `basaltViteConfig` assumes,
246
251
  and where a raw `theme-color` or webmanifest `background_color` actually lives. `.json` is never
247
252
  blanket-scanned; `include` names one explicitly and is the only route to it. `profile:
@@ -375,7 +380,7 @@ Apply opacity with `alpha(token, a)` (backed by `color-mix`) — never `rgba()`,
375
380
  App-specific series colors live in the consumer, not the framework. The framework ships the factories:
376
381
 
377
382
  ```ts
378
- // src/theme/series.ts — the single guard-exempt file
383
+ // src/lib/series.ts — the ONE path check-theme exempts by default (see the note below)
379
384
  import { defineSeries, groupTokens } from 'basalt-ui/tokens'
380
385
 
381
386
  // 1. Declare the series with light/dark pairs (ColorPair shape — hex strings per scheme)
@@ -402,6 +407,22 @@ export const colors = groupTokens(GROUP, SERIES)
402
407
  export const paletteGroups = { [`${GROUP}-`]: SERIES }
403
408
  ```
404
409
 
410
+ > **Put it at `<first basalt.root>/lib/series.ts`, or the guard reports it.** `defineSeries` takes
411
+ > hex `ColorPair`s _by type_, so this is the one file the doctrine tells you to write raw hex in —
412
+ > and the exemption is an exact PATH, not a shape: `check-theme` skips
413
+ > `<first basalt.root>/lib/series.ts` (the same value `DESIGN.md`'s `{{SERIES_MODULE_PATH}}`
414
+ > renders, so the two cannot drift), and nothing special-cases a file merely _because_ it calls
415
+ > `defineSeries`. A series module anywhere else — `src/theme/series.ts`, say — reports `raw-hex`
416
+ > twice per pair. Two ways out, and the second is not the same as the first:
417
+ >
418
+ > ```json
419
+ > // package.json — `seriesModulePath` MOVES the default exemption; `exempt` REPLACES it wholesale
420
+ > { "basalt": { "seriesModulePath": "src/theme/series.ts" } }
421
+ > ```
422
+ >
423
+ > Writing your own `basalt.exempt` array REPLACES the series default rather than adding to it, so
424
+ > list your series module in that array explicitly whenever you use the key at all.
425
+
405
426
  ```tsx
406
427
  // main.tsx — wire paletteGroups into BasaltProvider
407
428
  import { BasaltProvider, createBasaltTheme } from 'basalt-ui'
@@ -42,8 +42,7 @@ size tier and one persistence binding, and basalt owns all three. The API is in
42
42
  | C15 | Every touch target inside a home clears the `touchControlHeight` floor below `sm`. The mobile `Filters (n)` sheet draws no row of its own — it renders the same `PanelRow` the aside's panel surface does. |
43
43
  | C16 | A new guard lands `warn` with a dated `promote` version, and the build fails once the package version reaches it while the rule is still `warn`. |
44
44
 
45
- C1's cross-file case, a hand-rolled section heading, C11 outside `BasaltDataTable` and C12 are
46
- **advisory** — the generated header above says so. A green lint run is not evidence they hold.
45
+ A green lint run is not evidence the advisory laws the generated header above lists hold.
47
46
 
48
47
  ## The three homes
49
48
 
@@ -52,12 +51,10 @@ C1's cross-file case, a hand-rolled section heading, C11 outside `BasaltDataTabl
52
51
  | `PageBar` | `actions`, `sync`, `filters`, `filtersEnd`, `tabs` | `ctl` | the page's filters, tabs, actions, refresh |
53
52
  | `Section` | `actions`, `tabs` | `ctl` | that section's own controls and its count |
54
53
  | `WidgetHeader` / `ChartCard` / `StatCard` / `BasaltDataTable` / `SettingsSection` | `actions` | `ctl` | that widget's own controls and its count |
55
- | `SettingsRow` | `control` | Mantine `md` | ONE form field, bound to a setting |
56
- | `FormRow` / `FormGroup` (`basalt-ui/forms`) | children | Mantine `md` | ONE form field or a labelled cluster |
54
+ | `SettingsRow` · `FormRow` / `FormGroup` (`basalt-ui/forms`) | `control` · children | Mantine `md` | ONE form field, or a labelled cluster |
57
55
 
58
56
  - **Inside `BasaltShell`** both `PageBar` rows are portals (header / the band above the scrollport)
59
57
  — where you write `<PageBar>` never moves it. Without a shell both rows render in flow, sticky.
60
- The header height is a token, never state.
61
58
  - **The form row keeps Mantine's own `md` tier** — a raw `Select` in `SettingsRow.control` is the
62
59
  right answer, `size` there is load-bearing. Neither `SettingsRow` nor `FormRow`/`FormGroup` is a
63
60
  tiered slot; no filter or size rule applies to them.
@@ -76,10 +73,9 @@ C1's cross-file case, a hand-rolled section heading, C11 outside `BasaltDataTabl
76
73
  | `touchControlHeight` | hit area below `sm` | every home |
77
74
  | `controlHeight` | `size="md"` | forms — unchanged |
78
75
 
79
- Each home wraps its **slot** — never its body — in a hoisted theme provider defaulting every
80
- Mantine control inside it to `size="ctl"`. A raw `Button` in `PageBar.actions` is already the right
81
- height with no prop; a `size="xs"` typed there is law C5 (`basalt/control-size-literal`). Mantine's
82
- own `sm`/`xs` are NOT re-pointed elsewhere. Exception: `ChartCard.actions` is Mantine-free, so it
76
+ A home sizes its **slot**, never its body: a raw `Button` in `PageBar.actions` is already the right
77
+ height with no prop, and a `size="xs"` typed there is law C5 (`basalt/control-size-literal`).
78
+ Mantine's own `sm`/`xs` are NOT re-pointed. Exception: `ChartCard.actions` is Mantine-free, so it
83
79
  carries only the tier attribute and its controls size themselves.
84
80
 
85
81
  ## Binding a control to a store
@@ -103,52 +99,56 @@ The store, its fields and their lanes are basalt-state.md. What this file adds:
103
99
  is a tsc error. `ActionGroup`/`OverflowMenu`/`SyncButton` take typed action DATA, never children.
104
100
 
105
101
  **The date picker is injected, never imported.** `basalt-ui/controls` resolves with no
106
- `@mantine/dates` installed; `DateRangePicker` comes from `basalt-ui/controls-dates` through
107
- `RangeFilter.customPicker`. Never import `@mantine/dates` from a shared module or reach for
108
- `DateInput`/`DatePickerInput` inside a home slot.
102
+ `@mantine/dates`; `DateRangePicker` comes from `basalt-ui/controls-dates` through
103
+ `RangeFilter.customPicker`. Never import it in a shared module or reach for `DatePickerInput`.
109
104
 
110
105
  ## `FilterSet` owns the responsive story
111
106
 
112
- Do not build one at the call site. Above `sm`, `FilterSet` measures its own children and folds the
113
- tail into a `+N` pill. Below `sm`, the first `inline` children stay pills and one `Filters (n)`
114
- pill opens a sheet where every child renders full-width and answers one `Reset all`. **`n` and
115
- `Reset all` are DERIVED** from whether each field differs from its fallback — adding a filter is
116
- one JSX line, no count or reset handler to maintain.
107
+ Do not build one at the call site. Above `sm` it measures its own children and folds the tail into
108
+ a `+N` pill. Below `sm` the first `inline` children stay pills and one `Filters (n)` pill opens a
109
+ sheet where every child is full-width and answers one `Reset all`. **`n` and `Reset all` are
110
+ DERIVED** from whether each field differs from its fallback — a filter is one JSX line, no handler.
117
111
 
118
- | Below `sm` | What basalt does |
119
- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
120
- | `PageBar` row 1 | breadcrumb · the `primary` action as an icon · ONE kebab holding every `more` action |
121
- | `PageBar` row 2 | line 1: `ViewTabs` full-width (a `Select` past three options) · line 2: one inline pill · `Filters (n)` · the aside's `Panel` pill |
122
- | a section header | title · count · one inline action, the rest in a kebab; tabs past three become a `Select` |
123
- | a widget header | value + delta wrap under the title; the sparkline drops to bleed; one `⋯` action |
112
+ | Below `sm` | What basalt does |
113
+ | --------------- | ------------------------------------------------------------------------------------------------------------------------------ |
114
+ | `PageBar` row 1 | breadcrumb · the `primary` action as an icon · ONE kebab holding every `more` action |
115
+ | `PageBar` row 2 | line 1: `ViewTabs` full-width (a `Select` past three options) · line 2: one inline pill · `Filters (n)` · the aside's own pill |
124
116
 
125
117
  Every swap is CSS inside the control (one mount), never a JS media query. Two mounts under
126
118
  `visibleFrom`/`hiddenFrom` is `basalt/responsive-twin`.
127
119
 
128
120
  ## The aside
129
121
 
130
- `PageAside` is a shell REGION, not a fourth home — C1 still names three. Its body IS a home: it
131
- scopes children to the `panel` surface, so the same `SelectFilter` that is a pill in the page bar
132
- is a ROW in the aside — never two components, the surface is read from where it's mounted.
122
+ `PageAside` is a shell REGION, not a fourth home — C1 still names three, and its body IS one:
123
+ children scope to the `panel` surface, so a `SelectFilter` renders its ROW form there, not a pill.
133
124
 
134
- | In the aside | Write |
125
+ ```tsx
126
+ <PageAside title="Composition" persistKey="composition">
127
+ <PanelRow label="Weight">…</PanelRow>
128
+ </PageAside>
129
+ ```
130
+
131
+ | In a `PageAside` | What it is |
135
132
  | ---------------- | --------------------------------------------------------------------------------- |
133
+ | `title` | REQUIRED — the header text AND the region's accessible name; omitted, it THROWS |
134
+ | `persistKey` | persists the fold at `basalt:aside:<persistKey>`; omitted the fold is per-mount |
135
+ | `defaultFolded` | the fold on first render, honoured only while nothing is persisted |
136
+ | `classNames` | the three boxes it paints — `root` / `header` / `body` |
136
137
  | an inspector row | `PanelRow` — label above, optional `hint`, mono `readout`, `end` slot |
137
138
  | a bound slider | `SliderControl` — min/max/step come off the handle, never props |
138
139
  | a facet list | `MultiSelectFilter` + `counts` (and `max`, past which the tail folds) |
139
140
  | a group of rows | `Section` — flush inside the aside, which draws the rhythm itself |
140
141
  | a choice | `PanelChoice` — a track only while ≤3 options AND every label fits, else `Select` |
141
142
 
142
- - ONE `PageAside` per page, written AFTER the main content — it portals into the region from `sm`
143
- up, its tree position is reading order, not layout.
144
- - Below `sm` it projects into `PageBar` row 2 as one `Panel` pill; with no row 2 — or shell-less —
145
- it renders in flow. One node either way (C9).
143
+ - ONE per page, written AFTER the main content — tree position is reading order, not layout.
144
+ - Below `sm` it projects into `PageBar` row 2 as one pill carrying its own `title`, and that row
145
+ exists ONLY when the bar has `tabs`, `filters` or filter-end actions — without one, or with no
146
+ shell, it renders in flow at the BOTTOM of the page. One node either way (C9).
146
147
  - A bound control outside all of this is `basalt/bound-control-outside-home` — a slot prop, a
147
148
  `FilterSet`, a `PageAside` or a `PanelRow` is a home; nothing else is.
148
149
 
149
150
  ## Sidebar blocks
150
151
 
151
152
  A non-destination list, a progress meter or a bespoke node in the sidebar is a `SidebarBlock` —
152
- declared data, three kinds (`list`/`progress`/`custom`), placed `'nav'`/`'bottom'`. Because it's
153
- data, basalt owns what a `ReactNode` slot couldn't: a rail dot/ring when collapsed, a persisted
154
- fold, one More-sheet row per block. A `custom` block is desktop-only, by design.
153
+ declared data, three kinds (`list`/`progress`/`custom`), placed `'nav'`/`'bottom'`; the rail and
154
+ More-sheet projections are basalt's from there (C13). A `custom` block is desktop-only, by design.
@@ -55,8 +55,10 @@ export default defineConfig({
55
55
  ```
56
56
 
57
57
  Then `BasaltShell` with a `useNav(NAV)` spread (basalt-state.md), `PageBar` per page
58
- (basalt-controls.md), and `<first basalt.root>/lib/series.ts` as the app's one guard-exempt series
59
- source (`/basalt-charts`). There is no Tailwind.
58
+ (basalt-controls.md), `PageAside` on any page that needs a right-hand inspector — a region a ROUTE
59
+ claims rather than a shell prop, so it never appears in `BasaltShell`'s props — and
60
+ `<first basalt.root>/lib/series.ts` as the app's one guard-exempt series source (`/basalt-charts`).
61
+ There is no Tailwind.
60
62
 
61
63
  ## 3. Verify the wiring took
62
64
 
@@ -2,11 +2,25 @@
2
2
  * Basalt query client — configure once, share via router context.
3
3
  * Extend staleTime / gcTime defaults and add per-query error handling here.
4
4
  *
5
+ * A FACTORY, not a module-level singleton, and that is load-bearing under SSR: a
6
+ * `export const queryClient = createBasaltQueryClient()` is created once per PROCESS,
7
+ * so every request a Node server handles shares one cache — one user's data served to
8
+ * the next. Call `makeQueryClient()` once per request on the server, and once at module
9
+ * scope in a browser-only entry:
10
+ *
11
+ * // main.tsx (CSR) — one client for the tab's lifetime
12
+ * const queryClient = makeQueryClient()
13
+ *
14
+ * // an SSR/prerender entry — one per request, never hoisted
15
+ * const queryClient = makeQueryClient()
16
+ *
5
17
  * Scaffold written by `basalt-ui init`. This file is yours — `basalt-ui sync` will not overwrite it.
6
18
  */
7
19
  import { createBasaltQueryClient } from 'basalt-ui'
8
20
 
9
- export const queryClient = createBasaltQueryClient({
10
- // staleTime: 60_000, // optional: global cache freshness window (ms)
11
- // gcTime: 5 * 60_000, // optional: inactive-query garbage-collection window (ms)
12
- })
21
+ export function makeQueryClient() {
22
+ return createBasaltQueryClient({
23
+ // staleTime: 60_000, // optional: global cache freshness window (ms)
24
+ // gcTime: 5 * 60_000, // optional: inactive-query garbage-collection window (ms)
25
+ })
26
+ }
@@ -10,10 +10,16 @@
10
10
  # `lefthook dump`: declare `pre-commit.commands.oxfmt.run` (or `glob:`) in your file and the
11
11
  # preset's value is what runs — yours is discarded silently, with no warning. Only keys this preset
12
12
  # does NOT define merge in from yours. So:
13
- # • `env:`, `exclude:`, `skip:` and any command name not used here — yours, they merge.
13
+ # • `env:`, `root:`, `exclude:`, `skip:` and any command name not used here — yours, they merge.
14
14
  # • `run:` and `glob:` on a command below — NOT overridable. Do not try.
15
15
  # The sanctioned seams are therefore:
16
- # • `BASALT_BIN` (below) — set it via `env:` to point the guard at a specific CLI.
16
+ # • `root:` — the ONE seam that fixes a monorepo, and the one nobody guesses. Lefthook runs every
17
+ # command at the REPO ROOT; `root:` moves one command into your package and rewrites
18
+ # `{staged_files}` relative to it. Every command below needs it when the app is not the repo
19
+ # root — otherwise `oxlint`/`oxfmt` run where there is no config and `check-theme` scans
20
+ # nothing. See the recipe at the bottom of this file.
21
+ # • `BASALT_CWD` / `BASALT_BIN` (below) — set them via `env:` to point the guard at a package and
22
+ # at a specific CLI. An alternative to `root:` for `check-theme` only.
17
23
  # • `exclude:` — carve files out of a glob you cannot narrow.
18
24
  # • a differently-named command of your own, plus `skip: true` on the one here you are replacing.
19
25
  #
@@ -62,23 +68,49 @@ pre-commit:
62
68
  # Without this, a consumer accumulates violations indefinitely while `check-theme` is never
63
69
  # invoked, and the kinds it never ran are indistinguishable from the kinds that found nothing.
64
70
  #
65
- # `$BASALT_BIN` is the one sanctioned override seam here, because `run:` itself is not
66
- # overridable (see the note at the top). It defaults to `bunx --no-install basalt-ui`:
67
- # • `--no-install` because a plain `bunx basalt-ui` in a repo where basalt is not resolvable
68
- # from the ROOT (bun's isolated linker puts it beside the package that depends on it)
69
- # silently downloads a DIFFERENT copy from npm and gates the commit with a version nobody
70
- # pinned. With the flag it fails loudly instead, which is the answer you can act on.
71
- # • point it at the local bin to fix that, exactly as the seeded check.yml does:
72
- # pre-commit:
73
- # commands:
74
- # check-theme:
75
- # env: { BASALT_BIN: ./packages/web/node_modules/.bin/basalt-ui }
71
+ # `$BASALT_CWD` and `$BASALT_BIN` are the sanctioned override seams here, because `run:` itself
72
+ # is not overridable (see the note at the top).
76
73
  #
77
74
  # Lefthook runs commands at the REPO ROOT, while in a monorepo the `basalt` config and the
78
- # manifest live in a package below it — verified to produce `0 files scanned`, i.e. following
79
- # this preset installed a gate that never gated. `check-theme` now relocates to the single
80
- # workspace package carrying a basalt config and says so; with more than one, set BASALT_CWD
81
- # in the same `env:` block to name the package:
82
- # env: { BASALT_CWD: apps/web }
75
+ # manifest live in a package below it. Since 1.29.0 the CLI resolves ONE way — `BASALT_CWD`,
76
+ # else the invocation cwd — and infers nothing from siblings or parents, so at the root it
77
+ # reads a directory with no basalt config and scans nothing. That used to be an unexplained
78
+ # `0 files scanned`; it now names this preset and the two fixes. `BASALT_CWD` is the fix that
79
+ # needs no other change, and this `run:` derives the bin from it so one setting covers both:
80
+ # pre-commit:
81
+ # commands:
82
+ # check-theme:
83
+ # env: { BASALT_CWD: apps/web }
84
+ #
85
+ # The default bin is `<BASALT_CWD>/node_modules/.bin/basalt-ui`, i.e. the local bin of the
86
+ # package the guard is being run FOR, and plain `node_modules/.bin/basalt-ui` when BASALT_CWD
87
+ # is unset. That replaces a `bunx --no-install basalt-ui` default which resolved to nothing at
88
+ # the root under bun's isolated linker — the linker puts basalt beside the package that depends
89
+ # on it, never at the root, so the default was broken in exactly the layout BASALT_CWD exists
90
+ # for. A missing bin now fails with the path it looked for instead of `bunx`'s resolver error.
91
+ # Override it (a hoisted install, a pinned copy) with `env: { BASALT_BIN: … }` in the same
92
+ # block; it wins outright.
83
93
  check-theme:
84
- run: ${BASALT_BIN:-bunx --no-install basalt-ui} check-theme
94
+ run: ${BASALT_BIN:-${BASALT_CWD:+$BASALT_CWD/}node_modules/.bin/basalt-ui} check-theme
95
+
96
+ # ── MONOREPO RECIPE (the app is NOT at the repo root) ────────────────────────────────────────────
97
+ # `extends` merges the commands above, but NOT a working directory — so at the repo root `oxlint`
98
+ # and `oxfmt` run where your `.oxlintrc.json` and your binaries are not, and `check-theme` scans a
99
+ # directory with no basalt config. Add `root:` per command in YOUR root lefthook.yml — it merges,
100
+ # because the preset deliberately does not define it:
101
+ #
102
+ # # <repo>/lefthook.yml
103
+ # extends:
104
+ # - apps/web/node_modules/basalt-ui/configs/lefthook.yml
105
+ # pre-commit:
106
+ # commands:
107
+ # oxlint: { root: 'apps/web/' }
108
+ # oxfmt: { root: 'apps/web/' }
109
+ # check-theme: { root: 'apps/web/' }
110
+ #
111
+ # One mechanism for all three, and `check-theme` needs nothing else: `root:` moves the invocation
112
+ # cwd, which is the only thing the CLI resolves from. `env: { BASALT_CWD: apps/web }` is the
113
+ # equivalent for the guard alone — reach for it when the command has to keep running at the root.
114
+ #
115
+ # `basalt-ui doctor` reads the MERGED config (`lefthook dump`) rather than this file, so it is what
116
+ # tells you whether the result actually gates.