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,39 @@
1
+ /**
2
+ * `NavCountBadge` — the sidebar/menu unread-count chip. Its own module rather than a member of the
3
+ * shell barrel so `app-mobile-nav.tsx` and `app-sidebar.tsx` can render it without importing the
4
+ * barrel that imports THEM (an ESM value cycle). `shell/index.tsx` re-exports it, so the published
5
+ * surface is unchanged.
6
+ */
7
+ import { Badge } from '@mantine/core'
8
+ import { VX } from '../tokens'
9
+
10
+ /**
11
+ * Sidebar nav count badge (docs/DESIGN-SPEC.md §5): mono 10.5px, ink-8% bg, radius 5, height 16,
12
+ * padding 0 5px, muted text; `marginLeft: auto` pins it to the row end on any render path.
13
+ * `styles` (inline) rather than a token color prop, since none of Mantine's variant/color
14
+ * combinations land on the ink-tint idiom. Returns `null` for a zero/empty count so the badge slot
15
+ * stays clean ("ink earns its color", DESIGN.md).
16
+ */
17
+ export function NavCountBadge({ count }: { count: number }) {
18
+ if (!count) return null
19
+ return (
20
+ <Badge
21
+ size="sm"
22
+ styles={{
23
+ root: {
24
+ backgroundColor: 'color-mix(in srgb, var(--vx-ink) 8%, transparent)',
25
+ color: 'var(--vx-muted)',
26
+ fontFamily: 'var(--basalt-font-mono)',
27
+ fontSize: VX.text.micro,
28
+ fontWeight: 500,
29
+ height: 16,
30
+ padding: '0 5px',
31
+ marginLeft: 'auto',
32
+ borderRadius: 'var(--vx-radius-tight)',
33
+ },
34
+ }}
35
+ >
36
+ {count}
37
+ </Badge>
38
+ )
39
+ }
package/src/surfaces.ts CHANGED
@@ -288,7 +288,7 @@ export const SURFACES = {
288
288
  skill: ['basalt-app'],
289
289
  guardKinds: [],
290
290
  description:
291
- 'TanStack Router bridge: useBasaltNav (active route) + useRouterBreadcrumbs + createSearchParamStore (single-select URL-state store) + createMultiSearchParamStore (multi-select URL-state store)',
291
+ '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)',
292
292
  optionalPeers: ['@tanstack/react-router'],
293
293
  globs: {
294
294
  shipped: [],
@@ -161,12 +161,28 @@ describe('Fix 4 — every interactive target clears the WCAG 2.5.8 24px floor at
161
161
  }
162
162
  })
163
163
 
