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/src/guard/index.ts
CHANGED
|
@@ -400,9 +400,55 @@ const RAW_CHART_LEGEND_ARRAY = /<ChartLegend\b[^>]*?\bitems\s*=\s*\{\s*\[/g
|
|
|
400
400
|
// alternative so their `>` never terminates the tag early. A bare `>` comparison inside a prop
|
|
401
401
|
// expression still ends the match (accepted limitation of the bounded scan).
|
|
402
402
|
const CHART_ENTRY_POINT_TAG =
|
|
403
|
-
/<(
|
|
403
|
+
/<(MultiLine|Bars|BandStrip|Donut|DualPanel|Heatmap|MirroredBars|ZonedLine|StackedArea|LineSparkline|BarSparkline)\b(?:<[^<>]*>)?(?:=>|[^>])*?>/g
|
|
404
404
|
const HAS_ARIA_LABEL_PROP = /\bariaLabel\s*=/
|
|
405
405
|
|
|
406
|
+
// ── Tag provenance, shared by the two chart tag rules ────────────────────────────────────────────
|
|
407
|
+
//
|
|
408
|
+
// Both `chart-missing-aria-label` and `unframed-chart` key on a JSX tag NAME, which is the whole of
|
|
409
|
+
// their signal — so a consumer's OWN component that merely shares a shipped kind's name collected
|
|
410
|
+
// the finding too. Reported by a consumer on 1.23.0: a hand-composed local `<MirroredBars>` was
|
|
411
|
+
// told to pass an `ariaLabel` prop it does not accept, by a rule that presents as a correctness
|
|
412
|
+
// finding rather than as a naming one. `shadow-basalt-export` / `ai-sdk-major` scope through
|
|
413
|
+
// `isBasaltScopedFile` (package-level); the analogous file-level signal here is where the name in
|
|
414
|
+
// THIS file came from.
|
|
415
|
+
//
|
|
416
|
+
// The gate is deliberately a one-directional NARROWING: a tag is skipped only when the file DEFINES
|
|
417
|
+
// a component of that name and does not also import it from basalt-ui. Everything else still fires
|
|
418
|
+
// — a tag imported from basalt-ui, one imported from a consumer barrel that re-exports it (`import
|
|
419
|
+
// { MultiLine } from '../charts'`, the shape a downstream component library uses), and one the scan
|
|
420
|
+
// cannot attribute at all. Requiring a POSITIVE basalt import instead would have silently switched
|
|
421
|
+
// both rules off for every barrel-wrapping consumer and for every file with no import statements —
|
|
422
|
+
// a far bigger hole than the false positive it closes.
|
|
423
|
+
const LOCAL_COMPONENT_DEF =
|
|
424
|
+
/(?:^|[\n;}])\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?(?:function|class|const|let|var)\s+([A-Z]\w*)/g
|
|
425
|
+
// `from 'basalt-ui'` and every subpath (`basalt-ui/charts`, `basalt-ui/tokens`, …).
|
|
426
|
+
const BASALT_NAMED_IMPORT =
|
|
427
|
+
/import\s+(?:type\s+)?\{([^}]*)\}\s+from\s+['"]basalt-ui(?:\/[^'"]*)?['"]/g
|
|
428
|
+
|
|
429
|
+
/** Component-shaped names DEFINED in this file — the provable "not the shipped kind" signal. */
|
|
430
|
+
function localComponentNames(codeText: string): ReadonlySet<string> {
|
|
431
|
+
const out = new Set<string>()
|
|
432
|
+
for (const m of codeText.matchAll(LOCAL_COMPONENT_DEF)) out.add(m[1] as string)
|
|
433
|
+
return out
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** Names bound in this file by a named import from `basalt-ui` — the local binding, so
|
|
437
|
+
* `{ Bars as BasaltBars }` records `BasaltBars`, which is the name the JSX tag will use. */
|
|
438
|
+
function basaltImportedNames(codeText: string): ReadonlySet<string> {
|
|
439
|
+
const out = new Set<string>()
|
|
440
|
+
for (const m of codeText.matchAll(BASALT_NAMED_IMPORT)) {
|
|
441
|
+
for (const raw of (m[1] as string).split(',')) {
|
|
442
|
+
const spec = raw.trim().replace(/^type\s+/, '')
|
|
443
|
+
if (spec.length === 0) continue
|
|
444
|
+
const parts = spec.split(/\s+as\s+/)
|
|
445
|
+
const bound = (parts[parts.length - 1] as string).trim()
|
|
446
|
+
if (bound.length > 0) out.add(bound)
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return out
|
|
450
|
+
}
|
|
451
|
+
|
|
406
452
|
// Raw lowercase form-control element — line-scoped, same shape as RAW_VISX_AXIS. `\b` after the
|
|
407
453
|
// tag name rejects a same-prefixed custom component (`<inputRef`, `<selectAll`).
|
|
408
454
|
const RAW_FORM_CONTROL = /<(?:input|select|textarea)\b/g
|
|
@@ -2047,10 +2093,25 @@ export function checkSource(text: string, relPath: string, cfg: GuardConfig): Fi
|
|
|
2047
2093
|
}
|
|
2048
2094
|
}
|
|
2049
2095
|
|
|
2096
|
+
// Both chart tag rules below gate on tag PROVENANCE (see the block above LOCAL_COMPONENT_DEF).
|
|
2097
|
+
// Computed once, and only if one of the two actually runs.
|
|
2098
|
+
const unframedChartRuns =
|
|
2099
|
+
GUARD_RULES['unframed-chart'].enabled!(cfg) && ruleApplies('unframed-chart', relPath)
|
|
2100
|
+
const ariaLabelRuns =
|
|
2101
|
+
GUARD_RULES['chart-missing-aria-label'].enabled!(cfg) &&
|
|
2102
|
+
ruleApplies('chart-missing-aria-label', relPath)
|
|
2103
|
+
const localNames =
|
|
2104
|
+
unframedChartRuns || ariaLabelRuns ? localComponentNames(codeText) : new Set<string>()
|
|
2105
|
+
const basaltNames =
|
|
2106
|
+
unframedChartRuns || ariaLabelRuns ? basaltImportedNames(codeText) : new Set<string>()
|
|
2107
|
+
const isShippedTag = (tag: string): boolean => basaltNames.has(tag) || !localNames.has(tag)
|
|
2108
|
+
|
|
2050
2109
|
// unframed-chart — full-text tag-scoped scan (not per-line, see RAW_CHART_LEGEND_ARRAY comment).
|
|
2051
2110
|
// Scans `codeText` (comment-stripped) so a legend example inside a comment can't match; reports
|
|
2052
|
-
// at the line of the `items={[` token itself.
|
|
2053
|
-
|
|
2111
|
+
// at the line of the `items={[` token itself. Skipped wholesale when `ChartLegend` is this file's
|
|
2112
|
+
// OWN component: a consumer's legend taking an `items` array is not the shipped one that derives
|
|
2113
|
+
// its entries, and the remedy ("pass deriveLegend(series)") is unreachable there.
|
|
2114
|
+
if (unframedChartRuns && isShippedTag('ChartLegend')) {
|
|
2054
2115
|
for (const m of codeText.matchAll(RAW_CHART_LEGEND_ARRAY)) {
|
|
2055
2116
|
const lineNo = codeText.slice(0, (m.index ?? 0) + m[0].length).split('\n').length
|
|
2056
2117
|
push('unframed-chart', lineNo, 'items={[')
|
|
@@ -2064,12 +2125,11 @@ export function checkSource(text: string, relPath: string, cfg: GuardConfig): Fi
|
|
|
2064
2125
|
// which is what `Finding.text` would then quote back — the closing bracket rather than the tag
|
|
2065
2126
|
// that is missing the prop. The allow-comment is honored anywhere in the tag's span, so a
|
|
2066
2127
|
// `theme-allow` that used to sit on the closing line keeps working.
|
|
2067
|
-
if (
|
|
2068
|
-
GUARD_RULES['chart-missing-aria-label'].enabled!(cfg) &&
|
|
2069
|
-
ruleApplies('chart-missing-aria-label', relPath)
|
|
2070
|
-
) {
|
|
2128
|
+
if (ariaLabelRuns) {
|
|
2071
2129
|
for (const m of codeText.matchAll(CHART_ENTRY_POINT_TAG)) {
|
|
2072
2130
|
const tagText = m[0]
|
|
2131
|
+
// A tag this file defines itself is not the shipped kind — it does not take `ariaLabel`.
|
|
2132
|
+
if (!isShippedTag(m[1] as string)) continue
|
|
2073
2133
|
if (HAS_ARIA_LABEL_PROP.test(tagText)) continue
|
|
2074
2134
|
const startLine = codeText.slice(0, m.index ?? 0).split('\n').length
|
|
2075
2135
|
const endLine = startLine + (tagText.split('\n').length - 1)
|