basalt-ui 1.29.2 → 1.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/MIGRATING.md +693 -368
  3. package/README.md +23 -2
  4. package/agent/rules/basalt-controls.md +34 -34
  5. package/agent/skills/basalt-app/SKILL.md +4 -2
  6. package/agent/templates/query-client.ts.tpl +18 -4
  7. package/configs/lefthook.yml +51 -19
  8. package/configs/oxlint-plugin.js +210 -84
  9. package/configs/oxlint-plugin.test.ts +182 -5
  10. package/configs/oxlint.json +4 -4
  11. package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
  12. package/dist/charts/primitives/ChartCard.js +2 -2
  13. package/dist/charts/primitives/ChartFrame.d.ts +23 -3
  14. package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
  15. package/dist/charts/primitives/ChartFrame.js +4 -4
  16. package/dist/charts/primitives/chart-frame-layout.d.ts +55 -0
  17. package/dist/charts/primitives/chart-frame-layout.d.ts.map +1 -1
  18. package/dist/charts/primitives/chart-frame-layout.js +16 -0
  19. package/dist/cli/check-theme.d.ts.map +1 -1
  20. package/dist/cli/check-theme.js +6 -0
  21. package/dist/cli/doctor.d.ts +10 -4
  22. package/dist/cli/doctor.d.ts.map +1 -1
  23. package/dist/cli/doctor.js +28 -1
  24. package/dist/cli/help.d.ts.map +1 -1
  25. package/dist/cli/help.js +2 -1
  26. package/dist/cli/index.d.ts +12 -0
  27. package/dist/cli/index.d.ts.map +1 -1
  28. package/dist/cli/index.js +29 -0
  29. package/dist/cli/init.d.ts.map +1 -1
  30. package/dist/cli/init.js +4 -15
  31. package/dist/cli/sync.d.ts.map +1 -1
  32. package/dist/cli/sync.js +8 -14
  33. package/dist/controls/actions.d.ts.map +1 -1
  34. package/dist/controls/actions.js +12 -2
  35. package/dist/dashboard/section.module.css +15 -7
  36. package/dist/dashboard/stat-card.d.ts.map +1 -1
  37. package/dist/dashboard/stat-card.js +7 -2
  38. package/dist/dashboard/stat-group.d.ts +8 -2
  39. package/dist/dashboard/stat-group.d.ts.map +1 -1
  40. package/dist/dashboard/stat-group.js +23 -15
  41. package/dist/dashboard/stat-group.module.css +85 -9
  42. package/dist/dashboard/stat-group.module.css.d.ts +1 -0
  43. package/dist/dashboard/widget-grid.d.ts.map +1 -1
  44. package/dist/dashboard/widget-grid.js +4 -5
  45. package/dist/dashboard/widget-grid.module.css +64 -21
  46. package/dist/dashboard/widget-grid.module.css.d.ts +1 -0
  47. package/dist/guard/index.d.ts +19 -2
  48. package/dist/guard/index.d.ts.map +1 -1
  49. package/dist/guard/index.js +27 -4
  50. package/dist/index.d.ts +9 -0
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +2 -0
  53. package/dist/shell/app-brand.module.css +12 -3
  54. package/dist/shell/app-header.module.css +49 -9
  55. package/dist/shell/app-main.module.css +24 -4
  56. package/dist/shell/app-mobile-nav.d.ts.map +1 -1
  57. package/dist/shell/app-mobile-nav.js +98 -51
  58. package/dist/shell/app-mobile-nav.module.css +99 -11
  59. package/dist/shell/app-mobile-nav.module.css.d.ts +1 -0
  60. package/dist/shell/app-sidebar.module.css +55 -2
  61. package/dist/shell/index.d.ts.map +1 -1
  62. package/dist/shell/index.js +3 -3
  63. package/dist/shell/mobile-nav-model.d.ts +26 -7
  64. package/dist/shell/mobile-nav-model.d.ts.map +1 -1
  65. package/dist/shell/mobile-nav-model.js +12 -3
  66. package/dist/shell/page-aside.module.css +8 -6
  67. package/dist/shell/page-bar.d.ts +13 -0
  68. package/dist/shell/page-bar.d.ts.map +1 -1
  69. package/dist/shell/page-bar.js +13 -3
  70. package/dist/shell/page-bar.module.css +20 -12
  71. package/dist/shell/page-title.module.css +3 -1
  72. package/dist/theme/use-basalt-spacing.d.ts.map +1 -1
  73. package/dist/tokens/index.d.ts.map +1 -1
  74. package/dist/tokens/index.js +51 -7
  75. package/dist/tokens/palette.d.ts +98 -17
  76. package/dist/tokens/palette.d.ts.map +1 -1
  77. package/dist/tokens/palette.js +110 -18
  78. package/dist/tokens.css +17 -5
  79. package/llms.txt +2 -2
  80. package/package.json +1 -1
  81. package/src/charts/primitives/ChartCard.tsx +12 -7
  82. package/src/charts/primitives/ChartFrame.tsx +30 -15
  83. package/src/charts/primitives/chart-frame-layout.ts +86 -0
  84. package/src/cli/check-theme.ts +13 -0
  85. package/src/cli/doctor.ts +49 -8
  86. package/src/cli/help.ts +5 -3
  87. package/src/cli/index.ts +49 -0
  88. package/src/cli/init.ts +6 -27
  89. package/src/cli/sync.ts +11 -22
  90. package/src/controls/actions.tsx +51 -0
  91. package/src/dashboard/section.module.css +15 -7
  92. package/src/dashboard/stat-card.tsx +7 -2
  93. package/src/dashboard/stat-group.module.css +85 -9
  94. package/src/dashboard/stat-group.module.css.d.ts +1 -0
  95. package/src/dashboard/stat-group.tsx +40 -20
  96. package/src/dashboard/widget-grid.module.css +64 -21
  97. package/src/dashboard/widget-grid.module.css.d.ts +1 -0
  98. package/src/dashboard/widget-grid.tsx +30 -12
  99. package/src/guard/index.ts +79 -11
  100. package/src/index.ts +9 -0
  101. package/src/shell/app-brand.module.css +12 -3
  102. package/src/shell/app-header.module.css +49 -9
  103. package/src/shell/app-main.module.css +24 -4
  104. package/src/shell/app-mobile-nav.module.css +99 -11
  105. package/src/shell/app-mobile-nav.module.css.d.ts +1 -0
  106. package/src/shell/app-mobile-nav.tsx +225 -83
  107. package/src/shell/app-sidebar.module.css +55 -2
  108. package/src/shell/index.tsx +26 -6
  109. package/src/shell/mobile-nav-model.ts +37 -9
  110. package/src/shell/page-aside.module.css +8 -6
  111. package/src/shell/page-bar.module.css +20 -12
  112. package/src/shell/page-bar.tsx +39 -5
  113. package/src/shell/page-title.module.css +3 -1
  114. package/src/theme/use-basalt-spacing.ts +8 -6
  115. package/src/tokens/index.ts +90 -6
  116. package/src/tokens/palette.ts +111 -18
@@ -14,6 +14,7 @@ import { dirname, resolve } from 'node:path'
14
14
  // eslint-disable-next-line -- the plugin is plain JS; the ledger + id set are named exports beside it
