basalt-ui 1.18.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 (92) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +7 -0
  3. package/agent/rules/basalt-mantine.md +71 -6
  4. package/agent/rules/basalt-router.md +129 -30
  5. package/agent/rules/basalt-tokens.md +2 -1
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +8 -0
  9. package/dist/nav/types.d.ts +110 -0
  10. package/dist/nav/types.d.ts.map +1 -0
  11. package/dist/nav/types.js +0 -0
  12. package/dist/router-tanstack/index.d.ts +9 -2
  13. package/dist/router-tanstack/index.d.ts.map +1 -1
  14. package/dist/router-tanstack/index.js +7 -0
  15. package/dist/router-tanstack/nav.d.ts +163 -0
  16. package/dist/router-tanstack/nav.d.ts.map +1 -0
  17. package/dist/router-tanstack/nav.js +38 -0
  18. package/dist/router-tanstack/nav.test.d.ts +2 -0
  19. package/dist/router-tanstack/nav.test.d.ts.map +1 -0
  20. package/dist/router-tanstack/search-param-store.d.ts +19 -9
  21. package/dist/router-tanstack/search-param-store.d.ts.map +1 -1
  22. package/dist/router-tanstack/use-nav.d.ts +30 -0
  23. package/dist/router-tanstack/use-nav.d.ts.map +1 -0
  24. package/dist/router-tanstack/use-nav.js +71 -0
  25. package/dist/shell/app-breadcrumbs.d.ts +7 -9
  26. package/dist/shell/app-breadcrumbs.d.ts.map +1 -1
  27. package/dist/shell/app-breadcrumbs.js +8 -4
  28. package/dist/shell/app-mobile-nav.d.ts +23 -24
  29. package/dist/shell/app-mobile-nav.d.ts.map +1 -1
  30. package/dist/shell/app-mobile-nav.js +290 -68
  31. package/dist/shell/app-mobile-nav.module.css +166 -11
  32. package/dist/shell/app-mobile-nav.module.css.d.ts +11 -2
  33. package/dist/shell/app-mobile-nav.test.d.ts +2 -0
  34. package/dist/shell/app-mobile-nav.test.d.ts.map +1 -0
  35. package/dist/shell/app-sidebar.d.ts +3 -28
  36. package/dist/shell/app-sidebar.d.ts.map +1 -1
  37. package/dist/shell/app-sidebar.js +40 -174
  38. package/dist/shell/app-sidebar.module.css +3 -2
  39. package/dist/shell/app-sidebar.module.css.d.ts +0 -2
  40. package/dist/shell/index.d.ts +22 -65
  41. package/dist/shell/index.d.ts.map +1 -1
  42. package/dist/shell/index.js +38 -76
  43. package/dist/shell/mobile-nav-model.d.ts +41 -0
  44. package/dist/shell/mobile-nav-model.d.ts.map +1 -0
  45. package/dist/shell/mobile-nav-model.js +244 -0
  46. package/dist/shell/mobile-nav-model.test.d.ts +2 -0
  47. package/dist/shell/mobile-nav-model.test.d.ts.map +1 -0
  48. package/dist/shell/nav-count-badge.d.ts +11 -0
  49. package/dist/shell/nav-count-badge.d.ts.map +1 -0
  50. package/dist/shell/nav-count-badge.js +29 -0
  51. package/dist/surfaces.d.ts +1 -1
  52. package/dist/surfaces.js +1 -1
  53. package/dist/theme/index.d.ts.map +1 -1
  54. package/dist/theme/index.js +8 -7
  55. package/dist/theme/nav-link.module.css +4 -2
  56. package/dist/tokens/index.d.ts.map +1 -1
  57. package/dist/tokens/index.js +2 -0
  58. package/dist/tokens/palette.d.ts +15 -0
  59. package/dist/tokens/palette.d.ts.map +1 -1
  60. package/dist/tokens/palette.js +13 -0
  61. package/dist/tokens.css +2 -0
  62. package/llms.txt +3 -3
  63. package/package.json +1 -1
  64. package/src/index.ts +15 -5
  65. package/src/nav/types.ts +115 -0
  66. package/src/router-tanstack/index.ts +21 -2
  67. package/src/router-tanstack/nav.test.ts +111 -0
  68. package/src/router-tanstack/nav.ts +205 -0
  69. package/src/router-tanstack/search-param-store.ts +19 -9
  70. package/src/router-tanstack/use-nav.ts +146 -0
  71. package/src/shell/app-breadcrumbs.tsx +21 -19
  72. package/src/shell/app-mobile-nav.module.css +166 -11
  73. package/src/shell/app-mobile-nav.module.css.d.ts +11 -2
  74. package/src/shell/app-mobile-nav.test.tsx +369 -0
  75. package/src/shell/app-mobile-nav.tsx +436 -102
  76. package/src/shell/app-sidebar.module.css +3 -2
  77. package/src/shell/app-sidebar.module.css.d.ts +0 -2
  78. package/src/shell/app-sidebar.test.tsx +6 -10
  79. package/src/shell/app-sidebar.tsx +49 -206
  80. package/src/shell/index.test.tsx +62 -7
  81. package/src/shell/index.tsx +105 -164
  82. package/src/shell/mobile-nav-model.test.ts +296 -0
  83. package/src/shell/mobile-nav-model.ts +389 -0
  84. package/src/shell/nav-count-badge.tsx +39 -0
  85. package/src/surfaces.ts +1 -1
  86. package/src/theme/density-relations.test.ts +19 -1
  87. package/src/theme/index.ts +8 -7
  88. package/src/theme/nav-link.module.css +4 -2
  89. package/src/theme/spacing.test.ts +11 -0
  90. package/src/tokens/density.test.ts +2 -0
  91. package/src/tokens/index.ts +8 -5
  92. package/src/tokens/palette.ts +16 -0
