basalt-ui 1.23.0 → 1.24.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 (81) hide show
  1. package/AGENTS.md +31 -17
  2. package/CHANGELOG.md +23 -0
  3. package/MIGRATING.md +222 -6
  4. package/README.md +159 -49
  5. package/agent/rules/basalt-app.md +8 -2
  6. package/agent/rules/basalt-charts.md +38 -2
  7. package/agent/rules/basalt-content.md +3 -3
  8. package/agent/rules/basalt-data.md +45 -13
  9. package/agent/rules/basalt-mantine.md +4 -4
  10. package/agent/rules/basalt-query.md +57 -0
  11. package/agent/rules/basalt-tokens.md +18 -5
  12. package/agent/skills/basalt-app/SKILL.md +32 -12
  13. package/agent/skills/basalt-charts/SKILL.md +4 -2
  14. package/agent/templates/CLAUDE-block.md.tpl +3 -2
  15. package/dist/charts/kinds/BandStrip.d.ts +20 -6
  16. package/dist/charts/kinds/BandStrip.d.ts.map +1 -1
  17. package/dist/charts/kinds/BandStrip.js +59 -6
  18. package/dist/charts/kinds/MirroredBars.d.ts +4 -2
  19. package/dist/charts/kinds/MirroredBars.d.ts.map +1 -1
  20. package/dist/charts/kinds/MirroredBars.js +13 -0
  21. package/dist/cli/index.d.ts +4 -2
  22. package/dist/cli/index.d.ts.map +1 -1
  23. package/dist/cli/index.js +269 -27
  24. package/dist/dashboard/empty-state.d.ts +6 -2
  25. package/dist/dashboard/empty-state.d.ts.map +1 -1
  26. package/dist/dashboard/empty-state.js +1 -1
  27. package/dist/dashboard/index.d.ts +1 -0
  28. package/dist/dashboard/index.d.ts.map +1 -1
  29. package/dist/dashboard/index.js +8 -0
  30. package/dist/dashboard/query-state.d.ts +96 -0
  31. package/dist/dashboard/query-state.d.ts.map +1 -0
  32. package/dist/dashboard/query-state.js +123 -0
  33. package/dist/dashboard/query-state.test.d.ts +2 -0
  34. package/dist/dashboard/query-state.test.d.ts.map +1 -0
  35. package/dist/data/data-table.d.ts +61 -4
  36. package/dist/data/data-table.d.ts.map +1 -1
  37. package/dist/data/data-table.js +41 -4
  38. package/dist/data/data-table.test.d.ts +2 -0
  39. package/dist/data/data-table.test.d.ts.map +1 -0
  40. package/dist/data/table.d.ts +1 -1
  41. package/dist/data/table.d.ts.map +1 -1
  42. package/dist/guard/index.d.ts.map +1 -1
  43. package/dist/guard/index.js +50 -9
  44. package/dist/index.d.ts +1 -1
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +7 -1
  47. package/dist/query/error-message.d.ts +41 -0
  48. package/dist/query/error-message.d.ts.map +1 -0
  49. package/dist/query/error-message.js +30 -0
  50. package/dist/query/error-message.test.d.ts +2 -0
  51. package/dist/query/error-message.test.d.ts.map +1 -0
  52. package/dist/query/index.d.ts +1 -19
  53. package/dist/query/index.d.ts.map +1 -1
  54. package/dist/query/index.js +2 -14
  55. package/dist/surfaces.d.ts +1 -1
  56. package/dist/surfaces.js +1 -1
  57. package/llms.txt +3 -3
  58. package/package.json +1 -1
  59. package/src/charts/kinds/BandStrip.test.tsx +87 -5
  60. package/src/charts/kinds/BandStrip.tsx +105 -12
  61. package/src/charts/kinds/MirroredBars.test.tsx +39 -0
  62. package/src/charts/kinds/MirroredBars.tsx +38 -2
  63. package/src/cli/check-theme.test.ts +34 -0
  64. package/src/cli/help.test.ts +74 -1
  65. package/src/cli/index.ts +496 -41
  66. package/src/cli/tokens-css.test.ts +129 -0
  67. package/src/cli/toolchain-wiring.test.ts +146 -0
  68. package/src/dashboard/empty-state.tsx +9 -3
  69. package/src/dashboard/index.ts +11 -0
  70. package/src/dashboard/query-state.test.tsx +152 -0
  71. package/src/dashboard/query-state.tsx +272 -0
  72. package/src/data/data-table.test.tsx +158 -0
  73. package/src/data/data-table.tsx +123 -5
  74. package/src/data/table.ts +6 -1
  75. package/src/guard/check-source.test.ts +214 -0
  76. package/src/guard/index.ts +169 -15
  77. package/src/index.ts +9 -0
  78. package/src/query/error-message.test.ts +67 -0
  79. package/src/query/error-message.ts +70 -0
  80. package/src/query/index.ts +2 -34
  81. package/src/surfaces.ts +1 -1
