basalt-ui 1.21.0 → 1.22.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/AGENTS.md +3 -1
- package/CHANGELOG.md +14 -0
- package/MIGRATING.md +93 -10
- package/README.md +20 -7
- package/agent/rules/basalt-app.md +15 -9
- package/agent/rules/basalt-charts.md +2 -2
- package/agent/rules/basalt-router.md +1 -1
- package/agent/rules/basalt-state.md +4 -4
- package/agent/rules/basalt-tokens.md +9 -4
- package/agent/skills/basalt-app/SKILL.md +4 -3
- package/agent/skills/basalt-charts/SKILL.md +1 -1
- package/configs/oxlint-plugin.js +37 -1
- package/configs/oxlint-plugin.test.ts +116 -0
- package/dist/cli/index.d.ts +7 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +230 -42
- package/dist/guard/index.d.ts +67 -0
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +40 -2
- package/dist/vite.d.ts +4 -1
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +21 -13
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/src/cli/check-theme.test.ts +35 -0
- package/src/cli/help.test.ts +5 -1
- package/src/cli/index.ts +434 -68
- package/src/cli/toolchain-wiring.test.ts +192 -3
- package/src/cli/waiver-audit.test.ts +84 -4
- package/src/guard/check-source.test.ts +197 -0
- package/src/guard/index.ts +129 -6
- package/src/vite.test.ts +33 -0
- package/src/vite.ts +28 -14
package/AGENTS.md
CHANGED
|
@@ -87,7 +87,9 @@ over the declaration it terminates. Whole-file scope is `theme-allow-file <id>
|
|
|
87
87
|
`theme-allow-file` waives nothing. A bare `theme-allow` still waives everything on its placement but
|
|
88
88
|
reports `theme-allow-unscoped`. A word in the id slot that names no rule waives nothing.
|
|
89
89
|
`.json`/`.webmanifest` use a `"basalt:theme-allow[-file]"` member. Audit them all with
|
|
90
|
-
`basalt-ui check-theme --audit-allows`, which exits 1 on a waiver that suppresses nothing.
|
|
90
|
+
`basalt-ui check-theme --audit-allows`, which exits 1 on a waiver that suppresses nothing. It judges
|
|
91
|
+
plugin-rule waivers too (since 1.21.1), by re-running oxlint — so it needs oxlint reachable;
|
|
92
|
+
otherwise the verdict is "cannot judge", never "dead".
|
|
91
93
|
|
|
92
94
|
> **Framework-internal only** — `bunx basalt-ui check-coverage` is a self-consistency gate for the
|
|
93
95
|
> basalt-ui repo itself (asserts SURFACES ↔ rule files ↔ skill files ↔ package.json exports). It
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [1.22.0](https://github.com/jkrumm/basalt-ui/compare/v1.21.0...v1.22.0) (2026-08-22)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* let `icons: false` reach the manifest it was only skipping in the head ([58abe27](https://github.com/jkrumm/basalt-ui/commit/58abe27be10e5baf7c4659aa79904a7646ff092d))
|
|
7
|
+
* scope the shadow-export tripwire to basalt consumers, and to components ([60716d4](https://github.com/jkrumm/basalt-ui/commit/60716d4ba00908c1a157f5568e192c6c50e438ec))
|
|
8
|
+
* stop sync scaffolding a second consumer, and give the audit the oxlint half ([45cf3d3](https://github.com/jkrumm/basalt-ui/commit/45cf3d340f081e61f3f10f852d12f79928993d2a))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* give the waiver audit an annotation reader, and fix two shapes ([0dde850](https://github.com/jkrumm/basalt-ui/commit/0dde85052269780a61ead0688005db16e24f009a))
|
|
14
|
+
|
|
1
15
|
# [1.21.0](https://github.com/jkrumm/basalt-ui/compare/v1.20.0...v1.21.0) (2026-08-22)
|
|
2
16
|
|
|
3
17
|
|
package/MIGRATING.md
CHANGED
|
@@ -8,9 +8,9 @@ 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.
|
|
12
|
-
written against source, not against
|
|
13
|
-
disagree.
|
|
11
|
+
row that was wrong. That pass corrected 4 table rows and 3 prose claims. The 1.21.0 and 1.21.1
|
|
12
|
+
sections were written against source, not against their commit messages. Check the types, not this
|
|
13
|
+
table, if the two disagree.
|
|
14
14
|
|
|
15
15
|
**No majors, by policy.** A rename or a removal ships as a plain `feat:` on the 1.x line, so a minor
|
|
16
16
|
bump can require code changes. Skipping several at once is the expensive case — read every section
|
|
@@ -20,12 +20,95 @@ between your version and the target.
|
|
|
20
20
|
> `## [1.` finds only the patches.
|
|
21
21
|
|
|
22
22
|
**Minors with no public API delta:** 1.1.0, 1.3.0, 1.4.0, 1.5.0, 1.6.0, 1.7.0, 1.8.0, 1.9.0,
|
|
23
|
-
1.10.0, 1.13.0, 1.14.0, 1.16.0, 1.18.0 — and every patch. Additive-only subpaths: `./tokens.css`
|
|
23
|
+
1.10.0, 1.13.0, 1.14.0, 1.16.0, 1.18.0 — and every patch except 1.21.1. Additive-only subpaths: `./tokens.css`
|
|
24
24
|
at 1.3.0, `./agent-chat` at 1.10.0.
|
|
25
25
|
|
|
26
26
|
---
|
|
27
27
|
|
|
28
|
-
## 1.
|
|
28
|
+
## 1.21.1 — the toolchain stops overclaiming
|
|
29
|
+
|
|
30
|
+
**No export removed or renamed; four added.** Nothing you wrote changed. Every entry below is a
|
|
31
|
+
check that reported an answer it had not earned, so a green CI on 1.21.0 was, for several of them,
|
|
32
|
+
green over an empty set.
|
|
33
|
+
|
|
34
|
+
**`sync` refuses where it used to scaffold.** Run from a sub-package that depends on basalt-ui but
|
|
35
|
+
holds no `.basalt/manifest.json` — `apps/dashboard`, `apps/web` — it printed `0 updated, 20
|
|
36
|
+
recreated`, wrote a second `basalt` key, and stood up a complete competing install beside the real
|
|
37
|
+
one at the repo root. argo and rb both reverted it by hand. It now resolves its project exactly as
|
|
38
|
+
`check-theme` and `doctor` do, and **exits 1** when the resolved project has no manifest, naming the
|
|
39
|
+
install it found above instead. The refusal runs before the `basalt.roots` backfill, which was half
|
|
40
|
+
the damage. Two ways to unblock it:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
cd <the package holding .basalt/manifest.json> && bunx basalt-ui sync
|
|
44
|
+
BASALT_CWD=<that package> bunx basalt-ui sync # or from anywhere
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The summary line gained a `created` counter. **`recreated` now means what it always claimed** — the
|
|
48
|
+
ledger placed that file once and it went missing. Twenty first-time writes were never that.
|
|
49
|
+
|
|
50
|
+
**`--audit-allows` judges plugin-rule annotations.** They used to print "not a check-theme kind" and
|
|
51
|
+
drop out of the exit-1 gate, so the gate covered an empty set wherever the waivers were plugin
|
|
52
|
+
rules: argo's whole tally was `0 live, 0 dead, 8 outside reach`, and 11 of linewatch's 14 went the
|
|
53
|
+
same way. Each is now probed by re-running oxlint over that one file with the annotation
|
|
54
|
+
neutralized. Judged now: argo 8 of 8, linewatch 14 of 14, basalt's own tree 23 of 23.
|
|
55
|
+
|
|
56
|
+
**It requires oxlint to be reachable.** The probe writes one neutralized sibling file and re-runs
|
|
57
|
+
oxlint over it (oxlint has no stdin mode), removing it in a `finally`. Where oxlint cannot run, the
|
|
58
|
+
annotation is reported as **"cannot judge"**, never as dead. The report now also prints the scope it
|
|
59
|
+
audited — `0 dead` over `basalt.roots` was reading as `0 dead anywhere`.
|
|
60
|
+
|
|
61
|
+
**`doctor`'s `lefthook-preset` check asks a different question.** It tested whether the config text
|
|
62
|
+
contained the `extends` string. It now asks whether the gate EXISTS, via `lefthook dump`, which
|
|
63
|
+
resolves `extends`, `include` and per-command `root:`. linewatch wires all three jobs with
|
|
64
|
+
`root: 'web/'` precisely because `extends` merges commands _without_ their working directory — it
|
|
65
|
+
was correctly configured, got warned, and the old advice would have broken it. Three outcomes: a
|
|
66
|
+
broken `extends` target is still a hard fail, a provably absent gate is a warn, and where
|
|
67
|
+
`lefthook dump` could not run you get an advisory warning naming what it could not see.
|
|
68
|
+
|
|
69
|
+
**`basalt/shadow-basalt-export` narrowed twice.** It now gates on `isBasaltScopedFile` like every
|
|
70
|
+
other rule in the file, and needs a component-SHAPED declaration — a function, an arrow, a
|
|
71
|
+
`memo`/`forwardRef` wrapper, or a class extending one. It had been firing on a `SlugTracker` class
|
|
72
|
+
in a React-free package carrying no basalt-ui dependency, telling it to import from basalt-ui. The
|
|
73
|
+
stated limit is unchanged: exact-name-only, a tripwire, not coverage.
|
|
74
|
+
|
|
75
|
+
**`basaltAppPlugin({ icons: false })` now omits the manifest's `icons` member too.** It skipped the
|
|
76
|
+
head `<link>` icons and emitted the two PNG manifest entries anyway, so `{ manifest: true, icons:
|
|
77
|
+
false }` shipped an installable app pointing at two 404s. **If you went hybrid over this** — the
|
|
78
|
+
plugin for the head, a hand-written manifest for the rest — **you can drop the hand-written half.**
|
|
79
|
+
The option's JSDoc said "skips the head `<link>` icons"; it now says what the option does.
|
|
80
|
+
|
|
81
|
+
**Two `theme-allow` comment shapes were silently broken and now work.** Plainly: this is the fourth
|
|
82
|
+
hole found in this one contract in three rounds. The full thirteen-shape matrix is now pinned in
|
|
83
|
+
both halves — `src/guard/check-source.test.ts` and `configs/oxlint-plugin.test.ts` — so the two
|
|
84
|
+
parsers can no longer disagree about what an annotation IS.
|
|
85
|
+
|
|
86
|
+
| Shape | What it did | Now |
|
|
87
|
+
| --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- |
|
|
88
|
+
| `/** theme-allow <id> — <why> */` on one line | waived under oxlint, reported under `check-theme` | both halves honour it |
|
|
89
|
+
| `{/*` + token on the next line + `*/}` | comment-stripping left a bare `}`, so the line read as CODE and the annotation was classified trailing — scoped to a brace | scoped to the node below it, as written |
|
|
90
|
+
|
|
91
|
+
**linewatch writes that wrapped shape for every hand-composed chart axis**, so any _guard_ kind
|
|
92
|
+
annotated that way was silently unwaivable. It only appeared to work because the rules it names
|
|
93
|
+
(`hand-rolled-plot`) live in the oxlint plugin, whose placement test is comment-node-based and never
|
|
94
|
+
saw the brace.
|
|
95
|
+
|
|
96
|
+
**New, additive — `./guard` gains four runtime exports**, the reader half of the audit:
|
|
97
|
+
|
|
98
|
+
| Export | Note |
|
|
99
|
+
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
100
|
+
| `findAllowAnnotations(text, relPath, cfg)` | every annotation as written, ids split into `guardKinds` / `pluginRules` / `unknownRules`. Shares `collectAllowAnnotations` with `checkSource`, so it cannot list a line the scan does not honour |
|
|
101
|
+
| `neutralizeAllowAnnotation(text, line, cfg)` | one annotation rewritten to the token below, every other left intact — the probe half of an audit |
|
|
102
|
+
| `NEUTRALIZED_ALLOW_TOKEN` | so a guard probe and an oxlint probe neutralize identically |
|
|
103
|
+
| `PLUGIN_RULE_IDS` | the ids only oxlint can judge |
|
|
104
|
+
|
|
105
|
+
The `AllowAnnotationSite` type ships alongside them.
|
|
106
|
+
|
|
107
|
+
**Known gaps, reported and not fixed:** `--audit-allows` says nothing about `basalt.exempt` — a
|
|
108
|
+
whole file removed from the scan, the broadest exception the config surface has — and its
|
|
109
|
+
`scoped to …` line does not distinguish `theme-allow` from `theme-allow-file`.
|
|
110
|
+
|
|
111
|
+
## 1.21.0 — the `theme-allow` grammar
|
|
29
112
|
|
|
30
113
|
**No export removed or renamed.** One break, and it is in the escape hatch itself: **file scope must
|
|
31
114
|
now be spelled `theme-allow-file`.** At 1.20.0 an annotation that named a rule and gave a reason —
|
|
@@ -73,7 +156,7 @@ manifest, `basaltAppPlugin` is the first remedy, since a hand-copied hex drifts
|
|
|
73
156
|
| Change | What to do |
|
|
74
157
|
| -------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
75
158
|
| `createSearchParamStore` / `createMultiSearchParamStore` gain `linkSearch` | replace every nav link's `search: { <param>: '<literal>' }` with `search: <store>.linkSearch`, passed BY REFERENCE. A module-scope literal pins the fallback on every click — argo's reader had ZERO call sites, so "remember my window" had never worked |
|
|
76
|
-
| `BasaltShell` collapse moves to `createPersistedState` | the key is now `basalt:<storageKey>` holding `{ v, value }`; read it with `readPersistedValue(storageKey, 1)`. A one-time migration adopts the raw pre-1.
|
|
159
|
+
| `BasaltShell` collapse moves to `createPersistedState` | the key is now `basalt:<storageKey>` holding `{ v, value }`; read it with `readPersistedValue(storageKey, 1)`. A one-time migration adopts the raw pre-1.21.0 value, so the sidebar does not re-expand on upgrade |
|
|
77
160
|
|
|
78
161
|
**New, additive:**
|
|
79
162
|
|
|
@@ -157,7 +240,7 @@ the rule AND giving a reason is what `hasFileDeclaration` matches, at any line
|
|
|
157
240
|
intended for one node silences the file. Dropping the reason keeps it node-scoped in the oxlint
|
|
158
241
|
plugin, but then `check-theme` reports `theme-allow-unscoped ("no reason")`. The two halves of the
|
|
159
242
|
contract intersect at exactly one legal shape and that shape is whole-file. Write the declaration in
|
|
160
|
-
the component's docblock, where it reads as the file-level decision it is. **Fixed in 1.
|
|
243
|
+
the component's docblock, where it reads as the file-level decision it is. **Fixed in 1.21.0** —
|
|
161
244
|
see that section; the declaration moves to `theme-allow-file`.
|
|
162
245
|
|
|
163
246
|
**`doctor` will go red.** `SKIPPED` is a third outcome beside pass/warn/fail and exits non-zero on
|
|
@@ -293,9 +376,9 @@ deleting an entry forces the level flip in the same commit.
|
|
|
293
376
|
| `basalt/agent-no-raw-usechat` | 1.10.0 as `warn` | **1.13.0** |
|
|
294
377
|
| `basalt/agent-resume-guard` | 1.10.0 as `warn` | **1.13.0** |
|
|
295
378
|
| `basalt/raw-size-literal` | 1.7.0 as `warn` | **1.20.0** |
|
|
296
|
-
| `basalt/hand-rolled-plot` | 1.15.0 as `warn` | still `warn` — widened again at 1.
|
|
379
|
+
| `basalt/hand-rolled-plot` | 1.15.0 as `warn` | still `warn` — widened again at 1.21.0; grace restarts with the widening |
|
|
297
380
|
| `basalt/chart-legend-literal` | 1.15.0 as `warn` | still `warn` — widened at 1.20.0 |
|
|
298
|
-
| `basalt/shadow-basalt-export` | 1.20.0 as `warn` | may stay `warn` permanently — widened at 1.
|
|
381
|
+
| `basalt/shadow-basalt-export` | 1.20.0 as `warn` | may stay `warn` permanently — widened at 1.21.0, narrowed at 1.21.1 |
|
|
299
382
|
| `basalt/hand-rolled-shell` | 1.20.0 as `warn` | — |
|
|
300
383
|
|
|
301
384
|
`card-with-border`, `inline-display`, `raw-html-layout`, `raw-form-control`, `raw-font-family` and
|
|
@@ -308,7 +391,7 @@ in the shipped preset). A config still naming `import-boundary` after 1.1.0 disa
|
|
|
308
391
|
|
|
309
392
|
## Deprecated, not yet removed
|
|
310
393
|
|
|
311
|
-
The 32 camelCase `--vx-*` aliases deprecated in 1.5.0 are **still emitted at 1.
|
|
394
|
+
The 32 camelCase `--vx-*` aliases deprecated in 1.5.0 are **still emitted at 1.21.0**.
|
|
312
395
|
`buildPaletteCss({ legacyAliases: false })` / `tokens:css --no-legacy-aliases` opts out now; a later
|
|
313
396
|
minor flips the default.
|
|
314
397
|
|
package/README.md
CHANGED
|
@@ -224,7 +224,7 @@ Other keys (`exempt`, `severity`, `spacingSteps`, `forbiddenAccents`, …) are d
|
|
|
224
224
|
**`exemptRules`** turns one kind off along a path. A pattern is a relative path
|
|
225
225
|
(`public/site.webmanifest`, or `src/agent` for everything under it), a glob (`*` stops at `/`, `**`
|
|
226
226
|
does not, and a slash-free glob is also tried against the basename, so `*.module.css` works), or a
|
|
227
|
-
bare path segment — the legacy shape, and the one nobody guesses, since before 1.
|
|
227
|
+
bare path segment — the legacy shape, and the one nobody guesses, since before 1.21.0 it was the
|
|
228
228
|
ONLY shape and a real relative path matched nothing and said nothing. The object form records the
|
|
229
229
|
reason, which is what a `theme-allow` always carried and this key could not. A pattern that
|
|
230
230
|
suppresses nothing is now reported by a normal run and fails `--audit-allows`.
|
|
@@ -242,7 +242,7 @@ stylesheet `tokens:css` just wrote is not reported back at you, pasting the mark
|
|
|
242
242
|
suppresses nothing, and an ordinary declaration smuggled into a sheet wearing the header is
|
|
243
243
|
reported on its own line.
|
|
244
244
|
|
|
245
|
-
**The escape hatch is scoped, and since 1.
|
|
245
|
+
**The escape hatch is scoped, and since 1.21.0 the two scopes are spelled apart:**
|
|
246
246
|
|
|
247
247
|
```text
|
|
248
248
|
theme-allow → this node/line, EVERY rule (reports theme-allow-unscoped)
|
|
@@ -252,7 +252,7 @@ theme-allow-file <id>[, <id>…] — <why> → the WHOLE FILE, those rules
|
|
|
252
252
|
```
|
|
253
253
|
|
|
254
254
|
An annotation must **start** its comment (after `//`, `/*`, `<!--`, a block gutter `*`, or
|
|
255
|
-
whitespace) — before 1.
|
|
255
|
+
whitespace) — before 1.21.0 any comment merely mentioning the token parsed as the bare blanket form,
|
|
256
256
|
so a file documenting its own waivers disarmed itself. Placements: the reported line, a comment-only
|
|
257
257
|
line directly above it (the only form JSX can express, and it reaches the first CODE line below,
|
|
258
258
|
walking through the rest of its comment block), or — in CSS — a trailing comment reaching back over
|
|
@@ -260,8 +260,13 @@ the declaration it terminates. The id slot is read strictly: a word there that n
|
|
|
260
260
|
nothing, so a typo is never the blanket form.
|
|
261
261
|
|
|
262
262
|
`basalt-ui check-theme --audit-allows` reports every annotation and every `exemptRules` entry with
|
|
263
|
-
what it still suppresses, proved by re-running the
|
|
264
|
-
|
|
263
|
+
what it still suppresses, proved by re-running the scan with that one waiver neutralized. Exits 1 on
|
|
264
|
+
a dead waiver, so it is usable as a CI gate. Since 1.21.1 it runs **both** halves — `checkSource` for
|
|
265
|
+
a guard kind, oxlint over one neutralized sibling file for a plugin rule — so a waiver naming
|
|
266
|
+
`hand-rolled-plot` is judged rather than skipped. **It needs oxlint reachable**; where it is not, the
|
|
267
|
+
verdict is "cannot judge", never "dead". The report prints the scope it audited, because `0 dead`
|
|
268
|
+
over `basalt.roots` is not `0 dead anywhere`. Two known gaps: `basalt.exempt` is not audited at all,
|
|
269
|
+
and the `scoped to …` line does not distinguish `theme-allow` from `theme-allow-file`.
|
|
265
270
|
|
|
266
271
|
`doctor`'s `ai-major-parity` hard check fails a monorepo where workspace packages declare different
|
|
267
272
|
`ai` package majors — unless the split is intentional and written down. A producer pinned to an
|
|
@@ -294,7 +299,7 @@ deleted rather than silently surviving into the next real skew.
|
|
|
294
299
|
| `./tokens` | **free** | `VX` token refs, `buildPaletteCss`, `defineSeries`, `seriesTokens`, `groupTokens`, `alpha`, `ColorPair` / `SeriesMap` types |
|
|
295
300
|
| `./theme-lab` | coupled | `ThemeLabControls`, `applyOverrides`, `loadOverrides`, `COLOR_GROUPS` for live theme inspection |
|
|
296
301
|
| `./vite` | — | `basaltViteConfig(opts)` — Vite preset for basalt-ui consumer apps; `basaltAppPlugin(opts)` — PWA head, manifest, and icon metadata derived from the token palette |
|
|
297
|
-
| `./guard` | **free** | `checkSource`, `GUARD_RULES`, `Finding` types — the headless theme-guard core
|
|
302
|
+
| `./guard` | **free** | `checkSource`, `GUARD_RULES`, `Finding` types — the headless theme-guard core; plus the annotation reader `--audit-allows` is built on: `findAllowAnnotations`, `neutralizeAllowAnnotation`, `NEUTRALIZED_ALLOW_TOKEN`, `PLUGIN_RULE_IDS`, `AllowAnnotationSite` |
|
|
298
303
|
| `./query` | **free** | `createBasaltQueryClient`, transport-agnostic `unwrap`, lazy `BasaltQueryDevtools` |
|
|
299
304
|
| `./router-tanstack` | **free** | TanStack Router bridge: `useBasaltNav` (active route) + `useRouterBreadcrumbs` |
|
|
300
305
|
| `./forms` | coupled | Mantine form adapter: `useBasaltForm`, `field`, `FormErrorSummary`, `useFormDraft` (Standard Schema) |
|
|
@@ -689,7 +694,15 @@ honour `BASALT_CWD`, and relocate to the single workspace package carrying a bas
|
|
|
689
694
|
invoked from a repo root that has none (two candidates is reported as ambiguous, never guessed).
|
|
690
695
|
`tokens:css` and `fonts:css` both take `--check` as a CI drift gate.
|
|
691
696
|
|
|
692
|
-
`sync` strategy per file: unchanged since last write → overwrite; locally edited → skip (show diff);
|
|
697
|
+
`sync` strategy per file: unchanged since last write → overwrite; locally edited → skip (show diff);
|
|
698
|
+
missing → recreate. `--force` overwrites local edits. The summary counts `created` and `recreated`
|
|
699
|
+
apart: `recreated` means the ledger placed that file once and it went missing.
|
|
700
|
+
|
|
701
|
+
**`sync` refreshes an existing install; it does not create one.** It resolves its project exactly as
|
|
702
|
+
`check-theme` and `doctor` do, and since 1.21.1 **exits 1** when the resolved project has no
|
|
703
|
+
`.basalt/manifest.json`, naming the install it found above instead of scaffolding a second one
|
|
704
|
+
beside it. Run it at the package that holds the manifest, or point `BASALT_CWD` at that package.
|
|
705
|
+
Creating an install is `basalt-ui init`'s decision.
|
|
693
706
|
|
|
694
707
|
---
|
|
695
708
|
|
|
@@ -98,12 +98,17 @@ Run `basalt-ui doctor` afterwards to confirm the wiring took. A check that canno
|
|
|
98
98
|
|
|
99
99
|
### The lefthook preset overrides YOU, not the other way round
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
101
|
+
`doctor`'s `lefthook-preset` check asks whether a pre-commit gate EXISTS — not whether your config
|
|
102
|
+
contains the extends string. Since 1.21.1 it asks `lefthook dump`, which resolves `extends`,
|
|
103
|
+
`include` and per-command `root:`. **Wiring the jobs yourself passes**: linewatch runs all three with
|
|
104
|
+
`root: 'web/'` precisely because `extends` merges commands _without_ their working directory, and the
|
|
105
|
+
old text match warned there and prescribed a change that would have broken it.
|
|
106
|
+
|
|
107
|
+
A broken `extends` target is still a hard fail — lefthook merges a missing target into **zero**
|
|
108
|
+
commands and exits 0, so `lefthook dump` looks clean and the repo has no gate with nothing saying so.
|
|
109
|
+
A provably absent gate is a warn; a repo where `lefthook dump` could not run gets an advisory warning
|
|
110
|
+
naming what it could not see. n/a with no lefthook at all. `sync` reports the same seam, since it is
|
|
111
|
+
the command an upgrade actually runs and neither file is one basalt owns.
|
|
107
112
|
|
|
108
113
|
**An `extends` target WINS on a colliding key.** Declare `pre-commit.commands.oxfmt.run` (or
|
|
109
114
|
`glob:`) in your own file and the preset's value is what runs — yours is discarded silently. Only
|
|
@@ -160,7 +165,7 @@ site-wide OG/Twitter defaults from `options.seo`.
|
|
|
160
165
|
plugins: [react(), ...basaltAppPlugin({ name: 'Argo', colorScheme: 'auto' })]
|
|
161
166
|
```
|
|
162
167
|
|
|
163
|
-
Before 1.
|
|
168
|
+
Before 1.21.0 the anti-FOUC rule emitted `html{color-scheme:dark}` **unlayered**, which outranks
|
|
164
169
|
Mantine's own `@layer mantine` declaration regardless of specificity — a light-scheme consumer got
|
|
165
170
|
dark scrollbars, dark `<select>` popups and dark date pickers permanently, with no opt-out. The rule
|
|
166
171
|
is now scoped to `html:not([data-mantine-color-scheme])`, so it expires the instant MantineProvider
|
|
@@ -191,8 +196,9 @@ source image — it lives in the CONSUMER's own devDependencies, not basalt-ui's
|
|
|
191
196
|
warns (does not fail) when `public/` exists but is missing one of these files.
|
|
192
197
|
|
|
193
198
|
`basaltAppPlugin` also emits `site.webmanifest` (served in dev too) with explicit `id`/`scope`/
|
|
194
|
-
`start_url` — pass `manifest: false` to skip it, or `icons: false` to skip the
|
|
195
|
-
|
|
199
|
+
`start_url` — pass `manifest: false` to skip it, or `icons: false` to skip the icons. Since 1.21.1
|
|
200
|
+
`icons: false` omits BOTH the head `<link>` tags and the manifest's `icons` member; before that it
|
|
201
|
+
skipped only the head, so a manifest shipped naming two PNGs the app never builds.
|
|
196
202
|
|
|
197
203
|
## Service worker (opt-in)
|
|
198
204
|
|
|
@@ -60,7 +60,7 @@ radial/matrix shape, is not optional — two `basalt` oxlint plugin rules
|
|
|
60
60
|
file went unpoliced. To waive ONE node, put `theme-allow hand-rolled-plot — <why>` on it (or on a
|
|
61
61
|
comment-only line above it). A genuinely non-single-plot shape (multi-pane, radial, matrix)
|
|
62
62
|
declares the whole file with `theme-allow-file hand-rolled-plot — two panes over one x scale`,
|
|
63
|
-
anywhere in it; `DualPanel` carries the repo's only one. **`theme-allow-file` is the 1.
|
|
63
|
+
anywhere in it; `DualPanel` carries the repo's only one. **`theme-allow-file` is the 1.21.0
|
|
64
64
|
spelling** — at 1.20.0 a node-scoped annotation was silently promoted to a file declaration, so
|
|
65
65
|
per-node scoping was not expressible; move the one word. The file that DEFINES `CartesianChart` is
|
|
66
66
|
exempt definitionally (detected by declaration, not by path), since a rule saying "compose X"
|
|
@@ -95,7 +95,7 @@ ships no map kind.
|
|
|
95
95
|
- **Want `@visx/geo` anyway?** Install it yourself — basalt doesn't pin it — and keep it under a
|
|
96
96
|
`charts/` segment like every other visx import, same as any bespoke chart.
|
|
97
97
|
- **Style map chrome with `--vx-*` tokens** (`VX.*`/`alpha()`) so it matches the rest of the app.
|
|
98
|
-
- **The guard trap, narrowed at 1.
|
|
98
|
+
- **The guard trap, narrowed at 1.21.0:** `check-theme`'s `inline-spacing` kind is a per-line regex
|
|
99
99
|
over raw source, not an AST pass. A **unitless** number now only reports inside a style-object
|
|
100
100
|
context (a `style=`/`sx=`/`css=` attribute, or a const whose name or type says styles), so a map's
|
|
101
101
|
pixel geometry — `fitBounds({ padding: 48 })`, marker offsets — no longer trips it: an options bag
|
|
@@ -83,7 +83,7 @@ against it.
|
|
|
83
83
|
- Update via `navigate({ search: { window: 'all', from: undefined, to: undefined } })` — prefer plain
|
|
84
84
|
objects over reducers to avoid `| undefined` assignability errors when the schema has defaults.
|
|
85
85
|
|
|
86
|
-
### Wire the reader, or the store does nothing (1.
|
|
86
|
+
### Wire the reader, or the store does nothing (1.21.0)
|
|
87
87
|
|
|
88
88
|
`validateSearch` is only half the store. It restores the value when you arrive with **no** param —
|
|
89
89
|
it cannot do anything about a nav link that declares the param itself, and a link written
|
|
@@ -25,7 +25,7 @@ Pick the right home for each kind of state — don't dump everything into one st
|
|
|
25
25
|
- **UI preferences that must survive navigation but aren't URL-worthy** (sidebar collapsed, panel
|
|
26
26
|
layout, draft filters) → `createPersistedState` from `basalt-ui/state` — the framework's own
|
|
27
27
|
versioned localStorage primitive (see below). `BasaltShell` persists its own collapse state
|
|
28
|
-
through that same primitive since 1.
|
|
28
|
+
through that same primitive since 1.21.0; use `createPersistedState` for app-level preferences.
|
|
29
29
|
Reach for a third-party store only when complex cross-component state genuinely warrants it (see
|
|
30
30
|
escape hatch below).
|
|
31
31
|
|
|
@@ -67,7 +67,7 @@ const [layout, setLayout] = usePanelLayout()
|
|
|
67
67
|
Keys are namespaced `basalt:<key>` automatically and never collide with the theme-scheme guard.
|
|
68
68
|
Pass a `schema` (Standard Schema) to validate persisted values and fall back to `initial` on mismatch.
|
|
69
69
|
|
|
70
|
-
### Mirroring `BasaltShell`'s collapse (changed key at 1.
|
|
70
|
+
### Mirroring `BasaltShell`'s collapse (changed key at 1.21.0)
|
|
71
71
|
|
|
72
72
|
`BasaltShell` used `@mantine/hooks`' `useLocalStorage` while `createPersistedState` was the
|
|
73
73
|
documented house API — so a consumer reading the raw key was complying with the shipped component,
|
|
@@ -77,11 +77,11 @@ moves the key:
|
|
|
77
77
|
```ts
|
|
78
78
|
import { readPersistedValue } from 'basalt-ui/state'
|
|
79
79
|
|
|
80
|
-
// 1.
|
|
80
|
+
// 1.21.0: localStorage['basalt:<storageKey>'] === '{"v":1,"value":true}'
|
|
81
81
|
const collapsed = readPersistedValue('basalt-sidebar-collapsed', 1)
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
-
A one-time migration adopts the raw pre-1.
|
|
84
|
+
A one-time migration adopts the raw pre-1.21.0 value, so an upgrade does not silently re-expand
|
|
85
85
|
every sidebar. It is a bridge, not a format — do not write against it.
|
|
86
86
|
|
|
87
87
|
## Zustand escape hatch (complex cross-component stores)
|
|
@@ -43,7 +43,7 @@ The scan reaches slightly beyond `roots`: each root's PARENT contributes its `in
|
|
|
43
43
|
meta and a webmanifest's `background_color` actually live. `.json` is never blanket-scanned — name a
|
|
44
44
|
design-surface JSON in `basalt.include`, the only route to one.
|
|
45
45
|
|
|
46
|
-
## `theme-allow` — the 1.
|
|
46
|
+
## `theme-allow` — the 1.21.0 contract
|
|
47
47
|
|
|
48
48
|
The full grammar, in both engines:
|
|
49
49
|
|
|
@@ -63,7 +63,7 @@ An id is a guard kind (`raw-surface`, `inline-spacing`, …) or an oxlint plugin
|
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
- **An annotation must START its comment** — after `//`, `/*`, `<!--`, a block-comment gutter `*`,
|
|
66
|
-
or nothing but whitespace. Before 1.
|
|
66
|
+
or nothing but whitespace. Before 1.21.0 any comment that merely _mentioned_ the token parsed as
|
|
67
67
|
the bare blanket form and switched every rule off on the line below: linewatch documented its own
|
|
68
68
|
waivers in a docblock and thereby disarmed the file. Prose about the escape hatch no longer waives
|
|
69
69
|
anything; everything a consumer actually writes still qualifies.
|
|
@@ -71,7 +71,7 @@ An id is a guard kind (`raw-surface`, `inline-spacing`, …) or an oxlint plugin
|
|
|
71
71
|
Plain `theme-allow` is the node/line waiver in both engines. At 1.20.0 the two halves of the
|
|
72
72
|
contract intersected at exactly one legal shape and that shape was whole-file — naming a rule and
|
|
73
73
|
giving a reason was read as a file declaration, which is why per-node scoping did not actually
|
|
74
|
-
ship. **This is the consumer break in 1.
|
|
74
|
+
ship. **This is the consumer break in 1.21.0**: move each existing file declaration one word.
|
|
75
75
|
- **A bare `theme-allow` still waives everything on its placement** but reports
|
|
76
76
|
`theme-allow-unscoped` (`warn` for one minor, then `error`). A bare `theme-allow-file` waives
|
|
77
77
|
nothing at all — whole-file blanket immunity off one unnamed comment is the thing this contract
|
|
@@ -141,7 +141,12 @@ nothing is forked.
|
|
|
141
141
|
- **Exact name match only** — the one limit that is structural. Round 5 confirmed the miss rate:
|
|
142
142
|
linewatch's forks are named `Cell` and `Box`, rb's is `Stat`. Rename the fork and the rule goes
|
|
143
143
|
quiet, which is what a fork's author naturally does.
|
|
144
|
-
- **Scope:
|
|
144
|
+
- **Scope: basalt consumers only, and component-shaped declarations only.** Since 1.21.1 the rule
|
|
145
|
+
gates on `isBasaltScopedFile` like every other rule in the file, and needs a function, an arrow, a
|
|
146
|
+
`memo`/`forwardRef` wrapper or a class extending one. A plain data class sharing a name with a
|
|
147
|
+
shipped export is a collision, not a fork — it fired on a `SlugTracker` class in a React-free
|
|
148
|
+
package that cannot import basalt-ui.
|
|
149
|
+
- **Scope: everything importable.** Since 1.21.0 it reads all nine published barrels, not just
|
|
145
150
|
`dist/index.d.ts` — so the `basalt-ui/charts` layer, which is where forks actually live, is finally
|
|
146
151
|
covered. Names a barrel merely re-exports from a third party are skipped: `./charts` passes
|
|
147
152
|
`Bar` / `Line` / `Pie` straight through from `@visx/shape`, and a local `Bar` is not a fork of
|
|
@@ -120,9 +120,10 @@ Lint wires as `oxlint . && basalt-ui check-theme` (seeded as the `lint:basalt` s
|
|
|
120
120
|
guard is the teeth behind the token doctrine. Before writing any composite, check whether basalt
|
|
121
121
|
already ships it: round 4 found ~15 forked copies of shipped components across seven repos, all
|
|
122
122
|
passing every gate, because a fork by a token-fluent author uses exactly the right tokens.
|
|
123
|
-
`basalt/shadow-basalt-export` reads all nine published barrels since 1.
|
|
124
|
-
included), but it warns only on an **exact** name collision
|
|
125
|
-
(`Cell`, `Box`, `Stat`) sail straight
|
|
123
|
+
`basalt/shadow-basalt-export` reads all nine published barrels since 1.21.0 (the charts layer
|
|
124
|
+
included), but it warns only on an **exact** name collision, in a basalt-scoped file, on a
|
|
125
|
+
component-shaped declaration — round 5 found the renamed forks (`Cell`, `Box`, `Stat`) sail straight
|
|
126
|
+
past it. It is a tripwire, not coverage; nothing catches the
|
|
126
127
|
rest. Ask before you build; do not wait to be told.
|
|
127
128
|
|
|
128
129
|
The consumer series file (`<first basalt.root>/lib/series.ts` by default — so `src/lib/series.ts` on
|
|
@@ -213,7 +213,7 @@ import { ChartCursorScope } from 'basalt-ui/charts'
|
|
|
213
213
|
rendered in a file that doesn't compose `CartesianChart`, per NODE. Escape:
|
|
214
214
|
`theme-allow hand-rolled-plot — <why>` on the one node, or `theme-allow-file hand-rolled-plot —
|
|
215
215
|
<why>` anywhere in the file — how a genuinely non-single-plot shape (`DualPanel`, `Heatmap`,
|
|
216
|
-
`Donut`) declares itself. `-file` is the 1.
|
|
216
|
+
`Donut`) declares itself. `-file` is the 1.21.0 spelling and it is required. The file that defines
|
|
217
217
|
`CartesianChart` is exempt definitionally.
|
|
218
218
|
- **No hand-authored legends.** `basalt/chart-legend-literal` fails the build on a hand-written
|
|
219
219
|
array literal passed to `ChartLegend`'s `items` — derive it from the same `series` the chart draws
|
package/configs/oxlint-plugin.js
CHANGED
|
@@ -1332,11 +1332,37 @@ const chartLegendLiteral = {
|
|
|
1332
1332
|
const PACKAGE_ROOT = resolvePath(PLUGIN_DIR, '..')
|
|
1333
1333
|
|
|
1334
1334
|
/**
|
|
1335
|
-
* A component-shaped
|
|
1335
|
+
* A component-shaped NAME: starts uppercase and carries a lowercase letter, which excludes the
|
|
1336
1336
|
* SCREAMING_CASE constants and the two-letter token namespaces in the same barrel.
|
|
1337
|
+
*
|
|
1338
|
+
* A name test alone is not a component test — see {@link isComponentInit}. Every PascalCase binding
|
|
1339
|
+
* passes this, which is how obsidian's `SlugTracker` (a plain data class in a React-free package)
|
|
1340
|
+
* became a "forked component" the moment 1.21.0 widened the rule to all nine barrels.
|
|
1337
1341
|
*/
|
|
1338
1342
|
const COMPONENT_NAME = /^[A-Z][A-Za-z0-9]*[a-z][A-Za-z0-9]*$/
|
|
1339
1343
|
|
|
1344
|
+
/** The two wrappers a function component is legitimately declared inside. */
|
|
1345
|
+
const COMPONENT_WRAPPERS = new Set(['memo', 'forwardRef'])
|
|
1346
|
+
|
|
1347
|
+
/**
|
|
1348
|
+
* Is this initializer component-SHAPED — a function, an arrow, or one wrapped in `memo` /
|
|
1349
|
+
* `forwardRef`?
|
|
1350
|
+
*
|
|
1351
|
+
* The second half of the fix for obsidian's false positive. `const Foo = new Map()` and
|
|
1352
|
+
* `class SlugTracker {}` share a name with a basalt export and nothing else; a fork of a component
|
|
1353
|
+
* is a function (or, historically, a class that `extends` one). Narrowing here costs the rule
|
|
1354
|
+
* nothing it was ever meant to catch and removes the whole "PascalCase binding" false-positive
|
|
1355
|
+
* class, which the nine-barrel widening made large.
|
|
1356
|
+
*/
|
|
1357
|
+
function isComponentInit(init) {
|
|
1358
|
+
if (init === null || init === undefined) return false
|
|
1359
|
+
if (init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression') return true
|
|
1360
|
+
if (init.type !== 'CallExpression') return false
|
|
1361
|
+
const callee = init.callee
|
|
1362
|
+
const name = callee?.type === 'MemberExpression' ? callee.property?.name : callee?.name
|
|
1363
|
+
return typeof name === 'string' && COMPONENT_WRAPPERS.has(name)
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1340
1366
|
/**
|
|
1341
1367
|
* The barrels a consumer can import a component FROM. The root one is not the whole surface: a
|
|
1342
1368
|
* chart-layer fork (`ChartFrame`, `CartesianChart`, `ChartCard`, …) collided with nothing, because
|
|
@@ -1428,6 +1454,11 @@ const shadowBasaltExport = {
|
|
|
1428
1454
|
// basalt's own source DEFINES these names; a consumer's does not.
|
|
1429
1455
|
if (BASALT_EXPORTS.size === 0 || filename.startsWith(`${PACKAGE_ROOT}/`)) return {}
|
|
1430
1456
|
if (isTestFile(context)) return {}
|
|
1457
|
+
// Scoped exactly like `ai-sdk-major`, and for the same reason: a package with no basalt-ui
|
|
1458
|
+
// dependency (or one outside a declared `basalt.roots`) cannot import the export it is accused
|
|
1459
|
+
// of forking, so the advice — "import it from 'basalt-ui' instead" — is not followable there.
|
|
1460
|
+
// obsidian's React-free `obsidian-vault-core` took the report and had to waive it.
|
|
1461
|
+
if (filename.length === 0 || !isBasaltScopedFile(filename)) return {}
|
|
1431
1462
|
|
|
1432
1463
|
const report = (node, name) => {
|
|
1433
1464
|
if (typeof name !== 'string' || !BASALT_EXPORTS.has(name)) return
|
|
@@ -1440,9 +1471,14 @@ const shadowBasaltExport = {
|
|
|
1440
1471
|
report(node, node.id?.name)
|
|
1441
1472
|
},
|
|
1442
1473
|
VariableDeclarator(node) {
|
|
1474
|
+
if (!isComponentInit(node.init)) return
|
|
1443
1475
|
report(node, node.id?.name)
|
|
1444
1476
|
},
|
|
1477
|
+
// A class only counts when it EXTENDS something — a legacy class component. basalt ships no
|
|
1478
|
+
// class components, so a standalone `class X {}` sharing a name with one of its exports is a
|
|
1479
|
+
// collision between two unrelated things, not a fork.
|
|
1445
1480
|
ClassDeclaration(node) {
|
|
1481
|
+
if (node.superClass === null || node.superClass === undefined) return
|
|
1446
1482
|
report(node, node.id?.name)
|
|
1447
1483
|
},
|
|
1448
1484
|
}
|