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
@@ -1,45 +1,22 @@
1
1
  import type { MouseEvent, ReactNode } from 'react';
2
- import type { NavLinkRenderer } from './app-sidebar.js';
3
- import type { BreadcrumbLinkRenderer } from './app-breadcrumbs.js';
4
2
  import type { BasaltAccountProps } from './account-types.js';
5
3
  import type { SidebarSearchConfig } from './sidebar-search.js';
6
- export { AppSidebar, type AppSidebarProps, type NavLinkRenderer } from './app-sidebar.js';
4
+ import type { MobileNavConfig, SidebarSection } from '../nav/types.js';
5
+ export { AppSidebar, type AppSidebarProps } from './app-sidebar.js';
6
+ export { NavCountBadge } from './nav-count-badge.js';
7
7
  export { SidebarSearch, type SidebarSearchConfig } from './sidebar-search.js';
8
8
  export { SidebarAccount } from './app-sidebar-account.js';
9
9
  export type { AccountBadgeTone, BasaltIdentity, BasaltRole, BasaltPlan, AccountMenuItem, BasaltAccountState, BasaltAccountActions, BasaltAccountProps, } from './account-types.js';
10
- export { MobileNav, type MobileNavItem, type MobileNavSection, type MobileNavLinkRenderer, } from './app-mobile-nav.js';
11
- export { AppBreadcrumbs, type BreadcrumbLinkRenderer } from './app-breadcrumbs.js';
10
+ export { MobileNav, type MobileNavProps } from './app-mobile-nav.js';
11
+ export { projectMobileNav, type ProjectMobileNavOptions, MOBILE_MAX_TABS_DEFAULT, MOBILE_MENU_MAX_DEFAULT, MOBILE_MORE_KEY, } from './mobile-nav-model.js';
12
+ export { AppBreadcrumbs } from './app-breadcrumbs.js';
12
13
  export { PageHeaderProvider, PageActions, PageActionsOutlet } from './page-header.js';
13
- /** A single sidebar destination. Grounded verbatim in argo's `SidebarItem`. */
14
- export type SidebarItem = {
15
- key: string;
16
- label: string;
17
- /** Short label for the mobile bottom-nav; falls back to `label`. */
18
- short?: string;
19
- /** Whether this destination appears in the mobile bottom-nav. */
20
- mobile?: boolean;
21
- icon: ReactNode;
22
- href?: string;
23
- active?: boolean;
24
- disabled?: boolean;
25
- onClick?: (e: MouseEvent) => void;
26
- badge?: ReactNode;
27
- /** Nested sub-navigation items — surfaced in a hover popover and inline when active. */
28
- children?: SidebarItem[];
29
- };
30
- /** A labelled group of sidebar items. Grounded verbatim in argo's `SidebarSection`. */
31
- export type SidebarSection = {
32
- label: string;
33
- items: SidebarItem[];
34
- /** Group icon, used by the mobile tabs. */
35
- icon?: ReactNode;
36
- /** Desktop: render a clickable group header that collapses its items. */
37
- collapsible?: boolean;
38
- /** Initial collapsed state when `collapsible`. */
39
- defaultCollapsed?: boolean;
40
- /** `false` excludes the section from the primary mobile group tabs. */
41
- mobileTab?: boolean;
42
- };
14
+ /**
15
+ * The shared nav vocabulary lives in `src/nav/types.ts` so the headless router bridge can `import
16
+ * type` it without reaching into the Mantine layer. Re-exported here so `SidebarItem` /
17
+ * `SidebarSection` keep resolving straight from `basalt-ui`, exactly as before.
18
+ */
19
+ export type { NavAnchor, NavAnchorProps, NavMobilePlacement, NavSectionMobile, SidebarItem, SidebarSection, MobileNavConfig, MobileNavGroup, MobileNavModel, MobileNavSlot, MobileNavSurface, } from '../nav/types.js';
43
20
  /** Brand identity shown in the sidebar header (logo + name). */
