basalt-ui 1.19.1 → 1.20.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 (78) hide show
  1. package/AGENTS.md +15 -1
  2. package/CHANGELOG.md +20 -0
  3. package/MIGRATING.md +228 -0
  4. package/README.md +53 -9
  5. package/agent/rules/basalt-app.md +43 -0
  6. package/agent/rules/basalt-charts.md +26 -14
  7. package/agent/rules/basalt-mantine.md +25 -10
  8. package/agent/rules/basalt-router.md +22 -10
  9. package/agent/rules/basalt-tokens.md +75 -6
  10. package/agent/skills/basalt-app/SKILL.md +42 -6
  11. package/configs/check.yml +11 -4
  12. package/configs/lefthook.yml +21 -2
  13. package/configs/oxlint-plugin.js +515 -25
  14. package/configs/oxlint-plugin.test.ts +383 -10
  15. package/configs/oxlint.json +10 -1
  16. package/dist/agent/stick-to-bottom.js +1 -1
  17. package/dist/agent-chat/thread-detail-panel.js +1 -1
  18. package/dist/agent-chat/thread-message.js +2 -2
  19. package/dist/agent-chat/tool-chip.js +1 -1
  20. package/dist/charts/kinds/Donut.d.ts.map +1 -1
  21. package/dist/charts/kinds/Donut.js +1 -1
  22. package/dist/charts/primitives/ChartLegend.js +2 -2
  23. package/dist/charts/primitives/ChartTooltip.js +1 -1
  24. package/dist/cli/index.d.ts +107 -15
  25. package/dist/cli/index.d.ts.map +1 -1
  26. package/dist/cli/index.js +610 -72
  27. package/dist/cli/toolchain-wiring.test.d.ts +2 -0
  28. package/dist/cli/toolchain-wiring.test.d.ts.map +1 -0
  29. package/dist/dashboard/delta-badge.js +2 -2
  30. package/dist/dashboard/stat-card.js +1 -1
  31. package/dist/data/virtual-list.js +5 -5
  32. package/dist/guard/index.d.ts +56 -2
  33. package/dist/guard/index.d.ts.map +1 -1
  34. package/dist/guard/index.js +326 -59
  35. package/dist/guard/types.d.ts +25 -5
  36. package/dist/guard/types.d.ts.map +1 -1
  37. package/dist/register.d.ts +25 -0
  38. package/dist/register.d.ts.map +1 -1
  39. package/dist/shell/app-mobile-nav.module.css +11 -3
  40. package/dist/surfaces.d.ts +1 -1
  41. package/dist/surfaces.js +1 -1
  42. package/dist/tokens/index.d.ts +3 -3
  43. package/dist/tokens/index.d.ts.map +1 -1
  44. package/dist/tokens/index.js +2 -0
  45. package/dist/tokens/palette.d.ts +35 -27
  46. package/dist/tokens/palette.d.ts.map +1 -1
  47. package/dist/tokens/palette.js +24 -18
  48. package/dist/tokens.css +19 -17
  49. package/dist/vite-color-mix.js +2 -2
  50. package/llms.txt +3 -3
  51. package/package.json +4 -3
  52. package/src/agent/stick-to-bottom.tsx +1 -1
  53. package/src/agent-chat/thread-detail-panel.tsx +1 -1
  54. package/src/agent-chat/thread-message.tsx +3 -3
  55. package/src/agent-chat/tool-chip.tsx +1 -1
  56. package/src/charts/kinds/Donut.tsx +2 -1
  57. package/src/charts/primitives/ChartLegend.tsx +2 -2
  58. package/src/charts/primitives/ChartTooltip.tsx +1 -1
  59. package/src/cli/index.ts +1070 -99
  60. package/src/cli/init-sync.test.ts +19 -3
  61. package/src/cli/tokens-css.test.ts +27 -14
  62. package/src/cli/toolchain-wiring.test.ts +694 -0
  63. package/src/dashboard/delta-badge.tsx +3 -3
  64. package/src/dashboard/stat-card.tsx +1 -1
  65. package/src/data/virtual-list.tsx +8 -8
  66. package/src/guard/check-source.test.ts +663 -11
  67. package/src/guard/index.ts +727 -94
  68. package/src/guard/types.ts +29 -4
  69. package/src/register.ts +32 -0
  70. package/src/shell/app-mobile-nav.module.css +11 -3
  71. package/src/shell/app-mobile-nav.tsx +1 -1
  72. package/src/surfaces.ts +1 -1
  73. package/src/theme/density-relations.test.ts +8 -5
  74. package/src/theme/spacing.test.ts +2 -0
  75. package/src/theme-toggle/index.tsx +2 -2
  76. package/src/tokens/index.ts +5 -3
  77. package/src/tokens/palette.ts +24 -18
  78. package/src/vite-color-mix.ts +2 -2
