basalt-ui 1.17.0 → 1.19.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 (112) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +14 -0
  3. package/agent/rules/basalt-charts.md +12 -0
  4. package/agent/rules/basalt-mantine.md +71 -6
  5. package/agent/rules/basalt-router.md +129 -30
  6. package/agent/rules/basalt-tokens.md +2 -1
  7. package/agent/skills/basalt-design/SKILL.md +2 -2
  8. package/dist/charts/kinds/DualPanel.d.ts +7 -0
  9. package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
  10. package/dist/charts/kinds/DualPanel.js +10 -3
  11. package/dist/charts/primitives/CartesianChart.d.ts +17 -0
  12. package/dist/charts/primitives/CartesianChart.d.ts.map +1 -1
  13. package/dist/charts/primitives/CartesianChart.js +5 -2
  14. package/dist/charts/primitives/ChartTooltip.d.ts +6 -1
  15. package/dist/charts/primitives/ChartTooltip.d.ts.map +1 -1
  16. package/dist/charts/primitives/ChartTooltip.js +2 -1
  17. package/dist/charts/series.d.ts +5 -0
  18. package/dist/charts/series.d.ts.map +1 -1
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +8 -0
  22. package/dist/nav/types.d.ts +110 -0
  23. package/dist/nav/types.d.ts.map +1 -0
  24. package/dist/nav/types.js +0 -0
  25. package/dist/router-tanstack/index.d.ts +9 -2
  26. package/dist/router-tanstack/index.d.ts.map +1 -1
  27. package/dist/router-tanstack/index.js +7 -0
  28. package/dist/router-tanstack/nav.d.ts +163 -0
  29. package/dist/router-tanstack/nav.d.ts.map +1 -0
  30. package/dist/router-tanstack/nav.js +38 -0
  31. package/dist/router-tanstack/nav.test.d.ts +2 -0
  32. package/dist/router-tanstack/nav.test.d.ts.map +1 -0
  33. package/dist/router-tanstack/search-param-store.d.ts +19 -9
  34. package/dist/router-tanstack/search-param-store.d.ts.map +1 -1
  35. package/dist/router-tanstack/use-nav.d.ts +30 -0
  36. package/dist/router-tanstack/use-nav.d.ts.map +1 -0
  37. package/dist/router-tanstack/use-nav.js +71 -0
  38. package/dist/shell/app-breadcrumbs.d.ts +7 -9
  39. package/dist/shell/app-breadcrumbs.d.ts.map +1 -1
  40. package/dist/shell/app-breadcrumbs.js +8 -4
  41. package/dist/shell/app-mobile-nav.d.ts +23 -24
  42. package/dist/shell/app-mobile-nav.d.ts.map +1 -1
  43. package/dist/shell/app-mobile-nav.js +290 -68
  44. package/dist/shell/app-mobile-nav.module.css +166 -11
  45. package/dist/shell/app-mobile-nav.module.css.d.ts +11 -2
  46. package/dist/shell/app-mobile-nav.test.d.ts +2 -0
  47. package/dist/shell/app-mobile-nav.test.d.ts.map +1 -0
  48. package/dist/shell/app-sidebar.d.ts +3 -28
  49. package/dist/shell/app-sidebar.d.ts.map +1 -1
  50. package/dist/shell/app-sidebar.js +40 -174
  51. package/dist/shell/app-sidebar.module.css +3 -2
  52. package/dist/shell/app-sidebar.module.css.d.ts +0 -2
  53. package/dist/shell/index.d.ts +22 -65
  54. package/dist/shell/index.d.ts.map +1 -1
  55. package/dist/shell/index.js +38 -76
  56. package/dist/shell/mobile-nav-model.d.ts +41 -0
  57. package/dist/shell/mobile-nav-model.d.ts.map +1 -0
  58. package/dist/shell/mobile-nav-model.js +244 -0
  59. package/dist/shell/mobile-nav-model.test.d.ts +2 -0
  60. package/dist/shell/mobile-nav-model.test.d.ts.map +1 -0
  61. package/dist/shell/nav-count-badge.d.ts +11 -0
  62. package/dist/shell/nav-count-badge.d.ts.map +1 -0
  63. package/dist/shell/nav-count-badge.js +29 -0
  64. package/dist/surfaces.d.ts +1 -1
  65. package/dist/surfaces.js +1 -1
  66. package/dist/theme/index.d.ts.map +1 -1
  67. package/dist/theme/index.js +8 -7
  68. package/dist/theme/nav-link.module.css +4 -2
  69. package/dist/tokens/index.d.ts.map +1 -1
  70. package/dist/tokens/index.js +2 -0
  71. package/dist/tokens/palette.d.ts +15 -0
  72. package/dist/tokens/palette.d.ts.map +1 -1
  73. package/dist/tokens/palette.js +13 -0
  74. package/dist/tokens.css +2 -0
  75. package/llms.txt +3 -3
  76. package/package.json +1 -1
  77. package/src/charts/kinds/DualPanel.test.tsx +81 -1
  78. package/src/charts/kinds/DualPanel.tsx +26 -4
  79. package/src/charts/primitives/CartesianChart.test.tsx +173 -1
  80. package/src/charts/primitives/CartesianChart.tsx +29 -5
  81. package/src/charts/primitives/ChartTooltip.test.tsx +22 -1
  82. package/src/charts/primitives/ChartTooltip.tsx +7 -1
  83. package/src/charts/series.ts +5 -0
  84. package/src/index.ts +15 -5
  85. package/src/nav/types.ts +115 -0
  86. package/src/router-tanstack/index.ts +21 -2
  87. package/src/router-tanstack/nav.test.ts +111 -0
  88. package/src/router-tanstack/nav.ts +205 -0
  89. package/src/router-tanstack/search-param-store.ts +19 -9
  90. package/src/router-tanstack/use-nav.ts +146 -0
  91. package/src/shell/app-breadcrumbs.tsx +21 -19
  92. package/src/shell/app-mobile-nav.module.css +166 -11
  93. package/src/shell/app-mobile-nav.module.css.d.ts +11 -2
  94. package/src/shell/app-mobile-nav.test.tsx +369 -0
  95. package/src/shell/app-mobile-nav.tsx +436 -102
  96. package/src/shell/app-sidebar.module.css +3 -2
  97. package/src/shell/app-sidebar.module.css.d.ts +0 -2
  98. package/src/shell/app-sidebar.test.tsx +6 -10
  99. package/src/shell/app-sidebar.tsx +49 -206
  100. package/src/shell/index.test.tsx +62 -7
  101. package/src/shell/index.tsx +105 -164
  102. package/src/shell/mobile-nav-model.test.ts +296 -0
  103. package/src/shell/mobile-nav-model.ts +389 -0
  104. package/src/shell/nav-count-badge.tsx +39 -0
  105. package/src/surfaces.ts +1 -1
  106. package/src/theme/density-relations.test.ts +19 -1
  107. package/src/theme/index.ts +8 -7
  108. package/src/theme/nav-link.module.css +4 -2
  109. package/src/theme/spacing.test.ts +11 -0
  110. package/src/tokens/density.test.ts +2 -0
  111. package/src/tokens/index.ts +8 -5
  112. package/src/tokens/palette.ts +16 -0