44
21
  export type BrandConfig = {
45
22
  /** Display name (e.g. "Argo"). */
@@ -61,25 +38,21 @@ export type SettingsMenuItem = {
61
38
  export type BasaltShellProps = {
62
39
  /** Brand identity for the sidebar header. */
63
40
  brand: BrandConfig;
64
- /** Grouped nav sections rendered in the sidebar (and projected to the mobile bottom nav). */
41
+ /** Grouped nav sections — the ONE definition, rendered as the sidebar and projected to the bar. */
65
42
  sections: SidebarSection[];
66
43
  /**
67
- * Optional consumer link renderer (e.g. a router `<Link>`). Receives the precomputed `active`
68
- * flag. When omitted, items fall back to a plain `<a href>` + `item.onClick`. This is the router
69
- * seam — no router primitive is imported by the shell.
44
+ * Mobile bar tuning. Every field optional — the defaults ARE the design, and a nav with nothing
45
+ * configured still produces a working bar (see `projectMobileNav`'s zero-config fallback).
70
46
  */
71
- renderNavLink?: NavLinkRenderer;
47
+ mobileNav?: MobileNavConfig;
72
48
  /** Persistent, shell-owned top-bar slot (timer, refresh, notifications). */
73
49
  globalActions?: ReactNode;
74
- /** Extra content appended to the sidebar footer, beside the settings menu (mobile close, etc.). */
75
- sidebarFooterExtra?: ReactNode;
76
50
  /**
77
- * Arbitrary content appended after `sections` inside the sidebar's nav scroll region (a tree, a
78
- * filter panel, a project list, …). Hidden on the collapsed desktop rail; still present in the
79
- * mobile drawer, which opens at full width — see `AppSidebarProps.navExtra`'s JSDoc for why.
80
- * `AppSidebar` renders once and serves both the desktop rail and the mobile drawer (collapse
81
- * styling is gated behind a `min-width: sm` media query, not a second render), so this slot
82
- * reaches both.
51
+ * DESKTOP ONLY. Arbitrary content appended after `sections` inside the sidebar's nav scroll
52
+ * region (a tree, a filter panel, a project list, …), for anything a set of `SidebarItem`s can't
53
+ * express. Hidden on the collapsed desktop rail; the sidebar itself no longer exists below the
54
+ * `sm` breakpoint, so this slot has no mobile representation — put mobile-reachable extras in
55
+ * `mobileNav.moreExtra` instead.
83
56
  */
84
57
  sidebarNavExtra?: ReactNode;
85
58
  /** Entries appended to the sidebar settings menu. */
@@ -109,24 +82,8 @@ export type BasaltShellProps = {
109
82
  * a no-op when omitted.
110
83
  */
111
84
  onCollapsedChange?: (collapsed: boolean) => void;
112
- /**
113
- * Optional router link renderer for the breadcrumb parent segment. When provided, the parent
114
- * breadcrumb label is rendered through this callback (e.g. a TanStack `<Link>`) instead of a
115
- * plain `<a href>`, enabling client-side navigation.
116
- */
117
- renderBreadcrumbLink?: BreadcrumbLinkRenderer | undefined;
118
85
  /** Page content. */
119
86
  children?: ReactNode;
120
87
  };
121
- /**
122
- * Sidebar nav count badge (docs/DESIGN-SPEC.md §5): mono 10.5px, ink-8% bg, radius 5, height 16,
123
- * padding 0 5px, muted text; `marginLeft: auto` pins it to the row end on any render path.
124
- * `styles` (inline) rather than a token color prop, since none of Mantine's variant/color
125
- * combinations land on the ink-tint idiom. Returns `null` for a zero/empty count so the badge slot
126
- * stays clean ("ink earns its color", DESIGN.md).
127
- */
128
- export declare function NavCountBadge({ count }: {
129
- count: number;
130
- }): import("react").JSX.Element | null;
131
- export declare function BasaltShell({ brand, sections, renderNavLink, globalActions, sidebarFooterExtra, sidebarNavExtra, settingsMenuItems, storageKey, collapsed: collapsedProp, onCollapsedChange, renderBreadcrumbLink, account, search, children, }: BasaltShellProps): import("react").JSX.Element;
88
+ export declare function BasaltShell({ brand, sections, mobileNav, globalActions, sidebarNavExtra, settingsMenuItems, storageKey, collapsed: collapsedProp, onCollapsedChange, account, search, children, }: BasaltShellProps): import("react").JSX.Element;
132
89
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/shell/index.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/shell/index.tsx"],"names":[],"mappings":"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"}
@@ -1,44 +1,28 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
- import { AppShell, Badge } from "@mantine/core";
3
- import { useDisclosure, useLocalStorage } from "@mantine/hooks";
2
+ import { AppShell } from "@mantine/core";
3
+ import { useLocalStorage } from "@mantine/hooks";
4
+ import { useMemo } from "react";
4
5
  import { AppSidebar } from "./app-sidebar.js";
5
- import { MobileNav } from "./app-mobile-nav.js";
6
+ import { MobileNav, accountRowCount } from "./app-mobile-nav.js";
7
+ import { projectMobileNav } from "./mobile-nav-model.js";
6
8
  import { AppBreadcrumbs } from "./app-breadcrumbs.js";
7
9
  import { PageActionsOutlet, PageHeaderProvider } from "./page-header.js";
8
- import { VX } from "../tokens/index.js";
9
10
  import { useBasaltSpacing } from "../theme/index.js";
10
11
  import headerClasses from "./app-header.module.css";
12
+ import mobileNavClasses from "./app-mobile-nav.module.css";
11
13
  import { AppSidebar as AppSidebar2 } from "./app-sidebar.js";
14
+ import { NavCountBadge } from "./nav-count-badge.js";
12
15
  import { SidebarSearch } from "./sidebar-search.js";
13
16
  import { SidebarAccount } from "./app-sidebar-account.js";
17
+ import { MobileNav as MobileNav2 } from "./app-mobile-nav.js";
14
18
  import {
15
- MobileNav as MobileNav2
16
- } from "./app-mobile-nav.js";
19
+ projectMobileNav as projectMobileNav2,
20
+ MOBILE_MAX_TABS_DEFAULT,
21
+ MOBILE_MENU_MAX_DEFAULT,
22
+ MOBILE_MORE_KEY
23
+ } from "./mobile-nav-model.js";
17
24
  import { AppBreadcrumbs as AppBreadcrumbs2 } from "./app-breadcrumbs.js";
18
25
  import { PageHeaderProvider as PageHeaderProvider2, PageActions, PageActionsOutlet as PageActionsOutlet2 } from "./page-header.js";
19
- function NavCountBadge({ count }) {
20
- if (!count) return null;
21
- return /* @__PURE__ */ jsx(
22
- Badge,
23
- {
24
- size: "sm",
25
- styles: {
26
- root: {
27
- backgroundColor: "color-mix(in srgb, var(--vx-ink) 8%, transparent)",
28
- color: "var(--vx-muted)",
29
- fontFamily: "var(--basalt-font-mono)",
30
- fontSize: VX.text.micro,
31
- fontWeight: 500,
32
- height: 16,
33
- padding: "0 5px",
34
- marginLeft: "auto",
35
- borderRadius: "var(--vx-radius-tight)"
36
- }
37
- },
38
- children: count
39
- }
40
- );
41
- }
42
26
  function findActiveCrumb(sections) {
43
27
  for (const section of sections) {
44
28
  const found = findActiveWithParent(section.items);
@@ -46,67 +30,38 @@ function findActiveCrumb(sections) {
46
30
  return {
47
31
  section: section.label,
48
32
  parent: found.parent,
33
+ parentAnchor: found.parentAnchor,
49
34
  parentHref: found.parentHref,
50
35
  page: found.page
51
36
  };
52
37
  }
53
38
  return void 0;
54
39
  }
55
- function findActiveWithParent(items, parentLabel, parentHref) {
40
+ function findActiveWithParent(items, parentLabel, parentAnchor, parentHref) {
56
41
  for (const item of items) {
57
42
  if (item.children) {
58
- const found = findActiveWithParent(item.children, item.label, item.href);
43
+ const found = findActiveWithParent(item.children, item.label, item.Anchor, item.href);
59
44
  if (found) return found;
60
45
  }
61
- if (item.active) return { parent: parentLabel, parentHref, page: item.label };
46
+ if (item.active) return { parent: parentLabel, parentAnchor, parentHref, page: item.label };
62
47
  }
63
48
  return void 0;
64
49
  }
65
- function toMobileSections(sections, closeMobile) {
66
- return sections.filter((s) => s.mobileTab !== false).map((s) => ({
67
- key: s.label,
68
- label: s.label,
69
- icon: s.icon,
70
- active: s.items.some((i) => i.active),
71
- items: flattenSidebarItems(s.items).map(
72
- (i) => ({
73
- key: i.key,
74
- label: i.label,
75
- icon: i.icon,
76
- ...i.href !== void 0 && { href: i.href },
77
- ...i.active !== void 0 && { active: i.active },
78
- onClick: (e) => {
79
- i.onClick?.(e);
80
- closeMobile();
81
- }
82
- })
83
- )
84
- }));
85
- }
86
- function flattenSidebarItems(items) {
87
- return items.flatMap((item) => [
88
- item,
89
- ...item.children ? flattenSidebarItems(item.children) : []
90
- ]);
91
- }
92
50
  function BasaltShell({
93
51
  brand,
94
52
  sections,
95
- renderNavLink,
53
+ mobileNav,
96
54
  globalActions,
97
- sidebarFooterExtra,
98
55
  sidebarNavExtra,
99
56
  settingsMenuItems,
100
57
  storageKey = "basalt-sidebar-collapsed",
101
58
  collapsed: collapsedProp,
102
59
  onCollapsedChange,
103
- renderBreadcrumbLink,
104
60
  account,
105
61
  search,
106
62
  children
107
63
  }) {
108
64
  const { step } = useBasaltSpacing();
109
- const [mobileOpened, { toggle: toggleMobile, close: closeMobile }] = useDisclosure();
110
65
  const [storedCollapsed, setStoredCollapsed] = useLocalStorage({
111
66
  key: storageKey,
112
67
  defaultValue: false,
@@ -120,7 +75,11 @@ function BasaltShell({
120
75
  onCollapsedChange?.(next);
121
76
  };
122
77
  const activeCrumb = findActiveCrumb(sections);
123
- const mobileSections = toMobileSections(sections, closeMobile);
78
+ const extraMoreRows = accountRowCount(account) + (settingsMenuItems?.length ?? 0) + (mobileNav?.moreExtra ? 1 : 0);
79
+ const model = useMemo(
80
+ () => projectMobileNav(sections, { config: mobileNav, extraMoreRows }),
81
+ [sections, mobileNav, extraMoreRows]
82
+ );
124
83
  return /* @__PURE__ */ jsx(PageHeaderProvider, { children: /* @__PURE__ */ jsxs(
125
84
  AppShell,
126
85
  {
@@ -135,13 +94,14 @@ function BasaltShell({
135
94
  sm: collapsed ? step.appShellNavbarRailWidth : step.appShellNavbarWidth
136
95
  },
137
96
  breakpoint: "sm",
138
- collapsed: { mobile: !mobileOpened }
97
+ // No mobile sidebar drawer, ever — the bottom bar is the entire mobile nav.
98
+ collapsed: { mobile: true }
139
99
  },
140
- footer: { height: { base: 52, sm: 0 } },
100
+ footer: { height: { base: step.mobileNavBarHeight, sm: 0 } },
141
101
  padding: "sm",
142
102
  children: [
143
103
  /* @__PURE__ */ jsx(AppShell.Header, { px: "md", withBorder: false, children: /* @__PURE__ */ jsxs("div", { className: headerClasses.bar, children: [
144
- /* @__PURE__ */ jsx("div", { className: headerClasses.lead, children: /* @__PURE__ */ jsx(AppBreadcrumbs, { ...activeCrumb, renderBreadcrumbLink }) }),
104
+ /* @__PURE__ */ jsx("div", { className: headerClasses.lead, children: /* @__PURE__ */ jsx(AppBreadcrumbs, { ...activeCrumb }) }),
145
105
  /* @__PURE__ */ jsx(PageActionsOutlet, { className: headerClasses.pageActions }),
146
106
  globalActions && /* @__PURE__ */ jsx("div", { className: headerClasses.global, children: globalActions })
147
107
  ] }) }),
@@ -152,22 +112,20 @@ function BasaltShell({
152
112
  sections,
153
113
  collapsed,
154
114
  onToggleCollapse: toggleCollapse,
155
- onClose: closeMobile,
156
- footerExtra: sidebarFooterExtra,
157
115
  navExtra: sidebarNavExtra,
158
- ...renderNavLink !== void 0 && { renderNavLink },
159
116
  ...settingsMenuItems !== void 0 && { settingsMenuItems },
160
117
  ...account !== void 0 && { account },
161
118
  ...search !== void 0 && { search }
162
119
  }
163
120
  ) }),
164
- /* @__PURE__ */ jsx(AppShell.Main, { children }),
165
- /* @__PURE__ */ jsx(AppShell.Footer, { hiddenFrom: "sm", p: 0, children: /* @__PURE__ */ jsx(
121
+ /* @__PURE__ */ jsx(AppShell.Main, { className: mobileNavClasses.mainSafeArea, children }),
122
+ /* @__PURE__ */ jsx(AppShell.Footer, { hiddenFrom: "sm", p: 0, withBorder: false, children: /* @__PURE__ */ jsx(
166
123
  MobileNav,
167
124
  {
168
- sections: mobileSections,
169
- onOpenMore: toggleMobile,
170
- ...renderNavLink !== void 0 && { renderNavLink }
125
+ model,
126
+ config: mobileNav,
127
+ ...account !== void 0 && { account },
128
+ ...settingsMenuItems !== void 0 && { settingsMenuItems }
171
129
  }
172
130
  ) })
173
131
  ]
@@ -178,11 +136,15 @@ export {
178
136
  AppBreadcrumbs2 as AppBreadcrumbs,
179
137
  AppSidebar2 as AppSidebar,
180
138
  BasaltShell,
139
+ MOBILE_MAX_TABS_DEFAULT,
140
+ MOBILE_MENU_MAX_DEFAULT,
141
+ MOBILE_MORE_KEY,
181
142
  MobileNav2 as MobileNav,
182
143
  NavCountBadge,
183
144
  PageActions,
184
145
  PageActionsOutlet2 as PageActionsOutlet,
185
146
  PageHeaderProvider2 as PageHeaderProvider,
186
147
  SidebarAccount,
187
- SidebarSearch
148
+ SidebarSearch,
149
+ projectMobileNav2 as projectMobileNav
188
150
  };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The mobile-bar projection — `SidebarSection[]` + `MobileNavConfig` → `MobileNavModel`.
3
+ *
4
+ * PURE BY CONTRACT: no React, no Mantine, no DOM, no JSX. Everything the bottom bar decides —
5
+ * which destinations get a slot, which slot is a plain navigation link and which raises a surface,
6
+ * whether that surface is a menu or a sheet, what falls into the overflow "More" slot — is decided
7
+ * here and unit-testable with no renderer. `app-mobile-nav.tsx` only paints the result.
8
+ *
9
+ * The law it implements is the interaction model, not a heuristic: a slot is a DESTINATION, so a
10
+ * tap navigates with no overlay to dismiss. An overlay exists only where a slot genuinely holds
11
+ * more than one destination, and its surface is INFERRED from how many rows it holds rather than
12
+ * configured — six rows is the arithmetic ceiling for a menu that pops out of a 56px bar without
13
+ * ever rendering below the fold (6 x 44px + 8px padding = 272px against 415px of headroom on the
14
+ * smallest supported viewport), so past that the surface becomes a bottom sheet.
15
+ */
16
+ import type { MobileNavConfig, MobileNavModel, SidebarSection } from '../nav/types.js';
17
+ /** Hard cap on bar slots, INCLUDING More. Five 72px slots is the widest a 360px viewport holds. */
18
+ export declare const MOBILE_MAX_TABS_DEFAULT = 5;
19
+ /**
20
+ * Rows a slot may hold before it becomes a sheet instead of a menu. Arithmetic, not taste — see
21
+ * the module doc: six rows is the last count a bottom-anchored, non-flipping menu fits above the
22
+ * bar on the smallest supported viewport.
23
+ */
24
+ export declare const MOBILE_MENU_MAX_DEFAULT = 6;
25
+ /** The overflow slot's reserved key. A slot like any other — it just exists only when needed. */
26
+ export declare const MOBILE_MORE_KEY = "__more";
27
+ export type ProjectMobileNavOptions = {
28
+ config?: MobileNavConfig | undefined;
29
+ /**
30
+ * Extra rows the More surface will hold — account + settings + `moreExtra`. Drives both
31
+ * `needsMore` and the menu-vs-sheet threshold. The model cannot see those rows (they are
32
+ * Mantine-rendered chrome, not `SidebarItem`s), so the shell counts them and passes the total.
33
+ */
34
+ extraMoreRows?: number;
35
+ };
36
+ /**
37
+ * Projects `sections` onto the bar. Implements §2.3's thirteen rules in order, and is the ONLY
38
+ * place slot selection happens — `MobileNav` receives a finished model and paints it.
39
+ */
40
+ export declare function projectMobileNav(sections: SidebarSection[], opts?: ProjectMobileNavOptions): MobileNavModel;
41
+ //# sourceMappingURL=mobile-nav-model.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mobile-nav-model.d.ts","sourceRoot":"","sources":["../../src/shell/mobile-nav-model.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EACV,eAAe,EAEf,cAAc,EAId,cAAc,EACf,MAAM,cAAc,CAAA;AAErB,mGAAmG;AACnG,eAAO,MAAM,uBAAuB,IAAI,CAAA;AAExC;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,IAAI,CAAA;AAExC,iGAAiG;AACjG,eAAO,MAAM,eAAe,WAAW,CAAA;AAEvC,MAAM,MAAM,uBAAuB,GAAG;IACpC,MAAM,CAAC,EAAE,eAAe,GAAG,SAAS,CAAA;IACpC;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB,CAAA;AAoQD;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,cAAc,EAAE,EAC1B,IAAI,CAAC,EAAE,uBAAuB,GAC7B,cAAc,CA2EhB"}
@@ -0,0 +1,244 @@
1
+ const MOBILE_MAX_TABS_DEFAULT = 5;
2
+ const MOBILE_MENU_MAX_DEFAULT = 6;
3
+ const MOBILE_MORE_KEY = "__more";
4
+ const DEV = process.env["NODE_ENV"] !== "production";
5
+ function placementOf(item, sectionHidden) {
6
+ if (sectionHidden) return "hidden";
7
+ const mobile = item.mobile;
8
+ if (mobile === void 0) return "more";
9
+ if (mobile === true) return "tab";
10
+ if (mobile === false) return "hidden";
11
+ return mobile;
12
+ }
13
+ function isSectionHidden(section) {
14
+ return section.mobile === false;
15
+ }
16
+ function hasActiveDestination(item) {
17
+ if (item.active) return true;
18
+ return item.children?.some(hasActiveDestination) ?? false;
19
+ }
20
+ function countRows(items) {
21
+ return items.reduce((total, item) => total + 1 + countRows(item.children ?? []), 0);
22
+ }
23
+ function collectKeys(items, out) {
24
+ for (const item of items) {
25
+ out.add(item.key);
26
+ collectKeys(item.children ?? [], out);
27
+ }
28
+ }
29
+ function flattenItems(items, out) {
30
+ for (const item of items) {
31
+ out.push(item);
32
+ flattenItems(item.children ?? [], out);
33
+ }
34
+ }
35
+ function pruneTree(items, keep) {
36
+ const out = [];
37
+ for (const item of items) {
38
+ const children = pruneTree(item.children ?? [], keep);
39
+ if (!keep(item)) {
40
+ out.push(...children);
41
+ continue;
42
+ }
43
+ const next = { ...item };
44
+ if (children.length > 0) next.children = children;
45
+ else delete next.children;
46
+ out.push(next);
47
+ }
48
+ return out;
49
+ }
50
+ function itemCandidate(item) {
51
+ return {
52
+ key: item.key,
53
+ covered: /* @__PURE__ */ new Set([item.key]),
54
+ toSlot: () => ({
55
+ kind: "link",
56
+ key: item.key,
57
+ label: item.label,
58
+ short: item.short ?? item.label,
59
+ icon: item.icon,
60
+ active: hasActiveDestination(item),
61
+ item
62
+ })
63
+ };
64
+ }
65
+ function sectionCandidate(section, menuMax) {
66
+ const mobile = section.mobile === false || section.mobile === void 0 ? void 0 : section.mobile;
67
+ const items = pruneTree(section.items, (item) => placementOf(item, false) !== "hidden");
68
+ const covered = /* @__PURE__ */ new Set();
69
+ collectKeys(items, covered);
70
+ const label = mobile?.label ?? section.label;
71
+ const rows = countRows(items);
72
+ const only = items[0];
73
+ return {
74
+ key: section.label,
75
+ covered,
76
+ toSlot: () => {
77
+ if (rows === 0) return void 0;
78
+ if (rows === 1 && only !== void 0 && !only.disabled) {
79
+ return {
80
+ kind: "link",
81
+ key: only.key,
82
+ label,
83
+ short: label,
84
+ // The section's own identity wins when it declares one — the consumer configured the
85
+ // SLOT — and falls back to the single destination's icon when it does not.
86
+ icon: mobile?.icon ?? section.icon ?? only.icon,
87
+ active: hasActiveDestination(only),
88
+ item: only
89
+ };
90
+ }
91
+ return {
92
+ kind: rows <= menuMax ? "menu" : "sheet",
93
+ key: section.label,
94
+ label,
95
+ short: label,
96
+ icon: mobile?.icon ?? section.icon,
97
+ active: items.some(hasActiveDestination),
98
+ // One section, so the rows need no group heading — the tab itself names them.
99
+ groups: [{ key: section.label, items }],
100
+ isMore: false
101
+ };
102
+ }
103
+ };
104
+ }
105
+ function buildCandidates(sections, config, maxTabs, menuMax) {
106
+ const flat = [];
107
+ for (const section of sections) {
108
+ if (isSectionHidden(section)) continue;
109
+ flattenItems(section.items, flat);
110
+ }
111
+ const tabbable = (item) => {
112
+ if (!item.disabled) return true;
113
+ if (DEV) {
114
+ console.warn(
115
+ `[basalt] MobileNav: "${item.key}" asks for a bar slot but is disabled \u2014 a disabled destination never becomes a tab. It still renders, disabled, in the More surface.`
116
+ );
117
+ }
118
+ return false;
119
+ };
120
+ if (config?.tabs !== void 0) {
121
+ const candidates2 = [];
122
+ for (const id of config.tabs) {
123
+ const item = flat.find((candidate) => candidate.key === id);
124
+ if (item !== void 0) {
125
+ if (tabbable(item)) candidates2.push(itemCandidate(item));
126
+ continue;
127
+ }
128
+ const section = sections.find((s) => s.label === id && !isSectionHidden(s));
129
+ if (section !== void 0) {
130
+ candidates2.push(sectionCandidate(section, menuMax));
131
+ continue;
132
+ }
133
+ if (DEV) {
134
+ console.warn(
135
+ `[basalt] MobileNav: mobileNav.tabs names "${id}", which is neither a destination key nor a visible section label \u2014 ignoring it.`
136
+ );
137
+ }
138
+ }
139
+ return candidates2;
140
+ }
141
+ const candidates = [];
142
+ for (const section of sections) {
143
+ if (isSectionHidden(section)) continue;
144
+ if (section.mobile !== void 0 && section.mobile !== false && section.mobile.tab === true) {
145
+ candidates.push(sectionCandidate(section, menuMax));
146
+ continue;
147
+ }
148
+ const items = [];
149
+ flattenItems(section.items, items);
150
+ for (const item of items) {
151
+ if (placementOf(item, false) !== "tab") continue;
152
+ if (tabbable(item)) candidates.push(itemCandidate(item));
153
+ }
154
+ }
155
+ if (candidates.length > 0) return candidates;
156
+ const fallback = [];
157
+ for (const section of sections) {
158
+ if (isSectionHidden(section)) continue;
159
+ for (const item of section.items) {
160
+ if (fallback.length >= maxTabs - 1) return fallback;
161
+ if (item.disabled) continue;
162
+ if (placementOf(item, false) === "hidden") continue;
163
+ fallback.push(itemCandidate(item));
164
+ }
165
+ }
166
+ return fallback;
167
+ }
168
+ function buildOverflow(sections, covered) {
169
+ const groups = [];
170
+ for (const section of sections) {
171
+ const hidden = isSectionHidden(section);
172
+ if (hidden) continue;
173
+ const items = pruneTree(
174
+ section.items,
175
+ (item) => placementOf(item, hidden) !== "hidden" && !covered.has(item.key)
176
+ );
177
+ if (items.length === 0) continue;
178
+ groups.push({ key: section.label, label: section.label, items });
179
+ }
180
+ return groups;
181
+ }
182
+ function projectMobileNav(sections, opts) {
183
+ const config = opts?.config;
184
+ const maxTabs = Math.max(1, config?.maxTabs ?? MOBILE_MAX_TABS_DEFAULT);
185
+ const menuMax = Math.max(1, config?.menuMax ?? MOBILE_MENU_MAX_DEFAULT);
186
+ const extraMoreRows = opts?.extraMoreRows ?? 0;
187
+ const candidates = buildCandidates(sections, config, maxTabs, menuMax);
188
+ const coveredByAll = /* @__PURE__ */ new Set();
189
+ for (const candidate of candidates) for (const key of candidate.covered) coveredByAll.add(key);
190
+ const overflowIfNoneDropped = buildOverflow(sections, coveredByAll);
191
+ const needsMore = countRows(overflowIfNoneDropped.flatMap((group) => group.items)) > 0 || extraMoreRows > 0 || candidates.length > maxTabs;
192
+ const limit = needsMore ? maxTabs - 1 : maxTabs;
193
+ const kept = candidates.slice(0, limit);
194
+ const dropped = candidates.slice(limit);
195
+ if (DEV && dropped.length > 0) {
196
+ console.warn(
197
+ `[basalt] MobileNav: ${candidates.length} slot candidates for ${limit} available slot(s) \u2014 the last ${dropped.length} moved into the More surface: ${dropped.map((candidate) => `"${candidate.key}"`).join(", ")}. Raise mobileNav.maxTabs or drop a mobile: 'tab'.`
198
+ );
199
+ }
200
+ const covered = /* @__PURE__ */ new Set();
201
+ for (const candidate of kept) for (const key of candidate.covered) covered.add(key);
202
+ const slots = [];
203
+ for (const candidate of kept) {
204
+ const slot = candidate.toSlot();
205
+ if (slot !== void 0) slots.push(slot);
206
+ }
207
+ if (!needsMore) return { slots };
208
+ const groups = buildOverflow(sections, covered);
209
+ const overflowItems = groups.flatMap((group) => group.items);
210
+ const rows = countRows(overflowItems) + extraMoreRows;
211
+ if (rows === 0) return { slots };
212
+ const label = config?.moreLabel ?? "More";
213
+ const only = overflowItems[0];
214
+ if (rows === 1 && extraMoreRows === 0 && only !== void 0 && !only.disabled) {
215
+ slots.push({
216
+ kind: "link",
217
+ key: only.key,
218
+ label: only.label,
219
+ short: only.short ?? only.label,
220
+ icon: only.icon,
221
+ active: hasActiveDestination(only),
222
+ item: only
223
+ });
224
+ return { slots };
225
+ }
226
+ slots.push({
227
+ kind: rows <= menuMax ? "menu" : "sheet",
228
+ key: MOBILE_MORE_KEY,
229
+ label,
230
+ short: label,
231
+ // No icon: the More glyph is chrome the renderer owns, and this module holds no JSX.
232
+ icon: void 0,
233
+ active: overflowItems.some(hasActiveDestination),
234
+ groups,
235
+ isMore: true
236
+ });
237
+ return { slots };
238
+ }
239
+ export {
240
+ MOBILE_MAX_TABS_DEFAULT,
241
+ MOBILE_MENU_MAX_DEFAULT,
242
+ MOBILE_MORE_KEY,
243
+ projectMobileNav
244
+ };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=mobile-nav-model.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mobile-nav-model.test.d.ts","sourceRoot":"","sources":["../../src/shell/mobile-nav-model.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Sidebar nav count badge (docs/DESIGN-SPEC.md §5): mono 10.5px, ink-8% bg, radius 5, height 16,
3
+ * padding 0 5px, muted text; `marginLeft: auto` pins it to the row end on any render path.
4
+ * `styles` (inline) rather than a token color prop, since none of Mantine's variant/color
5
+ * combinations land on the ink-tint idiom. Returns `null` for a zero/empty count so the badge slot
6
+ * stays clean ("ink earns its color", DESIGN.md).
7
+ */
8
+ export declare function NavCountBadge({ count }: {
9
+ count: number;
10
+ }): import("react").JSX.Element | null;
11
+ //# sourceMappingURL=nav-count-badge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nav-count-badge.d.ts","sourceRoot":"","sources":["../../src/shell/nav-count-badge.tsx"],"names":[],"mappings":"AASA;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,sCAsBzD"}