@@ -104,10 +104,16 @@ Install `@tanstack/react-router` as a peer, then import from `basalt-ui/router-t
104
104
 
105
105
  **Prerequisites:**
106
106
 
107
- - `@tanstack/router-plugin/vite` must be configured in your Vite config and `tsr generate` must
108
- have run at least once — the `StaticDataRouteOption` augmentation only takes effect once the route
109
- tree (`src/routeTree.gen.ts`) has been generated. In CI and after adding routes, run
110
- `tsr generate` (or `bun run typecheck`, which triggers it) before relying on breadcrumb or nav data.
107
+ - **The only hard prerequisite is the `Register` module augmentation** (see "The prerequisite that
108
+ makes all of this real" below). `defineNav` / `navGroup` / `navTarget` / `flattenNav` / `useNav` /
109
+ `createSearchParamStore` all work against a hand-written `createRouter({ routeTree })` with
110
+ code-defined routes, no Vite plugin and no generated tree — verified end-to-end in a consumer,
111
+ including that a bad `to` still prints the real route-path union.
112
+ - **`@tanstack/router-plugin/vite` + `tsr generate` are needed only for `staticData`** — that is,
113
+ for `useRouterBreadcrumbs` and any nav metadata read off the route tree. The
114
+ `StaticDataRouteOption` augmentation takes effect once `src/routeTree.gen.ts` exists. In CI and
115
+ after adding routes, run `tsr generate` (or `bun run typecheck`, which triggers it) before relying
116
+ on breadcrumb data.
111
117
  - The loader's `context.queryClient` requires `createRootRouteWithContext<{ queryClient: QueryClient }>()`.
112
118
  Run `basalt-ui init` to scaffold a `query-client.ts` + `__root.tsx` seed that wires the TanStack
113
119
  Router + Query context correctly — the generated `__root.tsx` uses
@@ -255,12 +261,18 @@ With that missing, every `to` widens to `string`, a nonexistent route path compi
255
261
  required `search` compiles, and the API looks like it is working while catching nothing. This is
256
262
  the single highest-value thing to verify when adopting the config: change one `to` to a garbage
257
263
  path and confirm you get a compile error listing the real route paths. If you don't, the
258
- augmentation is missing or `tsr generate` has not run.
259
-
260
- Second thing to know before you migrate: TypeScript cannot catch a destination that FORGETS a
261
- default `search` when the route validates with `validateSearch: (raw: Record<string, unknown>) => T`
262
- — that gives TanStack an input type where no key is required. Diff an old hand-written target table
263
- against the new definition by hand, key by key.
264
+ augmentation is missing (or, on a file-route setup, `tsr generate` has not run).
265
+
266
+ Second thing to know before you migrate: whether a missing `search` is caught depends on the
267
+ schema's OUTPUT type, not on the `validateSearch` signature. If every key of `T` is required —
268
+ which is what a Zod object with defaults produces, since defaults apply on parse and the output has
269
+ no optionals — TanStack raises `MakeRequiredSearchParams` and the compiler catches it. Two routes in
270
+ one consumer had been shipping nav links with no search params behind a `to={target.to as never}`
271
+ cast; removing the callback surfaced both immediately.
272
+
273
+ **The caveat holds only when `T` has no required keys** (every key optional, or the validator
274
+ returns a partial). There, a forgotten `search` compiles — diff the old target table against the new
275
+ definition by hand, key by key. Everywhere else, trust the error.
264
276
 
265
277
  Error messages here are the router's own and they are large: one wrong `to` prints the whole
266
278
  route-path union plus a `RouterCore<…>` blob, because TanStack's `Constrain` falls back to the
@@ -24,12 +24,76 @@ status pops — never floods. (Blueprint/Basalt zinc-charcoal are the historical
24
24
  ancestors; `docs/DESIGN-SPEC.md` in the basalt-ui repo supersedes both — see its "Doctrine
25
25
  inversions" section.) This
26
26
  rule is the operational checklist; it is enforced mechanically by **`basalt-ui check-theme`**
27
- (wire it into `lint`: `oxlint . && basalt-ui check-theme`). A violation fails the build. Escape hatch: a
28
- `theme-allow` line comment (diff-visible, deliberate).
27
+ (wire it into `lint`: `oxlint . && basalt-ui check-theme`; `init` seeds that as `lint:basalt`). A
28
+ violation fails the build. Escape hatch: a scoped `theme-allow <rule-id> — <reason>` comment
29
+ (diff-visible, deliberate) — see the contract below.
29
30
 
30
31
  `check-theme` reads its config from your `package.json` `"basalt"` key
31
- (`{ roots?, exempt?, spacingSteps?, forbiddenAccents? }`); set `roots` to your source dirs and `exempt`
32
- to the files that legitimately define palette values.
32
+ (`{ roots?, exempt?, include?, profile?, severity?, spacingSteps?, forbiddenAccents? }`); set `roots`
33
+ to your source dirs and `exempt` to the files that legitimately define palette values.
34
+
35
+ **`roots` is not optional in practice.** Omitted it falls back to `src` relative to the cwd — fine
36
+ for a single-app repo, zero files for anything under `apps/*/src`, and `check-theme` then exits 1
37
+ with `0 files scanned`. Since 1.20.0 `init` writes a real `roots` from the detected layout and
38
+ `doctor`'s `guard-scan` check fails when the configured roots resolve to nothing; before that a repo
39
+ could look fully wired while the gate scanned nothing.
40
+
41
+ The scan reaches slightly beyond `roots`: each root's PARENT contributes its `index.html` and its
42
+ `public/` tree (the Vite layout basalt's own preset assumes), because that is where a `theme-color`
43
+ meta and a webmanifest's `background_color` actually live. `.json` is never blanket-scanned — name a
44
+ design-surface JSON in `basalt.include`, the only route to one.
45
+
46
+ ## `theme-allow` — the 1.20.0 contract
47
+
48
+ Scope it. `theme-allow <rule-id> — <reason>` waives that ONE kind; the id is a guard kind
49
+ (`raw-surface`, `inline-spacing`, …) or an oxlint plugin rule (`hand-rolled-plot`,
50
+ `raw-scroll-container`, …), with or without the `basalt/` prefix.
51
+
52
+ ```tsx
53
+ // theme-allow raw-surface — third-party widget needs a literal corner
54
+ <Widget style={{ borderRadius: 3 }} />
55
+ ```
56
+
57
+ - **A bare `theme-allow` still waives everything**, so no upgrade takes a build down over comment
58
+ placement — but it now reports `theme-allow-unscoped` (`warn` for one minor, then `error`).
59
+ Rescope the ones you have; that is the whole migration.
60
+ - **Spell the id right — a typo waives nothing.** A word in the id slot that names no rule is
61
+ recorded as unknown and suppresses nothing; the annotation covers exactly the ids it got right.
62
+ Only a genuinely bare `theme-allow` is the blanket form. Because the id slot is read strictly, a
63
+ prose reason has to be introduced by a separator: `theme-allow: <why>` or
64
+ `theme-allow <id> — <why>`, never `theme-allow <why>`.
65
+ - **Three placements work and both engines agree on all three**: the reported line, a comment-ONLY
66
+ line directly above it (the only form JSX can express — the reported line is usually a multi-line
67
+ opening tag or a `{expr}` child), and in CSS a trailing annotation reaching back over the
68
+ declaration it terminates. The third is what survives the shipped `oxfmt` reflowing a long
69
+ `background-color` so the hex lands ABOVE the comment.
70
+ - **`basalt/hand-rolled-plot` no longer grants whole-file immunity** off whichever comment happened
71
+ to sit on the first assembly node. Every node is waived on its own; a file-scoped exception needs
72
+ a written declaration that both names the rule and gives a reason, anywhere in the file.
73
+
74
+ ### New in 1.20.0 — five guard kinds, all `warn` for one minor
75
+
76
+ | Kind | Fires on |
77
+ | ------------------------- | ------------------------------------------------------------------------------------------------------------------- |
78
+ | `theme-allow-unscoped` | a `theme-allow` with no rule id, no reason, or an id that names no rule |
79
+ | `surface-shadow-override` | a `boxShadow` built FROM tokens that REPLACES `--vx-shadow-card` instead of composing with it |
80
+ | `css-raw-surface` | the kebab dialect of the surface kinds in CSS (`border-radius: 6px`); sub-scale corners and circle/pill values pass |
81
+ | `inline-font-size` | `style={{ fontSize: 11 }}` — the `check-theme` half of `basalt/no-raw-font-size`, for a CI that runs only the guard |
82
+ | `hidden-inline-style` | a multi-line opening tag, or a style object hoisted to a const — `raw-html-layout`'s formatting-independent half |
83
+
84
+ Two new oxlint rules ship at `warn` alongside them: **`basalt/shadow-basalt-export`** (below) and
85
+ **`basalt/hand-rolled-shell`** (`AppShell` parts or `Burger` in a file that does not render
86
+ `BasaltShell`). **`basalt/raw-size-literal` is now `error`** — CSS-length strings on
87
+ `size`/`fz`/`fontSize`. Every kind keeps its own boolean, `basalt.severity` and `exemptRules`.
88
+
89
+ ## The guard sees palette, not vocabulary
90
+
91
+ Round 4 swept seven consumer repos: every gate green, and ~15 independently re-rolled copies of
92
+ components basalt already ships (`StatCard` in 4 of 4 apps, `EmptyState`, a 266-line hand-rolled
93
+ `AppShell`). A fork written by a token-fluent author uses exactly the right tokens, so no palette
94
+ guard can ever see it. **`basalt/shadow-basalt-export` is the cheap detector** — it warns when a
95
+ local component's name collides with a live basalt export, read from the real shipped barrel. The
96
+ habit it stands in for: check whether basalt already ships the thing before building it.
33
97
 
34
98
  ## Filled surfaces — the fill band
35
99
 
@@ -158,5 +222,10 @@ export const paletteGroups = { [`${GROUP}-`]: SERIES } // -> { 'activity-': SERI
158
222
  ## When the guard fires
159
223
 
160
224
  Fix the source, don't silence it — reach for the right token first. Only add `theme-allow` for a
161
- genuine, documented exception (e.g. a third-party widget needing a literal). The palette-definition
162
- files are listed in your `exempt` set so they don't self-trip.
225
+ genuine, documented exception (a third-party widget needing a literal), scoped to the rule id, with
226
+ a reason: a bare `// theme-allow` passes the check, tells the next reader nothing, and now reports
227
+ `theme-allow-unscoped` besides. The palette-definition files are listed in your `exempt` set so they
228
+ don't self-trip. A stylesheet emitted by `basalt-ui tokens:css` needs no entry — in a `.css` file
229
+ carrying that exact two-line `@generated basalt-ui` header, the guard skips the LINES that are
230
+ basalt custom properties, selectors, `}` or self-closing comments. Pasting the marker anywhere else
231
+ suppresses nothing, and an ordinary declaration added to such a file is still reported.
@@ -53,6 +53,28 @@ own the files.
53
53
  - `.basalt/manifest.json` — a sha256 per managed file + the basalt-ui version that wrote it, so
54
54
  `sync` can three-way diff and `doctor` can spot a stale install.
55
55
 
56
+ It also patches `package.json`: **`basalt.roots`** inferred from the real layout (workspace packages
57
+ depending on basalt or Mantine, else every workspace `src/`, else `src`) and a **`lint:basalt`**
58
+ script (`oxlint . && basalt-ui check-theme`). Everything derives from `roots` — the guard's scan, the
59
+ seeded CI `oxfmt` globs, the default exemption — so a workspace repo without it scaffolds a guard
60
+ that scans zero files while every gate reports green. Correct it if your sources live elsewhere. A
61
+ Mantine-free consumer adds `"basalt": { "profile": "tokens-only" }` by hand; `check-theme` never
62
+ infers that, because inferring it from a missing `@mantine/core` would silence 17 kinds on a repo
63
+ that simply keeps Mantine in another workspace package.
64
+
65
+ **`init` on an EXISTING app is a lint-debt event, not a no-op.** Adopting the shipped preset turns
66
+ on whole oxlint plugins the repo was never linted against, so previously-clean code lands with real
67
+ findings on the first run. `init` names the plugins and every file it kept — plus what keeping it
68
+ costs (an existing `.oxlintrc.json` that does not `extends` the preset means the entire basalt lint
69
+ half is off, and nothing else says so; `--merge-lint` splices it in). Run `oxlint .` and triage
70
+ before the next commit; turn a rule off with a written reason rather than disabling a plugin.
71
+
72
+ Then `basalt-ui doctor`. `SKIPPED` is a third outcome beside pass/warn/fail and exits non-zero on
73
+ its own — a check that could not run is not a check that passed — and the hard checks now include
74
+ `basalt-resolves`, `guard-scan` (would `check-theme` cover more than zero files?) and
75
+ `oxlint-preset` (does `.oxlintrc.json` really extend the preset? JSONC is parsed, not rejected).
76
+ Expect doctor to go red where it was green on an older basalt; that is the point.
77
+
56
78
  After `init`, do the runtime wiring (the CLI scaffolds files, not your app's composition):
57
79
 
58
80
  ```tsx
@@ -94,10 +116,15 @@ ordering (e.g. alongside `@content-collections/vite`) and the full head/PWA/mani
94
116
  import 'basalt-ui/styles.css' // @layer basalt base styles, iOS input safety net, font stack
95
117
  ```
96
118
 
97
- Lint wires as `oxlint . && basalt-ui check-theme` — the theme guard is the teeth behind the token
98
- doctrine. The consumer series file (`<first basalt.root>/lib/series.ts` by default — so `src/lib/series.ts`
99
- on a plain app, `apps/web/src/lib/series.ts` on a monorepo; override via `basalt.seriesModulePath`) is the
100
- single guard-exempt palette source — see `/basalt-charts`.
119
+ Lint wires as `oxlint . && basalt-ui check-theme` (seeded as the `lint:basalt` script) — the theme
120
+ guard is the teeth behind the token doctrine. Before writing any composite, check whether basalt
121
+ already ships it: round 4 found ~15 forked copies of shipped components across seven repos, all
122
+ passing every gate, because a fork by a token-fluent author uses exactly the right tokens.
123
+ `basalt/shadow-basalt-export` warns on the name collision; nothing catches the rest.
124
+
125
+ The consumer series file (`<first basalt.root>/lib/series.ts` by default — so `src/lib/series.ts` on
126
+ a plain app, `apps/web/src/lib/series.ts` on a monorepo; override via `basalt.seriesModulePath`) is
127
+ the single guard-exempt palette source — see `/basalt-charts`.
101
128
 
102
129
  ## Refresh: `bunx basalt-ui sync`
103
130
 
@@ -140,13 +167,22 @@ both.
140
167
  - A thin `DESIGN.md` (deltas) + the thirteen `basalt-*` rules + the managed CLAUDE block are present.
141
168
  - `<first basalt.root>/lib/series.ts` exists as the one guard-exempt series source (for any app metric colors).
142
169
  - `basalt-ui sync --check` wired in CI (recommended) to catch drift on future upgrades.
170
+ - `basalt-ui doctor` run after `init` and read to the last line — a `SKIPPED` check exits non-zero
171
+ and is not a pass.
143
172
 
144
173
  ## Notes
145
174
 
146
175
  - `init`, `sync`, and `check-theme` are all **real**. `sync` reconciles via a sha256 three-way diff
147
176
  (`--check` is a CI drift gate, `--force` overwrites local edits); `check-theme` is the palette
148
- guard; `doctor` verifies the installed basalt-ui version against the manifest. All via
149
- `bunx basalt-ui …`.
177
+ guard; `doctor` checks resolution, version, guard scan, oxlint preset, spacing-scale drift and
178
+ cross-package `ai` major parity. All via `bunx basalt-ui …`.
179
+ - **Not a React app?** `bunx basalt-ui tokens:css --out src/tokens.css` emits the `--vx-*` layer with
180
+ no package in your dependency tree, `--selector-class dark` for the Tailwind `<html class="dark">`
181
+ convention, and `fonts:css` emits the shipped `--basalt-font-*` stacks. Both carry the two-line
182
+ `@generated basalt-ui` header the guard skips on — that header verbatim, and then only the LINES
183
+ that are basalt custom properties, selectors, `}` or self-closing comments, so the marker pasted
184
+ into a `.tsx` suppresses nothing and a declaration added to such a file is still reported — and
185
+ both take `--check` as a CI drift gate. See `docs/FRAMEWORK-FREE.md` in the basalt-ui repo.
150
186
  - The framework ships **no** icon or notification dep — pass icons as `ReactNode` and wire toasts
151
187
  yourself (e.g. `ThemeLabControls`' `copyIcon` / `onCopy`).
152
188
  - The framework DOES ship `motion` (motion.dev, formerly framer-motion) as a bundled dependency —
package/configs/check.yml CHANGED
@@ -5,8 +5,14 @@
5
5
  # color literals / off-identity accents that bypass the --vx-* token system).
6
6
  # `basalt-ui sync --check` fails when the basalt-managed files (rules, skills, the
7
7
  # CLAUDE block) have drifted from the installed basalt-ui version.
8
- # The oxfmt globs below are rendered from `basalt.roots` at init time — this file is a
9
- # template, not a raw copy, so a monorepo doesn't seed a `src/**` that matches nothing.
8
+ # The oxfmt globs below are rendered from `basalt.roots` at init time — `init` writes that
9
+ # key from the repo's real layout (workspace packages included), so a monorepo does not
10
+ # seed a `src/**` that matches nothing.
11
+ # The basalt-ui command below is the LOCAL bin, rendered from where basalt actually
12
+ # installed. `bunx basalt-ui` is deliberately not used: when the package is not resolvable
13
+ # from the working directory (bun's isolated linker puts it beside the package that depends
14
+ # on it, not at the root) bunx silently downloads a DIFFERENT copy from npm and the gate
15
+ # runs a version nobody pinned.
10
16
  name: check
11
17
  on:
12
18
  pull_request:
@@ -23,8 +29,9 @@ jobs:
23
29
  bun-version: latest
24
30
  - run: bun install --frozen-lockfile
25
31
  - run: bunx oxlint .
26
- - run: bunx basalt-ui check-theme
27
- - run: bunx basalt-ui sync --check
32
+ - run: '{{BASALT_BIN}} doctor'
33
+ - run: '{{BASALT_BIN}} check-theme'
34
+ - run: '{{BASALT_BIN}} sync --check'
28
35
  - run: bunx oxfmt {{ROOTS_GLOBS}} '!**/dist/**' --check
29
36
  # Enable once your app defines these package.json scripts:
30
37
  # - run: bun run typecheck
@@ -4,8 +4,17 @@
4
4
  # - node_modules/basalt-ui/configs/lefthook.yml
5
5
  # lefthook merges these commands with the consumer's own, so this preset auto-updates
6
6
  # with the package. Runs oxlint + oxfmt on staged files and the theme guard on the whole
7
- # tree, pre-commit. Assumes oxlint/oxfmt are installed and your .oxlintrc.json extends
8
- # ./node_modules/basalt-ui/configs/oxlint.json.
7
+ # tree, pre-commit. Assumes oxlint/oxfmt are installed.
8
+ #
9
+ # It does NOT assume your `.oxlintrc.json` extends the shipped preset — that assumption was
10
+ # false for exactly the repos this seed exists for, since `init` KEEPS an existing oxlint
11
+ # config. `basalt-ui doctor` now checks the extends and hard-fails when it is missing; run it
12
+ # in CI (the seeded check.yml does) rather than trusting a comment nobody reads.
13
+ #
14
+ # `init` seeds the path RESOLVED from where basalt actually installed. Under bun's isolated
15
+ # linker basalt lives beside the package that depends on it, not at the repo root, and a
16
+ # root-relative `extends` there resolves to nothing — silently, because lefthook merges an
17
+ # empty file without complaint.
9
18
  pre-commit:
10
19
  parallel: true
11
20
  commands:
@@ -28,5 +37,15 @@ pre-commit:
28
37
  # was written against: a guard that only runs when someone remembers to run it is not a guard.
29
38
  # Without this, a consumer accumulates violations indefinitely while `check-theme` is never
30
39
  # invoked, and the kinds it never ran are indistinguishable from the kinds that found nothing.
40
+ #
41
+ # Lefthook runs commands at the REPO ROOT, while in a monorepo the `basalt` config and the
42
+ # manifest live in a package below it — verified to produce `0 files scanned`, i.e. following
43
+ # this preset installed a gate that never gated. `check-theme` now relocates to the single
44
+ # workspace package carrying a basalt config and says so; with more than one, set BASALT_CWD
45
+ # in your own lefthook.yml to name the package:
46
+ # pre-commit:
47
+ # commands:
48
+ # check-theme:
49
+ # env: { BASALT_CWD: apps/web }
31
50
  check-theme:
32
51
  run: bunx basalt-ui check-theme