package/AGENTS.md CHANGED
@@ -15,7 +15,7 @@ Toolchain: Bun runtime, oxlint + oxfmt, conventional commits (empty scope), `mas
15
15
  | `basalt-ui/vite` | mantine-coupled | basaltViteConfig(opts) — Vite preset for basalt-ui consumer apps; basaltAppPlugin(opts) — PWA head, manifest, and icon metadata derived from the token palette |
16
16
  | `basalt-ui/guard` | headless | checkSource, GUARD_RULES, Finding types — the headless theme-guard core |
17
17
  | `basalt-ui/query` | headless | createBasaltQueryClient, transport-agnostic unwrap, lazy BasaltQueryDevtools |
18
- | `basalt-ui/router-tanstack` | headless | TanStack Router bridge: useBasaltNav (active route) + useRouterBreadcrumbs + createSearchParamStore (single-select URL-state store) + createMultiSearchParamStore (multi-select URL-state store) |
18
+ | `basalt-ui/router-tanstack` | headless | 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) |
19
19
  | `basalt-ui/forms` | mantine-coupled | Mantine form adapter: useBasaltForm, field, FormErrorSummary, useFormDraft (Standard Schema) |
20
20
  | `basalt-ui/notifications` | mantine-coupled | Mantine notifications: notify helpers, typed registry, persisted history, NotificationBell, NotificationCenter |
21
21
  | `basalt-ui/commands` | mantine-coupled | typed command bus + overlay controller, toSpotlightActions, ShortcutsHelp, BasaltOverlays |
package/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ # [1.19.0](https://github.com/jkrumm/basalt-ui/compare/v1.18.0...v1.19.0) (2026-08-20)
2
+
3
+
4
+ ### Features
5
+
6
+ * make the mobile bar navigate, driven by one typed nav definition ([1ebba92](https://github.com/jkrumm/basalt-ui/commit/1ebba9267267ff26e988c876f17839b05d61689d))
7
+
1
8
  # [1.18.0](https://github.com/jkrumm/basalt-ui/compare/v1.17.0...v1.18.0) (2026-08-19)
2
9
 
3
10
 
@@ -60,8 +60,9 @@ Restraint is the #1 lever — neutrals carry the surface, the accent only points
60
60
  reads as a button sitting in the nav rather than as the current location. Inactive = muted text,
61
61
  faint icon, hover ink-6%; an active row hovers one step further to ink-13%. This is baked into
62
62
  the theme's NavLink defaults (`--nl-*` vars) and holds for _every_ render path, including a
63
- consumer's router `<Link>` passed via `renderNavLink`. Child items indent with a 1px `divider`
64
- left border; active child = accent text, weight 600.
63
+ consumer's router `<Link>` hosted through `SidebarItem.Anchor`. Child items indent with a 1px
64
+ `divider` left border; active child = accent text, weight 600. The MOBILE bar is the one
65
+ deliberate exception to the accent-icon half of this rule — see "Mobile bar" below.
65
66
  - **Buttons**: the primary action = filled accent (`variant="filled"`), **exactly one per view**.
66
67
  Every other/secondary action = `variant="default"` (neutral). Do **not** use a colored
67
68
  `variant="light"` for routine actions.
@@ -280,8 +281,8 @@ shadow-surfaces.test.ts` mechanically enumerates every site that applies either
280
281
  `BasaltShell` is the canonical single-mount. When a fully custom layout is needed, the shell
281
282
  sub-components are available individually from `basalt-ui`:
282
283
 
283
- - `AppSidebar` — desktop sidebar (sections, collapse, brand, footer/settings extras)
284
- - `MobileNav` — bottom-tab mobile nav
284
+ - `AppSidebar` — desktop sidebar (sections, collapse, brand, settings/account extras)
285
+ - `MobileNav` — the bottom tab bar (takes a `MobileNavModel` from `projectMobileNav`)
285
286
  - `AppBreadcrumbs` — breadcrumb bar (reads from `useRouterBreadcrumbs` or a manual trail)
286
287
  - `PageHeaderProvider` + `PageActions` + `PageActionsOutlet` — portal-based page action slots
287
288
  - `NavCountBadge` — count badge for sidebar nav items
@@ -294,13 +295,14 @@ import {
294
295
  PageHeaderProvider,
295
296
  PageActionsOutlet,
296
297
  NavCountBadge,
298
+ projectMobileNav,
297
299
  } from 'basalt-ui'
298
300
 
