basalt-ui 1.29.2 → 1.30.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.
Files changed (149) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/MIGRATING.md +901 -366
  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/index.d.ts +1 -1
  12. package/dist/charts/index.d.ts.map +1 -1
  13. package/dist/charts/kinds/BandStrip.d.ts +5 -3
  14. package/dist/charts/kinds/BandStrip.d.ts.map +1 -1
  15. package/dist/charts/kinds/Bars.d.ts +4 -2
  16. package/dist/charts/kinds/Bars.d.ts.map +1 -1
  17. package/dist/charts/kinds/Donut.d.ts +4 -2
  18. package/dist/charts/kinds/Donut.d.ts.map +1 -1
  19. package/dist/charts/kinds/DualPanel.d.ts +4 -2
  20. package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
  21. package/dist/charts/kinds/Heatmap.d.ts +5 -2
  22. package/dist/charts/kinds/Heatmap.d.ts.map +1 -1
  23. package/dist/charts/kinds/MirroredBars.d.ts +4 -2
  24. package/dist/charts/kinds/MirroredBars.d.ts.map +1 -1
  25. package/dist/charts/kinds/MultiLine.d.ts +4 -2
  26. package/dist/charts/kinds/MultiLine.d.ts.map +1 -1
  27. package/dist/charts/kinds/StackedArea.d.ts +4 -2
  28. package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
  29. package/dist/charts/kinds/ZonedLine.d.ts +4 -2
  30. package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
  31. package/dist/charts/primitives/CartesianChart.d.ts +4 -1
  32. package/dist/charts/primitives/CartesianChart.d.ts.map +1 -1
  33. package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
  34. package/dist/charts/primitives/ChartCard.js +2 -2
  35. package/dist/charts/primitives/ChartFrame.d.ts +25 -4
  36. package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
  37. package/dist/charts/primitives/ChartFrame.js +23 -16
  38. package/dist/charts/primitives/chart-frame-layout.d.ts +115 -6
  39. package/dist/charts/primitives/chart-frame-layout.d.ts.map +1 -1
  40. package/dist/charts/primitives/chart-frame-layout.js +46 -6
  41. package/dist/cli/check-theme.d.ts.map +1 -1
  42. package/dist/cli/check-theme.js +6 -0
  43. package/dist/cli/doctor.d.ts +10 -4
  44. package/dist/cli/doctor.d.ts.map +1 -1
  45. package/dist/cli/doctor.js +28 -1
  46. package/dist/cli/help.d.ts.map +1 -1
  47. package/dist/cli/help.js +2 -1
  48. package/dist/cli/index.d.ts +12 -0
  49. package/dist/cli/index.d.ts.map +1 -1
  50. package/dist/cli/index.js +29 -0
  51. package/dist/cli/init.d.ts.map +1 -1
  52. package/dist/cli/init.js +4 -15
  53. package/dist/cli/sync.d.ts.map +1 -1
  54. package/dist/cli/sync.js +8 -14
  55. package/dist/controls/actions.d.ts.map +1 -1
  56. package/dist/controls/actions.js +12 -2
  57. package/dist/dashboard/section.module.css +15 -7
  58. package/dist/dashboard/stat-card.d.ts.map +1 -1
  59. package/dist/dashboard/stat-card.js +7 -2
  60. package/dist/dashboard/stat-group.d.ts +8 -2
  61. package/dist/dashboard/stat-group.d.ts.map +1 -1
  62. package/dist/dashboard/stat-group.js +23 -15
  63. package/dist/dashboard/stat-group.module.css +85 -9
  64. package/dist/dashboard/stat-group.module.css.d.ts +1 -0
  65. package/dist/dashboard/widget-grid.d.ts.map +1 -1
  66. package/dist/dashboard/widget-grid.js +4 -5
  67. package/dist/dashboard/widget-grid.module.css +64 -21
  68. package/dist/dashboard/widget-grid.module.css.d.ts +1 -0
  69. package/dist/guard/index.d.ts +19 -2
  70. package/dist/guard/index.d.ts.map +1 -1
  71. package/dist/guard/index.js +27 -4
  72. package/dist/index.d.ts +9 -0
  73. package/dist/index.d.ts.map +1 -1
  74. package/dist/index.js +2 -0
  75. package/dist/shell/app-brand.module.css +12 -3
  76. package/dist/shell/app-header.module.css +49 -9
  77. package/dist/shell/app-main.module.css +24 -4
  78. package/dist/shell/app-mobile-nav.d.ts.map +1 -1
  79. package/dist/shell/app-mobile-nav.js +98 -51
  80. package/dist/shell/app-mobile-nav.module.css +99 -11
  81. package/dist/shell/app-mobile-nav.module.css.d.ts +1 -0
  82. package/dist/shell/app-sidebar.module.css +55 -2
  83. package/dist/shell/index.d.ts.map +1 -1
  84. package/dist/shell/index.js +3 -3
  85. package/dist/shell/mobile-nav-model.d.ts +26 -7
  86. package/dist/shell/mobile-nav-model.d.ts.map +1 -1
  87. package/dist/shell/mobile-nav-model.js +12 -3
  88. package/dist/shell/page-aside.module.css +8 -6
  89. package/dist/shell/page-bar.d.ts +13 -0
  90. package/dist/shell/page-bar.d.ts.map +1 -1
  91. package/dist/shell/page-bar.js +13 -3
  92. package/dist/shell/page-bar.module.css +20 -12
  93. package/dist/shell/page-title.module.css +3 -1
  94. package/dist/theme/use-basalt-spacing.d.ts.map +1 -1
  95. package/dist/tokens/index.d.ts.map +1 -1
  96. package/dist/tokens/index.js +51 -7
  97. package/dist/tokens/palette.d.ts +98 -17
  98. package/dist/tokens/palette.d.ts.map +1 -1
  99. package/dist/tokens/palette.js +110 -18
  100. package/dist/tokens.css +17 -5
  101. package/llms.txt +2 -2
  102. package/package.json +1 -1
  103. package/src/charts/index.ts +1 -0
  104. package/src/charts/kinds/BandStrip.tsx +5 -3
  105. package/src/charts/kinds/Bars.tsx +4 -2
  106. package/src/charts/kinds/Donut.tsx +4 -2
  107. package/src/charts/kinds/DualPanel.tsx +4 -2
  108. package/src/charts/kinds/Heatmap.tsx +5 -2
  109. package/src/charts/kinds/MirroredBars.tsx +4 -2
  110. package/src/charts/kinds/MultiLine.tsx +4 -2
  111. package/src/charts/kinds/StackedArea.tsx +4 -2
  112. package/src/charts/kinds/ZonedLine.tsx +4 -2
  113. package/src/charts/primitives/CartesianChart.tsx +4 -1
  114. package/src/charts/primitives/ChartCard.tsx +12 -7
  115. package/src/charts/primitives/ChartFrame.tsx +51 -37
  116. package/src/charts/primitives/chart-frame-layout.ts +172 -13
  117. package/src/cli/check-theme.ts +13 -0
  118. package/src/cli/doctor.ts +49 -8
  119. package/src/cli/help.ts +5 -3
  120. package/src/cli/index.ts +49 -0
  121. package/src/cli/init.ts +6 -27
  122. package/src/cli/sync.ts +11 -22
  123. package/src/controls/actions.tsx +51 -0
  124. package/src/dashboard/section.module.css +15 -7
  125. package/src/dashboard/stat-card.tsx +7 -2
  126. package/src/dashboard/stat-group.module.css +85 -9
  127. package/src/dashboard/stat-group.module.css.d.ts +1 -0
  128. package/src/dashboard/stat-group.tsx +40 -20
  129. package/src/dashboard/widget-grid.module.css +64 -21
  130. package/src/dashboard/widget-grid.module.css.d.ts +1 -0
  131. package/src/dashboard/widget-grid.tsx +30 -12
  132. package/src/guard/index.ts +79 -11
  133. package/src/index.ts +9 -0
  134. package/src/shell/app-brand.module.css +12 -3
  135. package/src/shell/app-header.module.css +49 -9
  136. package/src/shell/app-main.module.css +24 -4
  137. package/src/shell/app-mobile-nav.module.css +99 -11
  138. package/src/shell/app-mobile-nav.module.css.d.ts +1 -0
  139. package/src/shell/app-mobile-nav.tsx +225 -83
  140. package/src/shell/app-sidebar.module.css +55 -2
  141. package/src/shell/index.tsx +26 -6
  142. package/src/shell/mobile-nav-model.ts +37 -9
  143. package/src/shell/page-aside.module.css +8 -6
  144. package/src/shell/page-bar.module.css +20 -12
  145. package/src/shell/page-bar.tsx +39 -5
  146. package/src/shell/page-title.module.css +3 -1
  147. package/src/theme/use-basalt-spacing.ts +8 -6
  148. package/src/tokens/index.ts +90 -6
  149. package/src/tokens/palette.ts +111 -18