package/AGENTS.md CHANGED
@@ -8,7 +8,7 @@ Toolchain: Bun runtime, oxlint + oxfmt, conventional commits (empty scope), `mas
8
8
 
9
9
  | Subpath | Layer | Purpose |
10
10
  | --------------------------- | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
11
- | `basalt-ui` | mantine-coupled | BasaltProvider, createBasaltTheme, BasaltShell + sidebar/mobile-nav/breadcrumbs, NavCountBadge, ThemeToggle, ThreadWorkspace + thread-chat components, dashboard composites (DeltaBadge, StatCard with threshold tone, EmptyState, SettingsSection/SettingsRow/DangerZone) |
11
+ | `basalt-ui` | mantine-coupled | BasaltProvider, createBasaltTheme, BasaltShell + sidebar/mobile-nav/breadcrumbs, NavCountBadge, ThemeToggle, ThreadWorkspace + thread-chat components, dashboard composites (DeltaBadge, StatCard with threshold tone, EmptyState, QueryState/LoadingState/ErrorState, SettingsSection/SettingsRow/DangerZone) |
12
12
  | `basalt-ui/charts` | headless | CartesianChart + visx chart primitives, kinds, sparklines, hooks, and token re-exports (Mantine-free) |
13
13
  | `basalt-ui/tokens` | headless | VX token refs, buildPaletteCss, defineSeries, seriesTokens, groupTokens, alpha (Mantine-free) |
14
14
  | `basalt-ui/theme-lab` | mantine-coupled | ThemeLabControls, applyOverrides, COLOR_GROUPS — a low-level inspector for the non-derived structural tokens; identity/color tuning lives in DeriveControls |
@@ -62,29 +62,43 @@ duration/spring/ease literal in a `transition={{...}}` prop (enforced by `basalt
62
62
 
63
63
  ## Key commands
64
64
 
65
+ `init` and `tokens:css`/`fonts:css` are the only commands with no install to resolve, so `bunx` is
66
+ correct for them **and only while basalt is not installed here** — once it is, they run through the
67
+ local bin like everything else. See the note below.
68
+
65
69
  ```bash
66
- bunx basalt-ui init # FIRST STEP: scaffold .claude/rules/ + .claude/skills/, managed CLAUDE.md block, DESIGN.md seed, toolchain seeds
67
- bunx basalt-ui check-theme # fail on off-palette colors in consumer source
68
- bunx basalt-ui sync # reconcile managed files (.claude/rules/, .claude/skills/, CLAUDE.md block)
69
- bunx basalt-ui sync --check # CI freshness gate — exits non-zero if any managed file drifted
70
- bunx basalt-ui doctor # integration health — SKIPPED is a third outcome and exits non-zero
71
- bunx basalt-ui tokens:css # emit the --vx-* stylesheet (no React, no Mantine, no bundler)
72
- bunx basalt-ui fonts:css # emit the shipped --basalt-font-* stacks
73
- bunx basalt-ui info # print published surface map
74
- bunx basalt-ui info --json # stable JSON surface map
75
- bunx basalt-ui guard-hook # PreToolUse theme-guard adapter: reads a Write/Edit payload on stdin, denies off-palette writes
70
+ bunx basalt-ui init # FIRST STEP: scaffold .claude/rules/ + .claude/skills/, managed CLAUDE.md block, DESIGN.md seed, toolchain seeds
71
+ basalt-ui --version # one bare line, exit 0 — which CLI is actually running (also -v / version)
72
+ basalt-ui check-theme # fail on off-palette colors in consumer source
73
+ basalt-ui sync # reconcile managed files (.claude/rules/, .claude/skills/, CLAUDE.md block)
74
+ basalt-ui sync --check # CI freshness gate — exits non-zero if any managed file drifted
75
+ basalt-ui doctor # integration health — SKIPPED is a third outcome and exits non-zero
76
+ bunx basalt-ui tokens:css # emit the --vx-* stylesheet (no React, no Mantine, no bundler) — bunx ONLY if basalt is not installed
77
+ bunx basalt-ui fonts:css # emit the shipped --basalt-font-* stacks — same exception
78
+ basalt-ui info --json # stable JSON surface map
79
+ basalt-ui guard-hook # PreToolUse theme-guard adapter: reads a Write/Edit payload on stdin, denies off-palette writes
76
80
  ```