299
301
  function CustomShell({ children }: { children: React.ReactNode }) {
300
302
  return (
301
303
  <PageHeaderProvider>
302
- <AppSidebar sections={sections} brand={brand} renderNavLink={renderNavLink} />
303
- <MobileNav sections={mobileSections} renderNavLink={renderNavLink} />
304
+ <AppSidebar sections={sections} brand={brand} />
305
+ <MobileNav model={projectMobileNav(sections)} />
304
306
  <main>
305
307
  <AppBreadcrumbs />
306
308
  <PageActionsOutlet />
@@ -314,6 +316,69 @@ function CustomShell({ children }: { children: React.ReactNode }) {
314
316
  Prefer `BasaltShell` unless the layout genuinely diverges — it already composes all of the above
315
317
  with the correct wiring (collapse persistence, mobile breakpoint, breadcrumb integration).
316
318
 
319
+ ## Mobile bar
320
+
321
+ The bottom bar is a **tab bar, not a menu**: a slot is a DESTINATION, so a tap navigates instantly
322
+ with nothing to dismiss. There is no full-height mobile sidebar drawer — everything it used to hold
323
+ (nav, account, settings, theme switcher) is reachable from the trailing More slot instead.
324
+
325
+ `BasaltShell` builds the whole thing from the same `SidebarSection[]` the desktop sidebar renders:
326
+ **`projectMobileNav`** projects those sections onto the bar and **`MobileNav`** paints the result.
327
+ There is no second nav model to maintain, and in the common case no configuration at all.
328
+
329
+ **Surface inference — the part you do not configure.** For any slot holding `n` rows:
330
+
331
+ | `n` | surface |
332
+ | ----------------------- | ------------------------------------------------------------------------- |
333
+ | 0 | the slot is dropped |
334
+ | 1 | a plain navigation link — a group of one IS a destination |
335
+ | ≤ `menuMax` (default 6) | a Mantine `Menu` that pops upward out of the tab, content-sized, no scrim |
336
+ | > `menuMax` | a bottom `Drawer` sheet with a grabber and an internal `ScrollArea` |
337
+
338
+ Six is arithmetic, not taste: 6 × 44px + 8px padding = 272px against 415px of headroom on the
339
+ smallest supported viewport, which is why the menu is configured `flip: false` and can never render
340
+ below the fold. Move a destination in or out of the More slot and the surface changes itself.
341
+
342
+ **Placement** is one field per destination — `SidebarItem`'s `mobile`: `'tab'` gives it a slot of
343
+ its own, `'more'` (the default) makes it an overflow row, `'hidden'` drops it from mobile entirely.
344
+ A whole section claims one slot with `mobile: { tab: true }` (its destinations then open per the
345
+ table above) or leaves mobile with `mobile: false`. Configure nothing and the first four non-disabled
346
+ top-level destinations take slots — a nav that works before anyone has thought about mobile.
347
+
348
+ **`MobileNavConfig`** (the `mobileNav` prop) is the escape hatch, not the interface: `tabs` for an
349
+ explicit slot order by key when the bar order must differ from the sidebar's, `maxTabs` (5),
350
+ `menuMax` (6), `moreLabel`, `moreExtra`, and `getScrollElement` for re-tap-to-top when the page
351
+ scrolls inside a container rather than the document.
352
+
353
+ Design rules specific to the bar, each one load-bearing:
354
+
355
+ - **The active indicator is a neutral pill behind the ICON only** — `--vx-ink` at 10% plus
356
+ `--vx-radius-ctrl`, the Material-3 idiom. Never the identity blue, and never a full-tab fill: a
357
+ filled tab reads as chrome instead of as a tab bar. This is the exception to "active nav = accent
358
+ icon" in the palette rules above; the desktop sidebar keeps that rule unchanged.
359
+ - **A count renders as an 8px accent dot** on the slot icon, not a number — a count glyph is
360
+ unreadable at 56px. `SidebarItem.count` drives it; `SidebarItem.badge` (a rich node) stays
361
+ desktop-only.
362
+ - **Press feedback is the icon, not the hit box**: `.tab:active` scales the icon to 0.9 over 140ms.
363
+ `touch-action: manipulation` + `-webkit-tap-highlight-color: transparent` kill the 300ms
364
+ double-tap delay and the blue flash.
365
+ - **Every `:hover` rule sits behind `@media (hover: hover)`.** Ungated, a tapped row stays lit until
366
+ something else is tapped.
367
+ - **Re-tapping the ACTIVE slot scrolls to top** instead of pushing a redundant history entry.
368
+ - **Never add `padding-bottom: env(safe-area-inset-bottom)` to the bar.** Mantine's own
369
+ `AppShell.Footer` rule already grows the footer box by the inset and pads its content; adding it
370
+ again double-counts. The one real gap is `AppShell.Main`, whose padding is computed from an offset
371
+ var that excludes the inset — the shell closes that itself.
372
+ - **Reduced motion** is read explicitly via `useReducedMotion` (Mantine's
373
+ `respectReducedMotion` defaults to `false` and basalt does not change it) and zeroes both the menu
374
+ and sheet transitions.
375
+
376
+ Accessibility, all handled by the shell: the bar is `<nav aria-label="Primary">`, the active slot
377
+ carries `aria-current="page"`, every slot carries `aria-label` (the visible text is the short
378
+ label), menus get `menuItemTabIndex={0}` for the WAI-ARIA disclosure pattern, and the row height
379
+ floors at 44px with the bar at 48px regardless of the `density` level. **Do not hand-set
380
+ `aria-haspopup` / `aria-expanded`** on a `Menu.Target` child — Mantine clones them on.
381
+
317
382
  ## Icons
318
383
 
319
384
  basalt-ui takes icons as `ReactNode` (no icon dependency shipped). Use a single icon library across the
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  source: basalt-ui
3
- description: TanStack Router conventions for basalt-ui apps — file-based routes, typed search params, and loader/query coupling. Advisory (ships an optional ./router-tanstack adapter for active-state and breadcrumb wiring).
3
+ description: TanStack Router conventions for basalt-ui apps — file-based routes, typed search params, loader/query coupling, and the one typed nav definition that drives both the desktop sidebar and the mobile bar. Advisory (ships an optional ./router-tanstack adapter).
4
4
  paths:
5
5
  - 'src/routes/**'
6
6
  - 'apps/**/src/routes/**'
@@ -8,13 +8,14 @@ paths:
8
8
 
9
9
  # Basalt Router — TanStack Router Conventions
10
10
 
11
- basalt-ui is **router-agnostic** — `BasaltShell` takes a `NavLinkRenderer` for nav rendering and
12
- receives active state from the consumer. Nav active-state can now be resolved via
13
- `useBasaltNav().isActive(item.href)` from `basalt-ui/router-tanstack`, and breadcrumbs via
14
- `useRouterBreadcrumbs()`; navigation itself stays with TanStack's typed `<Link>` / `useNavigate`.
15
- Badge counts and navigation wiring remain consumer-side. This rule is the recommended opinion layer
16
- when you use TanStack Router (the basalt-ui default). It is **advisory** — adopt it for
17
- consistency; nothing in the framework enforces it.
11
+ basalt-ui is **router-agnostic** — the shell renders every pixel of nav chrome and hosts the
12
+ consumer's router component through one seam, `SidebarItem.Anchor` (a **`NavAnchor`**). With
13
+ TanStack Router you never write that seam by hand: **`defineNav`** declares the whole navigation
14
+ once and **`useNav`** resolves it against the live router, returning the two props `BasaltShell`
15
+ needs. Breadcrumbs come from **`useRouterBreadcrumbs`**, ad-hoc active checks from
16
+ **`useBasaltNav`**. This rule is the recommended opinion layer when you use TanStack Router (the
17
+ basalt-ui default). It is **advisory** — adopt it for consistency; nothing in the framework
18
+ enforces it.
18
19
 
19
20
  ## Adding a page
20
21
 
@@ -23,7 +24,7 @@ consistency; nothing in the framework enforces it.
23
24
  3. Add `validateSearch` with a Zod schema parsed via a function — not the schema passed directly.
24
25
  4. Define `loaderDeps` to forward search params to the loader (required for search-param-driven queries).
25
26
  5. Add a `loader` that calls `ensureQueryData` for each query the page needs.
26
- 6. Register the page in your `BasaltShell` sidebar sections (`SidebarSection[]`).
27
+ 6. Register the page in your `defineNav` definition (see "One typed nav definition" below).
27
28
 
28
29
  ## Route structure
29
30
 
@@ -146,28 +147,126 @@ function AppBreadcrumbs() {
146
147
  }
147
148
  ```
148
149
 
149
- **renderNavLink pattern** — wire the shell's `NavLinkRenderer` with typed TanStack routes.
150
- basalt does NOT ship this renderer; typed-route `to` is the consumer's typed concern:
150
+ **One typed nav definition** — `defineNav` / `navGroup` / `navTarget` / `useNav`.
151
+
152
+ The whole navigation — desktop sidebar and mobile bar — is declared once in a **leaf** module
153
+ (`src/lib/nav.tsx`), which imports `@tanstack/react-router` and `basalt-ui/router-tanstack` and
154
+ **never** `routeTree.gen` or `__root.tsx`, so command palettes and redirects can import it without
155
+ closing a cycle.
156
+
157
+ ```tsx
158
+ import { linkOptions } from '@tanstack/react-router'
159
+ import { defineNav, navGroup } from 'basalt-ui/router-tanstack'
160
+
161
+ const ICON = 18
162
+
163
+ export const NAV = defineNav({
164
+ groups: [
165
+ navGroup({ id: 'health', label: 'Health', icon: <IconHeartbeat size={ICON} /> }, [
166
+ {
167
+ id: 'garmin',
168
+ label: 'Garmin Health',
169
+ short: 'Garmin', // bar/menu label — keep it ≤ 10 chars
170
+ mobile: 'tab', // give this destination its own slot on the mobile bar
171
+ icon: <IconHeartbeat size={ICON} />,
172
+ link: linkOptions({ to: '/garmin-health', search: { window: '30d' } }),
173
+ },
174
+ {
175
+ id: 'reading',
176
+ label: 'Reading',
177
+ icon: <IconBook size={ICON} />,
178
+ link: linkOptions({ to: '/reading' }),
179
+ children: [
180
+ {
181
+ id: 'reading-stats',
182
+ label: 'Stats',
183
+ icon: <IconChartBar size={ICON} />,
184
+ link: linkOptions({ to: '/reading/stats' }),
185
+ },
186
+ ],
187
+ },
188
+ ]),
189
+ ],
190
+ })
191
+ ```
151
192
 
152
- ```ts
153
- import { Link } from '@tanstack/react-router'
154
- import { NavLink as MantineNavLink } from '@mantine/core'
155
- import { useBasaltNav } from 'basalt-ui/router-tanstack'
156
- import type { NavLinkRenderer } from 'basalt-ui'
157
-
158
- const { isActive } = useBasaltNav()
159
-
160
- const renderNavLink: NavLinkRenderer = (item, _flags) => (
161
- <MantineNavLink
162
- component={Link}
163
- to={item.href ?? '/'}
164
- label={item.label}
165
- leftSection={item.icon}
166
- rightSection={item.badge}
167
- active={isActive(item.href ?? '/')}
168
- />
193
+ Rules that are not stylistic:
194
+
195
+ - **Route options live under `link:`, wrapped in TanStack's own `linkOptions`** — never spread flat
196
+ onto the item. `to` / `search` / `params` get the router's full validation there, while `id`,
197
+ `label`, `short`, `icon`, `mobile`, `disabled` and `exact` ride outside it. A flat shape
198
+ (`{ id, label, to }`) validates `to` by ASSIGNABILITY, which does no excess-property checking, so
199
+ a typo'd metadata key would compile silently. A top-level `to:` is a compile error by design.
200
+ - **`search` may be a thunk** — `search: () => ({ date: format(new Date(), 'yyyy-MM-dd') })`
201
+ compiles without `as const` and re-evaluates at click time, so "today" never goes stale in a
202
+ long-lived tab.
203
+ - **Nesting is `children`**, and nested destinations are indexed by id just like top-level ones.
204
+ - **`NavItemMeta`**'s `mobile` field places the destination on the bar: `'tab'` (own slot),
205
+ `'more'` (the default — reachable through the More overlay) or `'hidden'`. A group claims a slot
206
+ of its own with `mobile: { tab: true }`, and leaves mobile entirely with `mobile: false`.
207
+
208
+ ```tsx
209
+ // __root.tsx — four lines replace a hand-written sections array, two render callbacks,
210
+ // a NAV_TARGETS table and one useMatchRoute call per destination.
211
+ import { useNav } from 'basalt-ui/router-tanstack'
212
+ import { NAV } from '../lib/nav'
213
+
214
+ const nav = useNav(NAV, { badges: { calendar: unread } }) // keys autocomplete; a typo is an error
215
+ return (
216
+ <BasaltShell brand={brand} {...nav}>
217
+ {children}
218
+ </BasaltShell>
169
219
  )
170
220
  ```
171
221
 
172
- Navigation (`.navigate()`, `<Link to={…}>`) is always the consumer's typed concern — the adapter
173
- exposes no `navigate()` helper so that TanStack's route-tree types remain authoritative.
222
+ **`useNav`** returns `{ sections, mobileNav }` — both are `BasaltShell` props, so spread it. It
223
+ resolves `active` per destination through `useMatchRoute` (prefix match by default, exact when the
224
+ item sets `exact: true`, and always exact for `'/'`), and builds each destination's **`NavAnchor`**
225
+ so `preload="intent"`, middle-click and back/forward keep working. `badges` values that are
226
+ `number` become `SidebarItem.count` (a desktop **`NavCountBadge`** plus an accent dot on the mobile
227
+ slot); anything else becomes `SidebarItem.badge`, which is desktop-only because a count glyph is
228
+ unreadable in a 56px tab.
229
+
230
+ **`navTarget`** returns one destination's link options, typed per id — spread it into `<Link>`,
231
+ `router.navigate()` or `redirect()` instead of restating the route and its default search:
232
+
233
+ ```ts
234
+ throw redirect(navTarget(NAV, 'garmin')) // exactly { to: '/garmin-health', search: { window: '30d' } }
235
+ ```
236
+
237
+ **`flattenNav`** walks the definition depth-first (parent, then children), tagging each destination
238
+ with `groupId` / `groupLabel` — the leaf every other surface reads, e.g. projecting the whole nav
239
+ into Spotlight commands in one `.map()`.
240
+
241
+ ### The prerequisite that makes all of this real
242
+
243
+ **Without the `Register` module augmentation, `defineNav` validates NOTHING and reports zero
244
+ errors.** `RegisteredRouter` falls back to `AnyRouter` when the consumer has not written:
245
+
246
+ ```ts
247
+ declare module '@tanstack/react-router' {
248
+ interface Register {
249
+ router: typeof router
250
+ }
251
+ }
252
+ ```
253
+
254
+ With that missing, every `to` widens to `string`, a nonexistent route path compiles, a missing
255
+ required `search` compiles, and the API looks like it is working while catching nothing. This is
256
+ the single highest-value thing to verify when adopting the config: change one `to` to a garbage
257
+ path and confirm you get a compile error listing the real route paths. If you don't, the
258
+ augmentation is missing or `tsr generate` has not run.
259
+
260
+ Second thing to know before you migrate: TypeScript cannot catch a destination that FORGETS a
261
+ default `search` when the route validates with `validateSearch: (raw: Record<string, unknown>) => T`
262
+ — that gives TanStack an input type where no key is required. Diff an old hand-written target table
263
+ against the new definition by hand, key by key.
264
+
265
+ Error messages here are the router's own and they are large: one wrong `to` prints the whole
266
+ route-path union plus a `RouterCore<…>` blob, because TanStack's `Constrain` falls back to the
267
+ whole constraint. That is not misuse — read the union for the intended path and fix the string.
268
+ **Do not reach for `as never` to make it quiet**; the checking is the entire point of the config.
269
+
270
+ Navigation itself (`.navigate()`, `<Link to={…}>`) stays the consumer's typed concern — the adapter
271
+ still exposes no `navigate()` helper, so TanStack's route-tree types remain authoritative. What
272
+ `navTarget` hands back is the ARGUMENT to those calls, never the call.
@@ -67,7 +67,8 @@ Retuning a fill OUTSIDE the band breaks its label. `theme/contrast.test.ts` fail
67
67
  or genuine multi-series separation. A count badge is a signal → it may carry blue; a nav active-state
68
68
  is UI chrome → it stays neutral (`NavCountBadge` already encodes this). Active nav renders a
69
69
  **neutral surface fill** + plain text + a weight bump, never the accent — baked into the theme's
70
- NavLink `--nl-*` defaults across every render path (including a router `<Link>` via `renderNavLink`).
70
+ NavLink `--nl-*` defaults across every render path (including a router `<Link>` hosted through
71
+ `SidebarItem.Anchor`).
71
72
  - **Accent restraint, mechanically.** The accent lands on exactly the single primary CTA per view
72
73
  (`variant="filled"`), focus rings, links, and small status pops — nowhere else. Routine/secondary
73
74
  buttons use `variant="default"` (neutral); don't reach for a colored `variant="light"` (it reads
package/dist/index.d.ts CHANGED
@@ -10,7 +10,7 @@ export { createBasaltTheme, baseTheme, cssVariablesResolver } from './theme/inde
10
10
  export type { BasaltFontsConfig, CreateBasaltThemeOptions } from './theme/index.js';
11
11
  export { ThemeToggle, type ThemeToggleProps } from './theme-toggle/index.js';
12
12
  export { MOTION_DURATION, MOTION_SPRING, MOTION_EASE_STANDARD } from './motion/index.js';
13
- export { BasaltShell, NavCountBadge, AppSidebar, SidebarSearch, type SidebarSearchConfig, SidebarAccount, MobileNav, AppBreadcrumbs, type BreadcrumbLinkRenderer, PageHeaderProvider, PageActions, PageActionsOutlet, type SidebarSection, type SidebarItem, type BasaltShellProps, type BrandConfig, type SettingsMenuItem, type AppSidebarProps, type MobileNavItem, type MobileNavSection, type NavLinkRenderer, type MobileNavLinkRenderer, type AccountBadgeTone, type BasaltIdentity, type BasaltRole, type BasaltPlan, type AccountMenuItem, type BasaltAccountState, type BasaltAccountActions, type BasaltAccountProps, } from './shell/index.js';
13
+ export { BasaltShell, NavCountBadge, AppSidebar, SidebarSearch, type SidebarSearchConfig, SidebarAccount, MobileNav, type MobileNavProps, projectMobileNav, type ProjectMobileNavOptions, MOBILE_MAX_TABS_DEFAULT, MOBILE_MENU_MAX_DEFAULT, MOBILE_MORE_KEY, AppBreadcrumbs, PageHeaderProvider, PageActions, PageActionsOutlet, type SidebarSection, type SidebarItem, type NavAnchor, type NavAnchorProps, type NavMobilePlacement, type NavSectionMobile, type MobileNavConfig, type MobileNavGroup, type MobileNavModel, type MobileNavSlot, type MobileNavSurface, type BasaltShellProps, type BrandConfig, type SettingsMenuItem, type AppSidebarProps, type AccountBadgeTone, type BasaltIdentity, type BasaltRole, type BasaltPlan, type AccountMenuItem, type BasaltAccountState, type BasaltAccountActions, type BasaltAccountProps, } from './shell/index.js';
14
14
  export { type BasaltRegister, type Slot, type Series, type SeriesKey, type AsyncState, assertNever, type StandardSchemaV1, } from './register.js';
15
15
  export { createPersistedState, type PersistedStateOptions, useOnlineStatus, readPersistedValue, } from './state.js';
16
16
  export { DeltaBadge, type DeltaBadgeProps, StatCard, type StatCardProps, type StatCardTone, EmptyState, type EmptyStateProps, SettingsSection, type SettingsSectionProps, SettingsRow, type SettingsRowProps, DangerZone, type DangerZoneProps, } from './dashboard/index.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -12,6 +12,10 @@ import {
12
12
  SidebarSearch,
13
13
  SidebarAccount,
14
14
  MobileNav,
15
+ projectMobileNav,
16
+ MOBILE_MAX_TABS_DEFAULT,
17
+ MOBILE_MENU_MAX_DEFAULT,
18
+ MOBILE_MORE_KEY,
15
19
  AppBreadcrumbs,
16
20
  PageHeaderProvider,
17
21
  PageActions,
@@ -64,6 +68,9 @@ export {
64
68
  DangerZone,
65
69
  DeltaBadge,
66
70
  EmptyState,
71
+ MOBILE_MAX_TABS_DEFAULT,
72
+ MOBILE_MENU_MAX_DEFAULT,
73
+ MOBILE_MORE_KEY,
67
74
  MOTION_DURATION,
68
75
  MOTION_EASE_STANDARD,
69
76
  MOTION_SPRING,
@@ -93,6 +100,7 @@ export {
93
100
  createThreadsStore,
94
101
  cssVariablesResolver,
95
102
  heuristicOutcome,
103
+ projectMobileNav,
96
104
  readPersistedValue,
97
105
  threadPartRenderers,
98
106
  threadsStoreAdapterContract,
@@ -0,0 +1,110 @@
1
+ /**
2
+ * The shared nav vocabulary. TYPE-ONLY BY CONTRACT — never add a value export here.
3
+ * `src/shell/**` (Mantine) and `src/router-tanstack/**` (headless) both `import type` from it,
4
+ * which is erased by tsup, so `dist/router-tanstack/index.js` gains no edge into `shell/` and
5
+ * `scripts/check-dist-layering.mjs` stays green with no config change.
6
+ */
7
+ import type { MouseEvent, ReactNode } from 'react';
8
+ /** Where a destination appears on the mobile bar. `true` ≡ `'tab'`, `false` ≡ `'hidden'`. */
9
+ export type NavMobilePlacement = 'tab' | 'more' | 'hidden';
10
+ /**
11
+ * Props basalt hands a nav anchor. Deliberately a PLAIN FUNCTION TYPE, not `ComponentType`
12
+ * or `ElementType`: both are unions, and a union defeats Mantine's polymorphic `component`
13
+ * inference (`<NavLink component={…}>` then rejects `label`). VERIFIED both ways.
14
+ */
15
+ export type NavAnchorProps = {
16
+ className?: string;
17
+ children?: ReactNode;
18
+ onClick?: (e: MouseEvent<HTMLElement>) => void;
19
+ 'aria-current'?: 'page' | undefined;
20
+ 'aria-label'?: string;
21
+ };
22
+ export type NavAnchor = (props: NavAnchorProps) => ReactNode;
23
+ /** A single nav destination. */
24
+ export type SidebarItem = {
25
+ key: string;
26
+ label: string;
27
+ /** Bar/menu label. Falls back to `label`. Keep ≤ 10 chars — a 5-slot bar is ~72px wide. */
28
+ short?: string;
29
+ /** Mobile placement. `true` ≡ `'tab'`, `false` ≡ `'hidden'`. @default 'more' */
30
+ mobile?: boolean | NavMobilePlacement;
31
+ icon: ReactNode;
32
+ /**
33
+ * The router seam. basalt renders every pixel of chrome (desktop row, 56px slot, 44px sheet
34
+ * row) and only hosts this component. When absent the row falls back to `<a href>` + `onClick`.
35
+ */
36
+ Anchor?: NavAnchor;
37
+ /** Plain href — the no-router fallback, and the breadcrumb parent target. */
38
+ href?: string;
39
+ active?: boolean;
40
+ disabled?: boolean;
41
+ onClick?: (e: MouseEvent) => void;
42
+ /** Rich desktop badge (e.g. `NavCountBadge`). Desktop only. */
43
+ badge?: ReactNode;
44
+ /** Unread count. Renders `NavCountBadge` on desktop when `badge` is absent, and an accent
45
+ * dot on the mobile slot icon. `0` renders nothing. */
46
+ count?: number;
47
+ children?: SidebarItem[];
48
+ };
49
+ /** Per-section mobile configuration. `false` hides the whole section from mobile. */
50
+ export type NavSectionMobile = {
51
+ /** Give this SECTION a bar slot; its destinations open per §2.2 cardinality inference. */
52
+ tab?: true;
53
+ /** Slot label override; falls back to `SidebarSection.label`. */
54
+ label?: string;
55
+ /** Slot icon override; falls back to `SidebarSection.icon`. */
56
+ icon?: ReactNode;
57
+ };
58
+ export type SidebarSection = {
59
+ label: string;
60
+ items: SidebarItem[];
61
+ icon?: ReactNode;
62
+ collapsible?: boolean;
63
+ defaultCollapsed?: boolean;
64
+ /** Mobile behaviour for this section. Omit for the default (items place themselves). */
65
+ mobile?: false | NavSectionMobile;
66
+ };
67
+ export type MobileNavSurface = 'link' | 'menu' | 'sheet';
68
+ /** One row inside a `menu`/`sheet` slot, grouped under its source section. */
69
+ export type MobileNavGroup = {
70
+ key: string;
71
+ label?: string;
72
+ items: SidebarItem[];
73
+ };
74
+ export type MobileNavSlot = {
75
+ kind: 'link';
76
+ key: string;
77
+ label: string;
78
+ short: string;
79
+ icon: ReactNode;
80
+ active: boolean;
81
+ item: SidebarItem;
82
+ } | {
83
+ kind: 'menu' | 'sheet';
84
+ key: string;
85
+ label: string;
86
+ short: string;
87
+ icon: ReactNode;
88
+ active: boolean;
89
+ groups: MobileNavGroup[];
90
+ isMore: boolean;
91
+ };
92
+ /** Pure projection of `sections` onto the bar. Unit-testable with no DOM. */
93
+ export type MobileNavModel = {
94
+ slots: MobileNavSlot[];
95
+ };
96
+ export type MobileNavConfig = {
97
+ /** Explicit slot order by item/section key. Overrides every `item.mobile` placement. */
98
+ tabs?: readonly string[];
99
+ /** Hard cap on bar slots, INCLUDING More. @default 5 */
100
+ maxTabs?: number;
101
+ /** Rows a slot may hold before it becomes a sheet instead of a menu. @default 6 */
102
+ menuMax?: number;
103
+ /** @default 'More' */
104
+ moreLabel?: string;
105
+ /** Extra content pinned to the bottom of the More surface. */
106
+ moreExtra?: ReactNode;
107
+ /** Scroll container for active-slot re-tap. @default document.scrollingElement */
108
+ getScrollElement?: () => HTMLElement | null;
109
+ };
110
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/nav/types.ts"],"names":[],"mappings":"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"}
File without changes
@@ -1,11 +1,18 @@
1
1
  /**
2
2
  * ./router-tanstack — headless TanStack Router bridge for the app shell.
3
3
  * Optional peer: @tanstack/react-router.
4
- * Augments StaticDataRouteOption with title/icon/navSection for breadcrumb
5
- * and nav-active wiring — no Mantine, no JSX, no navigate() helper.
4
+ * Carries the typed navigation definition (`defineNav`/`navGroup`/`navTarget`/`flattenNav`) and
5
+ * `useNav`, which resolves it into `BasaltShell`'s `sections` + `mobileNav` props; plus the
6
+ * StaticDataRouteOption augmentation (title/icon/navSection) for router-derived breadcrumbs and
7
+ * the search-param stores — no Mantine, no JSX.
6
8
  */
7
9
  export { createSearchParamStore, type SearchParamStore, type SearchParamStoreOptions, } from './search-param-store.js';
8
10
  export { createMultiSearchParamStore, type MultiSearchParamStore, type MultiSearchParamStoreOptions, } from './multi-search-param-store.js';
11
+ export { defineNav, navGroup, navTarget, flattenNav } from './nav.js';
12
+ export type { NavItemMeta, NavGroupMeta, AnyNavItem, AnyNavGroup, AnyNavLink, FlatNavItem, NavConfig, NavItemId, NavGroupId, NavTabId, NavIndex, } from './nav.js';
13
+ export { useNav } from './use-nav.js';
14
+ export type { UseNavOptions, NavBinding } from './use-nav.js';
15
+ export type { NavAnchor, NavAnchorProps, NavMobilePlacement } from '../nav/types.js';
9
16
  import type { ReactNode } from 'react';
10
17
  declare module '@tanstack/react-router' {
11
18
  interface StaticDataRouteOption {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/router-tanstack/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACL,sBAAsB,EACtB,KAAK,gBAAgB,EACrB,KAAK,uBAAuB,GAC7B,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EACL,2BAA2B,EAC3B,KAAK,qBAAqB,EAC1B,KAAK,4BAA4B,GAClC,MAAM,4BAA4B,CAAA;AACnC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAKtC,OAAO,QAAQ,wBAAwB,CAAC;IACtC,UAAU,qBAAqB;QAC7B,0EAA0E;QAC1E,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,oEAAoE;QACpE,IAAI,CAAC,EAAE,SAAS,CAAA;QAChB,wDAAwD;QACxD,UAAU,CAAC,EAAE,MAAM,CAAA;KACpB;CACF;AAID,mFAAmF;AACnF,MAAM,MAAM,SAAS,GAAG;IACtB,gEAAgE;IAChE,WAAW,EAAE,MAAM,CAAA;IACnB;;;;;OAKG;IACH,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAA;CAChE,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,IAAI,SAAS,CAUxC;AAID,iEAAiE;AACjE,MAAM,MAAM,gBAAgB,GAAG;IAC7B,sCAAsC;IACtC,KAAK,EAAE,MAAM,CAAA;IACb,oFAAoF;IACpF,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,oBAAoB,IAAI,gBAAgB,EAAE,CASzD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/router-tanstack/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EACL,sBAAsB,EACtB,KAAK,gBAAgB,EACrB,KAAK,uBAAuB,GAC7B,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EACL,2BAA2B,EAC3B,KAAK,qBAAqB,EAC1B,KAAK,4BAA4B,GAClC,MAAM,4BAA4B,CAAA;AACnC,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,OAAO,CAAA;AAClE,YAAY,EACV,WAAW,EACX,YAAY,EACZ,UAAU,EACV,WAAW,EACX,UAAU,EACV,WAAW,EACX,SAAS,EACT,SAAS,EACT,UAAU,EACV,QAAQ,EACR,QAAQ,GACT,MAAM,OAAO,CAAA;AACd,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAA;AAClC,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAC1D,YAAY,EAAE,SAAS,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AACjF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAKtC,OAAO,QAAQ,wBAAwB,CAAC;IACtC,UAAU,qBAAqB;QAC7B,0EAA0E;QAC1E,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,oEAAoE;QACpE,IAAI,CAAC,EAAE,SAAS,CAAA;QAChB,wDAAwD;QACxD,UAAU,CAAC,EAAE,MAAM,CAAA;KACpB;CACF;AAID,mFAAmF;AACnF,MAAM,MAAM,SAAS,GAAG;IACtB,gEAAgE;IAChE,WAAW,EAAE,MAAM,CAAA;IACnB;;;;;OAKG;IACH,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAA;CAChE,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,IAAI,SAAS,CAUxC;AAID,iEAAiE;AACjE,MAAM,MAAM,gBAAgB,GAAG;IAC7B,sCAAsC;IACtC,KAAK,EAAE,MAAM,CAAA;IACb,oFAAoF;IACpF,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,oBAAoB,IAAI,gBAAgB,EAAE,CASzD"}
@@ -4,6 +4,8 @@ import {
4
4
  import {
5
5
  createMultiSearchParamStore
6
6
  } from "./multi-search-param-store.js";
7
+ import { defineNav, navGroup, navTarget, flattenNav } from "./nav.js";
8
+ import { useNav } from "./use-nav.js";
7
9
  import { useLocation, useMatches } from "@tanstack/react-router";
8
10
  function useBasaltNav() {
9
11
  const currentPath = useLocation({ select: (l) => l.pathname });
@@ -24,6 +26,11 @@ function useRouterBreadcrumbs() {
24
26
  export {
25
27
  createMultiSearchParamStore,
26
28
  createSearchParamStore,
29
+ defineNav,
30
+ flattenNav,
31
+ navGroup,
32
+ navTarget,
27
33
  useBasaltNav,
34
+ useNav,
28
35
  useRouterBreadcrumbs
29
36
  };