@@ -169,8 +169,15 @@ const SPACE_ANCHORS_BASE = {
169
169
  controlHeightWidget: 24,
170
170
  /** The `size="ctl"` Mantine tier's resolved height (`docs/CONTROLS-SPEC.md` §5, C5) —
171
171
  * `PageBar`/`Section`/table toolbar/sidebar-block controls. Floored at 28px in
172
- * {@link deriveSpacing}. */
173
- controlHeightCtl: 30,
172
+ * {@link deriveSpacing}.
173
+ *
174
+ * 32, up from 30. At 30 this tier was the ONLY chrome number in the shell that did not agree with
175
+ * its neighbours: the sidebar search trigger is 32 (`sidebarSearchTriggerHeight`), a sidebar block
176
+ * row is 32 (`sidebarBlockRowHeight`), and a NavLink row resolves to ~32 (`2 * rowInsetY +
177
+ * rowLineHeight * text.md`) — so a `size="ctl"` button sitting beside any of them read 2px short
178
+ * and the whole band looked unresolved. 32 lands the entire chrome tier on ONE number, which is
179
+ * also the smallest square that still holds a 16px icon with a 8px surround. */
180
+ controlHeightCtl: 32,
174
181
  /** The touch hit-area size (`::before`) every home control expands to below `sm` (C15,
175
182
  * `docs/CONTROLS-SPEC.md` §5) — independent of the visible `controlHeightCtl` box, same shape as
176
183
  * `mobileNavBarHeight`/`mobileNavRowHeight`'s Apple HIG 44pt / WCAG 2.5.5 floors elsewhere in this
@@ -194,7 +201,7 @@ const SPACE_STEP_BASE = {
194
201
  // literal could drift from that header, which is exactly what happened. `SpaceValues.step` still
195
202
  // carries the key — see that type's own doc — it is just not part of this BASE table. The
196
203
  // `stickyHeaderClearanceMobile` sibling went with the two-row mobile header in 1.26.0: the header
197
- // is one 48px row at every viewport now, so one clearance covers both.
204
+ // is one `appShellHeaderHeight` row at every viewport now, so one clearance covers both.
198
205
  /** NavLink leftSection icon-to-label gap — shared by `theme/nav-link.module.css` (every render
199
206
  * path) and `shell/app-sidebar.module.css`'s own `.link` rule (belt-and-suspenders on the same
200
207
  * DOM), and reused verbatim for `.footerBtn`/`.accountRow`'s icon gap (their own doc comments call
@@ -202,8 +209,14 @@ const SPACE_STEP_BASE = {
202
209
  navIconGap: 10,
203
210
  /** Sidebar region-to-region gap: `.brand`'s bottom padding and `.searchSlot`'s bottom padding (in
204
211
  * both the expanded and collapsed rail variants) — the searchSlot comment explicitly says its gap
205
- * opens the nav region "the same way the brand row's own bottom padding does". */
206
- sidebarRegionGap: 10,
212
+ * opens the nav region "the same way the brand row's own bottom padding does".
213
+ *
214
+ * 12, up from 10. This is the ONE gap the sidebar spends at all three of its seams (brand → search,
215
+ * search → nav, nav → account), so it is what separates the column's regions from each other; at 10
216
+ * it was indistinguishable from the gaps WITHIN a region (`sidebarSectionLabelGap`, the child-list
217
+ * margins), and the column read as one undifferentiated list of rows. 12 is the first step that
218
+ * reads as a region break without opening a hole. */
219
+ sidebarRegionGap: 12,
207
220
  // ── content/prose.module.css ──────────────────────────────────────────────────────────────────
208
221
  /** Blockquote rail's own vertical inset. */
209
222
  proseQuoteInsetY: 2,
@@ -354,8 +367,11 @@ const SPACE_STEP_BASE = {
354
367
  // ── shell/app-sidebar.module.css (beyond the shared/reused anchors above) ────────────────────
355
368
  /** Brand row's horizontal inset. */
356
369
  sidebarBrandInsetX: 7,
357
- /** Gap between nav sections. */
358
- sidebarSectionGap: 12,
370
+ /** Gap between nav sections. Paired with `sidebarSectionLabelGap` below: together they are the
371
+ * section's proximity law — MORE space above a section label than below it, so the label reads as
372
+ * belonging to the list it heads. 16/6, up from 12/2: at 12 above and 2 below the pair was
373
+ * INVERTED, gluing each label to the last row of the section before it. */
374
+ sidebarSectionGap: 16,
359
375
  /** Account row's top inset. */
360
376
  sidebarAccountInsetTop: 9,
361
377
  /** Account row's horizontal inset. */
@@ -364,8 +380,10 @@ const SPACE_STEP_BASE = {
364
380
  sidebarAccountInsetBottom: 2,
365
381
  /** Identity-initials avatar block's fixed size (width and height). */
366
382
  sidebarAvatarSize: 28,
367
- /** Section-label row's bottom gap (before the first nav row). */
368
- sidebarSectionLabelGap: 2,
383
+ /** Section-label row's bottom gap (before the first nav row) — the smaller half of the
384
+ * proximity pair documented on `sidebarSectionGap` above. 6, up from 2: 2 was hairline enough that
385
+ * the label looked welded to the first row rather than titling the list. */
386
+ sidebarSectionLabelGap: 6,
369
387
  /** Collapsible child-list wrapper's top margin. */
370
388
  sidebarChildListGapTop: 2,
371
389
  /** Collapsible child-list wrapper's bottom margin. */
@@ -391,15 +409,22 @@ const SPACE_STEP_BASE = {
391
409
  sidebarSettingsMenuWidth: 200,
392
410
  // ── shell/index.tsx (AppShell dimensions) ──────────────────────────────────────────────
393
411
  /** AppShell desktop header bar height — sized to hold one row of `size="md"` controls, so it
394
- * tracks `controlHeight`. Its level-0 coincidence with `appShellNavbarRailWidth` below is a
395
- * coincidence, not a law: this is a horizontal bar's HEIGHT (governed by control height), that
396
- * is a vertical rail's WIDTH (governed by icon footprint). They stay separate entries. */
412
+ * tracks `controlHeight`. It no longer coincides with `appShellNavbarRailWidth` below, which is
413
+ * the point that entry's own note was always making: this is a horizontal bar's HEIGHT (governed
414
+ * by control height), that is a vertical rail's WIDTH (governed by icon footprint). Two separate
415
+ * entries, and now visibly so. */
397
416
  /** AppShell header bar height — ONE value at every viewport since 1.26.0 (law C14,
398
417
  * `docs/CONTROLS-SPEC.md` §2.1). The pre-1.26.0 `appShellHeaderMobileHeight` (97) existed only
399
418
  * because the mobile header wrapped to a second, always-reserved page-actions row; `PageBar`
400
419
  * folds that row's overflow into a kebab and moves its filters/tabs into the page flow, so the
401
- * bar is 48px on a phone too and there is no sum to keep in step any more. */
402
- appShellHeaderHeight: 48,
420
+ * bar is one row on a phone too and there is no sum to keep in step any more.
421
+ *
422
+ * 44, down from 48. The band holds ONE `ctl`-tier row (32 since this pass), so 48 left 8px of
423
+ * dead air above and below it — the "too much spacing at the top bar" the header reads as. 44
424
+ * keeps 6px of breathing room a side, still clears `touchControlHeight` (36) below `sm` and
425
+ * `controlHeight` (42) for a `size="md"` control, and is the Apple HIG 44pt figure the rest of
426
+ * this table already floors its touch targets at. */
427
+ appShellHeaderHeight: 44,
403
428
  /** AppShell navbar width, expanded — ONE entry for both the `base` (mobile, where the navbar is
404
429
  * permanently collapsed and unused — see `MobileNav`) and the expanded `sm` value: the same
405
430
  * `.root` at full width, genuinely one concept.
@@ -411,8 +436,9 @@ const SPACE_STEP_BASE = {
411
436
  * (`sidebarAccountMenuWidth` 220 + the row inset), so the menu no longer overhangs the column it
412
437
  * belongs to. */
413
438
  appShellNavbarWidth: 256,
414
- /** AppShell navbar width, collapsed icon rail. Separate from `appShellHeaderHeight` above
415
- * despite sharing 48 at level 0 (see that entry's note). */
439
+ /** AppShell navbar width, collapsed icon rail. Separate from `appShellHeaderHeight` above, which
440
+ * used to share 48 at level 0 and no longer does (see that entry's note) — the two were always
441
+ * independent concepts and the shared literal was only ever hiding it. */
416
442
  appShellNavbarRailWidth: 48,
417
443
  /** AppShell ASIDE width, unfolded — the right-hand panel region a route claims by rendering
418
444
  * `PageAside` (`docs/ASIDE-SPEC.md` §0). ONE entry for every viewport, same reasoning as
@@ -427,6 +453,36 @@ const SPACE_STEP_BASE = {
427
453
  * `appShellNavbarRailWidth` (48) on purpose: the navbar rail hosts a full icon nav row and its
428
454
  * hover targets, this one hosts exactly one `ctl`-tier icon button and nothing else. */
429
455
  appShellAsideRailWidth: 36,
456
+ /** The PAGE GUTTER — `AppShell.Main`'s own horizontal inset, i.e. the distance from the sidebar
457
+ * seam (and from the viewport's right edge) to the first pixel of page content. Unlike its
458
+ * `appShell*` neighbours above this one IS emitted as a `--vx-*` var (see `spaceDecls`'s doc in
459
+ * `tokens/index.ts` for why), because it is the single most visible spacing number the framework
460
+ * ships and a consumer retuning its identity has to be able to reach it.
461
+ *
462
+ * 20, up from the 13 (`SPACE_SCALE.sm`) the main column used to borrow. The sidebar's nav ICON
463
+ * column already sits at x=20 — 10px of `.root` frame inset plus 10px of NavLink inset
464
+ * (`rowInsetX`) — and the brand row claims to align with it while sitting at 13. 20 makes that
465
+ * claim true across the seam: brand mark, nav icons and the page's first column all start on one
466
+ * vertical line, and the content gets a seam-side gutter that reads deliberate instead of
467
+ * incidental. */
468
+ appShellInset: 20,
469
+ /** The page gutter BELOW `sm` — the phone half of `appShellInset` above. A separate entry rather
470
+ * than one responsive value for the same reason `appShellNavbarWidth` is one: there is no sidebar
471
+ * seam to align to on a phone, so the number is answering a different question (how much of a
472
+ * 390px viewport to spend on margin) and must be free to move alone.
473
+ *
474
+ * 8, down from the same borrowed 13. 13 a side spends 26 of 390px — 6.7% of the viewport — on
475
+ * margin before a single card is drawn, which is the "content spacing is too big" a phone reads
476
+ * as. 8 keeps the content off the bezel without competing with the cards' own insets. */
477
+ appShellInsetMobile: 8,
478
+ /** The app header's BREADCRUMB FLOOR — `shell/app-header.module.css`'s `.lead` `min-width`, the
479
+ * width below which the row's one elastic side may not shrink. A step, not the raw `96px` it
480
+ * shipped as: every other dimension of that row (its height, its control tier, its gaps) tracks
481
+ * density, so a frozen floor would grow relatively narrower at every level up — and it is a floor
482
+ * measured in CHARACTERS of the crumb (~13 at level 0), which is exactly the quantity that moves
483
+ * when the type and the row around it move. The full derivation of 96 is on the `.lead` rule
484
+ * itself; this entry only owns the number. */
485
+ appShellHeaderLeadMin: 96,
430
486
  // ── shell/app-mobile-nav.module.css ───────────────────────────────────────────────────────────
431
487
  /** Tab's icon-to-label gap. */
432
488
  mobileNavTabGap: 3,
@@ -507,11 +563,47 @@ const SPACE_STEP_BASE = {
507
563
  virtualRowInsetY: 8,
508
564
  /** `data/virtual-list.tsx`'s skeleton row horizontal inset. */
509
565
  virtualRowInsetX: 12,
566
+ // ── The card/section inset ladder (`tokens/index.ts`'s `spaceMobileDecls` is its ONE home) ────
567
+ // The three numbers a page's CONTENT spends on itself, as opposed to the shell gutter around it
568
+ // (`appShellInset`/`appShellInsetMobile` above). They are grouped, and named, because they step
569
+ // TOGETHER below `sm` — the gutter went 20 -> 8 in the desktop-shell pass while the ladder inside
570
+ // it never moved, so a 390px phone still read "content spacing too big" after the gutter fix.
571
+ //
572
+ // Each value is deliberately the rung of an existing ladder rather than a fresh literal: the card
573
+ // inset IS `SPACE_SCALE.xs`/`.sm` (which is what `StatCard`/`ChartCard` spelled as
574
+ // `--mantine-spacing-xs`/`-sm` before they were named here) and the Section gap IS
575
+ // `SPACE.stackMd`. Naming them buys the ROLE, not a new number — a role the `< sm` override can
576
+ // then re-point without touching the scale every other consumer of `xs`/`sm` reads.
577
+ //
578
+ // There is deliberately NO `*Mobile` sibling for the two that step, unlike `appShellInsetMobile`:
579
+ // that one is a separate entry because a phone gutter answers a different question from a desktop
580
+ // one (how much of 390px to spend on margin, with no sidebar seam to align to). These two answer
581
+ // the SAME question one rung down, so the override reads the neighbouring rung (`scale.xs`,
582
+ // `anchors.stackSm`) and a second literal would only be a place for the ladder to drift apart.
583
+ /** Card inset, VERTICAL — `dashboard/stat-card.tsx` and `charts/primitives/ChartCard.tsx`, which
584
+ * must render identically (a StatCard and a ChartCard sit in the same grid row). Does NOT step
585
+ * below `sm`: the phone is short of WIDTH, and shaving the vertical inset of a card that already
586
+ * hugs its content buys a few pixels at the cost of the card's own breathing room. */
587
+ cardInsetY: 11,
588
+ /** Card inset, HORIZONTAL — same two files, same must-match reason. This IS the half that steps
589
+ * below `sm` (to `SPACE_SCALE.xs`), because horizontal is where a 390px viewport is short. */
590
+ cardInsetX: 13,
591
+ /** `dashboard/section.module.css`'s header→body and body-child gap — a page-level GROUPING
592
+ * rhythm, which is why it reads the stack scale rather than the card-inset one. Steps to
593
+ * `SPACE.stackSm` below `sm`. Distinct from `sidebarSectionGap` above, which is the gap between
594
+ * two NAV sections inside the sidebar column. */
595
+ sectionGap: 12,
510
596
  // ── Controls — homes/tiers (`docs/CONTROLS-SPEC.md` §5) ──────────────────────────────────────
511
597
  /** `PageBar` row height (row 1 or row 2, each measured independently — the bar's own overall
512
598
  * height is the sum published as `--basalt-page-bar-h`, computed by `PageBar` itself, not a
513
- * static token). */
514
- pageBarRowHeight: 40,
599
+ * static token).
600
+ *
601
+ * 36, down from 40. Row 1 portals into a shell-owned band directly under the app header
602
+ * (`shell/page-bar.tsx`), so the two stack as visible chrome: at 40 against a 44 header the
603
+ * secondary row all but matched the primary one, and the eye could not tell which band owned the
604
+ * page. 36 puts it a clear step below the header while still holding a 32px `ctl` row with 2px a
605
+ * side. */
606
+ pageBarRowHeight: 36,
515
607
  /** `WidgetHeader tier="section"` row height. */
516
608
  sectionHeaderHeight: 36,
517
609
  /** `WidgetHeader tier="widget"` row height. */
package/dist/tokens.css CHANGED
@@ -13,7 +13,7 @@
13
13
  --vx-space-control-height: 2.625rem;
14
14
  --vx-space-control-height-tag: 1.25rem;
15
15
  --vx-space-control-height-widget: 1.5rem;
16
- --vx-space-control-height-ctl: 1.875rem;
16
+ --vx-space-control-height-ctl: 2rem;
17
17
  --vx-space-touch-control-height: 2.25rem;
18
18
  --vx-space-stack-xs: 4px;
19
19
  --vx-space-stack-sm: 8px;
@@ -22,7 +22,7 @@
22
22
  --vx-space-stack-xl: 24px;
23
23
  --vx-space-sticky-header-clearance: 12px;
24
24
  --vx-space-nav-icon-gap: 10px;
25
- --vx-space-sidebar-region-gap: 10px;
25
+ --vx-space-sidebar-region-gap: 12px;
26
26
  --vx-space-prose-quote-inset-y: 2px;
27
27
  --vx-space-prose-quote-indent: 12px;
28
28
  --vx-space-prose-inline-code-inset-y: 1.5px;
@@ -85,17 +85,20 @@
85
85
  --vx-space-settings-row-gap: 16px;
86
86
  --vx-space-mermaid-container-inset: 16px;
87
87
  --vx-space-sidebar-brand-inset-x: 7px;
88
- --vx-space-sidebar-section-gap: 12px;
88
+ --vx-space-sidebar-section-gap: 16px;
89
89
  --vx-space-sidebar-account-inset-top: 9px;
90
90
  --vx-space-sidebar-account-inset-x: 7px;
91
91
  --vx-space-sidebar-account-inset-bottom: 2px;
92
92
  --vx-space-sidebar-avatar-size: 28px;
93
- --vx-space-sidebar-section-label-gap: 2px;
93
+ --vx-space-sidebar-section-label-gap: 6px;
94
94
  --vx-space-sidebar-child-list-gap-top: 2px;
95
95
  --vx-space-sidebar-child-list-gap-bottom: 3px;
96
96
  --vx-space-sidebar-child-list-indent: 15px;
97
97
  --vx-space-sidebar-child-row-inset-y: 4px;
98
98
  --vx-space-sidebar-child-row-indent: 12px;
99
+ --vx-space-app-shell-inset: 20px;
100
+ --vx-space-app-shell-inset-mobile: 8px;
101
+ --vx-space-app-shell-header-lead-min: 96px;
99
102
  --vx-space-mobile-nav-tab-gap: 3px;
100
103
  --vx-space-mobile-nav-icon-size: 24px;
101
104
  --vx-space-mobile-nav-tab-inset-y: 2px;
@@ -113,9 +116,12 @@
113
116
  --vx-space-badge-inset-y: 2px;
114
117
  --vx-space-badge-inset-x: 7px;
115
118
  --vx-space-stat-card-gap: 8px;
119
+ --vx-space-card-inset-y: 11px;
120
+ --vx-space-card-inset-x: 13px;
121
+ --vx-space-section-gap: 12px;
116
122
  --vx-space-virtual-row-inset-y: 8px;
117
123
  --vx-space-virtual-row-inset-x: 12px;
118
- --vx-space-page-bar-row-height: 2.5rem;
124
+ --vx-space-page-bar-row-height: 2.25rem;
119
125
  --vx-space-section-header-height: 2.25rem;
120
126
  --vx-space-widget-header-height: 1.75rem;
121
127
  --vx-space-sidebar-block-row-height: 2rem;
@@ -184,6 +190,12 @@
184
190
  --vx-badRef: var(--vx-bad-ref);
185
191
  --vx-warnRef: var(--vx-warn-ref);
186
192
  }
193
+ @media (max-width: 47.99375em) {
194
+ :root {
195
+ --vx-space-card-inset-x: 11px;
196
+ --vx-space-section-gap: 8px;
197
+ }
198
+ }
187
199
  :root,
188
200
  html[data-mantine-color-scheme='dark'] {
189
201
  --vx-status-excellent: #43bf4d;
package/llms.txt CHANGED
@@ -1,4 +1,4 @@
1
- # basalt-ui v1.29.2
1
+ # basalt-ui v1.30.1
2
2
  # Opinionated framework for Mantine-based React apps — theme, app shell, visx charts, toolchain presets
3
3
  #
4
4
  # basalt-ui is an opinionated framework for Mantine-based React apps.
@@ -19,7 +19,7 @@
19
19
  # ./tokens.css subpath. Guide: docs/FRAMEWORK-FREE.md in the repo.
20
20
 
21
21
  Package: basalt-ui
22
- Version: 1.29.2
22
+ Version: 1.30.1
23
23
 
24
24
  ## basalt-ui
25
25
  Import: import { ... } from 'basalt-ui'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "basalt-ui",
3
- "version": "1.29.2",
3
+ "version": "1.30.1",
4
4
  "description": "Opinionated framework for Mantine-based React apps — theme, app shell, visx charts, toolchain presets",
5
5
  "keywords": [
6
6
  "charts",
@@ -67,6 +67,7 @@ export {
67
67
  ChartFrame,
68
68
  type ChartFrameProps,
69
69
  type ChartFrameLegend,
70
+ type ResponsiveChartHeight,
70
71
  type PlotRect,
71
72
  resolveLegend,
72
73
  } from './primitives/ChartFrame'
@@ -10,6 +10,7 @@ import { useBandPlot } from '../hooks/useBandPlot'
10
10
  import type { BandFold, BandTooltipConfig } from '../hooks/useBandPlot'
11
11
  import { AxisBottomDate } from '../primitives/Axes'
12
12
  import { ChartFrame, resolveLegend } from '../primitives/ChartFrame'
13
+ import type { ResponsiveChartHeight } from '../primitives/ChartFrame'
13
14
  import {
14
15
  ChartTooltipFloat,
15
16
  TooltipBody,
@@ -94,9 +95,10 @@ export type BandStripProps<T> = BasaltProps & {
94
95
  series: readonly BandStripSeries<T>[]
95
96
  /** Which state each datum is in. */
96
97
  getBand: (d: T) => BandSpan
97
- /** Fixed height in px, forwarded to `ChartFrame`. The band row gets whatever the measured
98
- * margins and the measured legend band leave. Default 240. */
99
- height?: number
98
+ /** Height in px, or one per size step (`{ base, sm?, md?, lg? }`), forwarded to `ChartFrame`.
99
+ * The band row gets whatever the measured margins and legend band leave. Default 240.
100
+ * `ChartFrameProps.height` owns the law — steps resolve off the MEASURED width. */
101
+ height?: number | ResponsiveChartHeight
100
102
  /** Fill the parent flex/grid cell's measured height instead of a fixed one. */
101
103
  fill?: boolean
102
104
  /** X tick label formatter. Default `fmtAxisDate` (DD.MM). */
@@ -7,6 +7,7 @@ import { VX } from '../../tokens'
7
7
  import type { ChartMargin } from '../../tokens'
8
8
  import { CartesianChart } from '../primitives/CartesianChart'
9
9
  import type { AxisConfig, CartesianTooltipConfig, PlotContext } from '../primitives/CartesianChart'
10
+ import type { ResponsiveChartHeight } from '../primitives/ChartFrame'
10
11
  import type { ZoneSpec } from '../primitives/ZoneRects'
11
12
  import type { ChartState } from '../primitives/ChartPending'
12
13
  import { curveFor, LINE_OVERLAY_STROKE_WIDTH } from '../series'
@@ -65,8 +66,9 @@ export type BarsRefLine = {
65
66
 
66
67
  export type BarsProps<T> = BasaltProps & {
67
68
  data: T[]
68
- /** Fixed height in pixels, forwarded to `CartesianChart`. Default 240. */
69
- height?: number
69
+ /** Height in px, or one per size step (`{ base, sm?, md?, lg? }`). Default 240. Forwarded
70
+ * unchanged; `ChartFrameProps.height` owns the law — steps resolve off the MEASURED width. */
71
+ height?: number | ResponsiveChartHeight
70
72
  chartId: string
71
73
  getX: (d: T) => string
72
74
  /** Generic value accessor — given a data point and a bar/line key, returns the value or null. */
@@ -6,6 +6,7 @@ import { assertRequiredProps } from '../../common/validate'
6
6
  import type { BasaltProps } from '../../common/props'
7
7
  import { ChartTooltipFloat, TooltipBody, TooltipRow } from '../primitives/ChartTooltip'
8
8
  import { ChartFrame } from '../primitives/ChartFrame'
9
+ import type { ResponsiveChartHeight } from '../primitives/ChartFrame'
9
10
  import type { ChartState } from '../primitives/ChartPending'
10
11
  import { VX } from '../../tokens'
11
12
  import type { SeriesStyle } from '../series'
@@ -20,8 +21,9 @@ export type DonutDatum<K extends string = SeriesKey> = { key: K; value: number }
20
21
 
21
22
  export type DonutProps<K extends string = SeriesKey> = BasaltProps & {
22
23
  data: DonutDatum<K>[]
23
- /** Fixed height in pixels, forwarded to the internal `ChartFrame`. Default 240. */
24
- height?: number
24
+ /** Height in px, or one per size step (`{ base, sm?, md?, lg? }`). Default 240. Forwarded
25
+ * unchanged; `ChartFrameProps.height` owns the law — steps resolve off the MEASURED width. */
26
+ height?: number | ResponsiveChartHeight
25
27
  colorForKey: (key: K) => string
26
28
  formatValue: (v: number) => string
27
29
  seriesLabel?: (key: string) => string
@@ -15,6 +15,7 @@ import {
15
15
  TooltipRow,
16
16
  } from '../primitives/ChartTooltip'
17
17
  import { ChartFrame, resolveLegend } from '../primitives/ChartFrame'
18
+ import type { ResponsiveChartHeight } from '../primitives/ChartFrame'
18
19
  import { Crosshair, SeriesDot } from '../primitives/Crosshair'
19
20
  import { HoverOverlay } from '../primitives/HoverOverlay'
20
21
  import { ZoneRects } from '../primitives/ZoneRects'
@@ -31,8 +32,9 @@ import { smartTicks, xLabelPxFor } from '../utils/ticks'
31
32
 
32
33
  export type DualPanelProps<T> = BasaltProps & {
33
34
  data: T[]
34
- /** Fixed height in pixels, forwarded to the internal `ChartFrame`. Default 240. */
35
- height?: number
35
+ /** Height in px, or one per size step (`{ base, sm?, md?, lg? }`). Default 240. Forwarded
36
+ * unchanged; `ChartFrameProps.height` owns the law — steps resolve off the MEASURED width. */
37
+ height?: number | ResponsiveChartHeight
36
38
  chartId: string
37
39
  getX: (d: T) => string
38
40
  /** X tick label formatter. Default `fmtAxisDate` (DD.MM) — same default `CartesianChart` uses,
@@ -10,6 +10,7 @@ import {
10
10
  TooltipRow,
11
11
  } from '../primitives/ChartTooltip'
12
12
  import { ChartFrame } from '../primitives/ChartFrame'
13
+ import type { ResponsiveChartHeight } from '../primitives/ChartFrame'
13
14
  import { useChartTierMetrics } from '../primitives/chart-tier'
14
15
  import type { ChartState } from '../primitives/ChartPending'
15
16
  import { maxTextWidth } from '../utils/measure-text'
@@ -24,8 +25,10 @@ type HeatmapTip = HeatmapCell & { anchor: { x: number; y: number } }
24
25
 
25
26
  export type HeatmapProps<T> = BasaltProps & {
26
27
  data: T[]
27
- /** Fixed height in pixels. Used when neither `aspectRatio` nor `fill` is set. Default 240. */
28
- height?: number
28
+ /** Height in px, or one per size step (`{ base, sm?, md?, lg? }`). Used when neither
29
+ * `aspectRatio` nor `fill` is set. Default 240. `ChartFrameProps.height` owns the law —
30
+ * steps resolve off the MEASURED width. */
31
+ height?: number | ResponsiveChartHeight
29
32
  /** height = Math.round(containerWidth / aspectRatio). Ignored when `fill` is set. */
30
33
  aspectRatio?: number
31
34
  /** Fill the parent flex/grid cell's measured height instead of a fixed/derived one. */
@@ -12,6 +12,7 @@ import type { BandFold, BandTooltipConfig } from '../hooks/useBandPlot'
12
12
  import { probeAxisLabels } from '../layout/auto-margin'
13
13
  import { AxisBottomDate, AxisLeftNumeric } from '../primitives/Axes'
14
14
  import { ChartFrame, resolveLegend } from '../primitives/ChartFrame'
15
+ import type { ResponsiveChartHeight } from '../primitives/ChartFrame'
15
16
  import {
16
17
  ChartTooltipFloat,
17
18
  TooltipBody,
@@ -79,8 +80,9 @@ export type MirroredBarsProps<T> = BasaltProps & {
79
80
  /** Per-datum bar opacity — a real but qualified measurement (a short bucket, a partial interval)
80
81
  * drawn dimmer. Default 1. */
81
82
  getBarOpacity?: (d: T) => number
82
- /** Fixed height in px, forwarded to `ChartFrame`. Default 240. */
83
- height?: number
83
+ /** Height in px, or one per size step (`{ base, sm?, md?, lg? }`). Default 240. Forwarded
84
+ * unchanged; `ChartFrameProps.height` owns the law — steps resolve off the MEASURED width. */
85
+ height?: number | ResponsiveChartHeight
84
86
  fill?: boolean
85
87
  formatX?: (key: string) => string
86
88
  xTickValues?: (keys: readonly string[], plotWidth: number) => readonly string[]
@@ -6,6 +6,7 @@ import type { BasaltProps } from '../../common/props'
6
6
  import type { CursorResolution } from '../cursor/resolve'
7
7
  import type { CartesianTooltipConfig, AxisConfig, PlotContext } from '../primitives/CartesianChart'
8
8
  import { CartesianChart } from '../primitives/CartesianChart'
9
+ import type { ResponsiveChartHeight } from '../primitives/ChartFrame'
9
10
  import type { XZoneSpec } from '../primitives/XZoneRects'
10
11
  import type { ZoneSpec } from '../primitives/ZoneRects'
11
12
  import type { ChartState } from '../primitives/ChartPending'
@@ -15,8 +16,9 @@ import { VX } from '../../tokens'
15
16
 
16
17
  export type MultiLineProps<T> = BasaltProps & {
17
18
  data: T[]
18
- /** Fixed height in pixels, forwarded to `CartesianChart`. Default 240. */
19
- height?: number
19
+ /** Height in px, or one per size step (`{ base, sm?, md?, lg? }`). Default 240. Forwarded
20
+ * unchanged; `ChartFrameProps.height` owns the law — steps resolve off the MEASURED width. */
21
+ height?: number | ResponsiveChartHeight
20
22
  chartId: string
21
23
  /** Extracts the x-axis category (date string) from a data point. */
22
24
  getX: (d: T) => string
@@ -5,6 +5,7 @@ import type { BasaltProps } from '../../common/props'
5
5
  import type { CursorResolution } from '../cursor/resolve'
6
6
  import { CartesianChart } from '../primitives/CartesianChart'
7
7
  import type { AxisConfig, PlotContext } from '../primitives/CartesianChart'
8
+ import type { ResponsiveChartHeight } from '../primitives/ChartFrame'
8
9
  import type { ChartState } from '../primitives/ChartPending'
9
10
  import { curveFor } from '../series'
10
11
  import type { ChartLegendConfig, ChartSeries } from '../series'
@@ -12,8 +13,9 @@ import { isDev } from '../../common/is-dev'
12
13
 
13
14
  export type StackedAreaProps<T> = BasaltProps & {
14
15
  data: T[]
15
- /** Fixed height in pixels, forwarded to `CartesianChart`. Default 240. */
16
- height?: number
16
+ /** Height in px, or one per size step (`{ base, sm?, md?, lg? }`). Default 240. Forwarded
17
+ * unchanged; `ChartFrameProps.height` owns the law — steps resolve off the MEASURED width. */
18
+ height?: number | ResponsiveChartHeight
17
19
  chartId: string
18
20
  getX: (d: T) => string
19
21
  /** The stacked bands, bottom to top — the single source of truth for color, legend, and tooltip
@@ -7,6 +7,7 @@ import type { CursorResolution } from '../cursor/resolve'
7
7
  import { AreaGradient, areaFillUrl } from '../primitives/AreaGradient'
8
8
  import type { CartesianTooltipConfig, AxisConfig, PlotContext } from '../primitives/CartesianChart'
9
9
  import { CartesianChart } from '../primitives/CartesianChart'
10
+ import type { ResponsiveChartHeight } from '../primitives/ChartFrame'
10
11
  import type { XZoneSpec } from '../primitives/XZoneRects'
11
12
  import type { ZoneSpec } from '../primitives/ZoneRects'
12
13
  import type { ChartState } from '../primitives/ChartPending'
@@ -31,8 +32,9 @@ export type ZonedLineRefLine = {
31
32
 
32
33
  export type ZonedLineProps<T> = BasaltProps & {
33
34
  data: T[]
34
- /** Fixed height in pixels, forwarded to `CartesianChart`. Default 240. */
35
- height?: number
35
+ /** Height in px, or one per size step (`{ base, sm?, md?, lg? }`). Default 240. Forwarded
36
+ * unchanged; `ChartFrameProps.height` owns the law — steps resolve off the MEASURED width. */
37
+ height?: number | ResponsiveChartHeight
36
38
  chartId: string
37
39
  /** Extracts the x-axis category (date string) from a data point. */
38
40
  getX: (d: T) => string
@@ -17,6 +17,7 @@ import { fmtAxisDate } from '../utils/format'
17
17
  import { autoXLabelRotate, smartTicks, smartTicksEvery, xLabelPxFor } from '../utils/ticks'
18
18
  import { AxisBottomDate, AxisLeftNumeric, AxisRightNumeric } from './Axes'
19
19
  import { ChartFrame, resolveLegend } from './ChartFrame'
20
+ import type { ResponsiveChartHeight } from './ChartFrame'
20
21
  import { useChartTierMetrics } from './chart-tier'
21
22
  import type { ChartState } from './ChartPending'
22
23
  import { ChartTooltipFloat, TooltipBody, TooltipHeader, TooltipRow } from './ChartTooltip'
@@ -209,7 +210,9 @@ export type CartesianChartProps<T> = BasaltProps & {
209
210
  /** X-range bands (time windows), drawn behind the marks. */
210
211
  xZones?: XZoneSpec[]
211
212
  refLines?: { value: number; color: string; dashed?: boolean; axis?: 'left' | 'right' }[]
212
- height?: number
213
+ /** Height in px, or one per size step (`{ base, sm?, md?, lg? }`). Default 240. Forwarded
214
+ * unchanged; `ChartFrameProps.height` owns the law — steps resolve off the MEASURED width. */
215
+ height?: number | ResponsiveChartHeight
213
216
  aspectRatio?: number
214
217
  fill?: boolean
215
218
  legend?: ChartLegendConfig | false
@@ -45,15 +45,20 @@ const bodyClipStyle: CSSProperties = {
45
45
  borderBottomLeftRadius: VX.radiusCard,
46
46
  borderBottomRightRadius: VX.radiusCard,
47
47
  overflow: 'hidden',
48
- padding: '2px var(--mantine-spacing-sm, 0.8125rem) var(--mantine-spacing-xs, 0.6875rem)',
48
+ padding: '2px var(--vx-space-card-inset-x, 0.8125rem) var(--vx-space-card-inset-y, 0.6875rem)',
49
49
  }
50
- // Card inset = spacing xs (vertical) / sm (horizontal). The `--mantine-spacing-*` vars carry
51
- // basalt's OWNED scale (theme.spacing.xs 0.6875rem / sm 0.8125rem); the rem fallbacks keep a
52
- // Mantine-free `./charts`-only consumer (no MantineProvider vars) padded identically. No
53
- // header/body divider — the title block and the plot read as one continuous surface; a rule here
54
- // only doubled the card's shadow-embedded ring and made the header feel heavier.
50
+ // Card inset = the `cardInsetY`/`cardInsetX` role tokens (vertical / horizontal), the SAME two
51
+ // vars `StatCard` reads — a ChartCard and a StatCard in one grid row must not disagree about their
52
+ // inset, which is why the pair is one named role rather than two files each spelling `xs`/`sm`.
53
+ // They used to read `--mantine-spacing-xs`/`-sm`; the `--vx-*` pair carries the same numbers off
54
+ // the same owned scale, is emitted by the same stylesheet at a retuned density, and — unlike the
55
+ // Mantine vars — steps a rung down below `sm` (the mobile inset ladder, `spaceMobileDecls` in
56
+ // `tokens/index.ts`). The rem fallbacks are unchanged and keep a `./charts`-only consumer with
57
+ // neither MantineProvider nor BasaltProvider padded identically. No header/body divider — the
58
+ // title block and the plot read as one continuous surface; a rule here only doubled the card's
59
+ // shadow-embedded ring and made the header feel heavier.
55
60
  const headerWrapStyle: CSSProperties = {
56
- padding: 'var(--mantine-spacing-xs, 0.6875rem) var(--mantine-spacing-sm, 0.8125rem) 4px',
61
+ padding: 'var(--vx-space-card-inset-y, 0.6875rem) var(--vx-space-card-inset-x, 0.8125rem) 4px',
57
62
  }
58
63
 
59
64
  /**