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.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +14 -0
- package/agent/rules/basalt-charts.md +12 -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/agent/skills/basalt-design/SKILL.md +2 -2
- package/dist/charts/kinds/DualPanel.d.ts +7 -0
- package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
- package/dist/charts/kinds/DualPanel.js +10 -3
- package/dist/charts/primitives/CartesianChart.d.ts +17 -0
- package/dist/charts/primitives/CartesianChart.d.ts.map +1 -1
- package/dist/charts/primitives/CartesianChart.js +5 -2
- package/dist/charts/primitives/ChartTooltip.d.ts +6 -1
- package/dist/charts/primitives/ChartTooltip.d.ts.map +1 -1
- package/dist/charts/primitives/ChartTooltip.js +2 -1
- package/dist/charts/series.d.ts +5 -0
- package/dist/charts/series.d.ts.map +1 -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 +290 -68
- package/dist/shell/app-mobile-nav.module.css +166 -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/charts/kinds/DualPanel.test.tsx +81 -1
- package/src/charts/kinds/DualPanel.tsx +26 -4
- package/src/charts/primitives/CartesianChart.test.tsx +173 -1
- package/src/charts/primitives/CartesianChart.tsx +29 -5
- package/src/charts/primitives/ChartTooltip.test.tsx +22 -1
- package/src/charts/primitives/ChartTooltip.tsx +7 -1
- package/src/charts/series.ts +5 -0
- 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 +166 -11
- package/src/shell/app-mobile-nav.module.css.d.ts +11 -2
- package/src/shell/app-mobile-nav.test.tsx +369 -0
- package/src/shell/app-mobile-nav.tsx +436 -102
- 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/src/shell/index.tsx
CHANGED
|
@@ -1,32 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* BasaltShell — the application shell (collapsible sidebar rail + slim top bar + mobile bottom
|
|
3
|
-
*
|
|
4
|
-
* `apps/dashboard/src/
|
|
5
|
-
*
|
|
3
|
+
* bar). Grounded in argo's app-shell; the layout mirrors the `AppShell` composition in
|
|
4
|
+
* `apps/dashboard/src/routes/__root.tsx`.
|
|
5
|
+
*
|
|
6
|
+
* ONE nav definition drives both halves. `sections` renders the desktop sidebar, and
|
|
7
|
+
* `projectMobileNav` projects the SAME sections onto the mobile bar — so a destination is declared
|
|
8
|
+
* once and `SidebarItem.mobile` decides where it lands. There is no mobile sidebar drawer: below
|
|
9
|
+
* the `sm` breakpoint the navbar is permanently collapsed and the bottom bar IS the nav, which is
|
|
10
|
+
* why the More surface must also carry the account and settings rows the sidebar footer holds on
|
|
11
|
+
* desktop.
|
|
6
12
|
*
|
|
7
13
|
* Router-agnostic: argo's router coupling (typed `navigate`, `useMatchRoute` active detection, the
|
|
8
14
|
* sidebar-collapse zustand store) stays consumer-side. The consumer resolves `item.active`,
|
|
9
|
-
* `item.onClick`, `item.badge` and
|
|
10
|
-
*
|
|
11
|
-
* router hook. Collapse is persisted via
|
|
15
|
+
* `item.onClick`, `item.badge`/`item.count` and supplies `item.Anchor` — its router `<Link>`,
|
|
16
|
+
* which basalt HOSTS rather than delegating rendering to. The breadcrumb is derived from the
|
|
17
|
+
* active item across `sections`, not from a router hook. Collapse is persisted via
|
|
18
|
+
* `@mantine/hooks` `useLocalStorage` keyed by `storageKey`.
|
|
12
19
|
*/
|
|
13
|
-
import { AppShell
|
|
14
|
-
import {
|
|
20
|
+
import { AppShell } from '@mantine/core'
|
|
21
|
+
import { useLocalStorage } from '@mantine/hooks'
|
|
22
|
+
import { useMemo } from 'react'
|
|
15
23
|
import type { MouseEvent, ReactNode } from 'react'
|
|
16
24
|
import { AppSidebar } from './app-sidebar'
|
|
17
|
-
import
|
|
18
|
-
import {
|
|
19
|
-
import type { MobileNavItem, MobileNavSection } from './app-mobile-nav'
|
|
25
|
+
import { MobileNav, accountRowCount } from './app-mobile-nav'
|
|
26
|
+
import { projectMobileNav } from './mobile-nav-model'
|
|
20
27
|
import { AppBreadcrumbs } from './app-breadcrumbs'
|
|
21
|
-
import type { BreadcrumbLinkRenderer } from './app-breadcrumbs'
|
|
22
28
|
import { PageActionsOutlet, PageHeaderProvider } from './page-header'
|
|
23
29
|
import type { BasaltAccountProps } from './account-types'
|
|
24
30
|
import type { SidebarSearchConfig } from './sidebar-search'
|
|
25
|
-
import {
|
|
31
|
+
import type { MobileNavConfig, NavAnchor, SidebarItem, SidebarSection } from '../nav/types'
|
|
26
32
|
import { useBasaltSpacing } from '../theme'
|
|
27
33
|
import headerClasses from './app-header.module.css'
|
|
34
|
+
import mobileNavClasses from './app-mobile-nav.module.css'
|
|
28
35
|
|
|
29
|
-
export { AppSidebar, type AppSidebarProps
|
|
36
|
+
export { AppSidebar, type AppSidebarProps } from './app-sidebar'
|
|
37
|
+
export { NavCountBadge } from './nav-count-badge'
|
|
30
38
|
export { SidebarSearch, type SidebarSearchConfig } from './sidebar-search'
|
|
31
39
|
export { SidebarAccount } from './app-sidebar-account'
|
|
32
40
|
export type {
|
|
@@ -39,46 +47,35 @@ export type {
|
|
|
39
47
|
BasaltAccountActions,
|
|
40
48
|
BasaltAccountProps,
|
|
41
49
|
} from './account-types'
|
|
50
|
+
export { MobileNav, type MobileNavProps } from './app-mobile-nav'
|
|
42
51
|
export {
|
|
43
|
-
|
|
44
|
-
type
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
52
|
+
projectMobileNav,
|
|
53
|
+
type ProjectMobileNavOptions,
|
|
54
|
+
MOBILE_MAX_TABS_DEFAULT,
|
|
55
|
+
MOBILE_MENU_MAX_DEFAULT,
|
|
56
|
+
MOBILE_MORE_KEY,
|
|
57
|
+
} from './mobile-nav-model'
|
|
58
|
+
export { AppBreadcrumbs } from './app-breadcrumbs'
|
|
49
59
|
export { PageHeaderProvider, PageActions, PageActionsOutlet } from './page-header'
|
|
50
60
|
|
|
51
|
-
/**
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
/** A labelled group of sidebar items. Grounded verbatim in argo's `SidebarSection`. */
|
|
70
|
-
export type SidebarSection = {
|
|
71
|
-
label: string
|
|
72
|
-
items: SidebarItem[]
|
|
73
|
-
/** Group icon, used by the mobile tabs. */
|
|
74
|
-
icon?: ReactNode
|
|
75
|
-
/** Desktop: render a clickable group header that collapses its items. */
|
|
76
|
-
collapsible?: boolean
|
|
77
|
-
/** Initial collapsed state when `collapsible`. */
|
|
78
|
-
defaultCollapsed?: boolean
|
|
79
|
-
/** `false` excludes the section from the primary mobile group tabs. */
|
|
80
|
-
mobileTab?: boolean
|
|
81
|
-
}
|
|
61
|
+
/**
|
|
62
|
+
* The shared nav vocabulary lives in `src/nav/types.ts` so the headless router bridge can `import
|
|
63
|
+
* type` it without reaching into the Mantine layer. Re-exported here so `SidebarItem` /
|
|
64
|
+
* `SidebarSection` keep resolving straight from `basalt-ui`, exactly as before.
|
|
65
|
+
*/
|
|
66
|
+
export type {
|
|
67
|
+
NavAnchor,
|
|
68
|
+
NavAnchorProps,
|
|
69
|
+
NavMobilePlacement,
|
|
70
|
+
NavSectionMobile,
|
|
71
|
+
SidebarItem,
|
|
72
|
+
SidebarSection,
|
|
73
|
+
MobileNavConfig,
|
|
74
|
+
MobileNavGroup,
|
|
75
|
+
MobileNavModel,
|
|
76
|
+
MobileNavSlot,
|
|
77
|
+
MobileNavSurface,
|
|
78
|
+
} from '../nav/types'
|
|
82
79
|
|
|
83
80
|
/** Brand identity shown in the sidebar header (logo + name). */
|
|
84
81
|
export type BrandConfig = {
|
|
@@ -103,25 +100,21 @@ export type SettingsMenuItem = {
|
|
|
103
100
|
export type BasaltShellProps = {
|
|
104
101
|
/** Brand identity for the sidebar header. */
|
|
105
102
|
brand: BrandConfig
|
|
106
|
-
/** Grouped nav sections rendered
|
|
103
|
+
/** Grouped nav sections — the ONE definition, rendered as the sidebar and projected to the bar. */
|
|
107
104
|
sections: SidebarSection[]
|
|
108
105
|
/**
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
* seam — no router primitive is imported by the shell.
|
|
106
|
+
* Mobile bar tuning. Every field optional — the defaults ARE the design, and a nav with nothing
|
|
107
|
+
* configured still produces a working bar (see `projectMobileNav`'s zero-config fallback).
|
|
112
108
|
*/
|
|
113
|
-
|
|
109
|
+
mobileNav?: MobileNavConfig
|
|
114
110
|
/** Persistent, shell-owned top-bar slot (timer, refresh, notifications). */
|
|
115
111
|
globalActions?: ReactNode
|
|
116
|
-
/** Extra content appended to the sidebar footer, beside the settings menu (mobile close, etc.). */
|
|
117
|
-
sidebarFooterExtra?: ReactNode
|
|
118
112
|
/**
|
|
119
|
-
* Arbitrary content appended after `sections` inside the sidebar's nav scroll
|
|
120
|
-
* filter panel, a project list, …)
|
|
121
|
-
*
|
|
122
|
-
* `
|
|
123
|
-
*
|
|
124
|
-
* reaches both.
|
|
113
|
+
* DESKTOP ONLY. Arbitrary content appended after `sections` inside the sidebar's nav scroll
|
|
114
|
+
* region (a tree, a filter panel, a project list, …), for anything a set of `SidebarItem`s can't
|
|
115
|
+
* express. Hidden on the collapsed desktop rail; the sidebar itself no longer exists below the
|
|
116
|
+
* `sm` breakpoint, so this slot has no mobile representation — put mobile-reachable extras in
|
|
117
|
+
* `mobileNav.moreExtra` instead.
|
|
125
118
|
*/
|
|
126
119
|
sidebarNavExtra?: ReactNode
|
|
127
120
|
/** Entries appended to the sidebar settings menu. */
|
|
@@ -151,59 +144,19 @@ export type BasaltShellProps = {
|
|
|
151
144
|
* a no-op when omitted.
|
|
152
145
|
*/
|
|
153
146
|
onCollapsedChange?: (collapsed: boolean) => void
|
|
154
|
-
/**
|
|
155
|
-
* Optional router link renderer for the breadcrumb parent segment. When provided, the parent
|
|
156
|
-
* breadcrumb label is rendered through this callback (e.g. a TanStack `<Link>`) instead of a
|
|
157
|
-
* plain `<a href>`, enabling client-side navigation.
|
|
158
|
-
*/
|
|
159
|
-
renderBreadcrumbLink?: BreadcrumbLinkRenderer | undefined
|
|
160
147
|
/** Page content. */
|
|
161
148
|
children?: ReactNode
|
|
162
149
|
}
|
|
163
150
|
|
|
164
|
-
/**
|
|
165
|
-
|
|
166
|
-
* padding 0 5px, muted text; `marginLeft: auto` pins it to the row end on any render path.
|
|
167
|
-
* `styles` (inline) rather than a token color prop, since none of Mantine's variant/color
|
|
168
|
-
* combinations land on the ink-tint idiom. Returns `null` for a zero/empty count so the badge slot
|
|
169
|
-
* stays clean ("ink earns its color", DESIGN.md).
|
|
170
|
-
*/
|
|
171
|
-
export function NavCountBadge({ count }: { count: number }) {
|
|
172
|
-
if (!count) return null
|
|
173
|
-
return (
|
|
174
|
-
<Badge
|
|
175
|
-
size="sm"
|
|
176
|
-
styles={{
|
|
177
|
-
root: {
|
|
178
|
-
backgroundColor: 'color-mix(in srgb, var(--vx-ink) 8%, transparent)',
|
|
179
|
-
color: 'var(--vx-muted)',
|
|
180
|
-
fontFamily: 'var(--basalt-font-mono)',
|
|
181
|
-
fontSize: VX.text.micro,
|
|
182
|
-
fontWeight: 500,
|
|
183
|
-
height: 16,
|
|
184
|
-
padding: '0 5px',
|
|
185
|
-
marginLeft: 'auto',
|
|
186
|
-
borderRadius: 'var(--vx-radius-tight)',
|
|
187
|
-
},
|
|
188
|
-
}}
|
|
189
|
-
>
|
|
190
|
-
{count}
|
|
191
|
-
</Badge>
|
|
192
|
-
)
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/** Active nav item across all sections → `{ section, parent?, parentHref?, page }` for the breadcrumb. */
|
|
196
|
-
function findActiveCrumb(
|
|
197
|
-
sections: SidebarSection[],
|
|
198
|
-
):
|
|
199
|
-
| { section: string; parent?: string | undefined; parentHref?: string | undefined; page: string }
|
|
200
|
-
| undefined {
|
|
151
|
+
/** Active nav item across all sections → the breadcrumb's `{ section, parent?, …, page }`. */
|
|
152
|
+
function findActiveCrumb(sections: SidebarSection[]): ActiveCrumb | undefined {
|
|
201
153
|
for (const section of sections) {
|
|
202
154
|
const found = findActiveWithParent(section.items)
|
|
203
155
|
if (found)
|
|
204
156
|
return {
|
|
205
157
|
section: section.label,
|
|
206
158
|
parent: found.parent,
|
|
159
|
+
parentAnchor: found.parentAnchor,
|
|
207
160
|
parentHref: found.parentHref,
|
|
208
161
|
page: found.page,
|
|
209
162
|
}
|
|
@@ -211,72 +164,46 @@ function findActiveCrumb(
|
|
|
211
164
|
return undefined
|
|
212
165
|
}
|
|
213
166
|
|
|
214
|
-
|
|
167
|
+
type ActiveCrumb = {
|
|
168
|
+
section: string
|
|
169
|
+
parent?: string | undefined
|
|
170
|
+
parentAnchor?: NavAnchor | undefined
|
|
171
|
+
parentHref?: string | undefined
|
|
172
|
+
page: string
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Recursively search for the deepest active item, returning the parent's label and its ROUTER
|
|
177
|
+
* ANCHOR (not just an href) when nested — the crumb navigates client-side through the same seam
|
|
178
|
+
* every nav row uses, so no second render callback is needed for it.
|
|
179
|
+
*/
|
|
215
180
|
function findActiveWithParent(
|
|
216
181
|
items: SidebarItem[],
|
|
217
182
|
parentLabel?: string | undefined,
|
|
183
|
+
parentAnchor?: NavAnchor | undefined,
|
|
218
184
|
parentHref?: string | undefined,
|
|
219
|
-
):
|
|
185
|
+
): Omit<ActiveCrumb, 'section'> | undefined {
|
|
220
186
|
for (const item of items) {
|
|
221
187
|
// Recurse into children first — deeper active match wins over a prefix-matched parent.
|
|
222
188
|
if (item.children) {
|
|
223
|
-
const found = findActiveWithParent(item.children, item.label, item.href)
|
|
189
|
+
const found = findActiveWithParent(item.children, item.label, item.Anchor, item.href)
|
|
224
190
|
if (found) return found
|
|
225
191
|
}
|
|
226
|
-
if (item.active) return { parent: parentLabel, parentHref, page: item.label }
|
|
192
|
+
if (item.active) return { parent: parentLabel, parentAnchor, parentHref, page: item.label }
|
|
227
193
|
}
|
|
228
194
|
return undefined
|
|
229
195
|
}
|
|
230
196
|
|
|
231
|
-
/**
|
|
232
|
-
* Mobile bottom-nav projection: one tab per (non-`mobileTab:false`) section, each raising a bottom
|
|
233
|
-
* sheet of that section's destinations. Children are flattened into the section's item list so
|
|
234
|
-
* subpages appear in the mobile drawer.
|
|
235
|
-
*/
|
|
236
|
-
function toMobileSections(sections: SidebarSection[], closeMobile: () => void): MobileNavSection[] {
|
|
237
|
-
return sections
|
|
238
|
-
.filter((s) => s.mobileTab !== false)
|
|
239
|
-
.map((s) => ({
|
|
240
|
-
key: s.label,
|
|
241
|
-
label: s.label,
|
|
242
|
-
icon: s.icon,
|
|
243
|
-
active: s.items.some((i) => i.active),
|
|
244
|
-
items: flattenSidebarItems(s.items).map(
|
|
245
|
-
(i): MobileNavItem => ({
|
|
246
|
-
key: i.key,
|
|
247
|
-
label: i.label,
|
|
248
|
-
icon: i.icon,
|
|
249
|
-
...(i.href !== undefined && { href: i.href }),
|
|
250
|
-
...(i.active !== undefined && { active: i.active }),
|
|
251
|
-
onClick: (e: MouseEvent) => {
|
|
252
|
-
i.onClick?.(e)
|
|
253
|
-
closeMobile()
|
|
254
|
-
},
|
|
255
|
-
}),
|
|
256
|
-
),
|
|
257
|
-
}))
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/** Flatten a tree of SidebarItems (including children) into a single-level array. */
|
|
261
|
-
function flattenSidebarItems(items: SidebarItem[]): SidebarItem[] {
|
|
262
|
-
return items.flatMap((item) => [
|
|
263
|
-
item,
|
|
264
|
-
...(item.children ? flattenSidebarItems(item.children) : []),
|
|
265
|
-
])
|
|
266
|
-
}
|
|
267
|
-
|
|
268
197
|
export function BasaltShell({
|
|
269
198
|
brand,
|
|
270
199
|
sections,
|
|
271
|
-
|
|
200
|
+
mobileNav,
|
|
272
201
|
globalActions,
|
|
273
|
-
sidebarFooterExtra,
|
|
274
202
|
sidebarNavExtra,
|
|
275
203
|
settingsMenuItems,
|
|
276
204
|
storageKey = 'basalt-sidebar-collapsed',
|
|
277
205
|
collapsed: collapsedProp,
|
|
278
206
|
onCollapsedChange,
|
|
279
|
-
renderBreadcrumbLink,
|
|
280
207
|
account,
|
|
281
208
|
search,
|
|
282
209
|
children,
|
|
@@ -286,7 +213,6 @@ export function BasaltShell({
|
|
|
286
213
|
// (controls, the search trigger/avatar, nav labels) already track density, so a fixed literal
|
|
287
214
|
// container squeezes progressively worse as density rises.
|
|
288
215
|
const { step } = useBasaltSpacing()
|
|
289
|
-
const [mobileOpened, { toggle: toggleMobile, close: closeMobile }] = useDisclosure()
|
|
290
216
|
const [storedCollapsed, setStoredCollapsed] = useLocalStorage({
|
|
291
217
|
key: storageKey,
|
|
292
218
|
defaultValue: false,
|
|
@@ -303,7 +229,18 @@ export function BasaltShell({
|
|
|
303
229
|
}
|
|
304
230
|
|
|
305
231
|
const activeCrumb = findActiveCrumb(sections)
|
|
306
|
-
|
|
232
|
+
// The account row and every settings entry render as flat rows in the More surface (there is no
|
|
233
|
+
// mobile sidebar to reach them through any more), so they count toward BOTH `needsMore` and the
|
|
234
|
+
// menu-vs-sheet threshold. `accountRowCount` is the SAME function that renders those rows, not a
|
|
235
|
+
// second estimate of them: an account is worth 0 rows while `loading` and up to seven once
|
|
236
|
+
// authenticated, and §2.2's whole guarantee (`menuMax` rows fit the headroom above the bar, and
|
|
237
|
+
// the menu runs `flip: false` so it cannot escape upward) is arithmetic over this number.
|
|
238
|
+
const extraMoreRows =
|
|
239
|
+
accountRowCount(account) + (settingsMenuItems?.length ?? 0) + (mobileNav?.moreExtra ? 1 : 0)
|
|
240
|
+
const model = useMemo(
|
|
241
|
+
() => projectMobileNav(sections, { config: mobileNav, extraMoreRows }),
|
|
242
|
+
[sections, mobileNav, extraMoreRows],
|
|
243
|
+
)
|
|
307
244
|
|
|
308
245
|
return (
|
|
309
246
|
<PageHeaderProvider>
|
|
@@ -319,15 +256,18 @@ export function BasaltShell({
|
|
|
319
256
|
sm: collapsed ? step.appShellNavbarRailWidth : step.appShellNavbarWidth,
|
|
320
257
|
},
|
|
321
258
|
breakpoint: 'sm',
|
|
322
|
-
|
|
259
|
+
// No mobile sidebar drawer, ever — the bottom bar is the entire mobile nav.
|
|
260
|
+
collapsed: { mobile: true },
|
|
323
261
|
}}
|
|
324
|
-
|
|
262
|
+
// A plain number, NOT a `calc(... + env(safe-area-inset-bottom))` string: Mantine's own
|
|
263
|
+
// `.footer` rule already adds the inset to both the height and the padding (§2.7).
|
|
264
|
+
footer={{ height: { base: step.mobileNavBarHeight, sm: 0 } }}
|
|
325
265
|
padding="sm"
|
|
326
266
|
>
|
|
327
267
|
<AppShell.Header px="md" withBorder={false}>
|
|
328
268
|
<div className={headerClasses.bar}>
|
|
329
269
|
<div className={headerClasses.lead}>
|
|
330
|
-
<AppBreadcrumbs {...activeCrumb}
|
|
270
|
+
<AppBreadcrumbs {...activeCrumb} />
|
|
331
271
|
</div>
|
|
332
272
|
<PageActionsOutlet className={headerClasses.pageActions} />
|
|
333
273
|
{globalActions && <div className={headerClasses.global}>{globalActions}</div>}
|
|
@@ -340,23 +280,24 @@ export function BasaltShell({
|
|
|
340
280
|
sections={sections}
|
|
341
281
|
collapsed={collapsed}
|
|
342
282
|
onToggleCollapse={toggleCollapse}
|
|
343
|
-
onClose={closeMobile}
|
|
344
|
-
footerExtra={sidebarFooterExtra}
|
|
345
283
|
navExtra={sidebarNavExtra}
|
|
346
|
-
{...(renderNavLink !== undefined && { renderNavLink })}
|
|
347
284
|
{...(settingsMenuItems !== undefined && { settingsMenuItems })}
|
|
348
285
|
{...(account !== undefined && { account })}
|
|
349
286
|
{...(search !== undefined && { search })}
|
|
350
287
|
/>
|
|
351
288
|
</AppShell.Navbar>
|
|
352
289
|
|
|
353
|
-
|
|
290
|
+
{/* `mainSafeArea` closes the one real safe-area gap: Mantine sets
|
|
291
|
+
* `--app-shell-footer-offset` to the RAW footer height, so Main's own padding-bottom is
|
|
292
|
+
* short by exactly `env(safe-area-inset-bottom)` (§2.7). */}
|
|
293
|
+
<AppShell.Main className={mobileNavClasses.mainSafeArea}>{children}</AppShell.Main>
|
|
354
294
|
|
|
355
|
-
<AppShell.Footer hiddenFrom="sm" p={0}>
|
|
295
|
+
<AppShell.Footer hiddenFrom="sm" p={0} withBorder={false}>
|
|
356
296
|
<MobileNav
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
{...(
|
|
297
|
+
model={model}
|
|
298
|
+
config={mobileNav}
|
|
299
|
+
{...(account !== undefined && { account })}
|
|
300
|
+
{...(settingsMenuItems !== undefined && { settingsMenuItems })}
|
|
360
301
|
/>
|
|
361
302
|
</AppShell.Footer>
|
|
362
303
|
</AppShell>
|