basalt-ui 1.23.0 → 1.23.1
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 +22 -13
- package/CHANGELOG.md +9 -0
- package/MIGRATING.md +86 -4
- package/README.md +35 -16
- package/agent/rules/basalt-app.md +8 -2
- package/agent/rules/basalt-charts.md +38 -2
- package/agent/rules/basalt-content.md +3 -3
- package/agent/rules/basalt-mantine.md +4 -4
- package/agent/rules/basalt-tokens.md +18 -5
- package/agent/skills/basalt-app/SKILL.md +25 -12
- package/agent/skills/basalt-charts/SKILL.md +4 -2
- package/agent/templates/CLAUDE-block.md.tpl +3 -2
- package/dist/charts/kinds/BandStrip.d.ts +20 -6
- package/dist/charts/kinds/BandStrip.d.ts.map +1 -1
- package/dist/charts/kinds/BandStrip.js +59 -6
- package/dist/charts/kinds/MirroredBars.d.ts +4 -2
- package/dist/charts/kinds/MirroredBars.d.ts.map +1 -1
- package/dist/charts/kinds/MirroredBars.js +13 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +182 -24
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +29 -3
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/src/charts/kinds/BandStrip.test.tsx +87 -5
- package/src/charts/kinds/BandStrip.tsx +105 -12
- package/src/charts/kinds/MirroredBars.test.tsx +39 -0
- package/src/charts/kinds/MirroredBars.tsx +38 -2
- package/src/cli/help.test.ts +74 -1
- package/src/cli/index.ts +312 -35
- package/src/cli/tokens-css.test.ts +62 -0
- package/src/guard/check-source.test.ts +73 -0
- package/src/guard/index.ts +67 -7
package/AGENTS.md
CHANGED
|
@@ -62,29 +62,38 @@ 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 wrong for everything else — see the note below.
|
|
67
|
+
|
|
65
68
|
```bash
|
|
66
|
-
bunx basalt-ui init
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
bunx basalt-ui
|
|
73
|
-
bunx basalt-ui
|
|
74
|
-
|
|
75
|
-
|
|
69
|
+
bunx basalt-ui init # FIRST STEP: scaffold .claude/rules/ + .claude/skills/, managed CLAUDE.md block, DESIGN.md seed, toolchain seeds
|
|
70
|
+
basalt-ui --version # one bare line, exit 0 — which CLI is actually running (also -v / version)
|
|
71
|
+
basalt-ui check-theme # fail on off-palette colors in consumer source
|
|
72
|
+
basalt-ui sync # reconcile managed files (.claude/rules/, .claude/skills/, CLAUDE.md block)
|
|
73
|
+
basalt-ui sync --check # CI freshness gate — exits non-zero if any managed file drifted
|
|
74
|
+
basalt-ui doctor # integration health — SKIPPED is a third outcome and exits non-zero
|
|
75
|
+
bunx basalt-ui tokens:css # emit the --vx-* stylesheet (no React, no Mantine, no bundler)
|
|
76
|
+
bunx basalt-ui fonts:css # emit the shipped --basalt-font-* stacks
|
|
77
|
+
basalt-ui info --json # stable JSON surface map
|
|
78
|
+
basalt-ui guard-hook # PreToolUse theme-guard adapter: reads a Write/Edit payload on stdin, denies off-palette writes
|
|
76
79
|
```
|
|
77
80
|
|
|
81
|
+
Every subcommand rejects a flag it does not accept, exiting 1 and naming it; an unknown command says
|
|
82
|
+
so above the usage block. Neither is ever ignored — `doctor --json` used to run doctor and exit 0.
|
|
83
|
+
|
|
78
84
|
`check-theme`, `doctor` and `sync` resolve their project identically: `BASALT_CWD` wins, then the
|
|
79
85
|
cwd, then the workspace packages the root declares, then a two-level layout scan (which is what a
|
|
80
86
|
repo with no `workspaces` field and the install one level down needs). Two candidates is ambiguous
|
|
81
87
|
and reported, never guessed. `sync` on a `tokens-only` consumer prints `n/a` and exits 0.
|
|
82
88
|
|
|
83
|
-
**
|
|
84
|
-
|
|
89
|
+
**Run the local bin, not `bunx`.** `bunx` does not re-resolve a cached package, so it can execute
|
|
90
|
+
the version you just upgraded away from — one consumer filed a P0 against a 1.20.0 cache while
|
|
91
|
+
believing it was on 1.22.0. `init` and `sync` render every seeded script, CI step and hook with the
|
|
92
|
+
resolved local bin for this reason (`BASALT_BIN` overrides it). `basalt-ui --version` is the
|
|
93
|
+
one-second check.
|
|
85
94
|
|
|
86
95
|
Register `guard-hook` in `.claude/settings.json` under `hooks.PreToolUse` with matcher
|
|
87
|
-
`Write|Edit|MultiEdit` and command
|
|
96
|
+
`Write|Edit|MultiEdit` and command `./node_modules/.bin/basalt-ui guard-hook` (what `init` seeds) so the agent can't write off-palette
|
|
88
97
|
colors without a `theme-allow` comment.
|
|
89
98
|
|
|
90
99
|
**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,12 @@
|
|
|
1
|
+
## [1.23.1](https://github.com/jkrumm/basalt-ui/compare/v1.23.0...v1.23.1) (2026-08-22)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* gate the two chart tag rules on where the tag came from ([0075224](https://github.com/jkrumm/basalt-ui/commit/007522416bf9a3269e5f45b7ae7f45203434bae7))
|
|
7
|
+
* make the CLI answer which version ran, and stop it failing open ([0edf50e](https://github.com/jkrumm/basalt-ui/commit/0edf50e1d6f61b349f989fd10fe384d4a91a9af1))
|
|
8
|
+
* stop a typo'd band state from asserting absence ([6c1515c](https://github.com/jkrumm/basalt-ui/commit/6c1515c3ecbf019d9971e5b247e81e5cce3ae316))
|
|
9
|
+
|
|
1
10
|
# [1.23.0](https://github.com/jkrumm/basalt-ui/compare/v1.22.0...v1.23.0) (2026-08-22)
|
|
2
11
|
|
|
3
12
|
|
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.
|
|
12
|
-
|
|
13
|
-
|
|
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 `Unreleased` § 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,85 @@ 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
|
+
**One type widened, nothing removed or renamed.** `BandStripSeries.formatValue` is now
|
|
44
|
+
`(d: T) => string | null`. `null` renders an em dash — an absent READING — which `''` never could:
|
|
45
|
+
`''` is a state whose label is the whole row. Every existing `(d) => string` still typechecks.
|
|
46
|
+
|
|
47
|
+
**A typo'd `BandSpan.state` no longer renders as absence.** A state naming no `series` entry used to
|
|
48
|
+
be skipped, so the band was simply not drawn — and on a measured/not-measured strip a missing band
|
|
49
|
+
is a claim about the data, not an "unknown". A misspelling asserted a reading nobody took, in a mark
|
|
50
|
+
indistinguishable from a real one.
|
|
51
|
+
|
|
52
|
+
| Key comes off | Dev | Production |
|
|
53
|
+
| ------------------------------------------------------------- | ---------------------------------------- | ------------------------------------------------------------------------ |
|
|
54
|
+
| 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 |
|
|
55
|
+
| a PROP — `absentState`, `MirroredBars`' `up.key` / `down.key` | throws | throws |
|
|
56
|
+
|
|
57
|
+
The split is the whole design. `state` comes off data, so a feed that grows a state basalt has never
|
|
58
|
+
seen must degrade rather than take a dashboard down — while a typo, which is the same input, still
|
|
59
|
+
fails loudly wherever it is being written. The dashed neutral outline belongs to no legend entry and
|
|
60
|
+
no state fill, so it cannot read as data. Pane keys are props, never data-driven, so `assertPaneKey`
|
|
61
|
+
throws in every environment: an unresolvable `up.key` used to hide the pane AND its axis, which
|
|
62
|
+
reads as "that half measured zero" — the one thing `MirroredBars` exists to keep apart from absence.
|
|
63
|
+
|
|
64
|
+
**`chart-missing-aria-label` and `unframed-chart` are gated on where the tag came from.** Both key
|
|
65
|
+
on a JSX tag NAME, so a consumer's own 235-line `MirroredBars` — sharing nothing with the shipped
|
|
66
|
+
kind but the name — was told to pass an `ariaLabel` prop it does not accept, by a rule that presents
|
|
67
|
+
as a correctness finding. A tag is now skipped only when the file DEFINES a component of that name
|
|
68
|
+
and does not also import it from `basalt-ui`. Deliberately one-directional: a tag imported from
|
|
69
|
+
basalt-ui, one imported from a consumer barrel that re-exports it, and one the scan cannot attribute
|
|
70
|
+
at all still all fire. Verified old-vs-new over **945 source files across six repos: 0 findings
|
|
71
|
+
lost, 0 gained.**
|
|
72
|
+
|
|
73
|
+
**CLI: `--version` exists, and no subcommand fails open on a flag.** `--version` / `-v` / `version`
|
|
74
|
+
print one bare greppable line and exit 0, resolved BEFORE dispatch so it can never run a command to
|
|
75
|
+
answer "which basalt-ui is this". Six consumer reports reached for it in one round; all six fell
|
|
76
|
+
back to `info --json`.
|
|
77
|
+
|
|
78
|
+
The larger fix sits underneath it. **Every subcommand validates its flags** and exits 1 naming the
|
|
79
|
+
one it does not accept — `doctor --json` used to run doctor and exit **0**, and `check-theme
|
|
80
|
+
--audit-allow` scanned and reported success. An unknown COMMAND now says so above the usage block
|
|
81
|
+
instead of dumping help and letting the dump read like a choice.
|
|
82
|
+
|
|
83
|
+
- **`doctor` reads `basaltAppPlugin({ icons })`** out of the consumer's vite config instead of
|
|
84
|
+
hardcoding six filenames, so adopting 1.23.0's icons array stops producing a warning to generate
|
|
85
|
+
five files you deliberately lack. A named array is checked against itself (an icon missing from
|
|
86
|
+
`public/` still warns); an unparseable or absent config falls back to the six-filename check. It
|
|
87
|
+
can only narrow, never blind.
|
|
88
|
+
- **`doctor` and `sync` share one sentence about a parent install** (`parentInstallAdvice`). Run
|
|
89
|
+
from a package whose install is above it, `doctor` now names the parent and says `basalt-ui init`
|
|
90
|
+
is NOT the fix. Following its old advice literally scaffolded the second consumer that 1.22.0
|
|
91
|
+
exists to prevent.
|
|
92
|
+
- **Every seeded invocation resolves the local bin** (`basaltBinCommand`, overridable via
|
|
93
|
+
`BASALT_BIN`): the `lint` script, the CI steps, the `.claude` PreToolUse hook and doctor's own
|
|
94
|
+
advice all render `./node_modules/.bin/basalt-ui`. `bunx` does not re-resolve a package it has
|
|
95
|
+
cached — that is what made a round-7 report file a P0 against a 1.20.0 cache while believing it
|
|
96
|
+
was on 1.22.0, and the seed was shipping `bunx` into consumer CI in ten places.
|
|
97
|
+
`configs/lefthook.yml`'s `${BASALT_BIN:-bunx --no-install basalt-ui}` default is unchanged and
|
|
98
|
+
deliberate: `--no-install` fails loudly instead of downloading a stranger.
|
|
99
|
+
- **`tokens:css --check` blanks the provenance line (line 2) before comparing**, so a version bump
|
|
100
|
+
alone no longer forces a no-op commit in a tokens-only consumer, where the gate is byte-equality.
|
|
101
|
+
A stale provenance line is now a note on an otherwise-passing check. The `@generated` header is
|
|
102
|
+
still emitted byte-identical — the line stays, it just stops gating.
|
|
103
|
+
|
|
104
|
+
### Corrections to the record
|
|
105
|
+
|
|
106
|
+
- **All six round-8 reports said `basalt-ui --version` "exits 0 printing usage". It exited 1, to
|
|
107
|
+
stderr** — the dispatcher's `default:` branch has always been `console.error(USAGE); return 1`.
|
|
108
|
+
The real fail-open was an unknown FLAG, which no report tested, and the misdiagnosis was relayed
|
|
109
|
+
verbatim into the fix brief. Report the symptom you measured, not the one you inferred from it.
|
|
110
|
+
- **`cb4e5b7`'s message is wrong** (`b9b99a6` on `origin/feat/round-7-band-kinds` is the same commit
|
|
111
|
+
pre-merge), and it is on `master` where it cannot be rewritten. It claims it taught
|
|
112
|
+
`unframed-chart` the two new kinds; it widened `CHART_ENTRY_POINT_TAG`, which only
|
|
113
|
+
`chart-missing-aria-label` reads. `unframed-chart` keys on `<ChartLegend items={[` and carries no
|
|
114
|
+
kind list at all — there was never an asymmetric pair to fix.
|
|
115
|
+
- **Open question, not a plan.** The import gate does not make `CHART_ENTRY_POINT_TAG` or the oxlint
|
|
116
|
+
plugin's `CHART_TAGS` redundant: that list still answers _which_ tags owe an `ariaLabel`. The gate
|
|
117
|
+
only converts a kind missing from the list from a false positive into an under-report. Collapsing
|
|
118
|
+
the two lists is a separate, larger change and has not been made.
|
|
119
|
+
|
|
120
|
+
## 1.23.0 — two band kinds, an x-tick seam, CLI resolution
|
|
121
|
+
|
|
42
122
|
**No export removed or renamed; six new runtime exports and nine new types.** Nothing you wrote
|
|
43
123
|
changed. Every entry is additive, and the batch runs the other way from the last three: those made
|
|
44
124
|
the guard stricter, this one makes the framework more expressive.
|
|
@@ -72,7 +152,9 @@ unchanged and still works; omit both and ticks are chosen to fit (`smartTicks`),
|
|
|
72
152
|
Reach for it on a dense time axis: the tick choosers append the final key unconditionally, so a
|
|
73
153
|
COUNT that does not land on the last index paints two labels on top of each other at the right edge
|
|
74
154
|
— at every count, not at an unlucky one. On the consumer that reported it, the local tick helper
|
|
75
|
-
went 200 →
|
|
155
|
+
went 200 → 170 lines; it shrinks, it does not disappear. (The promise was 160. The 10-line miss is
|
|
156
|
+
docblock — the port's own JSDoc grew, because the helper stopped being a fallback and became the one
|
|
157
|
+
seam every chart on the page passes through.)
|
|
76
158
|
|
|
77
159
|
**`basaltAppPlugin`'s `icons` takes an array now.**
|
|
78
160
|
|
package/README.md
CHANGED
|
@@ -450,6 +450,16 @@ bar panes over one x scale and one baseline, each in its own domain. Both fold t
|
|
|
450
450
|
width, hatch the share of a slot nothing measured, and share the page cursor like every other
|
|
451
451
|
chart.
|
|
452
452
|
|
|
453
|
+
On a strip, `series` IS the state set, so a `state` naming no entry has no colour, no legend entry
|
|
454
|
+
and no tooltip row. It **throws in dev**, naming the key and the valid set; in production it draws a
|
|
455
|
+
dashed neutral outline band — a treatment no legend entry and no state fill uses — plus an
|
|
456
|
+
`Unknown state` tooltip row. Drawing nothing was the old behaviour and the wrong one: on a
|
|
457
|
+
measured/not-measured strip a missing band claims a coverage gap. `absentState` and `MirroredBars`'
|
|
458
|
+
`up.key`/`down.key` are PROPS rather than data, so those throw in every environment; an
|
|
459
|
+
unresolvable pane key used to hide the pane and its axis, which reads as a measured zero.
|
|
460
|
+
`BandStripSeries.formatValue` returns `string | null`, `null` rendering an em dash — an absent
|
|
461
|
+
reading, distinct from `''`, which is a state whose label is the whole row.
|
|
462
|
+
|
|
453
463
|
Which x ticks get painted is `xTickValues?: (keys, xMax) => readonly string[]`, on
|
|
454
464
|
`CartesianChart` and forwarded by `Bars`/`MultiLine`/`StackedArea`/`ZonedLine` and both band kinds.
|
|
455
465
|
It resolves ahead of the `xTicks` COUNT, which is unchanged; omit both and ticks are chosen to fit.
|
|
@@ -694,19 +704,24 @@ Current slots: `series` (read by `./charts` + `./tokens`), `commands` (read by `
|
|
|
694
704
|
## CLI
|
|
695
705
|
|
|
696
706
|
```bash
|
|
697
|
-
bunx basalt-ui init # scaffold doctrine into a consumer repo
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
707
|
+
bunx basalt-ui init # scaffold doctrine into a consumer repo — the one command with no install to resolve
|
|
708
|
+
basalt-ui --version # one bare line, exit 0 (also -v / version) — which CLI is actually running
|
|
709
|
+
basalt-ui sync # three-way diff against .basalt/manifest.json after a basalt-ui upgrade
|
|
710
|
+
basalt-ui sync --check # CI drift gate — non-zero exit on any managed-file drift
|
|
711
|
+
basalt-ui check-theme # palette guard — fails on colors that bypass the central --vx-* system
|
|
712
|
+
basalt-ui doctor # integration health: basalt-resolves, guard-scan, oxlint-preset, lefthook-preset, manifest presence, node_modules-vs-manifest version, spacing-scale drift, and a cross-package `ai` major-version parity check (HARD failure — walks every workspace manifest, exemptable via `basalt.aiMajorSkewReason` — see above) + warn on an icon file basaltAppPlugin references and public/ does not have
|
|
713
|
+
basalt-ui doctor --tokens-only # force the Mantine-free profile (auto-detected otherwise; --framework forces the full set)
|
|
703
714
|
bunx basalt-ui tokens:css --out src/tokens.css # emit the --vx-* stylesheet as a file you own
|
|
704
715
|
bunx basalt-ui fonts:css --out src/fonts.css # emit the shipped --basalt-font-* stacks
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
bunx basalt-ui help # full usage; every subcommand also takes --help / -h
|
|
716
|
+
basalt-ui info --json # surface map as stable JSON (InfoOutput shape); bare `info` is the human-readable form
|
|
717
|
+
basalt-ui help # full usage; every subcommand also takes --help / -h
|
|
708
718
|
```
|
|
709
719
|
|
|
720
|
+
**Nothing fails open.** `--version` resolves before dispatch, so it can never run a command to
|
|
721
|
+
answer which CLI ran. Every subcommand validates its flags and exits 1 naming the first one it does
|
|
722
|
+
not accept — `doctor --json` used to run doctor and exit **0**, and `check-theme --audit-allow`
|
|
723
|
+
scanned and reported success. An unknown command says so above the usage block.
|
|
724
|
+
|
|
710
725
|
`doctor` reports **`SKIPPED`** as a third outcome beside pass/warn/fail and exits non-zero on it —
|
|
711
726
|
"All checks passed" is only printable when every check actually ran. **`check-theme`, `doctor` and
|
|
712
727
|
`sync` all resolve their project the same way**: `BASALT_CWD` wins, then the cwd itself, then the
|
|
@@ -715,13 +730,15 @@ ambiguous, never guessed. The scan is what a repo declaring **no `workspaces` fi
|
|
|
715
730
|
the install one level down and nothing declaring it, every command answered about the root and
|
|
716
731
|
found nothing there: `check-theme` printed "no off-palette colors" having scanned zero files, and
|
|
717
732
|
`doctor` inferred `tokens-only` for a full Mantine consumer. `tokens:css` and `fonts:css` both take
|
|
718
|
-
`--check` as a CI drift gate
|
|
733
|
+
`--check` as a CI drift gate, comparing everything but the header's provenance line — so a
|
|
734
|
+
basalt-ui version bump alone never forces a no-op commit in a tokens-only consumer.
|
|
719
735
|
|
|
720
|
-
**
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
736
|
+
**Run the local bin, not `bunx`.** `bunx` does not re-resolve a package it has already cached, so a
|
|
737
|
+
`bunx basalt-ui …` run can execute a version you upgraded away from — which is how one consumer
|
|
738
|
+
filed a P0 against a 1.20.0 cache while pinned to 1.22.0. `init` and `sync` now render every seeded
|
|
739
|
+
script, CI step, hook and piece of doctor advice with the resolved `./node_modules/.bin/basalt-ui`
|
|
740
|
+
(`BASALT_BIN` overrides it, and `configs/lefthook.yml` keeps `bunx --no-install` as a deliberately
|
|
741
|
+
loud fallback). `basalt-ui --version` is the one-second check.
|
|
725
742
|
|
|
726
743
|
`sync` strategy per file: unchanged since last write → overwrite; locally edited → skip (show diff);
|
|
727
744
|
missing → recreate. `--force` overwrites local edits. The summary counts `created` and `recreated`
|
|
@@ -736,7 +753,9 @@ release.
|
|
|
736
753
|
`check-theme` and `doctor` do, and since 1.22.0 **exits 1** when the resolved project has no
|
|
737
754
|
`.basalt/manifest.json`, naming the install it found above instead of scaffolding a second one
|
|
738
755
|
beside it. Run it at the package that holds the manifest, or point `BASALT_CWD` at that package.
|
|
739
|
-
Creating an install is `basalt-ui init`'s decision
|
|
756
|
+
Creating an install is `basalt-ui init`'s decision — and `doctor` now says the same sentence from
|
|
757
|
+
the same directory, where it used to prescribe `init` and so recommend the very scaffold that
|
|
758
|
+
refusal exists to prevent.
|
|
740
759
|
|
|
741
760
|
---
|
|
742
761
|
|
|
@@ -193,7 +193,11 @@ web-app-manifest-512x512.png
|
|
|
193
193
|
|
|
194
194
|
Generate the set with `@vite-pwa/assets-generator` (or realfavicongenerator.net) from a single
|
|
195
195
|
source image — it lives in the CONSUMER's own devDependencies, not basalt-ui's. `basalt-ui doctor`
|
|
196
|
-
warns (does not fail) when `public/` exists but is missing one of these files
|
|
196
|
+
warns (does not fail) when `public/` exists but is missing one of these files — and it now
|
|
197
|
+
parses `basaltAppPlugin`'s `icons` option out of your vite config first, so it checks the files you
|
|
198
|
+
declared rather than the six it assumes. `icons: false` or an empty array is "nothing to check"; an
|
|
199
|
+
unparseable or absent config falls back to the six defaults. Adopting the array is no longer a way
|
|
200
|
+
to earn a warning.
|
|
197
201
|
|
|
198
202
|
**If your `public/` does not match those six filenames, name your icons instead.** `icons` takes
|
|
199
203
|
`false | { dir?: string } | readonly BasaltAppIcon[]`, and an array uses the manifest's own field
|
|
@@ -217,7 +221,9 @@ One SVG at `sizes: 'any'` is a complete, installable icon set for a single-page
|
|
|
217
221
|
shape every non-scaffolded consumer already has. If you keep a hand-written `manifest.webmanifest`
|
|
218
222
|
only because the plugin could not name your icon, delete it: the generated manifest reproduces it
|
|
219
223
|
member for member, including the two hexes derived from `SURFACE.bg` rather than hand-copied, and it
|
|
220
|
-
takes the permanent `theme-allow-file` on that file with it.
|
|
224
|
+
takes the permanent `theme-allow-file` on that file with it. **Delete that app's own
|
|
225
|
+
`<link rel="manifest">` in the same change** — the plugin injects its own, so leaving both ships two
|
|
226
|
+
manifest links pointing at different files, one of them a 404 the moment the hand-written file goes.
|
|
221
227
|
|
|
222
228
|
`basaltAppPlugin` also emits `site.webmanifest` (served in dev too) with explicit `id`/`scope`/
|
|
223
229
|
`start_url` — pass `manifest: false` to skip it, or `icons: false` to skip the icons. Since 1.22.0
|
|
@@ -14,8 +14,10 @@ primitives so we can build exactly the chart we want; the trade-off is that ever
|
|
|
14
14
|
structure unless we compose shared building blocks. **`CartesianChart` plus a small kind set are
|
|
15
15
|
the contract, not optional polish** — composing `CartesianChart` (single-plot) or `ChartFrame`
|
|
16
16
|
directly (multi-pane/radial/matrix) is mandatory for every chart, mechanically enforced by
|
|
17
|
-
`basalt/hand-rolled-plot` (see "Mechanical enforcement" below).
|
|
18
|
-
|
|
17
|
+
`basalt/hand-rolled-plot` (see "Mechanical enforcement" below). **This file is the contract you
|
|
18
|
+
receive** — the basalt-ui repo's `docs/CHARTS-SPEC.md` is the same argument at length, and it does
|
|
19
|
+
not ship in the package. Read it at
|
|
20
|
+
<https://github.com/jkrumm/basalt-ui/blob/master/docs/CHARTS-SPEC.md> or not at all.
|
|
19
21
|
|
|
20
22
|
## The boundary (lint-enforced)
|
|
21
23
|
|
|
@@ -348,6 +350,8 @@ per-series `getValue` accessors on `ChartSeries<T>`):
|
|
|
348
350
|
hatch; `marker` carries a fact that must not be readable off the `fill` ramp. `cursorResolution`
|
|
349
351
|
defaults `'leading'` here (a band IS a bucket), not `'nearest'`. Composes `ChartFrame` directly —
|
|
350
352
|
`CartesianChart` renders `AxisLeftNumeric` unconditionally, so this shape could never use it.
|
|
353
|
+
`BandStripSeries.formatValue` is `(d) => string | null` — `null` renders an em dash, an absent
|
|
354
|
+
READING, which `''` cannot say (`''` is a state whose label is the whole row).
|
|
351
355
|
- **`MirroredBars`** — two BAR panes over one x scale, mirrored around one baseline, each pane in
|
|
352
356
|
its own domain. `up`/`down` take `{ key, max?, autoMaxFloor?, ticks?, format }`; `upFraction`
|
|
353
357
|
(default 0.35) is the up pane's share of the band height, which is where the shared baseline sits;
|
|
@@ -360,6 +364,29 @@ Both band kinds fold their domain by width (`fold: { minBandPx?, merge }`), and
|
|
|
360
364
|
exported so a consumer can test their merge against the grouping that will actually run.
|
|
361
365
|
`HatchPattern` / `hatchFill` / `hatchSizeFor` ship the absence fill.
|
|
362
366
|
|
|
367
|
+
**`merge` does not run on a sub-cap dataset.** `foldBands` returns `[...data]` unchanged when
|
|
368
|
+
`data.length <= cap`, which on a wide viewport is the common case — so any bookkeeping the merge
|
|
369
|
+
adds (how many source buckets a slot folded, how many were unmeasured) is absent exactly where you
|
|
370
|
+
are least likely to look for it. Put that bookkeeping on the datum at CONSTRUCTION and have `merge`
|
|
371
|
+
sum it; do not compute it from `group.length` inside the merge. Summing is also the composable
|
|
372
|
+
form, and it keeps "every source bucket accounted for exactly once" true by construction.
|
|
373
|
+
|
|
374
|
+
**A band state naming no `series` entry throws in dev.** `series` IS the state set, so an
|
|
375
|
+
unresolvable `state` has no legend entry, no colour and no tooltip row — and the old behaviour,
|
|
376
|
+
skipping the band, drew a coverage GAP, which on a measured/not-measured strip is a false claim
|
|
377
|
+
about the data rather than an "unknown". Where the key comes from decides the treatment:
|
|
378
|
+
|
|
379
|
+
| Key comes off | Dev | Production |
|
|
380
|
+
| ------------------------------------------------------------- | ---------------------------------------- | ---------------------------------------------------------------------- |
|
|
381
|
+
| the DATUM — `BandSpan.state`, `marker.state` | throws, naming the key and the valid set | dashed neutral outline band + an `Unknown state` / `<key>` tooltip row |
|
|
382
|
+
| a PROP — `absentState`, `MirroredBars`' `up.key` / `down.key` | throws | throws |
|
|
383
|
+
|
|
384
|
+
A feed that grows a state basalt has never seen must degrade rather than take a dashboard down; a
|
|
385
|
+
typo, which is the same input, still fails loudly wherever it is written. The dashed neutral outline
|
|
386
|
+
is used by no legend entry and no state fill, so it can never be mistaken for data. Pane keys are
|
|
387
|
+
props, never data-driven — an unresolvable `up.key` used to hide the pane AND its axis, which reads
|
|
388
|
+
as "that half measured zero".
|
|
389
|
+
|
|
363
390
|
`ZonedLine` and `MultiLine` also accept `xZones?: XZoneSpec[]` — the vertical counterpart to a
|
|
364
391
|
kind's horizontal zone bands, for marking a time window rather than a value range. Each
|
|
365
392
|
`{ from?, to?, fill, align? }` bound is a `getX` **domain key** (the label string the kind's
|
|
@@ -512,6 +539,15 @@ left, 12% right): uncapped, half a `DD.MM HH:MM` label is 48px of a 338px chart.
|
|
|
512
539
|
probes real tick labels from both panes, so it takes the plain measured margin and neither the
|
|
513
540
|
floor nor the cap applies.
|
|
514
541
|
|
|
542
|
+
**The height law is the number you actually have to compute.** A band row is
|
|
543
|
+
`height − legend − margin.top − margin.bottom`: `ChartFrame` reserves the measured legend band out
|
|
544
|
+
of the plot rect first, then the kind takes the margins out of what is left. There is no
|
|
545
|
+
`bandHeight` prop — to get a 44px band, hand the frame `44 + margins + legend`. `margin={{ top: 0 }}`
|
|
546
|
+
reclaims the whole top gutter, which a 1-D strip draws nothing in; the bottom is not reclaimable,
|
|
547
|
+
`autoMargin` floors it at `VX.margin.bottom`. Derive the constants from `VX.margin` rather than
|
|
548
|
+
restating them — a consumer that copies `30` into three chart files has three silent-drift sites the
|
|
549
|
+
moment the density scale is retuned.
|
|
550
|
+
|
|
515
551
|
`chartMargin(opts?)` (`basalt-ui/tokens`, also re-exported from `basalt-ui/charts`) still exists,
|
|
516
552
|
but only matters for a bespoke chart that composes `ChartFrame` directly (not `CartesianChart`) and
|
|
517
553
|
needs a right-axis-sized margin without running its own measurement pass — `DualPanel` and `Heatmap`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
source: basalt-ui
|
|
3
|
-
description: Content/prose surface from basalt-ui/content — Prose, CodeBlock, Callout, TableOfContents, ReadingProgress, Markdown (streaming-aware), MermaidDiagram, MDX component map, ArticleLayout, ArticleCard/Grid, GuideLink/GuideDrawer, and the content-collections + TanStack Start recipe. Mantine-coupled
|
|
3
|
+
description: Content/prose surface from basalt-ui/content — Prose, CodeBlock, Callout, TableOfContents, ReadingProgress, Markdown (streaming-aware), MermaidDiagram, MDX component map, ArticleLayout, ArticleCard/Grid, GuideLink/GuideDrawer, and the content-collections + TanStack Start recipe. Mantine-coupled.
|
|
4
4
|
paths:
|
|
5
5
|
- 'src/content/**'
|
|
6
6
|
- 'apps/**/src/**/*content*'
|
|
@@ -253,7 +253,7 @@ step 3).
|
|
|
253
253
|
surface (`basalt-ui/commands`) — the same tier as `toRouteActions`. It joins title, description,
|
|
254
254
|
category, tags, and slug into a `keywords` string for substring search; it does not tokenize, rank,
|
|
255
255
|
index, or persist. An app that outgrows substring matching over a client-side array brings its own
|
|
256
|
-
search index — see docs/CONTENT-SPEC.md §9 (Non-goals) in the
|
|
256
|
+
search index — see <https://github.com/jkrumm/basalt-ui/blob/master/docs/CONTENT-SPEC.md> §9 (Non-goals), which is not in the package.
|
|
257
257
|
|
|
258
258
|
`ArticleGrid` stays a dumb layout wrapper — deliberate, so callers keep composition (sorting,
|
|
259
259
|
filtering, and pagination all happen above it, not inside it).
|
|
@@ -406,4 +406,4 @@ const articleActions = toRouteActions(
|
|
|
406
406
|
|
|
407
407
|
KaTeX/math, full mermaid grammar bundling (d2, pintora), Vega-Lite streamed specs, versioned docs/
|
|
408
408
|
i18n routing, and a `create-basalt-docs` scaffold are all out of scope for 1.0 — see
|
|
409
|
-
|
|
409
|
+
<https://github.com/jkrumm/basalt-ui/blob/master/docs/CONTENT-SPEC.md> §9 for the full non-goals list and rationale (not in the package).
|
|
@@ -30,15 +30,15 @@ renders.
|
|
|
30
30
|
|
|
31
31
|
- Use `createBasaltTheme(overrides?)` for the theme — it merges your overrides over `baseTheme`
|
|
32
32
|
(last-wins), so you can retune any field without forking. Don't hand-build `createTheme`.
|
|
33
|
-
- The theme reskins **every** Mantine accent to the modern-zinc identity (
|
|
34
|
-
|
|
33
|
+
- The theme reskins **every** Mantine accent to the modern-zinc identity (long form:
|
|
34
|
+
<https://github.com/jkrumm/basalt-ui/blob/master/docs/DESIGN-SPEC.md>, not shipped in the package) —
|
|
35
35
|
**cool-neutral zinc** ramps on both light and dark — plus a **single saturated sky-blue accent**
|
|
36
36
|
— split by role: as INK (links, active-nav icon, chart lines, focus ring) `#0077bd` light /
|
|
37
37
|
`#8ec5ff` dark; as a FILLED SURFACE `#0077bd` in BOTH schemes with a white label — and binds
|
|
38
38
|
Mantine's surface vars AND its color families to the same `--vx-*` tokens
|
|
39
39
|
the charts use (`cssVariablesResolver`, pre-wired in `BasaltProvider`), so chrome and charts share
|
|
40
40
|
one scheme-reactive identity. (Blueprint / Basalt zinc-charcoal are the historical hue-tuning
|
|
41
|
-
ancestors;
|
|
41
|
+
ancestors; DESIGN-SPEC supersedes both.) Don't add a second
|
|
42
42
|
`cssVariablesResolver`.
|
|
43
43
|
- Color scheme: read/write via `useMantineColorScheme()`. **Never** read the color scheme from
|
|
44
44
|
localStorage directly, and never `localStorage.getItem('theme')` — see basalt-state.md.
|
|
@@ -220,7 +220,7 @@ shape doctrine lives here; the spacing/radius/type **tokens** are in basalt-toke
|
|
|
220
220
|
Mechanically enforced by `basalt-ui check-theme`'s `raw-surface` guard. Outer spacing comes from the
|
|
221
221
|
parent `Stack`/`SimpleGrid` gap, not an intrinsic card margin.
|
|
222
222
|
- **Depth = a whisper shadow with a 1px ring baked in, not a plain hairline** (see
|
|
223
|
-
|
|
223
|
+
<https://github.com/jkrumm/basalt-ui/blob/master/docs/DESIGN-SPEC.md>, doctrine inversion #1 — this supersedes the old
|
|
224
224
|
"never a drop shadow" rule). **Three depth tiers, split control-vs-panel-vs-floating:**
|
|
225
225
|
- **`shadow-card` — panels.** A surface that HOLDS content: `Card`, `Paper`, `Notification`,
|
|
226
226
|
`ChartCard`, `SettingsSection`. Its light-mode ring is an **outset** 1px hairline; dark is a
|
|
@@ -10,7 +10,8 @@ paths:
|
|
|
10
10
|
# Basalt Tokens — Color, Spacing, Radius, Type
|
|
11
11
|
|
|
12
12
|
basalt-ui ships one color/type/spacing/radius identity shared by the Mantine chrome and the visx
|
|
13
|
-
charts. The **identity is modern zinc** (
|
|
13
|
+
charts. The **identity is modern zinc** (long form: <https://github.com/jkrumm/basalt-ui/blob/master/docs/DESIGN-SPEC.md>, which does not ship in the
|
|
14
|
+
package) —
|
|
14
15
|
**cool-neutral zinc surfaces**
|
|
15
16
|
(Tailwind zinc family) on both light and dark, low-contrast panel lift on a slightly darker page,
|
|
16
17
|
depth via a whisper shadow + 1px ring (`shadow-card` on panels, `shadow-raised` on interactive
|
|
@@ -21,8 +22,7 @@ lines, focus ring) it is `#0077bd` light / `#8ec5ff` dark; as a FILLED SURFACE i
|
|
|
21
22
|
`#0077bd` in BOTH schemes with a WHITE label (`--vx-accent-fill`, never `--vx-accent`). Neutrals do ~90% of the
|
|
22
23
|
surface (60/30/10, pushed toward 90/10); the accent only points — primary CTA, focus, links, small
|
|
23
24
|
status pops — never floods. (Blueprint/Basalt zinc-charcoal are the historical hue-tuning
|
|
24
|
-
ancestors;
|
|
25
|
-
inversions" section.) This
|
|
25
|
+
ancestors; DESIGN-SPEC supersedes both — see its "Doctrine inversions" section.) This
|
|
26
26
|
rule is the operational checklist; it is enforced mechanically by **`basalt-ui check-theme`**
|
|
27
27
|
(wire it into `lint`: `oxlint . && basalt-ui check-theme`; `init` seeds that as `lint:basalt`). A
|
|
28
28
|
violation fails the build. Escape hatch: a scoped `theme-allow <rule-id> — <reason>` comment
|
|
@@ -120,13 +120,26 @@ the guard with that one occurrence neutralized, so the verdict comes from the sc
|
|
|
120
120
|
parsing the waiver's text:
|
|
121
121
|
|
|
122
122
|
```text
|
|
123
|
+
theme-allow annotations (3):
|
|
123
124
|
src/shell/x.tsx:42 suppresses raw-surface@43
|
|
124
125
|
src/theme/y.ts:18 SUPPRESSES NOTHING — dead, delete it
|
|
125
|
-
src/charts/z.tsx:7
|
|
126
|
+
src/charts/z.tsx:7 suppresses hand-rolled-plot@8 (oxlint)
|
|
127
|
+
|
|
128
|
+
basalt.exemptRules entries (1):
|
|
126
129
|
raw-hex: "site.webmanifest" suppresses findings in public/site.webmanifest — <the recorded reason>
|
|
130
|
+
|
|
131
|
+
3 live, 1 dead, 0 unjudgeable, 0 unaccountable (reported as theme-allow-unscoped by a normal run).
|
|
132
|
+
Scope: the N file(s) check-theme scans — everything under basalt.roots (…), plus each root's
|
|
133
|
+
sibling index.html and public/ tree, plus anything named in basalt.include (none). …
|
|
127
134
|
```
|
|
128
135
|
|
|
129
|
-
|
|
136
|
+
**Two sections, not one list** — annotations first, then `basalt.exemptRules`, each with its own
|
|
137
|
+
count. A plugin-rule annotation is judged too (since 1.22.0, by re-running oxlint over one
|
|
138
|
+
neutralized sibling file), so `unjudgeable` now means only "names no rule at all" or "oxlint could
|
|
139
|
+
not be run here" — never "not a check-theme kind". The scope line is part of the report: `0 dead`
|
|
140
|
+
is not `0 dead anywhere`.
|
|
141
|
+
|
|
142
|
+
It closes with a `live / dead / unjudgeable / unaccountable` tally and **exits 1 on a dead
|
|
130
143
|
waiver**, so it wires into CI. A waiver that stopped covering anything is not harmless: it silently
|
|
131
144
|
covers the next real finding on that line. Basalt's own source had one; it is deleted.
|
|
132
145
|
|
|
@@ -6,18 +6,26 @@ when_to_use: User is adding basalt-ui to a new or existing app, asks how to set
|
|
|
6
6
|
|
|
7
7
|
`/basalt-app` scaffolds a basalt-ui consumer and keeps its shipped doctrine fresh. It wraps the two
|
|
8
8
|
CLI subcommands — `basalt-ui init` (first run) and `basalt-ui sync` (refresh) — and explains the
|
|
9
|
-
wiring they don't do for you (provider, shell, vite).
|
|
9
|
+
wiring they don't do for you (provider, shell, vite). Bun runtime.
|
|
10
10
|
|
|
11
11
|
## One package, one command, one version
|
|
12
12
|
|
|
13
13
|
Everything ships in the `basalt-ui` npm package — components, toolchain presets, rules, skills,
|
|
14
14
|
templates. Becoming a consumer is exactly two steps:
|
|
15
15
|
|
|
16
|
-
```
|
|
16
|
+
```bash
|
|
17
17
|
bun add basalt-ui
|
|
18
|
-
bunx basalt-ui init
|
|
18
|
+
bunx basalt-ui init # `init` is the ONE command that legitimately predates the install
|
|
19
|
+
./node_modules/.bin/basalt-ui --version # every command after it: the LOCAL bin
|
|
19
20
|
```
|
|
20
21
|
|
|
22
|
+
**After `init`, never `bunx`.** `bunx` does not re-resolve a package it has cached, so it can run
|
|
23
|
+
a months-old CLI against a freshly-pinned install and report a green gate you did not configure —
|
|
24
|
+
which is exactly how one consumer filed a P0 against a 1.20.0 cache while believing it was on
|
|
25
|
+
1.22.0. Use `./node_modules/.bin/basalt-ui` (or a `package.json` script, which `bun run` resolves
|
|
26
|
+
from the same place). `basalt-ui --version` / `-v` prints one bare line and exits 0; that is the
|
|
27
|
+
check, and it costs nothing to run first.
|
|
28
|
+
|
|
21
29
|
There is no plugin and no marketplace. `init`/`sync` place every file with one of two modes, and
|
|
22
30
|
which mode a file gets answers a single question — **does Claude read it?**
|
|
23
31
|
|
|
@@ -130,7 +138,7 @@ The consumer series file (`<first basalt.root>/lib/series.ts` by default — so
|
|
|
130
138
|
a plain app, `apps/web/src/lib/series.ts` on a monorepo; override via `basalt.seriesModulePath`) is
|
|
131
139
|
the single guard-exempt palette source — see `/basalt-charts`.
|
|
132
140
|
|
|
133
|
-
## Refresh: `
|
|
141
|
+
## Refresh: `basalt-ui sync`
|
|
134
142
|
|
|
135
143
|
`sync` re-applies shipped doctrine after a basalt-ui upgrade, three-way against
|
|
136
144
|
`.basalt/manifest.json`:
|
|
@@ -158,14 +166,15 @@ both.
|
|
|
158
166
|
## Checklist
|
|
159
167
|
|
|
160
168
|
- Ran `bun add basalt-ui && bunx basalt-ui init` — rules, skills, CLAUDE block, DESIGN.md all
|
|
161
|
-
present from the one package.
|
|
169
|
+
present from the one package. Every command after it runs the local bin, not `bunx`.
|
|
162
170
|
- `BasaltProvider` wraps the app; `createBasaltTheme` used for theme deltas; `basalt-ui/styles.css`
|
|
163
171
|
imported.
|
|
164
172
|
- `basaltViteConfig` adopted; `oxlint . && basalt-ui check-theme` is the lint command.
|
|
165
|
-
- `basaltAppPlugin` composed into `plugins` (see the vite.config.ts snippet above);
|
|
166
|
-
(`favicon.ico`, `favicon.svg`, `favicon-96x96.png`, `apple-touch-icon.png`,
|
|
167
|
-
`web-app-manifest-192x192.png`, `web-app-manifest-512x512.png`)
|
|
168
|
-
|
|
173
|
+
- `basaltAppPlugin` composed into `plugins` (see the vite.config.ts snippet above); either the six
|
|
174
|
+
default icon files (`favicon.ico`, `favicon.svg`, `favicon-96x96.png`, `apple-touch-icon.png`,
|
|
175
|
+
`web-app-manifest-192x192.png`, `web-app-manifest-512x512.png`) under `public/`, or an `icons`
|
|
176
|
+
array naming the ones you actually have. `doctor` reads the option out of your vite config, so an
|
|
177
|
+
app that names one SVG is not told to generate five PNGs it does not want.
|
|
169
178
|
- `site.webmanifest` is emitted by `basaltAppPlugin` — no manual manifest file needed unless
|
|
170
179
|
`manifest: false` was passed.
|
|
171
180
|
- A thin `DESIGN.md` (deltas) + the thirteen `basalt-*` rules + the managed CLAUDE block are present.
|
|
@@ -179,14 +188,18 @@ both.
|
|
|
179
188
|
- `init`, `sync`, and `check-theme` are all **real**. `sync` reconciles via a sha256 three-way diff
|
|
180
189
|
(`--check` is a CI drift gate, `--force` overwrites local edits); `check-theme` is the palette
|
|
181
190
|
guard; `doctor` checks resolution, version, guard scan, oxlint preset, spacing-scale drift and
|
|
182
|
-
cross-package `ai` major parity. All via
|
|
183
|
-
|
|
191
|
+
cross-package `ai` major parity. All via the local bin. An unrecognized flag exits 1 naming it
|
|
192
|
+
(`doctor --json` used to run doctor and exit 0), and an unknown command says so above the usage
|
|
193
|
+
block.
|
|
194
|
+
- **Not a React app?** `bunx basalt-ui tokens:css --out src/tokens.css` (no install to resolve, so
|
|
195
|
+
`bunx` is right here) emits the `--vx-*` layer with
|
|
184
196
|
no package in your dependency tree, `--selector-class dark` for the Tailwind `<html class="dark">`
|
|
185
197
|
convention, and `fonts:css` emits the shipped `--basalt-font-*` stacks. Both carry the two-line
|
|
186
198
|
`@generated basalt-ui` header the guard skips on — that header verbatim, and then only the LINES
|
|
187
199
|
that are basalt custom properties, selectors, `}` or self-closing comments, so the marker pasted
|
|
188
200
|
into a `.tsx` suppresses nothing and a declaration added to such a file is still reported — and
|
|
189
|
-
both take `--check` as a CI drift gate
|
|
201
|
+
both take `--check` as a CI drift gate — which compares everything but the header's provenance
|
|
202
|
+
line, so a basalt-ui version bump alone never forces a no-op commit. See <https://github.com/jkrumm/basalt-ui/blob/master/docs/FRAMEWORK-FREE.md> (not in the package).
|
|
190
203
|
- The framework ships **no** icon or notification dep — pass icons as `ReactNode` and wire toasts
|
|
191
204
|
yourself (e.g. `ThemeLabControls`' `copyIcon` / `onCopy`).
|
|
192
205
|
- The framework DOES ship `motion` (motion.dev, formerly framer-motion) as a bundled dependency —
|
|
@@ -7,8 +7,10 @@ when_to_use: User wants to add a chart, restyle an existing one structurally, in
|
|
|
7
7
|
`/basalt-charts` is the how-to for the chart system. `/basalt-design` owns the aesthetic law
|
|
8
8
|
(palette, restraint, the theme-lab loop) and the DESIGN.md precedence — read it for "what color /
|
|
9
9
|
how should this look". This skill answers "how do I wire it" with basalt-ui's `./charts` and
|
|
10
|
-
`./tokens` surfaces.
|
|
11
|
-
everything below assumes
|
|
10
|
+
`./tokens` surfaces. `.claude/rules/basalt-charts.md` — which you have — is the chart-primitive
|
|
11
|
+
contract everything below assumes (the repo's `docs/CHARTS-SPEC.md` is the long form and is not in
|
|
12
|
+
the package:
|
|
13
|
+
<https://github.com/jkrumm/basalt-ui/blob/master/docs/CHARTS-SPEC.md>) — composing `CartesianChart` (single-plot) or `ChartFrame` directly
|
|
12
14
|
(multi-pane/radial/matrix) is mandatory, mechanically enforced (see "Respect the boundary" below).
|
|
13
15
|
|
|
14
16
|
## The decision: kind vs bespoke (Rule of Three)
|
|
@@ -12,8 +12,9 @@ Colors come from the three-tier `--vx-*` token system — read `VX.*` / a `defin
|
|
|
12
12
|
never a raw hex/`rgb()`/`hsl()`. Charts are visx via `basalt-ui/charts`: every single-plot chart
|
|
13
13
|
composes `CartesianChart` (owns margins, scales, axes, grid, cursor, tooltip — draw only marks);
|
|
14
14
|
legends/tooltip rows are DERIVED from `series`, never hand-authored (`basalt/hand-rolled-plot` +
|
|
15
|
-
`basalt/chart-legend-literal` enforce both); `DualPanel`/`
|
|
16
|
-
exceptions
|
|
15
|
+
`basalt/chart-legend-literal` enforce both); `DualPanel`/`BandStrip`/`MirroredBars` declare
|
|
16
|
+
themselves exceptions, and `Donut`/`Heatmap` render no plot-assembly element so the rule never
|
|
17
|
+
fires on them. Add a kind on the third repeat, don't loosen the primitives. `basalt-ui/charts` and
|
|
17
18
|
`basalt-ui/tokens` are Mantine-free internally (a framework invariant, not something your own app
|
|
18
19
|
code must follow) — never import `@visx/*` outside a `charts/` directory (oxlint-enforced).
|
|
19
20
|
Toolchain is oxlint + oxfmt (no ESLint/Biome/Prettier) and `basalt-ui check-theme` guards the
|