77
81
 
82
+ Every subcommand rejects a flag it does not accept, exiting 1 and naming it; an unknown command says
83
+ so above the usage block. Neither is ever ignored — `doctor --json` used to run doctor and exit 0.
84
+
78
85
  `check-theme`, `doctor` and `sync` resolve their project identically: `BASALT_CWD` wins, then the
79
86
  cwd, then the workspace packages the root declares, then a two-level layout scan (which is what a
80
- repo with no `workspaces` field and the install one level down needs). Two candidates is ambiguous
81
- and reported, never guessed. `sync` on a `tokens-only` consumer prints `n/a` and exits 0.
82
-
83
- **Checking what an upgrade actually does? Run the local bin, not `bunx`.** `bunx` does not
84
- re-resolve a cached package, so it can execute the version you just upgraded away from.
87
+ repo with no `workspaces` field and the install one level down needs), then an **ascend** to the
88
+ nearest ancestor carrying a basalt project, bounded by the repo root. Both directions announce the
89
+ relocation in one sentence — `check-theme` used to fabricate `roots: ["src"]` on the ascend path and
90
+ pass silently. Two candidates is ambiguous and reported, never guessed. `sync` on a `tokens-only`
91
+ consumer prints `n/a` and exits 0; from a sub-package it refreshes the parent install rather than
92
+ refusing, and still will not scaffold a second consumer.
93
+
94
+ **Run the local bin, not `bunx`.** `bunx` does not re-resolve a cached package, so it can execute
95
+ the version you just upgraded away from — one consumer filed a P0 against a 1.20.0 cache while
96
+ believing it was on 1.22.0. `init` and `sync` render every seeded script, CI step and hook with the
97
+ resolved local bin for this reason (`BASALT_BIN` overrides it). `basalt-ui --version` is the
98
+ one-second check.
85
99
 
86
100
  Register `guard-hook` in `.claude/settings.json` under `hooks.PreToolUse` with matcher
87
- `Write|Edit|MultiEdit` and command `bunx basalt-ui guard-hook` so the agent can't write off-palette
101
+ `Write|Edit|MultiEdit` and command `./node_modules/.bin/basalt-ui guard-hook` (what `init` seeds) so the agent can't write off-palette
88
102
  colors without a `theme-allow` comment.
89
103
 
90
104
  **The escape hatch is `theme-allow <rule-id> — <reason>`**, scoped to that one kind and to that one