@@ -0,0 +1,29 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { Badge } from "@mantine/core";
3
+ import { VX } from "../tokens/index.js";
4
+ function NavCountBadge({ count }) {
5
+ if (!count) return null;
6
+ return /* @__PURE__ */ jsx(
7
+ Badge,
8
+ {
9
+ size: "sm",
10
+ styles: {
11
+ root: {
12
+ backgroundColor: "color-mix(in srgb, var(--vx-ink) 8%, transparent)",
13
+ color: "var(--vx-muted)",
14
+ fontFamily: "var(--basalt-font-mono)",
15
+ fontSize: VX.text.micro,
16
+ fontWeight: 500,
17
+ height: 16,
18
+ padding: "0 5px",
19
+ marginLeft: "auto",
20
+ borderRadius: "var(--vx-radius-tight)"
21
+ }
22
+ },
23
+ children: count
24
+ }
25
+ );
26
+ }
27
+ export {
28
+ NavCountBadge
29
+ };
@@ -209,7 +209,7 @@ export declare const SURFACES: {
209
209
  readonly rule: "router";
210
210
  readonly skill: readonly ["basalt-app"];
211
211
  readonly guardKinds: readonly [];
212
- readonly description: "TanStack Router bridge: useBasaltNav (active route) + useRouterBreadcrumbs + createSearchParamStore (single-select URL-state store) + createMultiSearchParamStore (multi-select URL-state store)";
212
+ readonly description: "TanStack Router bridge: defineNav/navGroup/navTarget (one typed nav definition) + useNav (sections + mobileNav, spread onto BasaltShell) + useBasaltNav (active route) + useRouterBreadcrumbs + createSearchParamStore (single-select URL-state store) + createMultiSearchParamStore (multi-select URL-state store)";
213
213
  readonly optionalPeers: readonly ["@tanstack/react-router"];
214
214
  readonly globs: {
215
215
  readonly shipped: readonly [];
package/dist/surfaces.js CHANGED
@@ -120,7 +120,7 @@ const SURFACES = {
120
120
  rule: "router",
121
121
  skill: ["basalt-app"],
122
122
  guardKinds: [],
123
- description: "TanStack Router bridge: useBasaltNav (active route) + useRouterBreadcrumbs + createSearchParamStore (single-select URL-state store) + createMultiSearchParamStore (multi-select URL-state store)",
123
+ description: "TanStack Router bridge: defineNav/navGroup/navTarget (one typed nav definition) + useNav (sections + mobileNav, spread onto BasaltShell) + useBasaltNav (active route) + useRouterBreadcrumbs + createSearchParamStore (single-select URL-state store) + createMultiSearchParamStore (multi-select URL-state store)",
124
124
  optionalPeers: ["@tanstack/react-router"],
125
125
  globs: {
126
126
  shipped: [],
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/theme/index.ts"],"names":[],"mappings":"AA+DA,OAAO,KAAK,EACV,oBAAoB,EAGpB,oBAAoB,EAErB,MAAM,eAAe,CAAA;AAStB,OAAO,KAAK,EAAe,YAAY,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAE/E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAEpD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAA;AAOlD;;;;;GAKG;AACH,YAAY,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAA;AAIlD,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AASvD,OAAO,QAAQ,eAAe,CAAC;IAC7B,UAAU,iBAAiB;QACzB,YAAY,CAAC,EAAE,YAAY,CAAA;QAC3B,WAAW,CAAC,EAAE,iBAAiB,CAAA;QAC/B,YAAY,CAAC,EAAE,YAAY,CAAA;QAC3B,aAAa,CAAC,EAAE,WAAW,CAAA;KAC5B;CACF;AAk5BD;+EAC+E;AAC/E,eAAO,MAAM,SAAS,EAAE,oBAAuD,CAAA;AAmJ/E;;8FAE8F;AAC9F,eAAO,MAAM,oBAAoB,EAAE,oBACc,CAAA;AAEjD,kEAAkE;AAClE,MAAM,MAAM,wBAAwB,GAAG;IACrC;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,CAAA;IAE9B;;;;;;;;;;;;;;;;OAgBG;IACH,KAAK,CAAC,EAAE,iBAAiB,CAAA;IAEzB;;;;;;;;;;;;;;;;OAgBG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IAEf;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,CAAA;AAYD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,CAAC,EAAE,oBAAoB,EAChC,OAAO,CAAC,EAAE,wBAAwB,GACjC,oBAAoB,CA+BtB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/theme/index.ts"],"names":[],"mappings":"AA+DA,OAAO,KAAK,EACV,oBAAoB,EAGpB,oBAAoB,EAErB,MAAM,eAAe,CAAA;AAStB,OAAO,KAAK,EAAe,YAAY,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAE/E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAEpD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAA;AAOlD;;;;;GAKG;AACH,YAAY,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAA;AAIlD,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AASvD,OAAO,QAAQ,eAAe,CAAC;IAC7B,UAAU,iBAAiB;QACzB,YAAY,CAAC,EAAE,YAAY,CAAA;QAC3B,WAAW,CAAC,EAAE,iBAAiB,CAAA;QAC/B,YAAY,CAAC,EAAE,YAAY,CAAA;QAC3B,aAAa,CAAC,EAAE,WAAW,CAAA;KAC5B;CACF;AAm5BD;+EAC+E;AAC/E,eAAO,MAAM,SAAS,EAAE,oBAAuD,CAAA;AAmJ/E;;8FAE8F;AAC9F,eAAO,MAAM,oBAAoB,EAAE,oBACc,CAAA;AAEjD,kEAAkE;AAClE,MAAM,MAAM,wBAAwB,GAAG;IACrC;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,CAAA;IAE9B;;;;;;;;;;;;;;;;OAgBG;IACH,KAAK,CAAC,EAAE,iBAAiB,CAAA;IAEzB;;;;;;;;;;;;;;;;OAgBG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IAEf;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,CAAA;AAYD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,CAAC,EAAE,oBAAoB,EAChC,OAAO,CAAC,EAAE,wBAAwB,GACjC,oBAAoB,CA+BtB"}
@@ -304,10 +304,10 @@ function buildTheme(data, options = {}) {
304
304
  // panel-filled row read as a button sitting in the nav rather than as the current location.
305
305
  // Selection is carried by the tint + ink text + weight 600 + the accent icon instead. Forced
306
306
  // here at the THEME level via NavLink's `--nl-*` vars so it holds for every render path —
307
- // including a consumer's router `<Link>` via `renderNavLink`, which never sees the shell CSS
308
- // module; the active ICON is accent-colored via `nav-link.module.css` (targets the
309
- // `[data-position='left']` leftSection, which the flat `vars`/`styles` API can't express
310
- // conditionally on `[data-active]`).
307
+ // including a consumer's router `<Link>` hosted through `SidebarItem.Anchor`, which never
308
+ // sees the shell CSS module; the active ICON is accent-colored via `nav-link.module.css`
309
+ // (targets the `[data-position='left']` leftSection, which the flat `vars`/`styles` API
310
+ // can't express conditionally on `[data-active]`).
311
311
  //
312
312
  // `--nl-hover` (ink-13%) only ever reaches an ACTIVE row in practice — the inactive hover is
313
313
  // set at ink-6% by higher-specificity `:not([data-active])` rules in `nav-link.module.css` /
@@ -323,9 +323,10 @@ function buildTheme(data, options = {}) {
323
323
  children: {}
324
324
  }),
325
325
  // Dense, rounded nav rows — applied at the THEME level so they survive EVERY render path,
326
- // including a consumer's router `<Link>` via `renderNavLink`, which never sees the shell CSS
327
- // module (the module's `.link` only reaches the shell's own fallback `<NavLink>`). Same
328
- // reasoning as the `--nl-*` fill above: layout is single-sourced here, not in two places.
326
+ // including a consumer's router `<Link>` hosted through `SidebarItem.Anchor`, which never
327
+ // sees the shell CSS module (the module's `.link` only reaches the shell's own fallback
328
+ // `<NavLink>`). Same reasoning as the `--nl-*` fill above: layout is single-sourced here,
329
+ // not in two places.
329
330
  // The active-weight/shadow/icon-accent state selectors can't live in `styles` (flat inline
330
331
  // props only) — they're in nav-link.module.css, wired via `classNames` so they reach the
331
332
  // same every-render-path scope.
@@ -5,7 +5,8 @@
5
5
  * tint rather than as a button, so the row never competes with the real filled controls around it.
6
6
  *
7
7
  * Applied at the THEME level via `classNames` so it reaches EVERY NavLink render path — including a
8
- * consumer's router `<Link>` mounted through `renderNavLink`, which a shell-scoped CSS module never
8
+ * consumer's router `<Link>` mounted through `SidebarItem.Anchor`, which a shell-scoped CSS
9
+ * module never
9
10
  * sees. Mantine's `styles` prop only takes flat inline properties (a nested `&[data-active]` there
10
11
  * throws a React warning), so the state selectors live here. React Router's NavLink marks the
11
12
  * active item via `aria-current` rather than the `active` prop's `data-active`, so both states are
@@ -42,7 +43,8 @@
42
43
 
43
44
  /*
44
45
  * Inactive hover — the ink-6% ghost tint + ink text/icon (docs/DESIGN-SPEC.md §5). Lives here (not
45
- * only in the shell module) so a consumer's `renderNavLink` path gets the same hover; Mantine's own
46
+ * only in the shell module) so a consumer's `SidebarItem.Anchor` path gets the same hover;
47
+ * Mantine's own
46
48
  * hover fill uses `:where(scheme) .m_*:hover` (gray-0/dark-6), which this outranks on specificity.
47
49
  * Active rows keep their panel fill + `--nl-hover` untouched.
48
50
  */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tokens/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tokens/index.ts"],"names":[],"mappings":"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"}
@@ -357,6 +357,8 @@ function spaceDecls(space) {
357
357
  decl("space-sidebar-child-row-indent", `${space.step.sidebarChildRowIndent}px`),
358
358
  decl("space-app-header-mobile-actions-height", `${space.step.appHeaderMobileActionsHeight}px`),
359
359
  decl("space-mobile-nav-tab-gap", `${space.step.mobileNavTabGap}px`),
360
+ decl("space-mobile-nav-icon-size", `${space.step.mobileNavIconSize}px`),
361
+ decl("space-mobile-nav-row-height", `${space.step.mobileNavRowHeight}px`),
360
362
  decl("space-agent-rail-inset-x", `${space.step.agentRailInsetX}px`),
361
363
  decl("space-agent-part-gap-top", `${space.step.agentPartGapTop}px`),
362
364
  decl("space-agent-code-inset", `${space.step.agentCodeInset}px`),
@@ -495,6 +495,17 @@ declare const SPACE_STEP_BASE: {
495
495
  readonly appShellNavbarRailWidth: 48;
496
496
  /** Tab's icon-to-label gap. */
497
497
  readonly mobileNavTabGap: 3;
498
+ /** Bar height, EXCLUDING the safe-area inset — Mantine's own `AppShell.footer` rule adds
499
+ * `env(safe-area-inset-bottom)` to it. JS-consumed (AppShell `footer.height`), no `--vx-*` var,
500
+ * same shape as `appShellHeaderHeight`. Floored at 48px in `deriveSpacing`. */
501
+ readonly mobileNavBarHeight: 56;
502
+ /** Slot icon box. */
503
+ readonly mobileNavIconSize: 24;
504
+ /** Menu/sheet row height. Floored at 44px in `deriveSpacing` (Apple HIG 44pt / WCAG 2.5.5 AAA). */
505
+ readonly mobileNavRowHeight: 44;
506
+ /** More/group `<Menu width={…}>`. JS-consumed, no var — same rationale as
507
+ * `sidebarSettingsMenuWidth` directly above. */
508
+ readonly mobileNavMenuWidth: 232;
498
509
  /** `ChartLegend`'s column/row gap (`VX.legendGap`). */
499
510
  readonly chartLegendGap: 22;
500
511
  /** Chart plot-area margin, top (`VX.margin.top`). */
@@ -870,6 +881,10 @@ export declare const SPACE_STEP: {
870
881
  readonly appShellNavbarWidth: number;
871
882
  readonly appShellNavbarRailWidth: number;
872
883
  readonly mobileNavTabGap: number;
884
+ readonly mobileNavBarHeight: number;
885
+ readonly mobileNavIconSize: number;
886
+ readonly mobileNavRowHeight: number;
887
+ readonly mobileNavMenuWidth: number;
873
888
  readonly chartLegendGap: number;
874
889
  readonly chartMarginTop: number;
875
890
  readonly chartMarginRight: number;
@@ -1 +1 @@
1
- {"version":3,"file":"palette.d.ts","sourceRoot":"","sources":["../../src/tokens/palette.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"palette.d.ts","sourceRoot":"","sources":["../../src/tokens/palette.ts"],"names":[],"mappings":"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"}
@@ -410,6 +410,17 @@ const SPACE_STEP_BASE = {
410
410
  // ── shell/app-mobile-nav.module.css ───────────────────────────────────────────────────────────
411
411
  /** Tab's icon-to-label gap. */
412
412
  mobileNavTabGap: 3,
413
+ /** Bar height, EXCLUDING the safe-area inset — Mantine's own `AppShell.footer` rule adds
414
+ * `env(safe-area-inset-bottom)` to it. JS-consumed (AppShell `footer.height`), no `--vx-*` var,
415
+ * same shape as `appShellHeaderHeight`. Floored at 48px in `deriveSpacing`. */
416
+ mobileNavBarHeight: 56,
417
+ /** Slot icon box. */
418
+ mobileNavIconSize: 24,
419
+ /** Menu/sheet row height. Floored at 44px in `deriveSpacing` (Apple HIG 44pt / WCAG 2.5.5 AAA). */
420
+ mobileNavRowHeight: 44,
421
+ /** More/group `<Menu width={…}>`. JS-consumed, no var — same rationale as
422
+ * `sidebarSettingsMenuWidth` directly above. */
423
+ mobileNavMenuWidth: 232,
413
424
  // ── charts (tokens/index.ts's `VX` object — structural, tracks density) ──────────────────────
414
425
  // `VX.lineWidth`/`VX.line2Width` are DELIBERATELY absent here — they are stroke WEIGHTS (line
415
426
  // thickness), not spacing/gaps/sizes, so they stay fixed regardless of density (see `VX` in
@@ -515,6 +526,8 @@ function deriveSpacing(level) {
515
526
  anchors.stackXl = stackUnit * 6;
516
527
  const mappedStep = mapSpaceGroup(SPACE_STEP_BASE, level, multiplier);
517
528
  mappedStep.sidebarSearchTriggerHeight = Math.max(24, mappedStep.sidebarSearchTriggerHeight);
529
+ mappedStep.mobileNavBarHeight = Math.max(48, mappedStep.mobileNavBarHeight);
530
+ mappedStep.mobileNavRowHeight = Math.max(44, mappedStep.mobileNavRowHeight);
518
531
  const step = {
519
532
  ...mappedStep,
520
533
  // RESPONSIVE, one per AppShell breakpoint — each DERIVED from its own header, plus
package/dist/tokens.css CHANGED
@@ -95,6 +95,8 @@
95
95
  --vx-space-sidebar-child-row-indent: 12px;
96
96
  --vx-space-app-header-mobile-actions-height: 52px;
97
97
  --vx-space-mobile-nav-tab-gap: 3px;
98
+ --vx-space-mobile-nav-icon-size: 24px;
99
+ --vx-space-mobile-nav-row-height: 44px;
98
100
  --vx-space-agent-rail-inset-x: 10px;
99
101
  --vx-space-agent-part-gap-top: 6px;
100
102
  --vx-space-agent-code-inset: 8px;
package/llms.txt CHANGED
@@ -1,4 +1,4 @@
1
- # basalt-ui v1.17.0
1
+ # basalt-ui v1.19.0
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.17.0
22
+ Version: 1.19.0
23
23
 
24
24
  ## basalt-ui
25
25
  Import: import { ... } from 'basalt-ui'
@@ -65,7 +65,7 @@ OptionalPeers: @tanstack/react-query-devtools@^5.101.0
65
65
 
66
66
  ## basalt-ui/router-tanstack
67
67
  Import: import { ... } from 'basalt-ui/router-tanstack'
68
- Description: TanStack Router bridge: useBasaltNav (active route) + useRouterBreadcrumbs + createSearchParamStore (single-select URL-state store) + createMultiSearchParamStore (multi-select URL-state store)
68
+ Description: TanStack Router bridge: defineNav/navGroup/navTarget (one typed nav definition) + useNav (sections + mobileNav, spread onto BasaltShell) + useBasaltNav (active route) + useRouterBreadcrumbs + createSearchParamStore (single-select URL-state store) + createMultiSearchParamStore (multi-select URL-state store)
69
69
  Layer: headless
70
70
  Rule: basalt-router
71
71
  OptionalPeers: @tanstack/react-router@^1.170.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "basalt-ui",
3
- "version": "1.17.0",
3
+ "version": "1.19.0",
4
4
  "description": "Opinionated framework for Mantine-based React apps — theme, app shell, visx charts, toolchain presets",
5
5
  "keywords": [
6
6
  "charts",
@@ -6,7 +6,7 @@
6
6
  * `Bars.test.tsx`) — DualPanel has two `HoverOverlay`s sharing one cursor, but only the TOP one is
7
7
  * keyboard-focusable, so `getByRole('slider')` is unambiguous.
8
8
  */
9
- import { fireEvent, render, screen } from '@testing-library/react'
9
+ import { fireEvent, render, screen, within } from '@testing-library/react'
10
10
  import { describe, expect, test } from 'bun:test'
11
11
  import { renderToStaticMarkup } from 'react-dom/server'
12
12
  import { ChartCursorScope } from '../cursor/scope'
@@ -439,3 +439,83 @@ describe('DualPanel — cursorResolution threads through to sibling resolution',
439
439
  expect(foldedSlider.getAttribute('aria-valuetext')).toBe('08.08')
440
440
  })
441
441
  })
442
+
443
+ describe('DualPanel — onFollow (follower behaviour parity with CartesianChart)', () => {
444
+ type SourceRow = { date: string; shown: number }
445
+ const sourceRows: SourceRow[] = [{ date: '2026-08-01', shown: 10 }]
446
+ const sourceSeries: ChartSeries<SourceRow>[] = [
447
+ { key: 'shown', label: 'Shown', color: '#111', mark: 'line', getValue: (d) => d.shown },
448
+ ]
449
+
450
+ function renderPair(onFollow?: boolean, followerRows: Row[] = rows) {
451
+ render(
452
+ <ChartCursorScope>
453
+ <CartesianChart<SourceRow>
454
+ data={sourceRows}
455
+ chartId="dp-of-source"
456
+ getX={(d) => d.date}
457
+ series={sourceSeries}
458
+ ariaLabel="Source"
459
+ legend={false}
460
+ >
461
+ {() => null}
462
+ </CartesianChart>
463
+ <DualPanel<Row>
464
+ data={followerRows}
465
+ chartId="dp-of-follower"
466
+ getX={(d) => d.date}
467
+ series={series}
468
+ getBar={(d) => d.bar}
469
+ barLabel="Bar"
470
+ barColorPositive="#0a0"
471
+ barColorNegative="#a00"
472
+ formatTop={(v) => String(v)}
473
+ formatBottom={(v) => v.toFixed(2)}
474
+ ariaLabel="Follower"
475
+ legend={false}
476
+ {...(onFollow !== undefined && { onFollow })}
477
+ />
478
+ </ChartCursorScope>,
479
+ )
480
+ }
481
+
482
+ test('onFollow: true renders the DualPanel tooltip while a sibling owns the cursor', async () => {
483
+ renderPair(true)
484
+ fireEvent.keyDown(screen.getByRole('slider', { name: 'Source' }), { key: 'ArrowRight' })
485
+
486
+ expect(await screen.findByText('V')).toBeTruthy()
487
+ })
488
+
489
+ test('without onFollow (default false), DualPanel renders no tooltip as a follower', async () => {
490
+ renderPair()
491
+ fireEvent.keyDown(screen.getByRole('slider', { name: 'Source' }), { key: 'ArrowRight' })
492
+
493
+ // The SOURCE still renders its own tooltip — only the DualPanel follower stays silent.
494
+ expect(await screen.findByText('Shown')).toBeTruthy()
495
+ expect(screen.queryByText('V')).toBeNull()
496
+ expect(screen.queryAllByRole('tooltip')).toHaveLength(1)
497
+ })
498
+
499
+ test('the follower tooltip is not aria-live; the source is', async () => {
500
+ renderPair(true)
501
+ fireEvent.keyDown(screen.getByRole('slider', { name: 'Source' }), { key: 'ArrowRight' })
502
+ await screen.findByText('V')
503
+
504
+ const tooltips = screen.getAllByRole('tooltip')
505
+ expect(tooltips).toHaveLength(2)
506
+ const sourceTooltip = tooltips.find((t) => within(t).queryByText('Shown') !== null)
507
+ const followerTooltip = tooltips.find((t) => within(t).queryByText('V') !== null)
508
+ expect(sourceTooltip?.getAttribute('aria-live')).toBe('polite')
509
+ expect(followerTooltip?.hasAttribute('aria-live')).toBe(false)
510
+ })
511
+
512
+ test('a follower whose domain never resolves the broadcast key renders no tooltip shell', async () => {
513
+ const foreignRows: Row[] = [{ date: '2027-01-15', v: 99, bar: 0.5 }]
514
+ renderPair(true, foreignRows)
515
+ fireEvent.keyDown(screen.getByRole('slider', { name: 'Source' }), { key: 'ArrowRight' })
516
+
517
+ expect(await screen.findByText('Shown')).toBeTruthy()
518
+ expect(screen.queryByText('V')).toBeNull()
519
+ expect(screen.queryAllByRole('tooltip')).toHaveLength(1)
520
+ })
521
+ })
@@ -4,7 +4,7 @@ import { Group } from '@visx/group'
4
4
  import { scaleLinear, scalePoint } from '@visx/scale'
5
5
  import { Bar, LinePath } from '@visx/shape'
6
6
  import { Threshold } from '@visx/threshold'
7
- import { memo, useMemo } from 'react'
7
+ import { memo, useMemo, useRef } from 'react'
8
8
  import type { ReactNode } from 'react'
9
9
  import { AxisBottomDate, AxisLeftNumeric } from '../primitives/Axes'
10
10
  import {
@@ -84,6 +84,13 @@ export type DualPanelProps<T> = {
84
84
  * alongside the hovered datum.
85
85
  */
86
86
  formatHeader?: (key: string, d: T) => string
87
+ /**
88
+ * Render this chart's tooltip when it is a cursor FOLLOWER, not only when it is the SOURCE —
89
+ * same seam and same anchored-positioning behaviour as `CartesianTooltipConfig.onFollow`. Default
90
+ * false (today's behaviour). Only the SOURCE's tooltip is `aria-live`; a follower's is visual
91
+ * only.
92
+ */
93
+ onFollow?: boolean
87
94
  /** Legend config forwarded to `ChartFrame`; `false` disables the legend (sparkline escape).
88
95
  * Default `{ placement: 'bottom' }`. */
89
96
  legend?: ChartLegendConfig | false
@@ -175,6 +182,7 @@ function DualPanelPlot<T>(props: DualPanelPlotProps<T>) {
175
182
  bottomMaxAbsFloor,
176
183
  tooltipLabel,
177
184
  formatHeader,
185
+ onFollow,
178
186
  plot,
179
187
  } = props
180
188
 
@@ -424,13 +432,27 @@ function DualPanelPlot<T>(props: DualPanelPlotProps<T>) {
424
432
  [visibleSeries, lineValid, xScale, topYScale, getX],
425
433
  )
426
434
 
435
+ const svgRef = useRef<SVGSVGElement>(null)
427
436
  const point = cursor.point
428
437
  const sx = point !== null ? (xScale(getX(point)) ?? 0) : 0
429
438
  const syncedBar = point !== null ? getBar(point) : null
430
439
 
440
+ // Same seam as `CartesianChart`: this chart renders its tooltip as the cursor SOURCE (always) or,
441
+ // opted in via `onFollow`, as a FOLLOWER. A follower has no pointer under it, so it is always
442
+ // positioned via the anchored (crosshair) path — `cursor.anchor` stays null on a chart nobody is
443
+ // hovering, so following it here would render nothing.
444
+ const isFollowerRender = !cursor.isSource && onFollow === true
445
+ const showTooltip = point !== null && (cursor.isSource || isFollowerRender)
446
+ const anchorX = isFollowerRender && point !== null ? sx : null
447
+ const svgRect = anchorX === null ? undefined : svgRef.current?.getBoundingClientRect()
448
+ const tooltipAnchor =
449
+ svgRect === undefined || anchorX === null
450
+ ? cursor.anchor
451
+ : { x: svgRect.left + margin.left + anchorX, y: svgRect.top + margin.top }
452
+
431
453
  return (
432
454
  <>
433
- <svg width={plot.width} height={plot.height}>
455
+ <svg ref={svgRef} width={plot.width} height={plot.height}>
434
456
  {/* Top pane: line series + fill-between + zones + ref lines. */}
435
457
  <Group left={margin.left} top={margin.top}>
436
458
  <GridRows scale={topYScale} width={xMax} stroke={VX.grid} numTicks={4} />
@@ -593,8 +615,8 @@ function DualPanelPlot<T>(props: DualPanelPlotProps<T>) {
593
615
  )}
594
616
  </svg>
595
617
 
596
- {cursor.isSource && point !== null && (
597
- <ChartTooltipFloat anchor={cursor.anchor}>
618
+ {showTooltip && (
619
+ <ChartTooltipFloat anchor={tooltipAnchor} ariaLive={cursor.isSource}>
598
620
  <TooltipHeader
599
621
  date={getX(point)}
600
622
  {...(formatHeader !== undefined && {
@@ -1,4 +1,4 @@
1
- import { fireEvent, render, screen } from '@testing-library/react'
1
+ import { fireEvent, render, screen, within } from '@testing-library/react'
2
2
  import { describe, expect, test } from 'bun:test'
3
3
  import { renderToStaticMarkup } from 'react-dom/server'
4
4
  import { ChartCursorScope } from '../cursor/scope'
@@ -267,3 +267,175 @@ describe('tooltip extraRows — ctx.visible/ctx.hidden track legend toggling', (
267
267
  expect((await screen.findByTestId('extra-ctx')).textContent).toContain('visible:a|hidden:b')
268
268
  })
269
269
  })
270
+
271
+ describe('tooltip.onFollow — the follower renders when a SIBLING owns the cursor', () => {
272
+ function renderPair(onFollow?: boolean) {
273
+ render(
274
+ <ChartCursorScope>
275
+ <CartesianChart<Row>
276
+ data={rows}
277
+ chartId="of-source"
278
+ getX={(d) => d.date}
279
+ series={[seriesFor('a')]}
280
+ ariaLabel="Source"
281
+ legend={false}
282
+ >
283
+ {() => null}
284
+ </CartesianChart>
285
+ <CartesianChart<Row>
286
+ data={rows}
287
+ chartId="of-follower"
288
+ getX={(d) => d.date}
289
+ series={[seriesFor('b')]}
290
+ ariaLabel="Follower"
291
+ legend={false}
292
+ tooltip={{ follow: false, ...(onFollow !== undefined && { onFollow }) }}
293
+ >
294
+ {() => null}
295
+ </CartesianChart>
296
+ </ChartCursorScope>,
297
+ )
298
+ }
299
+
300
+ test('onFollow: true renders the follower tooltip rows while the source owns the cursor', async () => {
301
+ renderPair(true)
302
+ fireEvent.keyDown(screen.getByRole('slider', { name: 'Source' }), { key: 'ArrowRight' })
303
+
304
+ expect(await screen.findByText('b')).toBeTruthy()
305
+ })
306
+
307
+ test('without onFollow (default false), the follower renders no tooltip at all', async () => {
308
+ renderPair()
309
+ fireEvent.keyDown(screen.getByRole('slider', { name: 'Source' }), { key: 'ArrowRight' })
310
+
311
+ // The SOURCE still renders its own tooltip ('a') — only the follower ('b') stays silent.
312
+ expect(await screen.findByText('a')).toBeTruthy()
313
+ expect(screen.queryByText('b')).toBeNull()
314
+ expect(screen.queryAllByRole('tooltip')).toHaveLength(1)
315
+ })
316
+
317
+ // `follow` is deliberately left at its default (true, pointer-tracking) here — unlike every
318
+ // other case in this file, which hardcodes `follow: false` on the follower. That hardcoding
319
+ // means `useAnchoredPosition`'s FIRST clause (`tooltipCfg?.follow === false`) always already
320
+ // satisfies the anchored branch, so those tests can't tell `|| isFollowerRender` apart from a
321
+ // no-op. With `follow` left at its pointer-tracking default, anchoring can only come from
322
+ // `isFollowerRender` — and a follower's own `cursor.anchor` (set only by pointer/keyboard
323
+ // events ON THIS CHART, never by a sibling's) stays null for the whole test, since only the
324
+ // SOURCE is ever interacted with. So: `isFollowerRender` present → anchored position resolves
325
+ // from the crosshair and the tooltip mounts; `isFollowerRender` absent → the pointer-tracking
326
+ // branch falls through to the follower's own (never-set) `cursor.anchor`, which is null, and
327
+ // `ChartTooltipFloat` renders nothing at all for a null anchor. Presence/absence of the
328
+ // follower tooltip is what's observable here — happy-dom zeroes every `getBoundingClientRect`,
329
+ // so the anchor's actual pixel coordinates can't discriminate the two branches.
330
+ test('onFollow: true with follow left at its pointer-tracking default still anchors to the crosshair', async () => {
331
+ render(
332
+ <ChartCursorScope>
333
+ <CartesianChart<Row>
334
+ data={rows}
335
+ chartId="of-anchor-source"
336
+ getX={(d) => d.date}
337
+ series={[seriesFor('a')]}
338
+ ariaLabel="Source"
339
+ legend={false}
340
+ >
341
+ {() => null}
342
+ </CartesianChart>
343
+ <CartesianChart<Row>
344
+ data={rows}
345
+ chartId="of-anchor-follower"
346
+ getX={(d) => d.date}
347
+ series={[seriesFor('b')]}
348
+ ariaLabel="Follower"
349
+ legend={false}
350
+ tooltip={{ onFollow: true }}
351
+ >
352
+ {() => null}
353
+ </CartesianChart>
354
+ </ChartCursorScope>,
355
+ )
356
+
357
+ fireEvent.keyDown(screen.getByRole('slider', { name: 'Source' }), { key: 'ArrowRight' })
358
+
359
+ expect(await screen.findByText('b')).toBeTruthy()
360
+ })
361
+ })
362
+
363
+ describe('tooltip.onFollow — aria-live stays SOURCE-only', () => {
364
+ test('the source tooltip is aria-live; the follower tooltip is not', async () => {
365
+ render(
366
+ <ChartCursorScope>
367
+ <CartesianChart<Row>
368
+ data={rows}
369
+ chartId="al-source"
370
+ getX={(d) => d.date}
371
+ series={[seriesFor('a')]}
372
+ ariaLabel="Source"
373
+ legend={false}
374
+ >
375
+ {() => null}
376
+ </CartesianChart>
377
+ <CartesianChart<Row>
378
+ data={rows}
379
+ chartId="al-follower"
380
+ getX={(d) => d.date}
381
+ series={[seriesFor('b')]}
382
+ ariaLabel="Follower"
383
+ legend={false}
384
+ tooltip={{ follow: false, onFollow: true }}
385
+ >
386
+ {() => null}
387
+ </CartesianChart>
388
+ </ChartCursorScope>,
389
+ )
390
+
391
+ fireEvent.keyDown(screen.getByRole('slider', { name: 'Source' }), { key: 'ArrowRight' })
392
+ await screen.findByText('b')
393
+
394
+ const tooltips = screen.getAllByRole('tooltip')
395
+ expect(tooltips).toHaveLength(2)
396
+ const sourceTooltip = tooltips.find((t) => within(t).queryByText('a') !== null)
397
+ const followerTooltip = tooltips.find((t) => within(t).queryByText('b') !== null)
398
+ expect(sourceTooltip?.getAttribute('aria-live')).toBe('polite')
399
+ expect(followerTooltip?.hasAttribute('aria-live')).toBe(false)
400
+ })
401
+ })
402
+
403
+ describe('tooltip.onFollow — a follower whose domain never resolves the broadcast key renders nothing', () => {
404
+ test('an unrelated calendar stays silent, not an empty tooltip shell', async () => {
405
+ const foreignRows: Row[] = [{ date: '2027-01-15', a: 99, b: 99 }]
406
+
407
+ render(
408
+ <ChartCursorScope>
409
+ <CartesianChart<Row>
410
+ data={rows}
411
+ chartId="unres-source"
412
+ getX={(d) => d.date}
413
+ series={[seriesFor('a')]}
414
+ ariaLabel="Source"
415
+ legend={false}
416
+ >
417
+ {() => null}
418
+ </CartesianChart>
419
+ <CartesianChart<Row>
420
+ data={foreignRows}
421
+ chartId="unres-follower"
422
+ getX={(d) => d.date}
423
+ series={[seriesFor('b')]}
424
+ ariaLabel="Follower"
425
+ legend={false}
426
+ tooltip={{ follow: false, onFollow: true }}
427
+ >
428
+ {() => null}
429
+ </CartesianChart>
430
+ </ChartCursorScope>,
431
+ )
432
+
433
+ fireEvent.keyDown(screen.getByRole('slider', { name: 'Source' }), { key: 'ArrowRight' })
434
+
435
+ // The SOURCE still renders its own tooltip — only the follower with no resolvable point stays
436
+ // silent, rather than mounting an empty tooltip shell.
437
+ expect(await screen.findByText('a')).toBeTruthy()
438
+ expect(screen.queryByText('b')).toBeNull()
439
+ expect(screen.queryAllByRole('tooltip')).toHaveLength(1)
440
+ })
441
+ })