basalt-ui 1.22.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 +29 -12
- package/CHANGELOG.md +28 -0
- package/MIGRATING.md +207 -12
- package/README.md +74 -21
- package/agent/rules/basalt-app.md +33 -3
- package/agent/rules/basalt-charts.md +78 -7
- package/agent/rules/basalt-content.md +3 -3
- package/agent/rules/basalt-mantine.md +4 -4
- package/agent/rules/basalt-tokens.md +35 -6
- package/agent/skills/basalt-app/SKILL.md +25 -12
- package/agent/skills/basalt-charts/SKILL.md +27 -10
- package/agent/templates/CLAUDE-block.md.tpl +3 -2
- package/agent/templates/DESIGN.md.tpl +3 -1
- package/configs/oxlint-plugin.js +49 -8
- package/configs/oxlint-plugin.test.ts +88 -31
- package/dist/charts/hooks/useBandPlot.d.ts +138 -0
- package/dist/charts/hooks/useBandPlot.d.ts.map +1 -0
- package/dist/charts/hooks/useBandPlot.js +127 -0
- package/dist/charts/index.d.ts +4 -0
- package/dist/charts/index.d.ts.map +1 -1
- package/dist/charts/index.js +14 -0
- package/dist/charts/kinds/BandStrip.d.ts +127 -0
- package/dist/charts/kinds/BandStrip.d.ts.map +1 -0
- package/dist/charts/kinds/BandStrip.js +271 -0
- package/dist/charts/kinds/BandStrip.test.d.ts +2 -0
- package/dist/charts/kinds/BandStrip.test.d.ts.map +1 -0
- package/dist/charts/kinds/Bars.d.ts +5 -1
- package/dist/charts/kinds/Bars.d.ts.map +1 -1
- package/dist/charts/kinds/Bars.js +2 -0
- package/dist/charts/kinds/MirroredBars.d.ts +95 -0
- package/dist/charts/kinds/MirroredBars.d.ts.map +1 -0
- package/dist/charts/kinds/MirroredBars.js +328 -0
- package/dist/charts/kinds/MirroredBars.test.d.ts +2 -0
- package/dist/charts/kinds/MirroredBars.test.d.ts.map +1 -0
- package/dist/charts/kinds/MultiLine.d.ts +5 -1
- package/dist/charts/kinds/MultiLine.d.ts.map +1 -1
- package/dist/charts/kinds/MultiLine.js +2 -0
- package/dist/charts/kinds/StackedArea.d.ts +5 -1
- package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
- package/dist/charts/kinds/StackedArea.js +2 -0
- package/dist/charts/kinds/ZonedLine.d.ts +5 -1
- package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
- package/dist/charts/kinds/ZonedLine.js +2 -0
- package/dist/charts/primitives/CartesianChart.d.ts +18 -2
- package/dist/charts/primitives/CartesianChart.d.ts.map +1 -1
- package/dist/charts/primitives/CartesianChart.js +5 -2
- package/dist/charts/primitives/HatchPattern.d.ts +31 -0
- package/dist/charts/primitives/HatchPattern.d.ts.map +1 -0
- package/dist/charts/primitives/HatchPattern.js +31 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +228 -31
- package/dist/cli/shipped-versions.test.d.ts +2 -0
- package/dist/cli/shipped-versions.test.d.ts.map +1 -0
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +60 -13
- package/dist/vite.d.ts +42 -3
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +67 -30
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/src/charts/hooks/useBandPlot.ts +302 -0
- package/src/charts/index.ts +14 -0
- package/src/charts/kinds/BandStrip.test.tsx +282 -0
- package/src/charts/kinds/BandStrip.tsx +457 -0
- package/src/charts/kinds/Bars.tsx +7 -1
- package/src/charts/kinds/DualPanel.test.tsx +10 -4
- package/src/charts/kinds/MirroredBars.test.tsx +232 -0
- package/src/charts/kinds/MirroredBars.tsx +491 -0
- package/src/charts/kinds/MultiLine.tsx +7 -1
- package/src/charts/kinds/StackedArea.tsx +7 -1
- package/src/charts/kinds/ZonedLine.tsx +7 -1
- package/src/charts/primitives/CartesianChart.test.tsx +86 -4
- package/src/charts/primitives/CartesianChart.tsx +28 -3
- package/src/charts/primitives/HatchPattern.tsx +55 -0
- package/src/cli/help.test.ts +74 -1
- package/src/cli/index.ts +436 -52
- package/src/cli/shipped-versions.test.ts +83 -0
- package/src/cli/tokens-css.test.ts +62 -0
- package/src/cli/toolchain-wiring.test.ts +171 -4
- package/src/cli/waiver-audit.test.ts +7 -3
- package/src/guard/check-source.test.ts +211 -42
- package/src/guard/index.ts +149 -23
- package/src/vite.test.ts +79 -0
- package/src/vite.ts +141 -39
|
@@ -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
|
|
|
@@ -58,11 +60,13 @@ radial/matrix shape, is not optional — two `basalt` oxlint plugin rules
|
|
|
58
60
|
compose `CartesianChart`. **Since 1.20.0 every assembly node is reported and waived on its own**
|
|
59
61
|
— one comment no longer grants the whole file permanent immunity, which is how a 604-line chart
|
|
60
62
|
file went unpoliced. To waive ONE node, put `theme-allow hand-rolled-plot — <why>` on it (or on a
|
|
61
|
-
comment-only line above it). A genuinely non-single-plot shape (multi-pane, radial, matrix
|
|
62
|
-
declares the whole file with `theme-allow-file hand-rolled-plot —
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
comment-only line above it). A genuinely non-single-plot shape (multi-pane, radial, matrix, or a
|
|
64
|
+
strip with no y dimension) declares the whole file with `theme-allow-file hand-rolled-plot —
|
|
65
|
+
<why>`, anywhere in it; basalt's own tree carries three, on `DualPanel`, `BandStrip` and
|
|
66
|
+
`MirroredBars`. `Donut` and `Heatmap` are non-single-plot too and carry none — they render no
|
|
67
|
+
assembly primitive, so the rule never fires and there is nothing to waive.
|
|
68
|
+
**`theme-allow-file` is the 1.21.0 spelling** — at 1.20.0 a node-scoped annotation was silently
|
|
69
|
+
promoted to a file declaration, so per-node scoping was not expressible; move the one word. The file that DEFINES `CartesianChart` is
|
|
66
70
|
exempt definitionally (detected by declaration, not by path), since a rule saying "compose X"
|
|
67
71
|
cannot fire inside X.
|
|
68
72
|
- **`basalt/chart-legend-literal`** — a hand-written array literal passed to `ChartLegend`'s
|
|
@@ -160,6 +164,13 @@ ships no map kind.
|
|
|
160
164
|
onto one domain value, silently dropping one from the plot (see #6 below for why that's the
|
|
161
165
|
cursor's problem too). `Heatmap` is deliberately excluded: its existing `colLabel`/`rowLabel`
|
|
162
166
|
already are that seam, and a second prop over one concern would fork them.
|
|
167
|
+
**Which x ticks get painted is a separate seam**, `xTickValues?: (keys, xMax) => readonly
|
|
168
|
+
string[]`, on `CartesianChart` and forwarded by `Bars`/`MultiLine`/`StackedArea`/`ZonedLine`
|
|
169
|
+
plus both band kinds. Resolution order is explicit VALUES → explicit COUNT (`xTicks`, unchanged)
|
|
170
|
+
→ as many as fit (`smartTicks`); omit both and nothing moves. Reach for it on a dense time axis,
|
|
171
|
+
because **a count cannot express one**: `smartTicks`/`smartTicksEvery` append the final key
|
|
172
|
+
unconditionally, so any count that does not land exactly on the last index paints two labels on
|
|
173
|
+
top of each other at the right edge — at every count, not at an unlucky one.
|
|
163
174
|
**The x axis is CATEGORICAL, and this is not inferable from the API.** `CartesianChart` builds
|
|
164
175
|
its x scale as `scalePoint<string>`, so N points are N evenly spaced positions whatever the
|
|
165
176
|
values behind the keys — there is no linear or time x scale. Correct and invisible for a domain
|
|
@@ -332,6 +343,49 @@ per-series `getValue` accessors on `ChartSeries<T>`):
|
|
|
332
343
|
wrapper.
|
|
333
344
|
- **`Donut`** — proportional donut with an optional center-content overlay. Composes `ChartFrame`
|
|
334
345
|
directly (radial, not cartesian).
|
|
346
|
+
- **`BandStrip`** — 1-D categorical bands over a shared x axis, **no y dimension at all**: one rect
|
|
347
|
+
per slot, `getBand(d) => { state, fill?, absentFraction?, marker? }`. `series` IS the state set —
|
|
348
|
+
it drives the legend, each band's fill and the one derived tooltip row, so a strip cannot name a
|
|
349
|
+
state it does not draw. `absentFraction` paints the share of a folded slot nothing measured as a
|
|
350
|
+
hatch; `marker` carries a fact that must not be readable off the `fill` ramp. `cursorResolution`
|
|
351
|
+
defaults `'leading'` here (a band IS a bucket), not `'nearest'`. Composes `ChartFrame` directly —
|
|
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).
|
|
355
|
+
- **`MirroredBars`** — two BAR panes over one x scale, mirrored around one baseline, each pane in
|
|
356
|
+
its own domain. `up`/`down` take `{ key, max?, autoMaxFloor?, ticks?, format }`; `upFraction`
|
|
357
|
+
(default 0.35) is the up pane's share of the band height, which is where the shared baseline sits;
|
|
358
|
+
`getAbsentFraction` hatches an unmeasured slot and `getBarOpacity` dims one. A sibling of
|
|
359
|
+
`DualPanel`, not an extension: `DualPanel`'s top pane is a LINE pane and its bottom takes one
|
|
360
|
+
SIGNED `getBar` over a symmetric domain. Two independent magnitudes are not one signed quantity.
|
|
361
|
+
Composes `ChartFrame` directly.
|
|
362
|
+
|
|
363
|
+
Both band kinds fold their domain by width (`fold: { minBandPx?, merge }`), and `foldBands` is
|
|
364
|
+
exported so a consumer can test their merge against the grouping that will actually run.
|
|
365
|
+
`HatchPattern` / `hatchFill` / `hatchSizeFor` ship the absence fill.
|
|
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".
|
|
335
389
|
|
|
336
390
|
`ZonedLine` and `MultiLine` also accept `xZones?: XZoneSpec[]` — the vertical counterpart to a
|
|
337
391
|
kind's horizontal zone bands, for marking a time window rather than a value range. Each
|
|
@@ -477,6 +531,23 @@ margin. Passing `y2` to get a dual-axis chart no longer needs a manual margin nu
|
|
|
477
531
|
The escape hatch is `margin={{ left: n }}` (`Partial<ChartMargin>`) on `CartesianChart` — a per-side
|
|
478
532
|
override applied LAST, after measurement, so it always wins.
|
|
479
533
|
|
|
534
|
+
**A band plot has one extra law, because a band axis puts its FIRST tick on the plot's left edge.**
|
|
535
|
+
With no left axis to measure, `autoMargin` stops at `VX.margin.left` and half the first x label
|
|
536
|
+
hangs off the canvas — so `BandStrip` floors its left gutter at half the widest x label, mirroring
|
|
537
|
+
what `autoMargin` already does on the right. Both terminal gutters are then capped (14% of width
|
|
538
|
+
left, 12% right): uncapped, half a `DD.MM HH:MM` label is 48px of a 338px chart. `MirroredBars`
|
|
539
|
+
probes real tick labels from both panes, so it takes the plain measured margin and neither the
|
|
540
|
+
floor nor the cap applies.
|
|
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
|
+
|
|
480
551
|
`chartMargin(opts?)` (`basalt-ui/tokens`, also re-exported from `basalt-ui/charts`) still exists,
|
|
481
552
|
but only matters for a bespoke chart that composes `ChartFrame` directly (not `CartesianChart`) and
|
|
482
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
|
|
@@ -96,6 +96,22 @@ An id is a guard kind (`raw-surface`, `inline-spacing`, …) or an oxlint plugin
|
|
|
96
96
|
still ends the block; that is how you say "this comment is not about the next statement".
|
|
97
97
|
- **`basalt/hand-rolled-plot` grants whole-file immunity only through `theme-allow-file`.** Every
|
|
98
98
|
assembly node is otherwise waived on its own.
|
|
99
|
+
- **Eight shapes are asserted NOT to waive, in both halves.** They are pinned as unsupported rather
|
|
100
|
+
than merely left out, so "unsupported" and "silently broken" stop reading the same. Don't write
|
|
101
|
+
them:
|
|
102
|
+
|
|
103
|
+
| Shape | Why it does not waive |
|
|
104
|
+
| ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
105
|
+
| a blank line between the annotation and the code — after `//`, after `{/* */}`, or after a `{/*` whose closer sits on its own line | a blank line is how you say "this comment is not about the next statement" |
|
|
106
|
+
| the token mid-sentence in a line comment, a docblock gutter, or a JSX expression comment | prose that MENTIONS the token is not an annotation — the reason it must START its comment |
|
|
107
|
+
| the token inside a string literal | same |
|
|
108
|
+
| above a multi-line OPENING tag, when the finding sits on a later attribute line | a waiver reaches the first line below its comment, not an arbitrary line further down — put it beside the attribute |
|
|
109
|
+
|
|
110
|
+
The whole grid — four axes, comment style × token position × where the closer falls × what
|
|
111
|
+
follows — is pinned row for row in `src/guard/check-source.test.ts` (37 supported + these 8) and
|
|
112
|
+
`configs/oxlint-plugin.test.ts` (32 + 8; five of the guard's rows are CSS/HTML/JSON dialects
|
|
113
|
+
oxlint never sees). Zero disagreements between the two, and no waiver tally moved in any of the
|
|
114
|
+
seven consumer repos.
|
|
99
115
|
|
|
100
116
|
### Audit them: `basalt-ui check-theme --audit-allows`
|
|
101
117
|
|
|
@@ -104,13 +120,26 @@ the guard with that one occurrence neutralized, so the verdict comes from the sc
|
|
|
104
120
|
parsing the waiver's text:
|
|
105
121
|
|
|
106
122
|
```text
|
|
123
|
+
theme-allow annotations (3):
|
|
107
124
|
src/shell/x.tsx:42 suppresses raw-surface@43
|
|
108
125
|
src/theme/y.ts:18 SUPPRESSES NOTHING — dead, delete it
|
|
109
|
-
src/charts/z.tsx:7
|
|
126
|
+
src/charts/z.tsx:7 suppresses hand-rolled-plot@8 (oxlint)
|
|
127
|
+
|
|
128
|
+
basalt.exemptRules entries (1):
|
|
110
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). …
|
|
111
134
|
```
|
|
112
135
|
|
|
113
|
-
|
|
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
|
|
114
143
|
waiver**, so it wires into CI. A waiver that stopped covering anything is not harmless: it silently
|
|
115
144
|
covers the next real finding on that line. Basalt's own source had one; it is deleted.
|
|
116
145
|
|
|
@@ -141,7 +170,7 @@ nothing is forked.
|
|
|
141
170
|
- **Exact name match only** — the one limit that is structural. Round 5 confirmed the miss rate:
|
|
142
171
|
linewatch's forks are named `Cell` and `Box`, rb's is `Stat`. Rename the fork and the rule goes
|
|
143
172
|
quiet, which is what a fork's author naturally does.
|
|
144
|
-
- **Scope: basalt consumers only, and component-shaped declarations only.** Since 1.
|
|
173
|
+
- **Scope: basalt consumers only, and component-shaped declarations only.** Since 1.22.0 the rule
|
|
145
174
|
gates on `isBasaltScopedFile` like every other rule in the file, and needs a function, an arrow, a
|
|
146
175
|
`memo`/`forwardRef` wrapper or a class extending one. A plain data class sharing a name with a
|
|
147
176
|
shipped export is a collision, not a fork — it fired on a `SlugTracker` class in a React-free
|
|
@@ -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)
|
|
@@ -16,9 +18,9 @@ everything below assumes — composing `CartesianChart` (single-plot) or `ChartF
|
|
|
16
18
|
Before writing chart code, decide which of three paths you are on:
|
|
17
19
|
|
|
18
20
|
1. **Reuse a shipped kind.** basalt-ui ships `ZonedLine`, `Bars`, `StackedArea`, `MultiLine`,
|
|
19
|
-
`DualPanel`, `Heatmap`, `Donut`. If the shape matches, use it —
|
|
20
|
-
`chartId`, `getX`, `series`/`positiveBars`, one `AxisConfig` per
|
|
21
|
-
as plain arrays). Done.
|
|
21
|
+
`DualPanel`, `Heatmap`, `Donut`, `BandStrip`, `MirroredBars`. If the shape matches, use it —
|
|
22
|
+
props are declarative (`data`, `chartId`, `getX`, `series`/`positiveBars`, one `AxisConfig` per
|
|
23
|
+
axis, zones/thresholds/refLines as plain arrays). Done.
|
|
22
24
|
- **`ZonedLine`** — a single-series line with zone bands, thresholds, x-zones, refLines.
|
|
23
25
|
- **`Bars`** — 1+ stacked positive/negative bar series, optional line overlays, dual-axis config.
|
|
24
26
|
- **`StackedArea`** — opaque stacked bands, auto cumulative-top y-domain.
|
|
@@ -33,6 +35,13 @@ Before writing chart code, decide which of three paths you are on:
|
|
|
33
35
|
- **`Heatmap`** — category×category intensity grid (`color-mix` alpha), per-cell tooltip,
|
|
34
36
|
optional gradient legend strip. Replaces bespoke time-of-day.
|
|
35
37
|
- **`Donut`** — proportional donut, optional `centerContent` overlay.
|
|
38
|
+
- **`BandStrip`** — 1-D categorical bands, no y dimension: one rect per slot,
|
|
39
|
+
`getBand(d) => { state, fill?, absentFraction?, marker? }`. `series` is the state set and
|
|
40
|
+
drives legend, fill and the one derived tooltip row. Replaces a bespoke state/availability
|
|
41
|
+
strip.
|
|
42
|
+
- **`MirroredBars`** — two BAR panes over one x scale and one baseline, each in its own domain
|
|
43
|
+
(`up`/`down` take `{ key, max?, autoMaxFloor?, ticks?, format }`). Not `DualPanel`, whose top
|
|
44
|
+
pane is a LINE and whose bottom takes one SIGNED accessor.
|
|
36
45
|
2. **Extract a new kind (second instance of a pattern).** When you are about to build the _second_
|
|
37
46
|
chart of a shape that has no shipped kind, extract a kind. Don't extract on the first; don't
|
|
38
47
|
wait past the third. A new kind lives in the consumer's chart kinds dir (or, if generic enough
|
|
@@ -43,6 +52,12 @@ Before writing chart code, decide which of three paths you are on:
|
|
|
43
52
|
genuinely different scales that don't fit any kind's config surface) composes `CartesianChart`
|
|
44
53
|
or `ChartFrame` directly in the page's chart file — not in `kinds/`.
|
|
45
54
|
|
|
55
|
+
**Extracting a kind? Prove it by porting.** A kind is proven by moving a real consumer's call
|
|
56
|
+
sites onto it and reporting what it could NOT express — not by a demo page, which is written
|
|
57
|
+
against the API that already exists. The two band kinds were built that way (linewatch, 1884 → 957
|
|
58
|
+
source lines, 11 `hand-rolled-plot` waivers retired, two live `NaN`-geometry bugs found). Ship the
|
|
59
|
+
"still cannot express" list with the kind.
|
|
60
|
+
|
|
46
61
|
> Anti-pattern: a single `<Chart type="..." config={...} />` god-component that switches by kind.
|
|
47
62
|
> That is the Recharts trap. Prefer N small kinds. **If a chart doesn't fit the primitives, add a
|
|
48
63
|
> kind — don't loosen the primitives.**
|
|
@@ -109,8 +124,9 @@ Theme tuning of these series happens in the theme lab — pass them as `groups`
|
|
|
109
124
|
## Compose the primitives (never hand-roll)
|
|
110
125
|
|
|
111
126
|
Every chart — kind or bespoke — composes `CartesianChart` for a single plot rect, or `ChartFrame`
|
|
112
|
-
directly for a multi-pane / non-cartesian shape
|
|
113
|
-
`
|
|
127
|
+
directly for a multi-pane / non-cartesian shape — five of them ship: `DualPanel`'s two panes,
|
|
128
|
+
`MirroredBars`' two bar panes, `BandStrip`'s missing y dimension, `Heatmap`'s grid, `Donut`'s ring.
|
|
129
|
+
Either way the contract is the same: the primitive owns margin, scales, grid,
|
|
114
130
|
axes, legend, the shared cursor, and the tooltip; you supply `series` and draw ONLY marks. This is
|
|
115
131
|
mandatory, not a convention — `basalt/hand-rolled-plot` fails the build on a bypass (see "Respect
|
|
116
132
|
the boundary" below); it is easier to compose it than to work around it.
|
|
@@ -212,9 +228,10 @@ import { ChartCursorScope } from 'basalt-ui/charts'
|
|
|
212
228
|
primitive (`AxisLeftNumeric`, `AxisRightNumeric`, `AxisBottomDate`, `HoverOverlay`, `Crosshair`)
|
|
213
229
|
rendered in a file that doesn't compose `CartesianChart`, per NODE. Escape:
|
|
214
230
|
`theme-allow hand-rolled-plot — <why>` on the one node, or `theme-allow-file hand-rolled-plot —
|
|
215
|
-
<why>` anywhere in the file — how a genuinely non-single-plot shape (`DualPanel`, `
|
|
216
|
-
`
|
|
217
|
-
`
|
|
231
|
+
<why>` anywhere in the file — how a genuinely non-single-plot shape (`DualPanel`, `MirroredBars`,
|
|
232
|
+
`BandStrip`; `Heatmap` and `Donut` render no assembly primitive and need none) declares itself.
|
|
233
|
+
`-file` is the 1.21.0 spelling and it is required. The file that defines `CartesianChart` is
|
|
234
|
+
exempt definitionally.
|
|
218
235
|
- **No hand-authored legends.** `basalt/chart-legend-literal` fails the build on a hand-written
|
|
219
236
|
array literal passed to `ChartLegend`'s `items` — derive it from the same `series` the chart draws
|
|
220
237
|
(`deriveLegend`, or let `ChartFrame`/`CartesianChart` do it) so it can't name a series the plot no
|
|
@@ -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
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# {{APP_NAME}} — Design
|
|
2
2
|
|
|
3
|
-
> Managed by basalt-ui
|
|
3
|
+
> Managed by basalt-ui — run `basalt-ui doctor` for the version, never trust a number written into
|
|
4
|
+
> a file (this one is a seed: written once at `init`, then owned by you, so any version stamped
|
|
5
|
+
> here would be frozen at scaffold time). This is a **thin** instantiation — it records this
|
|
4
6
|
> app's **deltas only** on top of the shipped `basalt-*` rules. The universal law (earned color,
|
|
5
7
|
> neutral-by-default, three-tier `--vx-*` tokens, theme-is-data, the chart primitive contract, the
|
|
6
8
|
> elevation/density/shape doctrine) lives in those rules and the `/basalt:design` skill, and is
|
package/configs/oxlint-plugin.js
CHANGED
|
@@ -245,8 +245,49 @@ function allComments(context) {
|
|
|
245
245
|
}
|
|
246
246
|
|
|
247
247
|
/**
|
|
248
|
-
*
|
|
249
|
-
*
|
|
248
|
+
* Every comment that REACHES `nodeLine` — the comment on the node's own line, the one directly
|
|
249
|
+
* above it, and any further comment separated from that one by nothing but comment.
|
|
250
|
+
*
|
|
251
|
+
* This is the plugin's half of "which node does this annotation scope to", and it has to answer the
|
|
252
|
+
* same question `src/guard`'s forward walk answers: **an annotation reaches the first line below its
|
|
253
|
+
* comment that is not itself comment.** The test used to be a flat `end.line === nodeLine ||
|
|
254
|
+
* nodeLine - 1`, which is the same rule only when the annotation is the LAST comment above the node
|
|
255
|
+
* — so anything sitting between the annotation and the node, comment included, was reported here
|
|
256
|
+
* while the guard waived it:
|
|
257
|
+
*
|
|
258
|
+
* ```text
|
|
259
|
+
* // theme-allow raw-hex — the vendor brand hex, kept in sync with ← a reason that wrapped
|
|
260
|
+
* // the marketing site's own palette onto a second comment
|
|
261
|
+
* const BRAND = '#ff0000'
|
|
262
|
+
*
|
|
263
|
+
* {/* theme-allow card-inset — … *\/}
|
|
264
|
+
* {/* 3. the density row *\/} ← an unrelated note
|
|
265
|
+
* <Card p={0} />
|
|
266
|
+
* ```
|
|
267
|
+
*
|
|
268
|
+
* The wrapped-reason one is not hypothetical: it is pinned in the guard's tests as a shape argo
|
|
269
|
+
* writes. A blank line still separates, because a blank line is not comment — which is what makes
|
|
270
|
+
* "this comment is not about the next statement" expressible in both halves.
|
|
271
|
+
*
|
|
272
|
+
* Returned bottom-up, which is also the cheap order to walk: stop at the first gap.
|
|
273
|
+
*/
|
|
274
|
+
function reachingComments(context, nodeLine) {
|
|
275
|
+
const comments = allComments(context).toSorted((a, b) => a.loc.start.line - b.loc.start.line)
|
|
276
|
+
const out = []
|
|
277
|
+
let reach = nodeLine
|
|
278
|
+
for (let i = comments.length - 1; i >= 0; i--) {
|
|
279
|
+
const comment = comments[i]
|
|
280
|
+
if (comment.loc.end.line > nodeLine) continue
|
|
281
|
+
if (comment.loc.end.line < reach - 1) break
|
|
282
|
+
out.push(comment)
|
|
283
|
+
reach = Math.min(reach, comment.loc.start.line)
|
|
284
|
+
}
|
|
285
|
+
return out
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* True when a NODE-scoped `theme-allow` covering `ruleId` reaches `node` (see
|
|
290
|
+
* {@link reachingComments}) — or when the file carries a `theme-allow-file` declaration for it.
|
|
250
291
|
*
|
|
251
292
|
* The `ruleId` argument is what stops one exemption from being a blanket one: a `theme-allow
|
|
252
293
|
* raw-hex — …` written for a color no longer silently switches off `card-inset` on the same line.
|
|
@@ -254,13 +295,11 @@ function allComments(context) {
|
|
|
254
295
|
*/
|
|
255
296
|
function hasThemeAllow(context, node, ruleId) {
|
|
256
297
|
if (hasFileDeclaration(context, ruleId)) return true
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
if (comment.loc.end.line !== nodeLine && comment.loc.end.line !== nodeLine - 1) return false
|
|
260
|
-
return parseThemeAllows(comment.value).some(
|
|
298
|
+
return reachingComments(context, node.loc.start.line).some((comment) =>
|
|
299
|
+
parseThemeAllows(comment.value).some(
|
|
261
300
|
(allow) => allow.scope === 'line' && allowCovers(allow, ruleId),
|
|
262
|
-
)
|
|
263
|
-
|
|
301
|
+
),
|
|
302
|
+
)
|
|
264
303
|
}
|
|
265
304
|
|
|
266
305
|
/**
|
|
@@ -535,10 +574,12 @@ const cardInset = {
|
|
|
535
574
|
// ── Rule 3 — chart-in-raw-surface ───────────────────────────────────────────────────────────────
|
|
536
575
|
|
|
537
576
|
const CHART_TAGS = new Set([
|
|
577
|
+
'BandStrip',
|
|
538
578
|
'Bars',
|
|
539
579
|
'Donut',
|
|
540
580
|
'DualPanel',
|
|
541
581
|
'Heatmap',
|
|
582
|
+
'MirroredBars',
|
|
542
583
|
'MultiLine',
|
|
543
584
|
'StackedArea',
|
|
544
585
|
'ZonedLine',
|