package/CHANGELOG.md CHANGED
@@ -1,3 +1,26 @@
1
+ # [1.24.0](https://github.com/jkrumm/basalt-ui/compare/v1.23.1...v1.24.0) (2026-08-22)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * stop check-theme fabricating a config, and re-gate the provenance line ([99a45c0](https://github.com/jkrumm/basalt-ui/commit/99a45c0dc284850b4140a6ff7719dbade4795f09))
7
+ * stop raw-hex reading HTML entities, and teach the guard the SFC dialect ([f1b0562](https://github.com/jkrumm/basalt-ui/commit/f1b0562efeb718921509253f9ee215bf461c3c01)), closes [#123](https://github.com/jkrumm/basalt-ui/issues/123) [#125](https://github.com/jkrumm/basalt-ui/issues/125)
8
+
9
+
10
+ ### Features
11
+
12
+ * playground routes for QueryState and the table body chrome ([e150a00](https://github.com/jkrumm/basalt-ui/commit/e150a0051bf743137751a3d5c17f202c0984aaa7))
13
+ * ship the query loading/error branch and the table body chrome ([963bf04](https://github.com/jkrumm/basalt-ui/commit/963bf047cf9ac4fac3cecceecbd0c378ee37f79d))
14
+
15
+ ## [1.23.1](https://github.com/jkrumm/basalt-ui/compare/v1.23.0...v1.23.1) (2026-08-22)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * gate the two chart tag rules on where the tag came from ([0075224](https://github.com/jkrumm/basalt-ui/commit/007522416bf9a3269e5f45b7ae7f45203434bae7))
21
+ * make the CLI answer which version ran, and stop it failing open ([0edf50e](https://github.com/jkrumm/basalt-ui/commit/0edf50e1d6f61b349f989fd10fe384d4a91a9af1))
22
+ * stop a typo'd band state from asserting absence ([6c1515c](https://github.com/jkrumm/basalt-ui/commit/6c1515c3ecbf019d9971e5b247e81e5cce3ae316))
23
+
1
24
  # [1.23.0](https://github.com/jkrumm/basalt-ui/compare/v1.22.0...v1.23.0) (2026-08-22)
2
25
 
3
26
 
package/MIGRATING.md CHANGED
@@ -8,9 +8,10 @@ renamed exports per minor, with the replacement.
8
8
  Reconstructed from `git diff` over the published export surface across `v1.0.0..v1.19.1`, then
9
9
  cross-checked against the repo's `scripts/export-surface.json` snapshot. **Every replacement below
10
10
  was re-audited against the built declaration files at 1.20.0 (2026-08-22)** after round 5 caught one
11
- row that was wrong. That pass corrected 4 table rows and 3 prose claims. The 1.21.0, 1.22.0 and
12
- `Unreleased` sections were written against source, not against their commit messages. Check the
13
- types, not this table, if the two disagree.
11
+ row that was wrong. That pass corrected 4 table rows and 3 prose claims. Every section from 1.21.0
12
+ on was written against source, not against its commit messages — which is not a formality: one
13
+ 1.23.0 commit message describes a change it did not make (see `1.23.1` § Corrections). Check
14
+ the types, not this table, if the two disagree.
14
15
 
15
16
  **The newest section is headed `## Unreleased`, and stays that way until npm serves it.** This file
16
17
  is written before `semantic-release` picks the number, so a number written here is a guess — and it
@@ -39,6 +40,219 @@ version `CHANGELOG.md` does not record. This file is not in that set; the same d
39
40
  by convention. Rename the section at release, or leave it — a reader can resolve `Unreleased`
40
41
  against `CHANGELOG.md`, and could never resolve `1.21.1`.
41
42
 
43
+ **Nothing removed or renamed. Twelve new runtime exports on `.`/`./query`, plus table props.** Every
44
+ API entry is additive; the behaviour changes are in the guard and the CLI, listed after them.
45
+
46
+ ### `QueryState` — the branch precedence, shipped as a component
47
+
48
+ `QueryState`, `LoadingState`, `ErrorState` (+ `QueryStateProps`, `QueryStateLike`,
49
+ `QueryStateVariant`, `QueryEmptyCopy`, `LoadingStateProps`, `ErrorStateProps`) on the root barrel,
50
+ beside `EmptyState`. `toErrorMessage` and `errorStatus` on `basalt-ui/query`.
51
+
52
+ basalt owned both ends of this file — `EmptyState` and `toErrorMessage` — and nothing in between, so
53
+ a consumer wrote the four-way switch and got it wrong in the direction the shape suggested:
54
+ image-share's library rendered `No images` on a **500** and a share detail rendered
55
+ `Share not found` on a dropped connection, until 204 hand-rolled lines stopped it. Open since round
56
+ 4, re-reported in round 6.
57
+
58
+ - **A component, not a hook** — the product IS the precedence, and a hook returns the same four-way
59
+ switch to every call site.
60
+ - **It lives under `src/dashboard/`, not `src/query/`**: `check-dist-layering.mjs` asserts
61
+ `dist/query/index.js` reaches no `@mantine/*`, and this renders Mantine. `query` is typed as a
62
+ five-field structural subset (`QueryStateLike`), so basalt couples to no query-library version and
63
+ a hand-composed object is legal. The subset gives up the compiler, so the shape is asserted at
64
+ **runtime**: a missing `isError` throws naming the field, because a missing `isError` is precisely
65
+ the "500 renders _No images_" bug.
66
+ - `errorTitle` / `errorFallback` / `errorAction` apply to the no-cached-data error branch only; the
67
+ error-with-cached-data branch renders a fixed section banner above `children`.
68
+ - **`EmptyState.description` is now optional.** Five argo features wrapped the component solely
69
+ because a compact panel had to invent a second sentence. Existing calls are unaffected.
70
+ - `toErrorMessage(err, fallback?)` and `errorStatus(err)` had two live bugs the port found: an
71
+ opaque envelope rendered the literal `"{}"`, and `toErrorMessage(undefined)` returned the
72
+ `undefined` VALUE despite a `string` return type. Both fixed; a status is folded into the fallback
73
+ when the body decodes to nothing readable. Split into `src/query/error-message.ts` so the
74
+ dashboard decodes without importing the peer.
75
+
76
+ **Port result:** image-share, all 10 call sites plus a standalone `ErrorState`, **by changing one
77
+ import line each** — no renames, no prop changes, no casts. Total 2467 → 2221; code-only 2056 →
78
+ 1882, **−174**; `query-state.tsx` 204 → 0.
79
+
80
+ ### `BasaltDataTable` body chrome — and the honest number
81
+
82
+ New: `maxHeight`, `minWidth`, `stickyHeader`, `stickyHeaderOffset`, `verticalSpacing`,
83
+ `horizontalSpacing`, `withRowBorders`, `withTableBorder`, per-column `meta.align` and
84
+ `meta.numeral`; `striped` widens from `boolean` to `boolean | 'odd' | 'even'`. `withTableBorder`
85
+ defaults to `true` in basalt, overriding Mantine's `false`; every other one is a conditional
86
+ pass-through, so omitting it keeps Mantine's default.
87
+
88
+ **Porting argo's three tables onto them made them 341 → 370 lines. 29 LONGER.** argo named these
89
+ props as the reason the tables stayed hand-rolled, and adding them shortened nothing: column defs
90
+ cost more than JSX rows when every cell is bespoke — eight accessor blocks at 4–6 lines each against
91
+ an eight-`<Table.Td>` row at ~3. **The ask was mis-specified.** Adopt them for what they buy —
92
+ the `type="native"` footgun, alignment stated once instead of on both `th` and `td` six times in one
93
+ file, and sorting/filtering/pagination no longer consumer-owned — not for a line count. This is the
94
+ counterexample to the band kinds, and the reason the port-before-shipping rule earns its keep.
95
+
96
+ - `maxHeight` (or `minWidth`) renders **`Table.ScrollContainer type="native"`**, and
97
+ `agent/rules/basalt-data.md` now prescribes that same node for a bespoke table — so the blessed
98
+ lane and the escape cannot contradict, and `type="scrollarea"`, which breaks a sticky `thead`, is
99
+ unreachable through the props. (The prop's JSDoc claimed the docs already sanctioned it; they had
100
+ never named `Table.ScrollContainer` at all.)
101
+ - `meta.align` is a `ColumnMeta` module augmentation: a typo'd key is a tsc error, a wrong value
102
+ throws naming the column. `meta.numeral` is read only as `!== false` — an opt-OUT of the
103
+ mono-numeral cell style, never an opt-in.
104
+ - **Not shipped, known:** `emptyState` renders inside a `<td colSpan>` so the header row survives an
105
+ empty table — there is no `emptyState="replace"` mode. No per-column `enableSorting` of basalt's
106
+ own; TanStack's `ColumnDef.enableSorting` still reaches `getCanSort()` and works.
107
+
108
+ ### Four false greens
109
+
110
+ 1. **`check-theme` fabricated a config on the ascend path and passed silently.** From a package with
111
+ no `basalt` key it invented `roots: ["src"]` and reported the invention back under the name
112
+ `basalt.roots`. In `basalt-ui-obsidian`, run from `apps/demo`, that scanned **22 of 44** guarded
113
+ files and made `--audit-allows` report **0 live waivers in a repo carrying 1** — exit 0, no note.
114
+ The audit exists so `0 dead` cannot read as `0 dead anywhere`, and it could be made to say zero
115
+ by standing in the wrong directory. `resolveProjectDir` now ascends to the nearest ancestor
116
+ carrying a basalt project, bounded by the repo root, and announces it in the sentence descend
117
+ already used. **No `.git` above cwd means no ascend**, so a standalone consumer keeps the
118
+ built-in defaults exactly as before. After: 44 files, the real 1.
119
+ 2. **`tokens:css --check` stopped verifying the regeneration command.** 1.23.1 blanked all of line 2
120
+ so a version bump would stop forcing a no-op commit — but line 2 also carries the exact
121
+ invocation line 1 tells the reader to regenerate with. Rewriting `--only core` to `--only all` in
122
+ that line **passed clean**. Only the version token is neutralized now; a line that does not parse
123
+ as a provenance line is compared verbatim, so a deleted or reworded header fails. The success
124
+ message also **parses** the versions instead of asserting them — it used to claim the file "still
125
+ names an older basalt-ui" without reading it, so `0.0.1-nonsense` earned the same sentence.
126
+ 3. **`doctor`'s icons check was unreachable from the only directory where `doctor` exits 0.** On a
127
+ monorepo the root run omitted it with no `⊘ SKIPPED` line — the exact failure mode `SKIPPED` was
128
+ introduced to eliminate — while the app-package run failed on artefacts of standing in a
129
+ non-install package. It resolves the app package off `basalt.roots` now. No `basaltAppPlugin(`
130
+ anywhere and no `public/` is a pass that says so; a plugin call with no `public/` beside it is a
131
+ `⊘ SKIPPED`, which exits non-zero on its own.
132
+ 4. **`SCANNABLE_EXT` gained `.astro`, `.jsx` and `.vue`.** rollhook's marketing site is Astro and its
133
+ two `.astro` templates are its entire markup layer — unguarded, while `check-theme` reported a
134
+ clean 4-file scan. It scans 6 now.
135
+
136
+ **Behaviour change to name: `sync` ascends too.** It shares the resolver, so from a sub-package it
137
+ relocates to the parent install and refreshes it — announced — rather than refusing. It still cannot
138
+ scaffold a second consumer: the refusal is keyed on the RESOLVED project and runs before the
139
+ `basalt.roots` backfill.
140
+
141
+ ### Guard changes the widening exposed
142
+
143
+ - **`raw-hex` no longer matches inside an HTML numeric character reference.** `&#123;` — the escaped
144
+ brace a template writes to show a literal `${…}` in prose — read as the hex colour `#123`. The
145
+ hole was in the KIND, not the extension: the same string produced the same findings in `.html`,
146
+ `.tsx`, `.css` and `.vue`, so `.astro` only walked into it first. The fix is precise, not blanket:
147
+ `HEX` rejects a full reference (`&#`, digits, `;`), so **`color: red&#fff` still flags** and
148
+ nothing is exempted by file type. Every neighbouring raw-text kind was checked and structurally
149
+ cannot share the blind spot — a character reference contains no `(`, and the rest anchor on a
150
+ property name, `var(`, or a JSX `=`.
151
+ - **A fourth guard syntax, `sfc`, for `.astro`/`.vue`.** They used to fall through to the `ts`
152
+ dialect, so `<!-- … -->` was never stripped: a `theme-allow` written in an HTML comment waived
153
+ nothing and a colour inside a commented-out block still reported. `sfc` strips both regions —
154
+ markup first, so an HTML comment holding an unterminated `/*` cannot open one that runs to EOF —
155
+ and keeps the **full 25-kind set**. A `markup` classification would have dropped 22 of them: an
156
+ `.astro` template is JSX-shaped and a `.vue` `<script setup>` is real TS. `.jsx` needs no branch.
157
+ Both the scan and `--audit-allows` now share one `stripGuardComments`, so they cannot disagree
158
+ about what a comment is.
159
+ - **Two limits, asserted rather than left ambiguous, both false-negative-only:** `css-raw-surface`
160
+ does not fire inside a `<style>` fence, and stripping is region-blind, so a `<!--` inside a script
161
+ string over-strips.
162
+ - **A known non-fix, deliberate:** an all-hex URL fragment or SVG reference (`href="#cafe"`,
163
+ `fill="url(#abcdef)"`) still reports. It is text-indistinguishable from a colour, so a fix would
164
+ cost real findings; `theme-allow` is the escape.
165
+ - **The widening ships at `error` with no `GRACE_PERIOD_KINDS` entry, and that table cannot express
166
+ it.** The table is keyed per KIND; widening `SCANNABLE_EXT` widens the file set for all 25 at
167
+ once. An entry for `raw-hex` — the kind that actually fired — would demote basalt's most
168
+ load-bearing kind to `warn` across every `.tsx` and `.css` in all seven consumers to buy runway on
169
+ a file type one consumer has. Measured: rollhook's marketing site scans 6 files with 0 findings,
170
+ and no other consumer holds a single `.astro`, `.vue` or `.jsx` file, so grace would have covered
171
+ zero incumbent violations.
172
+
173
+ ## 1.23.1 — the band-state throw, the tag gate, a CLI that answers
174
+
175
+ **One type widened, nothing removed or renamed.** `BandStripSeries.formatValue` is now
176
+ `(d: T) => string | null`. `null` renders an em dash — an absent READING — which `''` never could:
177
+ `''` is a state whose label is the whole row. Every existing `(d) => string` still typechecks.
178
+
179
+ **A typo'd `BandSpan.state` no longer renders as absence.** A state naming no `series` entry used to
180
+ be skipped, so the band was simply not drawn — and on a measured/not-measured strip a missing band
181
+ is a claim about the data, not an "unknown". A misspelling asserted a reading nobody took, in a mark
182
+ indistinguishable from a real one.
183
+
184
+ | Key comes off | Dev | Production |
185
+ | ------------------------------------------------------------- | ---------------------------------------- | ------------------------------------------------------------------------ |
186
+ | the DATUM — `BandSpan.state`, `marker.state` | throws, naming the key and the valid set | a dashed neutral outline band + an `Unknown state` / `<key>` tooltip row |
187
+ | a PROP — `absentState`, `MirroredBars`' `up.key` / `down.key` | throws | throws |
188
+
189
+ The split is the whole design. `state` comes off data, so a feed that grows a state basalt has never
190
+ seen must degrade rather than take a dashboard down — while a typo, which is the same input, still
191
+ fails loudly wherever it is being written. The dashed neutral outline belongs to no legend entry and
192
+ no state fill, so it cannot read as data. Pane keys are props, never data-driven, so `assertPaneKey`
193
+ throws in every environment: an unresolvable `up.key` used to hide the pane AND its axis, which
194
+ reads as "that half measured zero" — the one thing `MirroredBars` exists to keep apart from absence.
195
+
196
+ **`chart-missing-aria-label` and `unframed-chart` are gated on where the tag came from.** Both key
197
+ on a JSX tag NAME, so a consumer's own 235-line `MirroredBars` — sharing nothing with the shipped
198
+ kind but the name — was told to pass an `ariaLabel` prop it does not accept, by a rule that presents
199
+ as a correctness finding. A tag is now skipped only when the file DEFINES a component of that name
200
+ and does not also import it from `basalt-ui`. Deliberately one-directional: a tag imported from
201
+ basalt-ui, one imported from a consumer barrel that re-exports it, and one the scan cannot attribute
202
+ at all still all fire. Verified old-vs-new over **945 source files across six repos: 0 findings
203
+ lost, 0 gained.**
204
+
205
+ **CLI: `--version` exists, and no subcommand fails open on a flag.** `--version` / `-v` / `version`
206
+ print one bare greppable line and exit 0, resolved BEFORE dispatch so it can never run a command to
207
+ answer "which basalt-ui is this". Six consumer reports reached for it in one round; all six fell
208
+ back to `info --json`.
209
+
210
+ The larger fix sits underneath it. **Every subcommand validates its flags** and exits 1 naming the
211
+ one it does not accept — `doctor --json` used to run doctor and exit **0**, and `check-theme
212
+ --audit-allow` scanned and reported success. An unknown COMMAND now says so above the usage block
213
+ instead of dumping help and letting the dump read like a choice.
214
+
215
+ - **`doctor` reads `basaltAppPlugin({ icons })`** out of the consumer's vite config instead of
216
+ hardcoding six filenames, so adopting 1.23.0's icons array stops producing a warning to generate
217
+ five files you deliberately lack. A named array is checked against itself (an icon missing from
218
+ `public/` still warns); an unparseable or absent config falls back to the six-filename check. It
219
+ can only narrow, never blind.
220
+ - **`doctor` and `sync` share one sentence about a parent install** (`parentInstallAdvice`). Run
221
+ from a package whose install is above it, `doctor` now names the parent and says `basalt-ui init`
222
+ is NOT the fix. Following its old advice literally scaffolded the second consumer that 1.22.0
223
+ exists to prevent.
224
+ - **Every seeded invocation resolves the local bin** (`basaltBinCommand`, overridable via
225
+ `BASALT_BIN`): the `lint` script, the CI steps, the `.claude` PreToolUse hook and doctor's own
226
+ advice all render `./node_modules/.bin/basalt-ui`. `bunx` does not re-resolve a package it has
227
+ cached — that is what made a round-7 report file a P0 against a 1.20.0 cache while believing it
228
+ was on 1.22.0, and the seed was shipping `bunx` into consumer CI in ten places.
229
+ `configs/lefthook.yml`'s `${BASALT_BIN:-bunx --no-install basalt-ui}` default is unchanged and
230
+ deliberate: `--no-install` fails loudly instead of downloading a stranger.
231
+ - **`tokens:css --check` blanks the provenance line (line 2) before comparing**, so a version bump
232
+ alone no longer forces a no-op commit in a tokens-only consumer, where the gate is byte-equality.
233
+ A stale provenance line is now a note on an otherwise-passing check. The `@generated` header is
234
+ still emitted byte-identical — the line stays, it just stops gating. **Superseded in `Unreleased`:
235
+ blanking the WHOLE line also stopped gating the regeneration command it carries. Only the version
236
+ token is neutralized now.**
237
+
238
+ ### Corrections to the record
239
+
240
+ - **All six round-8 reports said `basalt-ui --version` "exits 0 printing usage". It exited 1, to
241
+ stderr** — the dispatcher's `default:` branch has always been `console.error(USAGE); return 1`.
242
+ The real fail-open was an unknown FLAG, which no report tested, and the misdiagnosis was relayed
243
+ verbatim into the fix brief. Report the symptom you measured, not the one you inferred from it.
244
+ - **`cb4e5b7`'s message is wrong** (`b9b99a6` on `origin/feat/round-7-band-kinds` is the same commit
245
+ pre-merge), and it is on `master` where it cannot be rewritten. It claims it taught
246
+ `unframed-chart` the two new kinds; it widened `CHART_ENTRY_POINT_TAG`, which only
247
+ `chart-missing-aria-label` reads. `unframed-chart` keys on `<ChartLegend items={[` and carries no
248
+ kind list at all — there was never an asymmetric pair to fix.
249
+ - **Open question, not a plan.** The import gate does not make `CHART_ENTRY_POINT_TAG` or the oxlint
250
+ plugin's `CHART_TAGS` redundant: that list still answers _which_ tags owe an `ariaLabel`. The gate
251
+ only converts a kind missing from the list from a false positive into an under-report. Collapsing
252
+ the two lists is a separate, larger change and has not been made.
253
+
254
+ ## 1.23.0 — two band kinds, an x-tick seam, CLI resolution
255
+
42
256
  **No export removed or renamed; six new runtime exports and nine new types.** Nothing you wrote
43
257
  changed. Every entry is additive, and the batch runs the other way from the last three: those made
44
258
  the guard stricter, this one makes the framework more expressive.
@@ -72,7 +286,9 @@ unchanged and still works; omit both and ticks are chosen to fit (`smartTicks`),
72
286
  Reach for it on a dense time axis: the tick choosers append the final key unconditionally, so a
73
287
  COUNT that does not land on the last index paints two labels on top of each other at the right edge
74
288
  — at every count, not at an unlucky one. On the consumer that reported it, the local tick helper
75
- went 200 → 160 lines; it shrinks, it does not disappear.
289
+ went 200 → 170 lines; it shrinks, it does not disappear. (The promise was 160. The 10-line miss is
290
+ docblock — the port's own JSDoc grew, because the helper stopped being a fallback and became the one
291
+ seam every chart on the page passes through.)
76
292
 
77
293
  **`basaltAppPlugin`'s `icons` takes an array now.**
78
294
 
@@ -147,8 +363,8 @@ install it found above instead. The refusal runs before the `basalt.roots` backf
147
363
  the damage. Two ways to unblock it:
148
364
 
149
365
  ```bash
150
- cd <the package holding .basalt/manifest.json> && bunx basalt-ui sync
151
- BASALT_CWD=<that package> bunx basalt-ui sync # or from anywhere
366
+ cd <the package holding .basalt/manifest.json> && ./node_modules/.bin/basalt-ui sync
367
+ BASALT_CWD=<that package> ./node_modules/.bin/basalt-ui sync # or from anywhere
152
368
  ```
153
369
 
154
370
  The summary line gained a `created` counter. **`recreated` now means what it always claimed** — the