basalt-ui 1.18.0 → 1.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +14 -0
- package/agent/rules/basalt-mantine.md +71 -6
- package/agent/rules/basalt-router.md +129 -30
- package/agent/rules/basalt-tokens.md +2 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/nav/types.d.ts +110 -0
- package/dist/nav/types.d.ts.map +1 -0
- package/dist/nav/types.js +0 -0
- package/dist/router-tanstack/index.d.ts +9 -2
- package/dist/router-tanstack/index.d.ts.map +1 -1
- package/dist/router-tanstack/index.js +7 -0
- package/dist/router-tanstack/nav.d.ts +163 -0
- package/dist/router-tanstack/nav.d.ts.map +1 -0
- package/dist/router-tanstack/nav.js +38 -0
- package/dist/router-tanstack/nav.test.d.ts +2 -0
- package/dist/router-tanstack/nav.test.d.ts.map +1 -0
- package/dist/router-tanstack/search-param-store.d.ts +19 -9
- package/dist/router-tanstack/search-param-store.d.ts.map +1 -1
- package/dist/router-tanstack/use-nav.d.ts +30 -0
- package/dist/router-tanstack/use-nav.d.ts.map +1 -0
- package/dist/router-tanstack/use-nav.js +71 -0
- package/dist/shell/app-breadcrumbs.d.ts +7 -9
- package/dist/shell/app-breadcrumbs.d.ts.map +1 -1
- package/dist/shell/app-breadcrumbs.js +8 -4
- package/dist/shell/app-mobile-nav.d.ts +23 -24
- package/dist/shell/app-mobile-nav.d.ts.map +1 -1
- package/dist/shell/app-mobile-nav.js +291 -69
- package/dist/shell/app-mobile-nav.module.css +191 -11
- package/dist/shell/app-mobile-nav.module.css.d.ts +11 -2
- package/dist/shell/app-mobile-nav.test.d.ts +2 -0
- package/dist/shell/app-mobile-nav.test.d.ts.map +1 -0
- package/dist/shell/app-sidebar.d.ts +3 -28
- package/dist/shell/app-sidebar.d.ts.map +1 -1
- package/dist/shell/app-sidebar.js +40 -174
- package/dist/shell/app-sidebar.module.css +3 -2
- package/dist/shell/app-sidebar.module.css.d.ts +0 -2
- package/dist/shell/index.d.ts +22 -65
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +38 -76
- package/dist/shell/mobile-nav-model.d.ts +41 -0
- package/dist/shell/mobile-nav-model.d.ts.map +1 -0
- package/dist/shell/mobile-nav-model.js +244 -0
- package/dist/shell/mobile-nav-model.test.d.ts +2 -0
- package/dist/shell/mobile-nav-model.test.d.ts.map +1 -0
- package/dist/shell/nav-count-badge.d.ts +11 -0
- package/dist/shell/nav-count-badge.d.ts.map +1 -0
- package/dist/shell/nav-count-badge.js +29 -0
- package/dist/surfaces.d.ts +1 -1
- package/dist/surfaces.js +1 -1
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js +8 -7
- package/dist/theme/nav-link.module.css +4 -2
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +2 -0
- package/dist/tokens/palette.d.ts +15 -0
- package/dist/tokens/palette.d.ts.map +1 -1
- package/dist/tokens/palette.js +13 -0
- package/dist/tokens.css +2 -0
- package/llms.txt +3 -3
- package/package.json +1 -1
- package/src/index.ts +15 -5
- package/src/nav/types.ts +115 -0
- package/src/router-tanstack/index.ts +21 -2
- package/src/router-tanstack/nav.test.ts +111 -0
- package/src/router-tanstack/nav.ts +205 -0
- package/src/router-tanstack/search-param-store.ts +19 -9
- package/src/router-tanstack/use-nav.ts +146 -0
- package/src/shell/app-breadcrumbs.tsx +21 -19
- package/src/shell/app-mobile-nav.module.css +191 -11
- package/src/shell/app-mobile-nav.module.css.d.ts +11 -2
- package/src/shell/app-mobile-nav.test.tsx +403 -0
- package/src/shell/app-mobile-nav.tsx +449 -104
- package/src/shell/app-sidebar.module.css +3 -2
- package/src/shell/app-sidebar.module.css.d.ts +0 -2
- package/src/shell/app-sidebar.test.tsx +6 -10
- package/src/shell/app-sidebar.tsx +49 -206
- package/src/shell/index.test.tsx +62 -7
- package/src/shell/index.tsx +105 -164
- package/src/shell/mobile-nav-model.test.ts +296 -0
- package/src/shell/mobile-nav-model.ts +389 -0
- package/src/shell/nav-count-badge.tsx +39 -0
- package/src/surfaces.ts +1 -1
- package/src/theme/density-relations.test.ts +19 -1
- package/src/theme/index.ts +8 -7
- package/src/theme/nav-link.module.css +4 -2
- package/src/theme/spacing.test.ts +11 -0
- package/src/tokens/density.test.ts +2 -0
- package/src/tokens/index.ts +8 -5
- package/src/tokens/palette.ts +16 -0
package/dist/tokens/palette.d.ts
CHANGED
|
@@ -495,6 +495,17 @@ declare const SPACE_STEP_BASE: {
|
|
|
495
495
|
readonly appShellNavbarRailWidth: 48;
|
|
496
496
|
/** Tab's icon-to-label gap. */
|
|
497
497
|
readonly mobileNavTabGap: 3;
|
|
498
|
+
/** Bar height, EXCLUDING the safe-area inset — Mantine's own `AppShell.footer` rule adds
|
|
499
|
+
* `env(safe-area-inset-bottom)` to it. JS-consumed (AppShell `footer.height`), no `--vx-*` var,
|
|
500
|
+
* same shape as `appShellHeaderHeight`. Floored at 48px in `deriveSpacing`. */
|
|
501
|
+
readonly mobileNavBarHeight: 56;
|
|
502
|
+
/** Slot icon box. */
|
|
503
|
+
readonly mobileNavIconSize: 24;
|
|
504
|
+
/** Menu/sheet row height. Floored at 44px in `deriveSpacing` (Apple HIG 44pt / WCAG 2.5.5 AAA). */
|
|
505
|
+
readonly mobileNavRowHeight: 44;
|
|
506
|
+
/** More/group `<Menu width={…}>`. JS-consumed, no var — same rationale as
|
|
507
|
+
* `sidebarSettingsMenuWidth` directly above. */
|
|
508
|
+
readonly mobileNavMenuWidth: 232;
|
|
498
509
|
/** `ChartLegend`'s column/row gap (`VX.legendGap`). */
|
|
499
510
|
readonly chartLegendGap: 22;
|
|
500
511
|
/** Chart plot-area margin, top (`VX.margin.top`). */
|
|
@@ -870,6 +881,10 @@ export declare const SPACE_STEP: {
|
|
|
870
881
|
readonly appShellNavbarWidth: number;
|
|
871
882
|
readonly appShellNavbarRailWidth: number;
|
|
872
883
|
readonly mobileNavTabGap: number;
|
|
884
|
+
readonly mobileNavBarHeight: number;
|
|
885
|
+
readonly mobileNavIconSize: number;
|
|
886
|
+
readonly mobileNavRowHeight: number;
|
|
887
|
+
readonly mobileNavMenuWidth: number;
|
|
873
888
|
readonly chartLegendGap: number;
|
|
874
889
|
readonly chartMarginTop: number;
|
|
875
890
|
readonly chartMarginRight: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"palette.d.ts","sourceRoot":"","sources":["../../src/tokens/palette.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"palette.d.ts","sourceRoot":"","sources":["../../src/tokens/palette.ts"],"names":[],"mappings":"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"}
|
package/dist/tokens/palette.js
CHANGED
|
@@ -410,6 +410,17 @@ const SPACE_STEP_BASE = {
|
|
|
410
410
|
// ── shell/app-mobile-nav.module.css ───────────────────────────────────────────────────────────
|
|
411
411
|
/** Tab's icon-to-label gap. */
|
|
412
412
|
mobileNavTabGap: 3,
|
|
413
|
+
/** Bar height, EXCLUDING the safe-area inset — Mantine's own `AppShell.footer` rule adds
|
|
414
|
+
* `env(safe-area-inset-bottom)` to it. JS-consumed (AppShell `footer.height`), no `--vx-*` var,
|
|
415
|
+
* same shape as `appShellHeaderHeight`. Floored at 48px in `deriveSpacing`. */
|
|
416
|
+
mobileNavBarHeight: 56,
|
|
417
|
+
/** Slot icon box. */
|
|
418
|
+
mobileNavIconSize: 24,
|
|
419
|
+
/** Menu/sheet row height. Floored at 44px in `deriveSpacing` (Apple HIG 44pt / WCAG 2.5.5 AAA). */
|
|
420
|
+
mobileNavRowHeight: 44,
|
|
421
|
+
/** More/group `<Menu width={…}>`. JS-consumed, no var — same rationale as
|
|
422
|
+
* `sidebarSettingsMenuWidth` directly above. */
|
|
423
|
+
mobileNavMenuWidth: 232,
|
|
413
424
|
// ── charts (tokens/index.ts's `VX` object — structural, tracks density) ──────────────────────
|
|
414
425
|
// `VX.lineWidth`/`VX.line2Width` are DELIBERATELY absent here — they are stroke WEIGHTS (line
|
|
415
426
|
// thickness), not spacing/gaps/sizes, so they stay fixed regardless of density (see `VX` in
|
|
@@ -515,6 +526,8 @@ function deriveSpacing(level) {
|
|
|
515
526
|
anchors.stackXl = stackUnit * 6;
|
|
516
527
|
const mappedStep = mapSpaceGroup(SPACE_STEP_BASE, level, multiplier);
|
|
517
528
|
mappedStep.sidebarSearchTriggerHeight = Math.max(24, mappedStep.sidebarSearchTriggerHeight);
|
|
529
|
+
mappedStep.mobileNavBarHeight = Math.max(48, mappedStep.mobileNavBarHeight);
|
|
530
|
+
mappedStep.mobileNavRowHeight = Math.max(44, mappedStep.mobileNavRowHeight);
|
|
518
531
|
const step = {
|
|
519
532
|
...mappedStep,
|
|
520
533
|
// RESPONSIVE, one per AppShell breakpoint — each DERIVED from its own header, plus
|
package/dist/tokens.css
CHANGED
|
@@ -95,6 +95,8 @@
|
|
|
95
95
|
--vx-space-sidebar-child-row-indent: 12px;
|
|
96
96
|
--vx-space-app-header-mobile-actions-height: 52px;
|
|
97
97
|
--vx-space-mobile-nav-tab-gap: 3px;
|
|
98
|
+
--vx-space-mobile-nav-icon-size: 24px;
|
|
99
|
+
--vx-space-mobile-nav-row-height: 44px;
|
|
98
100
|
--vx-space-agent-rail-inset-x: 10px;
|
|
99
101
|
--vx-space-agent-part-gap-top: 6px;
|
|
100
102
|
--vx-space-agent-code-inset: 8px;
|
package/llms.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# basalt-ui v1.
|
|
1
|
+
# basalt-ui v1.19.1
|
|
2
2
|
# Opinionated framework for Mantine-based React apps — theme, app shell, visx charts, toolchain presets
|
|
3
3
|
#
|
|
4
4
|
# basalt-ui is an opinionated framework for Mantine-based React apps.
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
# ./tokens.css subpath. Guide: docs/FRAMEWORK-FREE.md in the repo.
|
|
20
20
|
|
|
21
21
|
Package: basalt-ui
|
|
22
|
-
Version: 1.
|
|
22
|
+
Version: 1.19.1
|
|
23
23
|
|
|
24
24
|
## basalt-ui
|
|
25
25
|
Import: import { ... } from 'basalt-ui'
|
|
@@ -65,7 +65,7 @@ OptionalPeers: @tanstack/react-query-devtools@^5.101.0
|
|
|
65
65
|
|
|
66
66
|
## basalt-ui/router-tanstack
|
|
67
67
|
Import: import { ... } from 'basalt-ui/router-tanstack'
|
|
68
|
-
Description: TanStack Router bridge: useBasaltNav (active route) + useRouterBreadcrumbs + createSearchParamStore (single-select URL-state store) + createMultiSearchParamStore (multi-select URL-state store)
|
|
68
|
+
Description: TanStack Router bridge: defineNav/navGroup/navTarget (one typed nav definition) + useNav (sections + mobileNav, spread onto BasaltShell) + useBasaltNav (active route) + useRouterBreadcrumbs + createSearchParamStore (single-select URL-state store) + createMultiSearchParamStore (multi-select URL-state store)
|
|
69
69
|
Layer: headless
|
|
70
70
|
Rule: basalt-router
|
|
71
71
|
OptionalPeers: @tanstack/react-router@^1.170.0
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -24,21 +24,31 @@ export {
|
|
|
24
24
|
type SidebarSearchConfig,
|
|
25
25
|
SidebarAccount,
|
|
26
26
|
MobileNav,
|
|
27
|
+
type MobileNavProps,
|
|
28
|
+
projectMobileNav,
|
|
29
|
+
type ProjectMobileNavOptions,
|
|
30
|
+
MOBILE_MAX_TABS_DEFAULT,
|
|
31
|
+
MOBILE_MENU_MAX_DEFAULT,
|
|
32
|
+
MOBILE_MORE_KEY,
|
|
27
33
|
AppBreadcrumbs,
|
|
28
|
-
type BreadcrumbLinkRenderer,
|
|
29
34
|
PageHeaderProvider,
|
|
30
35
|
PageActions,
|
|
31
36
|
PageActionsOutlet,
|
|
32
37
|
type SidebarSection,
|
|
33
38
|
type SidebarItem,
|
|
39
|
+
type NavAnchor,
|
|
40
|
+
type NavAnchorProps,
|
|
41
|
+
type NavMobilePlacement,
|
|
42
|
+
type NavSectionMobile,
|
|
43
|
+
type MobileNavConfig,
|
|
44
|
+
type MobileNavGroup,
|
|
45
|
+
type MobileNavModel,
|
|
46
|
+
type MobileNavSlot,
|
|
47
|
+
type MobileNavSurface,
|
|
34
48
|
type BasaltShellProps,
|
|
35
49
|
type BrandConfig,
|
|
36
50
|
type SettingsMenuItem,
|
|
37
51
|
type AppSidebarProps,
|
|
38
|
-
type MobileNavItem,
|
|
39
|
-
type MobileNavSection,
|
|
40
|
-
type NavLinkRenderer,
|
|
41
|
-
type MobileNavLinkRenderer,
|
|
42
52
|
type AccountBadgeTone,
|
|
43
53
|
type BasaltIdentity,
|
|
44
54
|
type BasaltRole,
|
package/src/nav/types.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
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
|
+
|
|
9
|
+
/** Where a destination appears on the mobile bar. `true` ≡ `'tab'`, `false` ≡ `'hidden'`. */
|
|
10
|
+
export type NavMobilePlacement = 'tab' | 'more' | 'hidden'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Props basalt hands a nav anchor. Deliberately a PLAIN FUNCTION TYPE, not `ComponentType`
|
|
14
|
+
* or `ElementType`: both are unions, and a union defeats Mantine's polymorphic `component`
|
|
15
|
+
* inference (`<NavLink component={…}>` then rejects `label`). VERIFIED both ways.
|
|
16
|
+
*/
|
|
17
|
+
export type NavAnchorProps = {
|
|
18
|
+
className?: string
|
|
19
|
+
children?: ReactNode
|
|
20
|
+
onClick?: (e: MouseEvent<HTMLElement>) => void
|
|
21
|
+
'aria-current'?: 'page' | undefined
|
|
22
|
+
'aria-label'?: string
|
|
23
|
+
}
|
|
24
|
+
export type NavAnchor = (props: NavAnchorProps) => ReactNode
|
|
25
|
+
|
|
26
|
+
/** A single nav destination. */
|
|
27
|
+
export type SidebarItem = {
|
|
28
|
+
key: string
|
|
29
|
+
label: string
|
|
30
|
+
/** Bar/menu label. Falls back to `label`. Keep ≤ 10 chars — a 5-slot bar is ~72px wide. */
|
|
31
|
+
short?: string
|
|
32
|
+
/** Mobile placement. `true` ≡ `'tab'`, `false` ≡ `'hidden'`. @default 'more' */
|
|
33
|
+
mobile?: boolean | NavMobilePlacement
|
|
34
|
+
icon: ReactNode
|
|
35
|
+
/**
|
|
36
|
+
* The router seam. basalt renders every pixel of chrome (desktop row, 56px slot, 44px sheet
|
|
37
|
+
* row) and only hosts this component. When absent the row falls back to `<a href>` + `onClick`.
|
|
38
|
+
*/
|
|
39
|
+
Anchor?: NavAnchor
|
|
40
|
+
/** Plain href — the no-router fallback, and the breadcrumb parent target. */
|
|
41
|
+
href?: string
|
|
42
|
+
active?: boolean
|
|
43
|
+
disabled?: boolean
|
|
44
|
+
onClick?: (e: MouseEvent) => void
|
|
45
|
+
/** Rich desktop badge (e.g. `NavCountBadge`). Desktop only. */
|
|
46
|
+
badge?: ReactNode
|
|
47
|
+
/** Unread count. Renders `NavCountBadge` on desktop when `badge` is absent, and an accent
|
|
48
|
+
* dot on the mobile slot icon. `0` renders nothing. */
|
|
49
|
+
count?: number
|
|
50
|
+
children?: SidebarItem[]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Per-section mobile configuration. `false` hides the whole section from mobile. */
|
|
54
|
+
export type NavSectionMobile = {
|
|
55
|
+
/** Give this SECTION a bar slot; its destinations open per §2.2 cardinality inference. */
|
|
56
|
+
tab?: true
|
|
57
|
+
/** Slot label override; falls back to `SidebarSection.label`. */
|
|
58
|
+
label?: string
|
|
59
|
+
/** Slot icon override; falls back to `SidebarSection.icon`. */
|
|
60
|
+
icon?: ReactNode
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type SidebarSection = {
|
|
64
|
+
label: string
|
|
65
|
+
items: SidebarItem[]
|
|
66
|
+
icon?: ReactNode
|
|
67
|
+
collapsible?: boolean
|
|
68
|
+
defaultCollapsed?: boolean
|
|
69
|
+
/** Mobile behaviour for this section. Omit for the default (items place themselves). */
|
|
70
|
+
mobile?: false | NavSectionMobile
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type MobileNavSurface = 'link' | 'menu' | 'sheet'
|
|
74
|
+
|
|
75
|
+
/** One row inside a `menu`/`sheet` slot, grouped under its source section. */
|
|
76
|
+
export type MobileNavGroup = { key: string; label?: string; items: SidebarItem[] }
|
|
77
|
+
|
|
78
|
+
export type MobileNavSlot =
|
|
79
|
+
| {
|
|
80
|
+
kind: 'link'
|
|
81
|
+
key: string
|
|
82
|
+
label: string
|
|
83
|
+
short: string
|
|
84
|
+
icon: ReactNode
|
|
85
|
+
active: boolean
|
|
86
|
+
item: SidebarItem
|
|
87
|
+
}
|
|
88
|
+
| {
|
|
89
|
+
kind: 'menu' | 'sheet'
|
|
90
|
+
key: string
|
|
91
|
+
label: string
|
|
92
|
+
short: string
|
|
93
|
+
icon: ReactNode
|
|
94
|
+
active: boolean
|
|
95
|
+
groups: MobileNavGroup[]
|
|
96
|
+
isMore: boolean
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Pure projection of `sections` onto the bar. Unit-testable with no DOM. */
|
|
100
|
+
export type MobileNavModel = { slots: MobileNavSlot[] }
|
|
101
|
+
|
|
102
|
+
export type MobileNavConfig = {
|
|
103
|
+
/** Explicit slot order by item/section key. Overrides every `item.mobile` placement. */
|
|
104
|
+
tabs?: readonly string[]
|
|
105
|
+
/** Hard cap on bar slots, INCLUDING More. @default 5 */
|
|
106
|
+
maxTabs?: number
|
|
107
|
+
/** Rows a slot may hold before it becomes a sheet instead of a menu. @default 6 */
|
|
108
|
+
menuMax?: number
|
|
109
|
+
/** @default 'More' */
|
|
110
|
+
moreLabel?: string
|
|
111
|
+
/** Extra content pinned to the bottom of the More surface. */
|
|
112
|
+
moreExtra?: ReactNode
|
|
113
|
+
/** Scroll container for active-slot re-tap. @default document.scrollingElement */
|
|
114
|
+
getScrollElement?: () => HTMLElement | null
|
|
115
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ./router-tanstack — headless TanStack Router bridge for the app shell.
|
|
3
3
|
* Optional peer: @tanstack/react-router.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
|
|
8
10
|
export {
|
|
@@ -15,6 +17,23 @@ export {
|
|
|
15
17
|
type MultiSearchParamStore,
|
|
16
18
|
type MultiSearchParamStoreOptions,
|
|
17
19
|
} from './multi-search-param-store'
|
|
20
|
+
export { defineNav, navGroup, navTarget, flattenNav } from './nav'
|
|
21
|
+
export type {
|
|
22
|
+
NavItemMeta,
|
|
23
|
+
NavGroupMeta,
|
|
24
|
+
AnyNavItem,
|
|
25
|
+
AnyNavGroup,
|
|
26
|
+
AnyNavLink,
|
|
27
|
+
FlatNavItem,
|
|
28
|
+
NavConfig,
|
|
29
|
+
NavItemId,
|
|
30
|
+
NavGroupId,
|
|
31
|
+
NavTabId,
|
|
32
|
+
NavIndex,
|
|
33
|
+
} from './nav'
|
|
34
|
+
export { useNav } from './use-nav'
|
|
35
|
+
export type { UseNavOptions, NavBinding } from './use-nav'
|
|
36
|
+
export type { NavAnchor, NavAnchorProps, NavMobilePlacement } from '../nav/types'
|
|
18
37
|
import type { ReactNode } from 'react'
|
|
19
38
|
import { useLocation, useMatches } from '@tanstack/react-router'
|
|
20
39
|
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { defineNav, flattenNav, navGroup, navTarget } from './nav'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The definition below is deliberately built with the real builders (not object literals) so the
|
|
6
|
+
* runtime-identity assertions below prove the builders are erased at runtime — every bit of value
|
|
7
|
+
* they add is compile-time. The type-level half of the contract lives in the playground's
|
|
8
|
+
* `nav.type-guard.ts` fixture, which is where a registered router exists to validate `to` against.
|
|
9
|
+
*/
|
|
10
|
+
const NAV = defineNav({
|
|
11
|
+
groups: [
|
|
12
|
+
navGroup({ id: 'main', label: 'Main' }, [
|
|
13
|
+
{ id: 'dash', label: 'Dashboard', short: 'Home', mobile: 'tab', link: { to: '/dashboard' } },
|
|
14
|
+
{
|
|
15
|
+
id: 'reports',
|
|
16
|
+
label: 'Reports',
|
|
17
|
+
link: { to: '/reports' },
|
|
18
|
+
children: [
|
|
19
|
+
{ id: 'reports-daily', label: 'Daily', link: { to: '/reports/daily' } },
|
|
20
|
+
{ id: 'reports-weekly', label: 'Weekly', link: { to: '/reports/weekly' } },
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
]),
|
|
24
|
+
navGroup({ id: 'system', label: 'System', collapsible: true }, [
|
|
25
|
+
{ id: 'usage', label: 'Usage', disabled: true, link: { to: '/usage', search: { r: '30d' } } },
|
|
26
|
+
]),
|
|
27
|
+
],
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
describe('defineNav / navGroup', () => {
|
|
31
|
+
test('defineNav is runtime identity — it returns the very object it was given', () => {
|
|
32
|
+
const config = { groups: [] } as const
|
|
33
|
+
expect(defineNav(config)).toBe(config)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('navGroup merges meta and items into one plain object, preserving item order', () => {
|
|
37
|
+
const group = navGroup({ id: 'g', label: 'G', collapsible: true }, [
|
|
38
|
+
{ id: 'a', label: 'A', link: { to: '/a' } },
|
|
39
|
+
{ id: 'b', label: 'B', link: { to: '/b' } },
|
|
40
|
+
])
|
|
41
|
+
expect(group).toEqual({
|
|
42
|
+
id: 'g',
|
|
43
|
+
label: 'G',
|
|
44
|
+
collapsible: true,
|
|
45
|
+
items: [
|
|
46
|
+
{ id: 'a', label: 'A', link: { to: '/a' } },
|
|
47
|
+
{ id: 'b', label: 'B', link: { to: '/b' } },
|
|
48
|
+
],
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('navGroup does not mutate the meta object it was handed', () => {
|
|
53
|
+
const meta = { id: 'g', label: 'G' }
|
|
54
|
+
navGroup(meta, [{ id: 'a', label: 'A', link: { to: '/a' } }])
|
|
55
|
+
expect(meta).toEqual({ id: 'g', label: 'G' })
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
describe('flattenNav', () => {
|
|
60
|
+
test('walks depth-first — parent immediately before its own children', () => {
|
|
61
|
+
expect(flattenNav(NAV).map((d) => d.id)).toEqual([
|
|
62
|
+
'dash',
|
|
63
|
+
'reports',
|
|
64
|
+
'reports-daily',
|
|
65
|
+
'reports-weekly',
|
|
66
|
+
'usage',
|
|
67
|
+
])
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('tags every destination — including nested children — with its declaring group', () => {
|
|
71
|
+
const byId = new Map(flattenNav(NAV).map((d) => [d.id, d]))
|
|
72
|
+
expect(byId.get('dash')).toMatchObject({ groupId: 'main', groupLabel: 'Main' })
|
|
73
|
+
expect(byId.get('reports-weekly')).toMatchObject({ groupId: 'main', groupLabel: 'Main' })
|
|
74
|
+
expect(byId.get('usage')).toMatchObject({ groupId: 'system', groupLabel: 'System' })
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('carries the authored metadata through untouched', () => {
|
|
78
|
+
const dash = flattenNav(NAV)[0]
|
|
79
|
+
expect(dash).toMatchObject({
|
|
80
|
+
label: 'Dashboard',
|
|
81
|
+
short: 'Home',
|
|
82
|
+
mobile: 'tab',
|
|
83
|
+
link: { to: '/dashboard' },
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('an empty definition flattens to an empty list', () => {
|
|
88
|
+
expect(flattenNav({ groups: [] })).toEqual([])
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
describe('navTarget', () => {
|
|
93
|
+
test('returns the destination link options by id', () => {
|
|
94
|
+
expect(navTarget(NAV, 'dash')).toEqual({ to: '/dashboard' })
|
|
95
|
+
expect(navTarget(NAV, 'usage')).toEqual({ to: '/usage', search: { r: '30d' } })
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('returns the SAME object the definition holds — spreadable with no copy', () => {
|
|
99
|
+
expect(navTarget(NAV, 'dash')).toBe(NAV.groups[0].items[0].link)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test('resolves ids nested under a parent destination', () => {
|
|
103
|
+
expect(navTarget(NAV, 'reports-weekly')).toEqual({ to: '/reports/weekly' })
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('throws a named error on an unknown id', () => {
|
|
107
|
+
// The id union makes this a compile error for a consumer; the throw is the runtime backstop
|
|
108
|
+
// for the untyped edge (a dynamic id, or an unregistered router degrading `NavItemId` away).
|
|
109
|
+
expect(() => navTarget(NAV, 'zzz' as 'dash')).toThrow('navTarget: no nav item with id "zzz"')
|
|
110
|
+
})
|
|
111
|
+
})
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed navigation definition. Wraps TanStack's own `linkOptions` rather than reimplementing its
|
|
3
|
+
* validator, so `to` / `search` / `params` get the router's full checking, and nav metadata rides
|
|
4
|
+
* OUTSIDE the validated object in a sibling `link:` key.
|
|
5
|
+
*
|
|
6
|
+
* Why not flat (`{ id, label, to, search }`) via a carrier `TComp` over `ValidateLinkOptions`:
|
|
7
|
+
* `Constrain<T, C> = (T extends C ? T : never) | C` is an ASSIGNABILITY check, and assignability
|
|
8
|
+
* does not do excess-property checking — so `{ id, label, to: '/charts', colour: 'red' }` compiles
|
|
9
|
+
* silently. Compile-verified. A config whose selling point is "typos are compile errors" cannot
|
|
10
|
+
* have a hole exactly where metadata typos live.
|
|
11
|
+
*
|
|
12
|
+
* No JSX and no Mantine in this module — `./router-tanstack` is a headless barrel by contract.
|
|
13
|
+
*/
|
|
14
|
+
import type { ReactNode } from 'react'
|
|
15
|
+
import type { NavMobilePlacement, NavSectionMobile } from '../nav/types'
|
|
16
|
+
|
|
17
|
+
// ── authoring shapes ─────────────────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
export type NavItemMeta = {
|
|
20
|
+
id: string
|
|
21
|
+
label: string
|
|
22
|
+
/** Bar/menu label. Falls back to `label`. */
|
|
23
|
+
short?: string
|
|
24
|
+
icon?: ReactNode
|
|
25
|
+
disabled?: boolean
|
|
26
|
+
/** Mobile placement. `true` ≡ `'tab'`, `false` ≡ `'hidden'`. @default 'more' */
|
|
27
|
+
mobile?: boolean | NavMobilePlacement
|
|
28
|
+
/** Active on an exact path match only. Default: prefix (`fuzzy: true`). `'/'` is always exact. */
|
|
29
|
+
exact?: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type NavGroupMeta = {
|
|
33
|
+
id: string
|
|
34
|
+
label: string
|
|
35
|
+
icon?: ReactNode
|
|
36
|
+
collapsible?: boolean
|
|
37
|
+
defaultCollapsed?: boolean
|
|
38
|
+
mobile?: false | NavSectionMobile
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Widened read shapes. The framework reads through these; a consumer never writes them. */
|
|
42
|
+
export type AnyNavLink = { to: string; search?: unknown; params?: unknown }
|
|
43
|
+
export type AnyNavItem = NavItemMeta & { link: AnyNavLink; children?: ReadonlyArray<AnyNavItem> }
|
|
44
|
+
export type AnyNavGroup = NavGroupMeta & { items: ReadonlyArray<AnyNavItem> }
|
|
45
|
+
|
|
46
|
+
// ── inference machinery ──────────────────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
/** Exact-object check: any key outside `Shape` is typed `never`, which surfaces as an error. */
|
|
49
|
+
export type Exact<T, Shape> = T & { readonly [K in Exclude<keyof T, keyof Shape>]: never }
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* `Exact` for ONE item, recursing into `children`.
|
|
53
|
+
*
|
|
54
|
+
* The recursion is the whole point: `AnyNavItem['children']` is a plain
|
|
55
|
+
* `ReadonlyArray<AnyNavItem>` constraint, and a constraint is an assignability check — so a
|
|
56
|
+
* top-level-only `Exact` leaves the metadata-typo hole wide open exactly one level down
|
|
57
|
+
* (`children: [{ id, label, colour: 'red', link }]` compiled silently). Reapplying `Exact` per
|
|
58
|
+
* child, at every depth, is what makes the nested `link:` design actually deliver "every typo is a
|
|
59
|
+
* compile error" rather than "every typo in the top row is a compile error".
|
|
60
|
+
*
|
|
61
|
+
* The `[K in Extract<keyof T, 'children'>]` form rather than a `children?:` property is what keeps
|
|
62
|
+
* this inference-safe: it evaluates to `{}` for an item that declares no children (so no phantom
|
|
63
|
+
* key is added to the checked shape and nothing becomes required), and the intersection with
|
|
64
|
+
* `Exact<T, AnyNavItem>` keeps `T` in a naked inference position so the caller's literal types
|
|
65
|
+
* still flow out of `navGroup`'s `const T`.
|
|
66
|
+
*/
|
|
67
|
+
export type ExactNavItem<T> = Exact<T, AnyNavItem> & {
|
|
68
|
+
readonly [K in Extract<keyof T, 'children'>]: T[K] extends ReadonlyArray<AnyNavItem>
|
|
69
|
+
? NavItemsInput<T[K]>
|
|
70
|
+
: T[K]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Per-element `Exact` over a tuple of items, so an unknown key errors at the item that owns it. */
|
|
74
|
+
export type NavItemsInput<T extends ReadonlyArray<AnyNavItem>> = {
|
|
75
|
+
readonly [K in keyof T]: ExactNavItem<T[K]>
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
type ItemsOf<G> = G extends ReadonlyArray<{ items: ReadonlyArray<infer I> }> ? I : never
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* One item plus every descendant, at arbitrary depth. Recursive so the TYPE side matches what
|
|
82
|
+
* `findById` / `flattenNav` already do at RUNTIME — a one-level walk made `navTarget(nav,
|
|
83
|
+
* 'grandchild')` a compile error against an id the runtime resolves fine.
|
|
84
|
+
*/
|
|
85
|
+
type WithDescendants<I> =
|
|
86
|
+
| I
|
|
87
|
+
| (I extends { children: ReadonlyArray<infer C> } ? WithDescendants<C> : never)
|
|
88
|
+
|
|
89
|
+
type AllItemsOf<G> = WithDescendants<ItemsOf<G>>
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* id → link options, as a mapped type with key remapping. Do NOT replace this with
|
|
93
|
+
* `Extract<AllItemsOf<G>, { id: K }>['link']` — that form was compile-tested and returns the
|
|
94
|
+
* WHOLE union, silently losing per-id precision.
|
|
95
|
+
*/
|
|
96
|
+
export type NavIndex<G> = {
|
|
97
|
+
[I in AllItemsOf<G> as I extends { id: infer K extends string } ? K : never]: I extends {
|
|
98
|
+
link: infer L
|
|
99
|
+
}
|
|
100
|
+
? L
|
|
101
|
+
: never
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The literal union of every addressable destination id in a definition — parents, children and
|
|
106
|
+
* grandchildren alike, at ARBITRARY depth, matching what `findById` / `flattenNav` resolve at
|
|
107
|
+
* runtime. Verified to seven levels of `children` with no instantiation-depth complaint.
|
|
108
|
+
*
|
|
109
|
+
* If this ever resolves to plain `string`, the entire nav API validates nothing while reporting
|
|
110
|
+
* zero errors — so any change to the generics above must be re-checked against
|
|
111
|
+
* `apps/playground/src/nav.type-guard.tsx`, whose `@ts-expect-error` rows go UNUSED (TS2578) the
|
|
112
|
+
* moment the union widens.
|
|
113
|
+
*/
|
|
114
|
+
export type NavItemId<G> = Extract<keyof NavIndex<G>, string>
|
|
115
|
+
export type NavGroupId<G> = G extends ReadonlyArray<{ id: infer I }> ? I : never
|
|
116
|
+
export type NavTabId<G> = NavItemId<G> | NavGroupId<G>
|
|
117
|
+
|
|
118
|
+
export type NavConfig<
|
|
119
|
+
G extends ReadonlyArray<AnyNavGroup>,
|
|
120
|
+
T extends ReadonlyArray<NavTabId<G>>,
|
|
121
|
+
> = {
|
|
122
|
+
groups: G
|
|
123
|
+
mobile?: {
|
|
124
|
+
/** Explicit bar order by item/group id. Omit to let each item's `mobile` field decide. */
|
|
125
|
+
tabs?: T
|
|
126
|
+
/** @default 5 */ maxTabs?: number
|
|
127
|
+
/** @default 6 */ menuMax?: number
|
|
128
|
+
/** @default 'More' */ moreLabel?: string
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── builders (identity at runtime) ───────────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* One nav group. `const M` on the meta is load-bearing: without it the group `id` widens to
|
|
136
|
+
* `string` from `NavGroupMeta` and `mobile.tabs` silently stops validating group ids.
|
|
137
|
+
*/
|
|
138
|
+
export function navGroup<const M extends NavGroupMeta, const T extends ReadonlyArray<AnyNavItem>>(
|
|
139
|
+
meta: Exact<M, NavGroupMeta>,
|
|
140
|
+
items: NavItemsInput<T>,
|
|
141
|
+
): M & { items: T } {
|
|
142
|
+
return { ...(meta as M), items: items as unknown as T }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The whole navigation — desktop and mobile — in one inferred definition.
|
|
147
|
+
*
|
|
148
|
+
* There is deliberately NO compile-time cap on `mobile.tabs.length`. Both a
|
|
149
|
+
* `T extends … & { length: 0|1|2|3|4|5 }` constraint and a `tabs: T & MaxFive` property form were
|
|
150
|
+
* compile-tested: each makes the length check work but silently degrades `NavTabId<G>` to `string`,
|
|
151
|
+
* killing the far more valuable id-union validation. The cap is a runtime concern — the shell's
|
|
152
|
+
* `projectMobileNav` slices to `maxTabs` and DEV-warns about what it dropped.
|
|
153
|
+
*/
|
|
154
|
+
export function defineNav<
|
|
155
|
+
const G extends ReadonlyArray<AnyNavGroup>,
|
|
156
|
+
const T extends ReadonlyArray<NavTabId<G>>,
|
|
157
|
+
>(config: NavConfig<G, T>): NavConfig<G, T> {
|
|
158
|
+
return config
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ── readers ──────────────────────────────────────────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
function findById(items: ReadonlyArray<AnyNavItem>, id: string): AnyNavItem | undefined {
|
|
164
|
+
for (const item of items) {
|
|
165
|
+
if (item.id === id) return item
|
|
166
|
+
const hit = item.children ? findById(item.children, id) : undefined
|
|
167
|
+
if (hit) return hit
|
|
168
|
+
}
|
|
169
|
+
return undefined
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* The link options for one destination, by id. Spreadable into `<Link>`, `router.navigate()`
|
|
174
|
+
* and `redirect()`. This is what collapses a consumer's Spotlight commands and index redirect.
|
|
175
|
+
*
|
|
176
|
+
* `id` is the definition's literal id union (`NavItemId`) at any nesting depth, so a nested child
|
|
177
|
+
* or grandchild is addressable exactly like a top-level item — the type side and `findById`'s
|
|
178
|
+
* runtime walk agree.
|
|
179
|
+
*/
|
|
180
|
+
export function navTarget<
|
|
181
|
+
const C extends { groups: ReadonlyArray<AnyNavGroup> },
|
|
182
|
+
K extends NavItemId<C['groups']>,
|
|
183
|
+
>(config: C, id: K): NavIndex<C['groups']>[K] {
|
|
184
|
+
for (const g of config.groups) {
|
|
185
|
+
const hit = findById(g.items, id)
|
|
186
|
+
if (hit) return hit.link as NavIndex<C['groups']>[K]
|
|
187
|
+
}
|
|
188
|
+
throw new Error(`navTarget: no nav item with id "${id}"`)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** A flattened destination, tagged with the group it was declared in. */
|
|
192
|
+
export type FlatNavItem = AnyNavItem & { groupId: string; groupLabel: string }
|
|
193
|
+
|
|
194
|
+
/** Depth-first flatten, parent then children — the leaf every other surface reads. */
|
|
195
|
+
export function flattenNav(config: { groups: ReadonlyArray<AnyNavGroup> }): FlatNavItem[] {
|
|
196
|
+
const out: FlatNavItem[] = []
|
|
197
|
+
const walk = (items: ReadonlyArray<AnyNavItem>, groupId: string, groupLabel: string): void => {
|
|
198
|
+
for (const item of items) {
|
|
199
|
+
out.push({ ...item, groupId, groupLabel })
|
|
200
|
+
if (item.children) walk(item.children, groupId, groupLabel)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
for (const g of config.groups) walk(g.items, g.id, g.label)
|
|
204
|
+
return out
|
|
205
|
+
}
|