15
15
  import basaltPlugin, {
16
16
  CTL_THEME_TAGS,
17
+ WAIVER_ID_ALIASES,
17
18
  DEPRECATED_EXPORTS,
18
19
  KNOWN_RULE_IDS,
19
20
  PLUGIN_RULE_ADVISORY,
@@ -21,7 +22,11 @@ import basaltPlugin, {
21
22
  RETIRED_RULE_IDS,
22
23
  } from './oxlint-plugin.js'
23
24
  import { CTL_THEME } from '../src/theme/ctl-theme'
24
- import { GUARD_RULES, PLUGIN_RULE_IDS } from '../src/guard/index.ts'
25
+ import {
26
+ GUARD_RULES,
27
+ PLUGIN_RULE_IDS,
28
+ WAIVER_ID_ALIASES as GUARD_WAIVER_ID_ALIASES,
29
+ } from '../src/guard/index.ts'
25
30
  import { PLUGIN_RULE_ID_LIST, SURFACES } from '../src/surfaces.ts'
26
31
 
27
32
  const PLUGIN_PATH = resolve(import.meta.dirname, 'oxlint-plugin.js')
@@ -1547,6 +1552,37 @@ describe('KNOWN_RULE_IDS', () => {
1547
1552
  })
1548
1553
  })
1549
1554
 
1555
+ // ── one law, two rule ids ──────────────────────────────────────────────────────────────────────
1556
+
1557
+ /**
1558
+ * `basalt/control-outside-home` and the guard's `raw-selection-control` are the SAME law read by
1559
+ * two engines — the guard's own message says so — and a waiver naming either used to silence only
1560
+ * the lane whose id it spelled. The working annotation was `theme-allow raw-selection-control
1561
+ * control-outside-home`: two ids, undocumented, one silent debug cycle per consumer.
1562
+ */
1563
+ describe('WAIVER_ID_ALIASES', () => {
1564
+ it('is identical in both lanes', () => {
1565
+ expect(WAIVER_ID_ALIASES).toEqual(GUARD_WAIVER_ID_ALIASES)
1566
+ })
1567
+
1568
+ it('names only ids both registries know', () => {
1569
+ for (const [id, aliases] of Object.entries(WAIVER_ID_ALIASES)) {
1570
+ expect([id, KNOWN_RULE_IDS.has(id)]).toEqual([id, true])
1571
+ for (const alias of aliases) expect([alias, KNOWN_RULE_IDS.has(alias)]).toEqual([alias, true])
1572
+ }
1573
+ })
1574
+
1575
+ it('waives the plugin rule under the TEXT lane’s id', () => {
1576
+ const { code, rules } = run(
1577
+ `import { Select } from '@mantine/core'\nexport const C = () => (\n <div>\n` +
1578
+ ` {/* theme-allow raw-selection-control — the Tauri header has no shell */}\n` +
1579
+ ` <Select data={[]} />\n </div>\n)\n`,
1580
+ )
1581
+ expect(code).toBe(0)
1582
+ expect(rules).not.toContain('control-outside-home')
1583
+ })
1584
+ })
1585
+
1550
1586
  // ── theme-allow fails closed on a typo ─────────────────────────────────────────────────────────
1551
1587
 
1552
1588
  describe('theme-allow with an unrecognized rule id', () => {
@@ -1605,6 +1641,23 @@ const MANTINE_IMPORT = `import { Button, ScrollArea, SegmentedControl, Select }
1605
1641
  const BASALT_IMPORT = `import { ChartCard, PageBar, Section, SettingsSection } from 'basalt-ui'\n`
1606
1642
 
1607
1643
  describe('basalt/hand-rolled-filter', () => {
1644
+ /**
1645
+ * The deliberate consequence of teaching the slot resolver to read an object spread: a slot IS a
1646
+ * slot in either spelling, so a raw Mantine control conditionally spread into one moves from
1647
+ * `control-outside-home` (warn, and the WRONG verdict — it does have a home) to this rule
1648
+ * (error, and the right one). One home model or none; a spread that is a home for the rule that
1649
+ * exempts and not for the rule that polices is the silent half-application this wave removes.
1650
+ */
1651
+ it('flags a raw Select handed to a slot written as an object spread', () => {
1652
+ const { code, rules } = run(
1653
+ `${BASALT_IMPORT}${MANTINE_IMPORT}export const C = ({ cond }) => (\n` +
1654
+ ` <Section {...(cond && { filters: <Select data={[]} /> })}>body</Section>\n)\n`,
1655
+ )
1656
+ expect(code).toBe(1)
1657
+ expect(rules).toContain('hand-rolled-filter')
1658
+ expect(rules).not.toContain('control-outside-home')
1659
+ })
1660
+
1608
1661
  it('flags a raw Mantine Select handed to a PageBar filters slot', () => {
1609
1662
  const { code, rules } = run(
1610
1663
  `${BASALT_IMPORT}${MANTINE_IMPORT}export const C = () => <PageBar filters={<Select data={[]} />} />\n`,
@@ -2208,9 +2261,81 @@ describe('basalt/bound-control-outside-home', () => {
2208
2261
  )
2209
2262
  expect(rules).toContain('bound-control-outside-home')
2210
2263
  })
2264
+
2265
+ /**
2266
+ * A slot written as an OBJECT SPREAD is the same home as a slot written as a JSXAttribute — and
2267
+ * `exactOptionalPropertyTypes` FORCES the spread, because `tabs={cond ? <X/> : undefined}` is not
2268
+ * assignable to an optional prop. Every control rule resolved homes off JSXAttribute nodes alone,
2269
+ * so a strict consumer's conditional slot was a false positive on a rule scheduled to go `error`.
2270
+ */
2271
+ describe('a slot written as an object spread', () => {
2272
+ it.each([
2273
+ '{...(cond && { tabs: <SelectFilter field={f} /> })}',
2274
+ '{...(cond ? { tabs: <SelectFilter field={f} /> } : {})}',
2275
+ '{...{ filters: <SelectFilter field={f} /> }}',
2276
+ ])('does NOT flag %s on a Section', (spread) => {
2277
+ const { code, rules } = run(
2278
+ `${ASIDE_IMPORT}${CONTROLS_IMPORT}export const C = ({ cond }) => (\n <Section ${spread}>body</Section>\n)\n`,
2279
+ )
2280
+ expect(code).toBe(0)
2281
+ expect(rules).not.toContain('bound-control-outside-home')
2282
+ })
2283
+
2284
+ it('does NOT flag a hoisted binding handed to a spread slot', () => {
2285
+ const { rules } = run(
2286
+ `${ASIDE_IMPORT}${CONTROLS_IMPORT}const tabs = <SelectFilter field={f} />\n` +
2287
+ `export const C = ({ cond }) => <Section {...(cond && { tabs })}>body</Section>\n`,
2288
+ )
2289
+ expect(rules).not.toContain('bound-control-outside-home')
2290
+ })
2291
+
2292
+ // Deliberately narrow: only a Property reaching a JSXSpreadAttribute through object / `&&` /
2293
+ // `?:` nodes is claimed. A non-slot key, and an object built anywhere else, are not homes.
2294
+ it('still flags a non-slot key in the same spread', () => {
2295
+ const { rules } = run(
2296
+ `${ASIDE_IMPORT}${CONTROLS_IMPORT}export const C = ({ cond }) => (\n` +
2297
+ ` <Section {...(cond && { footer: <SelectFilter field={f} /> })}>body</Section>\n)\n`,
2298
+ )
2299
+ expect(rules).toContain('bound-control-outside-home')
2300
+ })
2301
+
2302
+ it('still flags a slot-shaped object built inside a .map() callback', () => {
2303
+ const { rules } = run(
2304
+ `${CONTROLS_IMPORT}export const C = () => [1].map((k) => ({ tabs: <SelectFilter field={f} /> }))\n`,
2305
+ )
2306
+ expect(rules).toContain('bound-control-outside-home')
2307
+ })
2308
+
2309
+ it('still flags one on a tag that is not a slot owner', () => {
2310
+ const { rules } = run(
2311
+ `import { Stack } from '@mantine/core'\n${CONTROLS_IMPORT}export const C = ({ cond }) => (\n` +
2312
+ ` <Stack {...(cond && { tabs: <SelectFilter field={f} /> })} />\n)\n`,
2313
+ )
2314
+ expect(rules).toContain('bound-control-outside-home')
2315
+ })
2316
+ })
2317
+
2318
+ it('names PageBar as the reachable home for a shell-less app', () => {
2319
+ const { output } = run(
2320
+ `import { Stack } from '@mantine/core'\n${CONTROLS_IMPORT}` +
2321
+ `export const C = () => (\n <Stack>\n <SelectFilter field={f} />\n </Stack>\n)\n`,
2322
+ )
2323
+ expect(output).toContain('PageBar` needs no `BasaltShell`')
2324
+ })
2211
2325
  })