164
- test('the mobile header actions row (appHeaderMobileActionsHeight — the closest in-scope, density-tracked mobile nav target size; the app-mobile-nav tab bar itself is a fixed AppShell footer height, out of scope for this pass)', () => {
164
+ test('the mobile header actions row (appHeaderMobileActionsHeight)', () => {
165
165
  for (const level of ALL_LEVELS) {
166
166
  expect(deriveSpacing(level).step.appHeaderMobileActionsHeight).toBeGreaterThanOrEqual(24)
167
167
  }
168
168
  })
169
169
 
170
+ // The mobile bar's own two targets clear a HIGHER bar than WCAG 2.5.8's 24px: a thumb on a phone
171
+ // is not a mouse pointer, so `deriveSpacing` floors them at Apple HIG 44pt / WCAG 2.5.5 AAA
172
+ // instead. Without those floors the 1 + 0.1*level multiplier takes 56 -> 39 and 44 -> 31 at level
173
+ // -3 — both under even the 24px floor the rest of this block asserts.
174
+ test('the mobile nav bar height never drops below its own 48px floor', () => {
175
+ for (const level of ALL_LEVELS) {
176
+ expect(deriveSpacing(level).step.mobileNavBarHeight).toBeGreaterThanOrEqual(48)
177
+ }
178
+ })
179
+
180
+ test('a mobile nav menu/sheet row never drops below its own 44px floor', () => {
181
+ for (const level of ALL_LEVELS) {
182
+ expect(deriveSpacing(level).step.mobileNavRowHeight).toBeGreaterThanOrEqual(44)
183
+ }
184
+ })
185
+
170
186
  test('the resolved control/input height (size="md" Button/ActionIcon/Input) clears the floor too', () => {
171
187
  for (const level of ALL_LEVELS) {
172
188
  const { anchors } = deriveSpacing(level)
@@ -212,6 +228,8 @@ function oldLawFlatten(level: number): Record<string, number> {
212
228
  step[k] = scaleSpace(v, level, multiplier)
213
229
  }
214
230
  step['sidebarSearchTriggerHeight'] = Math.max(24, step['sidebarSearchTriggerHeight']!)
231
+ step['mobileNavBarHeight'] = Math.max(48, step['mobileNavBarHeight']!)
232
+ step['mobileNavRowHeight'] = Math.max(44, step['mobileNavRowHeight']!)
215
233
  step['stickyHeaderClearance'] = step['appShellHeaderHeight']! + anchors['stackMd']!
216
234
  step['stickyHeaderClearanceMobile'] = step['appShellHeaderMobileHeight']! + anchors['stackMd']!
217
235
  return { ...anchors, ...scale, ...step }
@@ -549,10 +549,10 @@ function buildTheme(data: PaletteData, options: BuildThemeOptions = {}): Mantine
549
549
  // panel-filled row read as a button sitting in the nav rather than as the current location.
550
550
  // Selection is carried by the tint + ink text + weight 600 + the accent icon instead. Forced
551
551
  // here at the THEME level via NavLink's `--nl-*` vars so it holds for every render path —
552
- // including a consumer's router `<Link>` via `renderNavLink`, which never sees the shell CSS
553
- // module; the active ICON is accent-colored via `nav-link.module.css` (targets the
554
- // `[data-position='left']` leftSection, which the flat `vars`/`styles` API can't express
555
- // conditionally on `[data-active]`).
552
+ // including a consumer's router `<Link>` hosted through `SidebarItem.Anchor`, which never
553
+ // sees the shell CSS module; the active ICON is accent-colored via `nav-link.module.css`
554
+ // (targets the `[data-position='left']` leftSection, which the flat `vars`/`styles` API
555
+ // can't express conditionally on `[data-active]`).
556
556
  //
557
557
  // `--nl-hover` (ink-13%) only ever reaches an ACTIVE row in practice — the inactive hover is
558
558
  // set at ink-6% by higher-specificity `:not([data-active])` rules in `nav-link.module.css` /
@@ -568,9 +568,10 @@ function buildTheme(data: PaletteData, options: BuildThemeOptions = {}): Mantine
568
568
  children: {},
569
569
  }),
570
570
  // Dense, rounded nav rows — applied at the THEME level so they survive EVERY render path,
571
- // including a consumer's router `<Link>` via `renderNavLink`, which never sees the shell CSS
572
- // module (the module's `.link` only reaches the shell's own fallback `<NavLink>`). Same
573
- // reasoning as the `--nl-*` fill above: layout is single-sourced here, not in two places.
571
+ // including a consumer's router `<Link>` hosted through `SidebarItem.Anchor`, which never
572
+ // sees the shell CSS module (the module's `.link` only reaches the shell's own fallback
573
+ // `<NavLink>`). Same reasoning as the `--nl-*` fill above: layout is single-sourced here,
574
+ // not in two places.
574
575
  // The active-weight/shadow/icon-accent state selectors can't live in `styles` (flat inline
575
576
  // props only) — they're in nav-link.module.css, wired via `classNames` so they reach the
576
577
  // 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
  */
@@ -352,6 +352,13 @@ const SPACE_STEP_SWEEP: ReadonlyArray<
352
352
  ['appShellNavbarWidth', 216, null],
353
353
  ['appShellNavbarRailWidth', 48, null],
354
354
  ['mobileNavTabGap', 3, 'space-mobile-nav-tab-gap'],
355
+ // JS-number-only, and for the same two reasons the six above are: `shell/index.tsx` hands
356
+ // `mobileNavBarHeight` to AppShell's `footer.height` (a number, not a `var()` string), and
357
+ // `app-mobile-nav.tsx` hands `mobileNavMenuWidth` to `<Menu width={…}>`.
358
+ ['mobileNavBarHeight', 56, null],
359
+ ['mobileNavIconSize', 24, 'space-mobile-nav-icon-size'],
360
+ ['mobileNavRowHeight', 44, 'space-mobile-nav-row-height'],
361
+ ['mobileNavMenuWidth', 232, null],
355
362
  ['agentRailInsetX', 10, 'space-agent-rail-inset-x'],
356
363
  ['agentPartGapTop', 6, 'space-agent-part-gap-top'],
357
364
  ['agentCodeInset', 8, 'space-agent-code-inset'],
@@ -404,6 +411,8 @@ describe('SPACE_STEP CSS-module spacing-sweep one-offs match the shipped identit
404
411
  'appShellHeaderMobileHeight',
405
412
  'appShellNavbarWidth',
406
413
  'appShellNavbarRailWidth',
414
+ 'mobileNavBarHeight',
415
+ 'mobileNavMenuWidth',
407
416
  'chartLegendGap',
408
417
  'chartMarginTop',
409
418
  'chartMarginRight',
@@ -420,6 +429,8 @@ describe('SPACE_STEP CSS-module spacing-sweep one-offs match the shipped identit
420
429
  expect(css).not.toContain('--vx-space-app-shell-header-mobile-height')
421
430
  expect(css).not.toContain('--vx-space-app-shell-navbar-width')
422
431
  expect(css).not.toContain('--vx-space-app-shell-navbar-rail-width')
432
+ expect(css).not.toContain('--vx-space-mobile-nav-bar-height')
433
+ expect(css).not.toContain('--vx-space-mobile-nav-menu-width')
423
434
  expect(css).not.toContain('--vx-space-chart-legend-gap')
424
435
  expect(css).not.toContain('--vx-space-progress-bar-size')
425
436
  expect(css).not.toContain('--vx-space-timeline-bullet')
@@ -239,6 +239,8 @@ describe('buildDensityCss', () => {
239
239
  'space-app-shell-header-mobile-height',
240
240
  'space-app-shell-navbar-width',
241
241
  'space-app-shell-navbar-rail-width',
242
+ 'space-mobile-nav-bar-height', // AppShell footer.height prop
243
+ 'space-mobile-nav-menu-width', // Menu width prop
242
244
  'space-chart-legend-gap', // VX.legendGap / VX.margin / VX.dotR — visx SVG props
243
245
  'space-chart-margin-top',
244
246
  'space-chart-margin-right',
@@ -400,14 +400,15 @@ function frameworkPrimitives(side: Side, data: PaletteData, legacyAliases: boole
400
400
  * only invite someone to think it does.
401
401
  *
402
402
  * Deliberately DOES NOT emit a var for every `SpaceValues` number — `space.scale.*` (the generic
403
- * Mantine spacing scale) and fourteen `space.step.*` keys have ZERO `var()` consumers anywhere in
403
+ * Mantine spacing scale) and seventeen `space.step.*` keys have ZERO `var()` consumers anywhere in
404
404
  * the framework or its tests beyond the CSS-emission test itself. Each is read straight off the
405
405
  * resolved `SpaceValues` as a JS NUMBER instead, in one of three ways:
406
406
  * `{timelineBullet,progressBarSize}` are Mantine `defaultProps` (`theme/index.ts`'s Timeline/
407
- * Progress); `{sidebarAccountMenuWidth,sidebarSettingsMenuWidth,appShellHeaderHeight,
408
- * appShellHeaderMobileHeight,appShellNavbarWidth,appShellNavbarRailWidth}` are numeric Mantine
409
- * component props (Menu `width`, AppShell `header`/`navbar`); and `{chartLegendGap,chartMarginTop,
410
- * chartMarginRight,chartMarginBottom,chartMarginLeft,chartDotR}` are visx SVG props via
407
+ * Progress); `{sidebarAccountMenuWidth,sidebarSettingsMenuWidth,mobileNavMenuWidth,
408
+ * appShellHeaderHeight,appShellHeaderMobileHeight,appShellNavbarWidth,appShellNavbarRailWidth,
409
+ * mobileNavBarHeight}` are numeric Mantine component props (Menu `width`, AppShell `header`/
410
+ * `navbar`/`footer`); and `{chartLegendGap,chartMarginTop,chartMarginRight,chartMarginRightAxis,
411
+ * chartMarginBottom,chartMarginLeft,chartDotR}` are visx SVG props via
411
412
  * `VX.legendGap`/`VX.margin`/`VX.dotR` in this file's own `VX` object. The full list is locked
412
413
  * against the `SpaceValues` key set by `tokens/density.test.ts`'s declaration-set test, so adding a
413
414
  * one-off silently omits nothing. A `--vx-space-*` declaration for any of them would be dead weight
@@ -531,6 +532,8 @@ function spaceDecls(space: SpaceValues): string[] {
531
532
  decl('space-sidebar-child-row-indent', `${space.step.sidebarChildRowIndent}px`),
532
533
  decl('space-app-header-mobile-actions-height', `${space.step.appHeaderMobileActionsHeight}px`),
533
534
  decl('space-mobile-nav-tab-gap', `${space.step.mobileNavTabGap}px`),
535
+ decl('space-mobile-nav-icon-size', `${space.step.mobileNavIconSize}px`),
536
+ decl('space-mobile-nav-row-height', `${space.step.mobileNavRowHeight}px`),
534
537
  decl('space-agent-rail-inset-x', `${space.step.agentRailInsetX}px`),
535
538
  decl('space-agent-part-gap-top', `${space.step.agentPartGapTop}px`),
536
539
  decl('space-agent-code-inset', `${space.step.agentCodeInset}px`),
@@ -609,6 +609,17 @@ const SPACE_STEP_BASE = {
609
609
  // ── shell/app-mobile-nav.module.css ───────────────────────────────────────────────────────────
610
610
  /** Tab's icon-to-label gap. */
611
611
  mobileNavTabGap: 3,
612
+ /** Bar height, EXCLUDING the safe-area inset — Mantine's own `AppShell.footer` rule adds
613
+ * `env(safe-area-inset-bottom)` to it. JS-consumed (AppShell `footer.height`), no `--vx-*` var,
614
+ * same shape as `appShellHeaderHeight`. Floored at 48px in `deriveSpacing`. */
615
+ mobileNavBarHeight: 56,
616
+ /** Slot icon box. */
617
+ mobileNavIconSize: 24,
618
+ /** Menu/sheet row height. Floored at 44px in `deriveSpacing` (Apple HIG 44pt / WCAG 2.5.5 AAA). */
619
+ mobileNavRowHeight: 44,
620
+ /** More/group `<Menu width={…}>`. JS-consumed, no var — same rationale as
621
+ * `sidebarSettingsMenuWidth` directly above. */
622
+ mobileNavMenuWidth: 232,
612
623
 
613
624
  // ── charts (tokens/index.ts's `VX` object — structural, tracks density) ──────────────────────
614
625
  // `VX.lineWidth`/`VX.line2Width` are DELIBERATELY absent here — they are stroke WEIGHTS (line
@@ -927,6 +938,11 @@ export function deriveSpacing(level: number): SpaceValues {
927
938
  // WCAG 2.5.8 minimum interactive-target size — see this function's doc for why the trigger's
928
939
  // density-tracking height alone can't be trusted to stay accessible at a negative level.
929
940
  mappedStep.sidebarSearchTriggerHeight = Math.max(24, mappedStep.sidebarSearchTriggerHeight)
941
+ // Touch targets: the 1 + 0.1*level multiplier would take 56 -> 39 and 44 -> 31 at level -3,
942
+ // silently breaking the minimum interactive-target size. Same guardrail, same reason, as the
943
+ // sidebar search trigger's 24px floor directly above.
944
+ mappedStep.mobileNavBarHeight = Math.max(48, mappedStep.mobileNavBarHeight)
945
+ mappedStep.mobileNavRowHeight = Math.max(44, mappedStep.mobileNavRowHeight)
930
946
  const step: SpaceValues['step'] = {
931
947
  ...mappedStep,
932
948
  // RESPONSIVE, one per AppShell breakpoint — each DERIVED from its own header, plus