2212
2326
 
2213
2327
  describe('basalt/control-size-literal', () => {
2328
+ // Same widening as `hand-rolled-filter` above, for the same reason: a slot is a slot in either
2329
+ // spelling, and the tier the message promises is mounted by the slot however it was written.
2330
+ it('flags a size prop inside a slot written as an object spread', () => {
2331
+ const { code, rules } = run(
2332
+ `${BASALT_IMPORT}${MANTINE_IMPORT}export const C = ({ cond }) => (\n` +
2333
+ ` <Section {...(cond && { actions: <Button size="xs">Go</Button> })}>body</Section>\n)\n`,
2334
+ )
2335
+ expect(code).toBe(1)
2336
+ expect(rules).toContain('control-size-literal')
2337
+ })
2338
+
2214
2339
  it.each(['size="xs"', 'w={200}', 'fullWidth', 'visibleFrom="sm"', 'hiddenFrom="sm"'])(
2215
2340
  'flags %s on an element inside a home slot',
2216
2341
  (prop) => {
@@ -2554,6 +2679,43 @@ describe('basalt/in-body-page-title', () => {
2554
2679
  )
2555
2680
  expect(rules).not.toContain('in-body-page-title')
2556
2681
  })
2682
+
2683
+ // The narrowing that made C8 satisfiable — the law is "one NAME per page", and a name is
2684
+ // written in words. rb hit both of these shapes 24 times across 12 files with no legal fix.
2685
+ it('does NOT flag a nav-less detail route rendering the document title', () => {
2686
+ const { code, rules } = run(
2687
+ `export const C = ({ knot }) => <Title order={1}>{knot.title}</Title>\n`,
2688
+ )
2689
+ expect(code).toBe(0)
2690
+ expect(rules).not.toContain('in-body-page-title')
2691
+ })
2692
+
2693
+ it("does NOT flag a card's hero VALUE at order={2}", () => {
2694
+ const { rules } = run(`export const C = ({ g }) => <Title order={2}>{RANK[g.rank]}</Title>\n`)
2695
+ expect(rules).not.toContain('in-body-page-title')
2696
+ })
2697
+
2698
+ // The escape is the EXPRESSION, not the braces: a literal laundered through `{…}` is still a
2699
+ // static page name, and so is a substitution-free template.
2700
+ it.each([`{'Users'}`, '{`Users`}'])('still flags a laundered literal %s', (child) => {
2701
+ const { rules } = run(`export const C = () => <Title order={1}>${child}</Title>\n`)
2702
+ expect(rules).toContain('in-body-page-title')
2703
+ })
2704
+
2705
+ it('still flags a name with a value interpolated into it', () => {
2706
+ const { rules } = run(`export const C = ({ n }) => <Title order={1}>Users {n}</Title>\n`)
2707
+ expect(rules).toContain('in-body-page-title')
2708
+ })
2709
+
2710
+ it('still flags a name nested one element deep', () => {
2711
+ const { rules } = run(`export const C = () => <Title order={1}><span>Users</span></Title>\n`)
2712
+ expect(rules).toContain('in-body-page-title')
2713
+ })
2714
+
2715
+ it('names PageTitle as the shell-less answer', () => {
2716
+ const { output } = run(`export const C = () => <Title order={1}>Page</Title>\n`)
2717
+ expect(output).toContain('<PageTitle>')
2718
+ })
2557
2719
  })
2558
2720
 
2559
2721
  describe('basalt/responsive-twin', () => {
@@ -3170,16 +3332,31 @@ describe('basalt/forms-field-key', () => {
3170
3332
  expect(rules).toContain('forms-field-key')
3171
3333
  })
3172
3334
 
3173
- // The second message: `field` still RETURNS `key`, so it is reported whether or not the element
3174
- // carries one — the remedy is the rename, never an added attribute.
3335
+ // The second message: reported whether or not the element carries a key — the remedy is the
3336
+ // rename, never an added attribute.
3175
3337
  it('flags a spread field() even when the element already has a key', () => {
3176
- const { code, rules, output } = run(
3338
+ const { code, rules } = run(
3177
3339
  `import { field } from 'basalt-ui/forms'\n` +
3178
3340
  `export const C = () => <TextInput key="e" {...field(form, 'email')} />\n`,
3179
3341
  )
3180
3342
  expect(code).toBe(1)
3181
3343
  expect(rules).toContain('forms-field-key')
3182
- expect(output).toContain('@deprecated 1.27 alias')
3344
+ })
3345
+
3346
+ /**
3347
+ * The message has to state the version it is RUNNING at, not the one the deprecation was
3348
+ * scheduled in. It used to say the alias "still bundles `key` INTO the returned object" and "is
3349
+ * removed in 1.29.0" while running AT 1.29.x, where `field` no longer exists at all — a consumer
3350
+ * reading it concluded nothing was broken, on a call site that does not compile.
3351
+ */
3352
+ it('says `field` no longer exists, not that it is merely deprecated', () => {
3353
+ const { output } = run(
3354
+ `import { field } from 'basalt-ui/forms'\n` +
3355
+ `export const C = () => <TextInput {...field(form, 'email')} />\n`,
3356
+ )
3357
+ expect(output).toContain('NO LONGER EXISTS')
3358
+ expect(output).toContain('does not compile')
3359
+ expect(output).not.toContain('still bundles')
3183
3360
  })
3184
3361
 
3185
3362
  it('honours a theme-allow naming the rule', () => {
@@ -34,17 +34,17 @@
34
34
  "basalt/raw-scroll-container": "error",
35
35
  "basalt/hand-rolled-filter": "error",
36
36
  "basalt/control-outside-home": "warn",
37
- "basalt/bound-control-outside-home": "warn",
37
+ "basalt/bound-control-outside-home": "error",
38
38
  "basalt/control-size-literal": "error",
39
39
  "basalt/page-bar-budget": "error",
40
40
  "basalt/in-body-page-title": "error",
41
41
  "basalt/responsive-twin": "error",
42
42
  "basalt/search-literal-link": "error",
43
43
  "basalt/use-search-from-literal": "error",
44
- "basalt/provider-above-router": "warn",
45
- "basalt/query-fn-unwrap": "warn",
44
+ "basalt/provider-above-router": "error",
45
+ "basalt/query-fn-unwrap": "error",
46
46
  "basalt/deprecated-export": "warn",
47
- "basalt/forms-field-key": "warn",
47
+ "basalt/forms-field-key": "error",
48
48
  "basalt/visx-boundary": "error",
49
49
  "basalt/visx-tooltip": "error",
50
50
  "basalt/agent-resume-guard": "error",
@@ -1 +1 @@
1
- {"version":3,"file":"ChartCard.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/ChartCard.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAiB,SAAS,EAAE,MAAM,OAAO,CAAA;AAErD,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AAGtE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAA;AAEhE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAqChD;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAA;AAEtD;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,cAAc,GAAG,WAAW,GACtC,eAAe,CAAC,aAAa,CAAC,GAAG;IAC/B,mGAAmG;IACnG,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,0DAA0D;IAC1D,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB,gGAAgG;IAChG,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;8FAC0F;IAC1F,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,uEAAuE;IACvE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,iEAAiE;IACjE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,mEAAmE;IACnE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;kEAC8D;IAC9D,aAAa,CAAC,EAAE,aAAa,CAAA;IAC7B,sCAAsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,+FAA+F;IAC/F,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,UAAU,CAAA;IAClB,8FAA8F;IAC9F,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B;sDACkD;IAClD,WAAW,CAAC,EAAE,SAAS,CAAA;IACvB;oGACgG;IAChG,QAAQ,CAAC,EAAE,SAAS,CAAA;CACrB,CAAA;AAEH,wBAAgB,SAAS,CAAC,EACxB,KAAK,EACL,IAAI,EACJ,QAAQ,EACR,IAAI,EACJ,KAAK,EACL,KAAK,EACL,WAAW,EACX,aAAa,EACb,KAAK,EACL,OAAO,EACP,KAAK,EACL,iBAAuB,EACvB,WAAW,EACX,SAAS,EACT,UAAU,EACV,KAAK,EACL,QAAQ,GACT,EAAE,cAAc,+BAgEhB"}
1
+ {"version":3,"file":"ChartCard.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/ChartCard.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAiB,SAAS,EAAE,MAAM,OAAO,CAAA;AAErD,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AAGtE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAA;AAEhE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AA0ChD;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAA;AAEtD;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,cAAc,GAAG,WAAW,GACtC,eAAe,CAAC,aAAa,CAAC,GAAG;IAC/B,mGAAmG;IACnG,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,0DAA0D;IAC1D,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB,gGAAgG;IAChG,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;8FAC0F;IAC1F,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,uEAAuE;IACvE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,iEAAiE;IACjE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,mEAAmE;IACnE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;kEAC8D;IAC9D,aAAa,CAAC,EAAE,aAAa,CAAA;IAC7B,sCAAsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,+FAA+F;IAC/F,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,UAAU,CAAA;IAClB,8FAA8F;IAC9F,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B;sDACkD;IAClD,WAAW,CAAC,EAAE,SAAS,CAAA;IACvB;oGACgG;IAChG,QAAQ,CAAC,EAAE,SAAS,CAAA;CACrB,CAAA;AAEH,wBAAgB,SAAS,CAAC,EACxB,KAAK,EACL,IAAI,EACJ,QAAQ,EACR,IAAI,EACJ,KAAK,EACL,KAAK,EACL,WAAW,EACX,aAAa,EACb,KAAK,EACL,OAAO,EACP,KAAK,EACL,iBAAuB,EACvB,WAAW,EACX,SAAS,EACT,UAAU,EACV,KAAK,EACL,QAAQ,GACT,EAAE,cAAc,+BAgEhB"}
@@ -17,10 +17,10 @@ const bodyClipStyle = {
17
17
  borderBottomLeftRadius: VX.radiusCard,
18
18
  borderBottomRightRadius: VX.radiusCard,
19
19
  overflow: "hidden",
20
- padding: "2px var(--mantine-spacing-sm, 0.8125rem) var(--mantine-spacing-xs, 0.6875rem)"
20
+ padding: "2px var(--vx-space-card-inset-x, 0.8125rem) var(--vx-space-card-inset-y, 0.6875rem)"
21
21
  };
22
22
  const headerWrapStyle = {
23
- padding: "var(--mantine-spacing-xs, 0.6875rem) var(--mantine-spacing-sm, 0.8125rem) 4px"
23
+ padding: "var(--vx-space-card-inset-y, 0.6875rem) var(--vx-space-card-inset-x, 0.8125rem) 4px"
24
24
  };
25
25
  function ChartCard({
26
26
  title,
@@ -1,16 +1,28 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import type { BasaltProps } from '../../common/props.js';
3
3
  import type { ChartLegendConfig, LegendPlacement, SeriesStyle } from '../series.js';
4
+ import type { ResponsiveChartHeight } from './chart-frame-layout.js';
4
5
  import type { ChartState } from './ChartPending.js';
5
6
  /** Re-exported for `ChartFrame.test.tsx` and any other consumer that previously reached these
6
7
  * through `ChartFrame` — the layout math itself lives in `./chart-frame-layout` (pure, DOM-free). */
7
8
  export { legendEntryCap, resolvePlotRect, resolveChartTier } from './chart-frame-layout.js';
9
+ export type { ResponsiveChartHeight } from './chart-frame-layout.js';
8
10
  /** Legend configuration for {@link ChartFrame}. Omit entirely (or pass `{}`) for the default
9
11
  * bottom-placed legend; pass `false` only for the sparkline exemption. */
10
12
  export type ChartFrameLegend = {
11
13
  /** Default 'bottom'. */
12
14
  placement?: LegendPlacement;
13
- /** Wrap cap → "+N more" rollup at high cardinality. */
15
+ /**
16
+ * Wrap cap → "+N more" rollup at high cardinality.
17
+ *
18
+ * An explicit value WINS OUTRIGHT — including over the phone tier's own two-row default
19
+ * (`chartTierMetrics`). It used to be `Math.min`'d against the tier, which made the documented
20
+ * per-chart opt-out ("opt back out with an explicit `legend.maxRows`") false in the one direction
21
+ * anybody needs it: a consumer's 380px inspector panel is "a phone" on a 1440px desktop, and a
22
+ * six-series legend rolled up to `+4 more` with no way to say no. Under `fill` the measured
23
+ * entry cap can still roll up FURTHER — that is a floor on the plot, not a tier default
24
+ * ({@link legendEntryCap}).
25
+ */
14
26
  maxRows?: number;
15
27
  /** Visually separate role: series | overlay | reference. */
16
28
  groups?: boolean;
@@ -28,8 +40,16 @@ export type ChartFrameLegend = {
28
40
  export type ChartFrameProps = BasaltProps & {
29
41
  /** Series identity — drives the derived legend. Pass the SAME array the kind draws + tooltips from. */
30
42
  series: readonly SeriesStyle[];
31
- /** Fixed height in pixels. Used when neither `aspectRatio` nor `fill` is set. Default 240. */
32
- height?: number;
43
+ /**
44
+ * Height in pixels, or one per size step. Used when neither `aspectRatio` nor `fill` is set.
45
+ * Default 240.
46
+ *
47
+ * `{ base: 180, md: 260 }` is the declarative escape from a page of fixed-height charts: the
48
+ * steps are compared against the frame's own MEASURED width, not the viewport, so a chart squeezed
49
+ * by a `PageAside` gets the short height on a desktop too. A plain number is unchanged and stays
50
+ * supported ({@link resolveFrameHeight}).
51
+ */
52
+ height?: number | ResponsiveChartHeight;
33
53
  /** height = Math.round(containerWidth / aspectRatio). Ignored when `fill` is set. */
34
54
  aspectRatio?: number;
35
55
  /** Fill the parent flex/grid cell's measured height instead of a fixed/derived one. */
@@ -1 +1 @@
1
- {"version":3,"file":"ChartFrame.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/ChartFrame.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,SAAS,EAAE,MAAM,OAAO,CAAA;AAErD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAIrD,OAAO,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAUhF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAEhD;qGACqG;AACrG,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAKxF;0EAC0E;AAC1E,MAAM,MAAM,gBAAgB,GAAG;IAC7B,wBAAwB;IACxB,SAAS,CAAC,EAAE,eAAe,CAAA;IAC3B,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,4DAA4D;IAC5D,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,wDAAwD;IACxD,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IAC1C;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,eAAe,GAAG,WAAW,GAAG;IAC1C,uGAAuG;IACvG,MAAM,EAAE,SAAS,WAAW,EAAE,CAAA;IAC9B,8FAA8F;IAC9F,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,qFAAqF;IACrF,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,uFAAuF;IACvF,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,iGAAiG;IACjG,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,6FAA6F;IAC7F,MAAM,CAAC,EAAE,gBAAgB,GAAG,KAAK,CAAA;IACjC;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,UAAU,CAAA;IAClB;;;;;;;;;;;OAWG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;oDACgD;IAChD,QAAQ,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,SAAS,CAAA;CACxC,CAAA;AAED,0FAA0F;AAC1F,MAAM,MAAM,QAAQ,GAAG;IACrB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;CAC5B,CAAA;AAcD;oFACoF;AACpF,MAAM,MAAM,qBAAqB,GAAG;IAClC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;CAC1C,CAAA;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,iBAAiB,GAAG,KAAK,GAAG,SAAS,EAC7C,KAAK,CAAC,EAAE,qBAAqB,EAC7B,WAAW,CAAC,EAAE,MAAM,GACnB,gBAAgB,GAAG,KAAK,CAU1B;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,UAAU,CAAC,EACzB,MAAM,EACN,MAAM,EACN,WAAW,EACX,IAAY,EACZ,QAA4B,EAC5B,OAAO,EACP,MAAW,EACX,SAAS,EACT,SAAiB,EACjB,KAAK,EACL,SAAS,EACT,KAAK,EACL,QAAQ,GACT,EAAE,eAAe,GAAG,SAAS,CAmH7B"}
1
+ {"version":3,"file":"ChartFrame.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/ChartFrame.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,SAAS,EAAE,MAAM,OAAO,CAAA;AAErD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAIrD,OAAO,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAQhF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAA;AAIjE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAEhD;qGACqG;AACrG,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACxF,YAAY,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAA;AAKjE;0EAC0E;AAC1E,MAAM,MAAM,gBAAgB,GAAG;IAC7B,wBAAwB;IACxB,SAAS,CAAC,EAAE,eAAe,CAAA;IAC3B;;;;;;;;;;OAUG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,4DAA4D;IAC5D,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,wDAAwD;IACxD,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IAC1C;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,eAAe,GAAG,WAAW,GAAG;IAC1C,uGAAuG;IACvG,MAAM,EAAE,SAAS,WAAW,EAAE,CAAA;IAC9B;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,qBAAqB,CAAA;IACvC,qFAAqF;IACrF,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,uFAAuF;IACvF,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,iGAAiG;IACjG,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,6FAA6F;IAC7F,MAAM,CAAC,EAAE,gBAAgB,GAAG,KAAK,CAAA;IACjC;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,UAAU,CAAA;IAClB;;;;;;;;;;;OAWG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;oDACgD;IAChD,QAAQ,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,SAAS,CAAA;CACxC,CAAA;AAED,0FAA0F;AAC1F,MAAM,MAAM,QAAQ,GAAG;IACrB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;CAC5B,CAAA;AAcD;oFACoF;AACpF,MAAM,MAAM,qBAAqB,GAAG;IAClC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;CAC1C,CAAA;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,iBAAiB,GAAG,KAAK,GAAG,SAAS,EAC7C,KAAK,CAAC,EAAE,qBAAqB,EAC7B,WAAW,CAAC,EAAE,MAAM,GACnB,gBAAgB,GAAG,KAAK,CAU1B;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,UAAU,CAAC,EACzB,MAAM,EACN,MAAM,EACN,WAAW,EACX,IAAY,EACZ,QAA4B,EAC5B,OAAO,EACP,MAAW,EACX,SAAS,EACT,SAAiB,EACjB,KAAK,EACL,SAAS,EACT,KAAK,EACL,QAAQ,GACT,EAAE,eAAe,GAAG,SAAS,CA6G7B"}
@@ -4,9 +4,10 @@ import { VX } from "../../tokens/index.js";
4
4
  import { useChartSize } from "../hooks/useChartSize.js";
5
5
  import { deriveLegend } from "../series.js";
6
6
  import {
7
- chartTierMetrics,
8
7
  legendEntryCap,
9
8
  resolveChartTier,
9
+ resolveFrameHeight,
10
+ resolveLegendMaxRows,
10
11
  resolvePlotRect
11
12
  } from "./chart-frame-layout.js";
12
13
  import { ChartTierProvider } from "./chart-tier.js";
@@ -67,7 +68,7 @@ function ChartFrame({
67
68
  const resolvedState = resolveChartState({ ...state !== void 0 && { state }, isPending });
68
69
  const legendVisible = legend !== false && resolvedState === null;
69
70
  const tier = resolveChartTier(containerW);
70
- const resolvedHeight = fill ? containerH : aspectRatio !== void 0 ? Math.round(containerW / aspectRatio) : height ?? DEFAULT_HEIGHT;
71
+ const resolvedHeight = fill ? containerH : aspectRatio !== void 0 ? Math.round(containerW / aspectRatio) : resolveFrameHeight(height ?? DEFAULT_HEIGHT, containerW);
71
72
  const sideLegendWidth = legendVisible && vertical ? legendW : 0;
72
73
  const topBottomLegendHeight = legendVisible && !vertical ? legendH : 0;
73
74
  const plot = resolvePlotRect({
@@ -79,8 +80,7 @@ function ChartFrame({
79
80
  });
80
81
  const legendItems = legend === false ? [] : deriveLegend(series);
81
82
  const togglable = legend !== false && (legend.toggle ?? legendItems.length > 1);
82
- const tierMaxRows = chartTierMetrics(tier).legendMaxRows;
83
- const callerMaxRows = legend === false ? void 0 : legend.maxRows === void 0 ? tierMaxRows : tierMaxRows === void 0 ? legend.maxRows : Math.min(legend.maxRows, tierMaxRows);
83
+ const callerMaxRows = legend === false ? void 0 : resolveLegendMaxRows({ callerMaxRows: legend.maxRows, tier });
84
84
  const maxRows = legend === false ? void 0 : fill && !vertical ? legendEntryCap({
85
85
  items: legendItems,
86
86
  containerW,
@@ -24,6 +24,42 @@ export type ChartTier = 'phone' | 'desktop';
24
24
  * later, and `resolvePlotRect` already treats an unmeasured width as the first-frame case.
25
25
  */
26
26
  export declare function resolveChartTier(containerW: number): ChartTier;
27
+ /**
28
+ * A chart height stated per size step instead of as one number — the declarative escape from "every
29
+ * chart on this page is 260px tall on a 375px phone too".
30
+ *
31
+ * `base` is required and is the height a box narrower than every named step gets; each further key
32
+ * is the height from that step UP. Keys are Mantine's breakpoint names so a consumer writes the
33
+ * vocabulary they already write, and nothing more than `sm`/`md`/`lg` is offered: `xs`/`xl` would
34
+ * be two more rungs nobody has asked a chart to have.
35
+ *
36
+ * ```tsx
37
+ * <ChartFrame series={series} height={{ base: 180, md: 260 }}>{…}</ChartFrame>
38
+ * ```
39
+ */
40
+ export type ResponsiveChartHeight = {
41
+ /** Below the narrowest STATED step — and NOT the first-frame value; see {@link resolveFrameHeight}. */
42
+ base: number;
43
+ /** From 768px of MEASURED container width up. */
44
+ sm?: number;
45
+ /** From 992px up. */
46
+ md?: number;
47
+ /** From 1200px up. */
48
+ lg?: number;
49
+ };
50
+ /**
51
+ * Resolve a `height` prop — a plain number, or a {@link ResponsiveChartHeight} — against the
52
+ * measured container width.
53
+ *
54
+ * A plain number passes through untouched: this is a WIDENING of the prop, not a replacement, and
55
+ * every existing `height={240}` must keep meaning exactly 240.
56
+ *
57
+ * An UNMEASURED box (`containerW <= 0` — SSR, or before the `ResizeObserver`'s first callback)
58
+ * resolves to the LARGEST stated step, matching {@link resolveChartTier}'s first-frame rule: the
59
+ * first paint must not be phone-shaped chrome that grows one frame later. Falling to `base` there
60
+ * would make every SSR'd chart short and then jump.
61
+ */
62
+ export declare function resolveFrameHeight(height: number | ResponsiveChartHeight, containerW: number): number;
27
63
  /** Every size a chart's chrome resolves per tier. One object so a new tier-sensitive size is added
28
64
  * in one place and read by name, rather than each primitive branching on the tier itself. */
29
65
  export type ChartTierMetrics = {
@@ -46,6 +82,25 @@ export type ChartTierMetrics = {
46
82
  };
47
83
  /** The resolved sizes for one tier. Frozen module constants — never a new object per render. */
48
84
  export declare function chartTierMetrics(tier: ChartTier): ChartTierMetrics;
85
+ /**
86
+ * Which legend cap applies: the caller's, or the tier's own default.
87
+ *
88
+ * **An explicit `callerMaxRows` wins OUTRIGHT.** This used to be `Math.min(caller, tier)`, which
89
+ * made the documented per-chart opt-out ("opt back out with an explicit `legend.maxRows`") false in
90
+ * the one direction anybody needs it. The tier's `PHONE_LEGEND_MAX_ROWS` is a DEFAULT for charts
91
+ * that said nothing — a chart that names a number has already answered the question the default
92
+ * exists to answer, and it matters because the tier keys on the measured box: a 380px inspector
93
+ * panel on a 1440px desktop is "a phone", so a six-series legend rolled up to two rows + `+4 more`
94
+ * with no way to say no. `margin` and `xLabelRotate` always had that escape; the legend did not.
95
+ *
96
+ * A `fill` frame's measured {@link legendEntryCap} still applies ON TOP of whatever wins here — it
97
+ * is a floor on the PLOT, not a tier default, so a chart that would otherwise render no plot at all
98
+ * still rolls up.
99
+ */
100
+ export declare function resolveLegendMaxRows(input: {
101
+ callerMaxRows?: number | undefined;
102
+ tier: ChartTier;
103
+ }): number | undefined;
49
104
  /**
50
105
  * Resolve the plot rect from the measured box and the measured legend band. Pure and exported so
51
106
  * the two floors below are testable without a `ResizeObserver`.
@@ -1 +1 @@
1
- {"version":3,"file":"chart-frame-layout.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/chart-frame-layout.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAUhD;;;;GAIG;AACH,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,SAAS,CAAA;AAE3C;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,CAE9D;AAqBD;6FAC6F;AAC7F,MAAM,MAAM,gBAAgB,GAAG;IAC7B,2FAA2F;IAC3F,IAAI,EAAE,SAAS,CAAA;IACf;uFACmF;IACnF,QAAQ,EAAE,MAAM,CAAA;IAChB,kCAAkC;IAClC,cAAc,EAAE,MAAM,CAAA;IACtB,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAA;IACZ,uCAAuC;IACvC,eAAe,EAAE,MAAM,CAAA;IACvB;yDACqD;IACrD,aAAa,EAAE,MAAM,GAAG,SAAS,CAAA;IACjC,6EAA6E;IAC7E,MAAM,EAAE,WAAW,CAAA;CACpB,CAAA;AA6BD,gGAAgG;AAChG,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,SAAS,GAAG,gBAAgB,CAElE;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE;IACrC,UAAU,EAAE,MAAM,CAAA;IAClB,cAAc,EAAE,MAAM,CAAA;IACtB,QAAQ,EAAE,MAAM,CAAA;IAChB,eAAe,EAAE,MAAM,CAAA;IACvB,qBAAqB,EAAE,MAAM,CAAA;CAC9B,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAOpC;AAmCD;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE;IACpC,KAAK,EAAE,SAAS,WAAW,EAAE,CAAA;IAC7B,UAAU,EAAE,MAAM,CAAA;IAClB,iFAAiF;IACjF,SAAS,EAAE,MAAM,CAAA;IACjB,6DAA6D;IAC7D,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB,GAAG,MAAM,GAAG,SAAS,CAerB"}
1
+ {"version":3,"file":"chart-frame-layout.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/chart-frame-layout.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAUhD;;;;GAIG;AACH,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,SAAS,CAAA;AAE3C;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,CAE9D;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC,uGAAuG;IACvG,IAAI,EAAE,MAAM,CAAA;IACZ,iDAAiD;IACjD,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,qBAAqB;IACrB,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,sBAAsB;IACtB,EAAE,CAAC,EAAE,MAAM,CAAA;CACZ,CAAA;AAiBD;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,MAAM,GAAG,qBAAqB,EACtC,UAAU,EAAE,MAAM,GACjB,MAAM,CAQR;AAqBD;6FAC6F;AAC7F,MAAM,MAAM,gBAAgB,GAAG;IAC7B,2FAA2F;IAC3F,IAAI,EAAE,SAAS,CAAA;IACf;uFACmF;IACnF,QAAQ,EAAE,MAAM,CAAA;IAChB,kCAAkC;IAClC,cAAc,EAAE,MAAM,CAAA;IACtB,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAA;IACZ,uCAAuC;IACvC,eAAe,EAAE,MAAM,CAAA;IACvB;yDACqD;IACrD,aAAa,EAAE,MAAM,GAAG,SAAS,CAAA;IACjC,6EAA6E;IAC7E,MAAM,EAAE,WAAW,CAAA;CACpB,CAAA;AA6BD,gGAAgG;AAChG,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,SAAS,GAAG,gBAAgB,CAElE;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE;IAC1C,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAClC,IAAI,EAAE,SAAS,CAAA;CAChB,GAAG,MAAM,GAAG,SAAS,CAErB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE;IACrC,UAAU,EAAE,MAAM,CAAA;IAClB,cAAc,EAAE,MAAM,CAAA;IACtB,QAAQ,EAAE,MAAM,CAAA;IAChB,eAAe,EAAE,MAAM,CAAA;IACvB,qBAAqB,EAAE,MAAM,CAAA;CAC9B,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAOpC;AAmCD;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE;IACpC,KAAK,EAAE,SAAS,WAAW,EAAE,CAAA;IAC7B,UAAU,EAAE,MAAM,CAAA;IAClB,iFAAiF;IACjF,SAAS,EAAE,MAAM,CAAA;IACjB,6DAA6D;IAC7D,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB,GAAG,MAAM,GAAG,SAAS,CAerB"}
@@ -6,6 +6,17 @@ const LEGEND_SWATCH_W = 24;
6
6
  function resolveChartTier(containerW) {
7
7
  return containerW > 0 && containerW < VX.phoneChartWidth ? "phone" : "desktop";
8
8
  }
9
+ const HEIGHT_STEP_WIDTH = { sm: 768, md: 992, lg: 1200 };
10
+ const HEIGHT_STEPS = ["lg", "md", "sm"];
11
+ function resolveFrameHeight(height, containerW) {
12
+ if (typeof height === "number") return height;
13
+ const widest = HEIGHT_STEPS.find((step2) => height[step2] !== void 0);
14
+ if (containerW <= 0) return widest === void 0 ? height.base : height[widest] ?? height.base;
15
+ const step = HEIGHT_STEPS.find(
16
+ (name) => containerW >= HEIGHT_STEP_WIDTH[name] && height[name] !== void 0
17
+ );
18
+ return step === void 0 ? height.base : height[step] ?? height.base;
19
+ }
9
20
  const PHONE_MARGIN_SCALE = 0.75;
10
21
  const PHONE_DOT_R = Math.max(VX.dotR - 1, 2);
11
22
  const PHONE_LEGEND_MAX_ROWS = 2;
@@ -38,6 +49,9 @@ const PHONE_METRICS = {
38
49
  function chartTierMetrics(tier) {
39
50
  return tier === "phone" ? PHONE_METRICS : DESKTOP_METRICS;
40
51
  }
52
+ function resolveLegendMaxRows(input) {
53
+ return input.callerMaxRows ?? chartTierMetrics(input.tier).legendMaxRows;
54
+ }
41
55
  function resolvePlotRect(input) {
42
56
  const { containerW, resolvedHeight, minWidth, sideLegendWidth, topBottomLegendHeight } = input;
43
57
  return {
@@ -85,5 +99,7 @@ export {
85
99
  chartTierMetrics,
86
100
  legendEntryCap,
87
101
  resolveChartTier,
102
+ resolveFrameHeight,
103
+ resolveLegendMaxRows,
88
104
  resolvePlotRect
89
105
  };
@@ -1 +1 @@
1
- {"version":3,"file":"check-theme.d.ts","sourceRoot":"","sources":["../../src/cli/check-theme.ts"],"names":[],"mappings":"AA8BA;;;;;;;GAOG;AACH,wBAAgB,UAAU,CACxB,aAAa,GAAE,MAAsB,EACrC,KAAK,GAAE,SAAS,MAAM,EAAO,GAC5B,MAAM,CA2JR"}
1
+ {"version":3,"file":"check-theme.d.ts","sourceRoot":"","sources":["../../src/cli/check-theme.ts"],"names":[],"mappings":"AA+BA;;;;;;;GAOG;AACH,wBAAgB,UAAU,CACxB,aAAa,GAAE,MAAsB,EACrC,KAAK,GAAE,SAAS,MAAM,EAAO,GAC5B,MAAM,CAuKR"}
@@ -12,6 +12,7 @@ import {
12
12
  conflictingProfileFlags,
13
13
  declaredProfile,
14
14
  DEFAULT_ROOTS,
15
+ hasBasaltKey,
15
16
  readBasaltConfig,
16
17
  resolveExemptRules,
17
18
  resolveProjectDir,
@@ -69,6 +70,11 @@ function checkTheme(invocationCwd = process.cwd(), flags = []) {
69
70
  findings.push(...checkSource(readFileSync(resolve(cwd, rel), "utf8"), rel, guardCfg));
70
71
  }
71
72
  if (scanned.length === 0) {
73
+ if (!hasBasaltKey(cwd)) {
74
+ console.error(
75
+ `\u2716 basalt-ui check-theme: ${cwd}/package.json has no "basalt" key \u2014 very likely not the package you meant to scan (BASALT_CWD, else the invocation cwd; nothing is inferred). At a repo root? Give the command \`root: <pkg>/\` (lefthook) or \`working-directory: <pkg>\` (Actions), or set BASALT_CWD=<pkg>. If this directory IS it, run \`basalt-ui init\`.`
76
+ );
77
+ }
72
78
  if (cfg.roots === void 0) {
73
79
  console.error(
74
80
  `\u2716 basalt-ui check-theme: 0 files scanned \u2014 no "basalt.roots" configured in package.json, and the built-in default roots (${DEFAULT_ROOTS.join(", ")}) matched zero files. Set "basalt": { "roots": [...] } in package.json to point at your source directories.`
@@ -15,16 +15,22 @@ export type DoctorResult = {
15
15
  * Check a consumer repo's basalt integration and print a pass/warn report.
16
16
  *
17
17
  * Narrowed to what a single-directory-scoped run can actually verify — no workspace-glob walking,
18
- * no ascend/descend project discovery (see {@link resolveProjectDir}). Four checks, numbered in
18
+ * no ascend/descend project discovery (see {@link resolveProjectDir}). Six checks, numbered in
19
19
  * the order they run and print:
20
20
  *
21
21
  * Hard failures (exit non-zero):
22
22
  * 1. `.basalt/manifest.json` exists (`basalt-ui init` was run).
23
- * 2. `oxlint-preset` ("rules in sync"): the consumer's `.oxlintrc.json` extends the shipped
23
+ * 2. `install-parity`: basalt-ui resolves, doctor prints WHERE and at what version, and that
24
+ * version agrees with the manifest's `basaltVersion`.
25
+ * 3. `guard-scan`: `check-theme` would scan more than zero files, sharing `scannableFiles` so the
26
+ * two cannot disagree. Both RESTORED in 1.30.0 after four minors gone with no MIGRATING row —
27
+ * README § `doctor` carries what their absence cost (a no-op CI gate, a green guard scanning
28
+ * nothing, and two basalt-WRITTEN docs describing output the CLI no longer produced).
29
+ * 4. `oxlint-preset` ("rules in sync"): the consumer's `.oxlintrc.json` extends the shipped
24
30
  * preset AND that path resolves. `init` keeps an existing config, so the framework's whole
25
31
  * lint half can be silently off — one repo carried six real violations invisibly across five
26
32
  * minors this way.
27
- * 4. `ai-major-parity`: within THIS package.json, `dependencies`/`devDependencies`/
33
+ * 6. `ai-major-parity`: within THIS package.json, `dependencies`/`devDependencies`/
28
34
  * `peerDependencies` agree on the `ai` package's major. `basalt/ai-sdk-major` (the oxlint
29
35
  * plugin rule) already catches this per linted FILE against its nearest package.json; this
30
36
  * check exists for the shape a lint run cannot see — a package whose own manifest asks its
@@ -34,7 +40,7 @@ export type DoctorResult = {
34
40
  * already agree warns that the exemption is stale.
35
41
  *
36
42
  * Warning (non-fatal):
37
- * 3. `lefthook-preset` ("lefthook wired"): a pre-commit command runs `check-theme`, read from
43
+ * 5. `lefthook-preset` ("lefthook wired"): a pre-commit command runs `check-theme`, read from
38
44
  * `lefthook dump --format json` (resolves `extends`/`include`/`root:` the way lefthook itself
39
45
  * does — see {@link inspectLefthookGate}). No lefthook config at all is a pass (a consumer may
40
46
  * gate elsewhere); a config that resolves but wires no guard command warns; a config lefthook
@@ -1 +1 @@
1
- {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/cli/doctor.ts"],"names":[],"mappings":"AA0BA,MAAM,MAAM,YAAY,GAAG;IACzB,+CAA+C;IAC/C,YAAY,EAAE,MAAM,CAAA;IACpB,+CAA+C;IAC/C,QAAQ,EAAE,MAAM,CAAA;IAChB;;;;;OAKG;IACH,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AA8DD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,MAAM,CAAC,aAAa,GAAE,MAAsB,EAAE,KAAK,GAAE,MAAM,EAAO,GAAG,MAAM,CAsM1F"}
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/cli/doctor.ts"],"names":[],"mappings":"AA6BA,MAAM,MAAM,YAAY,GAAG;IACzB,+CAA+C;IAC/C,YAAY,EAAE,MAAM,CAAA;IACpB,+CAA+C;IAC/C,QAAQ,EAAE,MAAM,CAAA;IAChB;;;;;OAKG;IACH,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AA8DD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,wBAAgB,MAAM,CAAC,aAAa,GAAE,MAAsB,EAAE,KAAK,GAAE,MAAM,EAAO,GAAG,MAAM,CAsO1F"}
@@ -1,5 +1,5 @@
1
1
  import { existsSync } from "node:fs";
2
- import { resolve } from "node:path";
2
+ import { relative, resolve } from "node:path";
3
3
  import {
4
4
  MANIFEST_PATH,
5
5
  basaltBinCommand,
@@ -12,7 +12,10 @@ import {
12
12
  parseJsonc,
13
13
  readBasaltConfig,
14
14
  readIfExists,
15
+ readManifest,
15
16
  resolveProjectDir,
17
+ resolveRoots,
18
+ scannableFiles,
16
19
  shippedAssetPath
17
20
  } from "./index.js";
18
21
  import { findManifestAbove, parentInstallAdvice } from "./sync.js";
@@ -93,6 +96,30 @@ basalt-ui doctor \u2014 ${cwd}
93
96
  fail(`${MANIFEST_PATH} missing \u2014 run \`basalt-ui init\` to scaffold the consumer repo`);
94
97
  }
95
98
  const install = findBasaltInstall(cwd);
99
+ const manifestVersion = profile === "tokens-only" ? void 0 : readManifest(cwd).basaltVersion;
100
+ const at = install.dir === null ? "" : `${relative(cwd, install.dir)} (${install.version})`;
101
+ if (install.dir === null) {
102
+ fail(
103
+ "install-parity: basalt-ui does not resolve here, above, or in a sibling workspace package."
104
+ );
105
+ } else if (manifestVersion === void 0 || install.version === manifestVersion) {
106
+ const matches = manifestVersion === void 0 ? "" : " and matches the manifest's basaltVersion";
107
+ pass(`basalt-ui resolves at ${at}${matches}`);
108
+ } else {
109
+ fail(
110
+ `install-parity: basalt-ui resolves at ${at}, but ${MANIFEST_PATH} was last written by ${manifestVersion} \u2014 the managed layer is from a different release than the code. Run \`basalt-ui sync\` (or \`bun install\`, if the lockfile is what is stale).`
111
+ );
112
+ }
113
+ const scanned = profile === "tokens-only" ? null : scannableFiles(cwd, cfg).length;
114
+ if (scanned === null) {
115
+ pass("guard-scan: n/a \u2014 a tokens-only consumer runs the token lane, not the file walk");
116
+ } else if (scanned === 0) {
117
+ fail(
118
+ "guard-scan: `check-theme` would scan ZERO files here \u2014 either `basalt.roots` points at paths that do not exist, or this is not the package the config lives in (BASALT_CWD, else the cwd; nothing is inferred)."
119
+ );
120
+ } else {
121
+ pass(`guard-scan: check-theme covers ${scanned} file(s) under ${resolveRoots(cfg).join(", ")}`);
122
+ }
96
123
  if (profile === "tokens-only") {
97
124
  pass("oxlint-preset: n/a \u2014 the shipped preset is a Mantine/React preset");
98
125
  } else {
@@ -1 +1 @@
1
- {"version":3,"file":"help.d.ts","sourceRoot":"","sources":["../../src/cli/help.ts"],"names":[],"mappings":"AAcA,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,IAAI,EAAE,QAAQ,GAAG,SAAS,CAAA;CAAE,CAAC,CAiC1F,CAAA;AAED;yCACyC;AACzC,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,GAAG,IAAI,CAWhF;AAED,0GAA0G;AAC1G,eAAO,MAAM,KAAK,QAmC0E,CAAA"}
1
+ {"version":3,"file":"help.d.ts","sourceRoot":"","sources":["../../src/cli/help.ts"],"names":[],"mappings":"AAcA,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,IAAI,EAAE,QAAQ,GAAG,SAAS,CAAA;CAAE,CAAC,CAkC1F,CAAA;AAED;yCACyC;AACzC,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,GAAG,IAAI,CAWhF;AAED,0GAA0G;AAC1G,eAAO,MAAM,KAAK,QAoC0E,CAAA"}