@object-ui/layout 3.0.3 → 3.1.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/.turbo/turbo-build.log +11 -8
- package/dist/index.js +3760 -299
- package/dist/index.umd.cjs +6 -2
- package/dist/layout/src/AppSchemaRenderer.d.ts +68 -0
- package/dist/layout/src/NavigationRenderer.d.ts +104 -0
- package/dist/layout/src/SidebarNav.d.ts +11 -2
- package/dist/layout/src/index.d.ts +2 -0
- package/package.json +11 -8
- package/src/AppSchemaRenderer.tsx +480 -0
- package/src/AppShell.tsx +1 -1
- package/src/NavigationRenderer.tsx +746 -0
- package/src/SidebarNav.tsx +130 -19
- package/src/__tests__/AppSchemaRenderer.test.tsx +408 -0
- package/src/__tests__/NavigationRenderer.test.tsx +562 -0
- package/src/index.ts +26 -0
- package/src/stories/SidebarNav.stories.tsx +223 -0
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @object-ui/layout - AppSchema Renderer
|
|
11
|
+
*
|
|
12
|
+
* Consumes an `AppSchema` JSON object and renders a complete application
|
|
13
|
+
* shell with branding, sidebar navigation (including area switching),
|
|
14
|
+
* and mobile navigation modes.
|
|
15
|
+
*
|
|
16
|
+
* This is the main P0.1 deliverable — it allows Console (or any consumer)
|
|
17
|
+
* to render a fully-functional AppShell from a single JSON document.
|
|
18
|
+
*
|
|
19
|
+
* @module AppSchemaRenderer
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import React, { useState, useEffect, useMemo } from 'react';
|
|
23
|
+
import { Link, useLocation } from 'react-router-dom';
|
|
24
|
+
import * as LucideIcons from 'lucide-react';
|
|
25
|
+
import {
|
|
26
|
+
Sidebar,
|
|
27
|
+
SidebarHeader,
|
|
28
|
+
SidebarContent,
|
|
29
|
+
SidebarFooter,
|
|
30
|
+
SidebarMenu,
|
|
31
|
+
SidebarMenuItem,
|
|
32
|
+
SidebarMenuButton,
|
|
33
|
+
SidebarGroup,
|
|
34
|
+
SidebarGroupLabel,
|
|
35
|
+
SidebarGroupContent,
|
|
36
|
+
SidebarInput,
|
|
37
|
+
useSidebar,
|
|
38
|
+
} from '@object-ui/components';
|
|
39
|
+
import type { AppSchema, NavigationItem, NavigationArea } from '@object-ui/types';
|
|
40
|
+
import { menuItemToNavigationItem } from '@object-ui/types';
|
|
41
|
+
import { AppShell, type AppShellBranding } from './AppShell';
|
|
42
|
+
import {
|
|
43
|
+
NavigationRenderer,
|
|
44
|
+
resolveIcon,
|
|
45
|
+
resolveLabel,
|
|
46
|
+
type VisibilityEvaluator,
|
|
47
|
+
type PermissionChecker,
|
|
48
|
+
} from './NavigationRenderer';
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Types
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
/** Mobile navigation display mode */
|
|
55
|
+
export type MobileNavMode = 'drawer' | 'bottom_nav' | 'hamburger';
|
|
56
|
+
|
|
57
|
+
export interface AppSchemaRendererProps {
|
|
58
|
+
/** The AppSchema JSON to render */
|
|
59
|
+
schema: AppSchema;
|
|
60
|
+
|
|
61
|
+
/** Base URL prefix for generated hrefs (e.g. "/apps/crm") */
|
|
62
|
+
basePath?: string;
|
|
63
|
+
|
|
64
|
+
/** Mobile navigation mode @default "drawer" */
|
|
65
|
+
mobileNavMode?: MobileNavMode;
|
|
66
|
+
|
|
67
|
+
/** Optional visibility evaluator passed to NavigationRenderer */
|
|
68
|
+
evaluateVisibility?: VisibilityEvaluator;
|
|
69
|
+
|
|
70
|
+
/** Optional permission checker passed to NavigationRenderer */
|
|
71
|
+
checkPermission?: PermissionChecker;
|
|
72
|
+
|
|
73
|
+
/** Called when an action-type navigation item is clicked */
|
|
74
|
+
onAction?: (item: NavigationItem) => void;
|
|
75
|
+
|
|
76
|
+
/** Slot: top navbar content (rendered beside the sidebar trigger) */
|
|
77
|
+
navbar?: React.ReactNode;
|
|
78
|
+
|
|
79
|
+
/** Slot: sidebar header (e.g. app switcher dropdown). Replaces default branding header when provided. */
|
|
80
|
+
sidebarHeader?: React.ReactNode;
|
|
81
|
+
|
|
82
|
+
/** Slot: sidebar footer (e.g. user profile menu) */
|
|
83
|
+
sidebarFooter?: React.ReactNode;
|
|
84
|
+
|
|
85
|
+
/** Slot: extra sidebar content rendered after navigation (e.g. favorites, recent items) */
|
|
86
|
+
sidebarExtra?: React.ReactNode;
|
|
87
|
+
|
|
88
|
+
/** Page content */
|
|
89
|
+
children: React.ReactNode;
|
|
90
|
+
|
|
91
|
+
/** Extra class on the <main> content area */
|
|
92
|
+
className?: string;
|
|
93
|
+
|
|
94
|
+
/** Whether the sidebar starts open @default true */
|
|
95
|
+
defaultOpen?: boolean;
|
|
96
|
+
|
|
97
|
+
// --- P1.7 Navigation Enhancements ---
|
|
98
|
+
|
|
99
|
+
/** Show a search input in the sidebar to filter navigation items */
|
|
100
|
+
enableSearch?: boolean;
|
|
101
|
+
|
|
102
|
+
/** Enable pin/favorite toggle on navigation items */
|
|
103
|
+
enablePinning?: boolean;
|
|
104
|
+
|
|
105
|
+
/** Called when a navigation item is pinned or unpinned */
|
|
106
|
+
onPinToggle?: (itemId: string, pinned: boolean) => void;
|
|
107
|
+
|
|
108
|
+
/** Enable drag-to-reorder for navigation items */
|
|
109
|
+
enableReorder?: boolean;
|
|
110
|
+
|
|
111
|
+
/** Called when navigation items are reordered via drag */
|
|
112
|
+
onReorder?: (reorderedItems: NavigationItem[]) => void;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// AreaSwitcher
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
function AreaSwitcher({
|
|
120
|
+
areas,
|
|
121
|
+
activeAreaId,
|
|
122
|
+
onAreaChange,
|
|
123
|
+
evalVis,
|
|
124
|
+
checkPerm,
|
|
125
|
+
}: {
|
|
126
|
+
areas: NavigationArea[];
|
|
127
|
+
activeAreaId: string;
|
|
128
|
+
onAreaChange: (id: string) => void;
|
|
129
|
+
evalVis: VisibilityEvaluator;
|
|
130
|
+
checkPerm: PermissionChecker;
|
|
131
|
+
}) {
|
|
132
|
+
// Filter areas by visibility & permissions
|
|
133
|
+
const visibleAreas = areas.filter((a) => {
|
|
134
|
+
if (!evalVis(a.visible)) return false;
|
|
135
|
+
if (a.requiredPermissions?.length && !checkPerm(a.requiredPermissions)) return false;
|
|
136
|
+
return true;
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (visibleAreas.length <= 1) return null;
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<SidebarGroup>
|
|
143
|
+
<SidebarGroupLabel className="flex items-center gap-1.5">
|
|
144
|
+
<LucideIcons.Layers className="h-3.5 w-3.5" />
|
|
145
|
+
Area
|
|
146
|
+
</SidebarGroupLabel>
|
|
147
|
+
<SidebarGroupContent>
|
|
148
|
+
<SidebarMenu>
|
|
149
|
+
{visibleAreas.map((area) => {
|
|
150
|
+
const AreaIcon = resolveIcon(area.icon);
|
|
151
|
+
return (
|
|
152
|
+
<SidebarMenuItem key={area.id}>
|
|
153
|
+
<SidebarMenuButton
|
|
154
|
+
isActive={area.id === activeAreaId}
|
|
155
|
+
tooltip={resolveLabel(area.label)}
|
|
156
|
+
onClick={() => onAreaChange(area.id)}
|
|
157
|
+
>
|
|
158
|
+
<AreaIcon className="h-4 w-4" />
|
|
159
|
+
<span>{resolveLabel(area.label)}</span>
|
|
160
|
+
</SidebarMenuButton>
|
|
161
|
+
</SidebarMenuItem>
|
|
162
|
+
);
|
|
163
|
+
})}
|
|
164
|
+
</SidebarMenu>
|
|
165
|
+
</SidebarGroupContent>
|
|
166
|
+
</SidebarGroup>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
// MobileBottomNav
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
function MobileBottomNav({
|
|
175
|
+
items,
|
|
176
|
+
basePath,
|
|
177
|
+
}: {
|
|
178
|
+
items: NavigationItem[];
|
|
179
|
+
basePath: string;
|
|
180
|
+
}) {
|
|
181
|
+
const location = useLocation();
|
|
182
|
+
// Show up to 5 non-group leaf items
|
|
183
|
+
const leaves = items
|
|
184
|
+
.filter((n) => n.type !== 'group' && n.type !== 'separator')
|
|
185
|
+
.slice(0, 5);
|
|
186
|
+
|
|
187
|
+
if (leaves.length === 0) return null;
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<div
|
|
191
|
+
className="fixed bottom-0 left-0 right-0 z-50 flex items-center justify-around border-t bg-background/95 backdrop-blur-sm px-2 py-1 sm:hidden safe-area-bottom"
|
|
192
|
+
role="navigation"
|
|
193
|
+
aria-label="Mobile navigation"
|
|
194
|
+
>
|
|
195
|
+
{leaves.map((item) => {
|
|
196
|
+
const NavIcon = resolveIcon(item.icon);
|
|
197
|
+
let href = '#';
|
|
198
|
+
if (item.type === 'object') {
|
|
199
|
+
href = `${basePath}/${item.objectName}`;
|
|
200
|
+
if (item.viewName) href += `/view/${item.viewName}`;
|
|
201
|
+
}
|
|
202
|
+
else if (item.type === 'dashboard') href = item.dashboardName ? `${basePath}/dashboard/${item.dashboardName}` : '#';
|
|
203
|
+
else if (item.type === 'page') href = item.pageName ? `${basePath}/page/${item.pageName}` : '#';
|
|
204
|
+
else if (item.type === 'report') href = item.reportName ? `${basePath}/report/${item.reportName}` : '#';
|
|
205
|
+
else if (item.type === 'url') href = item.url ?? '#';
|
|
206
|
+
|
|
207
|
+
const isActive = href !== '#' && location.pathname.startsWith(href);
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<Link
|
|
211
|
+
key={item.id}
|
|
212
|
+
to={href}
|
|
213
|
+
className={`flex flex-col items-center gap-0.5 px-2 py-1.5 transition-colors min-w-[44px] min-h-[44px] justify-center ${
|
|
214
|
+
isActive ? 'text-primary' : 'text-muted-foreground hover:text-foreground'
|
|
215
|
+
}`}
|
|
216
|
+
>
|
|
217
|
+
<NavIcon className="h-5 w-5" />
|
|
218
|
+
<span className="text-[10px] truncate max-w-[60px]">{resolveLabel(item.label)}</span>
|
|
219
|
+
</Link>
|
|
220
|
+
);
|
|
221
|
+
})}
|
|
222
|
+
</div>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
// InternalSidebar (wraps Sidebar primitive + header + navigation)
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
|
|
230
|
+
function InternalSidebar({
|
|
231
|
+
schema,
|
|
232
|
+
basePath,
|
|
233
|
+
evalVis,
|
|
234
|
+
checkPerm,
|
|
235
|
+
onAction,
|
|
236
|
+
sidebarHeader,
|
|
237
|
+
sidebarFooter,
|
|
238
|
+
sidebarExtra,
|
|
239
|
+
activeAreaId,
|
|
240
|
+
setActiveAreaId,
|
|
241
|
+
resolvedNavigation,
|
|
242
|
+
enableSearch,
|
|
243
|
+
enablePinning,
|
|
244
|
+
onPinToggle,
|
|
245
|
+
enableReorder,
|
|
246
|
+
onReorder,
|
|
247
|
+
}: {
|
|
248
|
+
schema: AppSchema;
|
|
249
|
+
basePath: string;
|
|
250
|
+
evalVis: VisibilityEvaluator;
|
|
251
|
+
checkPerm: PermissionChecker;
|
|
252
|
+
onAction?: (item: NavigationItem) => void;
|
|
253
|
+
sidebarHeader?: React.ReactNode;
|
|
254
|
+
sidebarFooter?: React.ReactNode;
|
|
255
|
+
sidebarExtra?: React.ReactNode;
|
|
256
|
+
activeAreaId: string | null;
|
|
257
|
+
setActiveAreaId: (id: string) => void;
|
|
258
|
+
resolvedNavigation: NavigationItem[];
|
|
259
|
+
enableSearch?: boolean;
|
|
260
|
+
enablePinning?: boolean;
|
|
261
|
+
onPinToggle?: (itemId: string, pinned: boolean) => void;
|
|
262
|
+
enableReorder?: boolean;
|
|
263
|
+
onReorder?: (reorderedItems: NavigationItem[]) => void;
|
|
264
|
+
}) {
|
|
265
|
+
const Icon = resolveIcon(schema.logo);
|
|
266
|
+
const areas = schema.areas ?? [];
|
|
267
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
268
|
+
|
|
269
|
+
return (
|
|
270
|
+
<Sidebar collapsible="icon">
|
|
271
|
+
{/* Header: custom slot or default branding */}
|
|
272
|
+
<SidebarHeader>
|
|
273
|
+
{sidebarHeader ?? (
|
|
274
|
+
<SidebarMenu>
|
|
275
|
+
<SidebarMenuItem>
|
|
276
|
+
<SidebarMenuButton size="lg" tooltip={schema.title ?? schema.name}>
|
|
277
|
+
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
|
|
278
|
+
{schema.logo && schema.logo.startsWith('http') ? (
|
|
279
|
+
<img
|
|
280
|
+
src={schema.logo}
|
|
281
|
+
alt={schema.title ?? ''}
|
|
282
|
+
className="size-6 object-contain"
|
|
283
|
+
/>
|
|
284
|
+
) : (
|
|
285
|
+
<Icon className="size-4" />
|
|
286
|
+
)}
|
|
287
|
+
</div>
|
|
288
|
+
<div className="grid flex-1 text-left text-sm leading-tight">
|
|
289
|
+
<span className="truncate font-semibold">
|
|
290
|
+
{schema.title ?? schema.name ?? 'App'}
|
|
291
|
+
</span>
|
|
292
|
+
{schema.description && (
|
|
293
|
+
<span className="truncate text-xs text-muted-foreground">
|
|
294
|
+
{schema.description}
|
|
295
|
+
</span>
|
|
296
|
+
)}
|
|
297
|
+
</div>
|
|
298
|
+
</SidebarMenuButton>
|
|
299
|
+
</SidebarMenuItem>
|
|
300
|
+
</SidebarMenu>
|
|
301
|
+
)}
|
|
302
|
+
{/* Search input */}
|
|
303
|
+
{enableSearch && (
|
|
304
|
+
<SidebarInput
|
|
305
|
+
placeholder="Search navigation…"
|
|
306
|
+
value={searchQuery}
|
|
307
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
308
|
+
aria-label="Search navigation"
|
|
309
|
+
/>
|
|
310
|
+
)}
|
|
311
|
+
</SidebarHeader>
|
|
312
|
+
|
|
313
|
+
<SidebarContent>
|
|
314
|
+
{/* Area Switcher */}
|
|
315
|
+
{areas.length > 1 && activeAreaId && (
|
|
316
|
+
<AreaSwitcher
|
|
317
|
+
areas={areas}
|
|
318
|
+
activeAreaId={activeAreaId}
|
|
319
|
+
onAreaChange={setActiveAreaId}
|
|
320
|
+
evalVis={evalVis}
|
|
321
|
+
checkPerm={checkPerm}
|
|
322
|
+
/>
|
|
323
|
+
)}
|
|
324
|
+
|
|
325
|
+
{/* Navigation tree */}
|
|
326
|
+
<NavigationRenderer
|
|
327
|
+
items={resolvedNavigation}
|
|
328
|
+
basePath={basePath}
|
|
329
|
+
evaluateVisibility={evalVis}
|
|
330
|
+
checkPermission={checkPerm}
|
|
331
|
+
onAction={onAction}
|
|
332
|
+
searchQuery={searchQuery}
|
|
333
|
+
enablePinning={enablePinning}
|
|
334
|
+
onPinToggle={onPinToggle}
|
|
335
|
+
enableReorder={enableReorder}
|
|
336
|
+
onReorder={onReorder}
|
|
337
|
+
/>
|
|
338
|
+
|
|
339
|
+
{/* Extra sidebar content slot (e.g. favorites, recent items) */}
|
|
340
|
+
{sidebarExtra}
|
|
341
|
+
</SidebarContent>
|
|
342
|
+
|
|
343
|
+
{/* Optional footer slot */}
|
|
344
|
+
{sidebarFooter && <SidebarFooter>{sidebarFooter}</SidebarFooter>}
|
|
345
|
+
</Sidebar>
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ---------------------------------------------------------------------------
|
|
350
|
+
// AppSchemaRenderer (main export)
|
|
351
|
+
// ---------------------------------------------------------------------------
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Renders a complete application shell from an `AppSchema` JSON document.
|
|
355
|
+
*
|
|
356
|
+
* Responsibilities:
|
|
357
|
+
* - Reads `name`, `title`, `description`, `logo`, `favicon` for branding
|
|
358
|
+
* - Renders sidebar navigation from `navigation` or `areas[].navigation`
|
|
359
|
+
* - Area switcher when multiple `areas` are defined
|
|
360
|
+
* - Mobile modes: `drawer` (sheet overlay, default), `bottom_nav` (fixed
|
|
361
|
+
* bottom bar), `hamburger` (collapsed sidebar)
|
|
362
|
+
* - Evaluates `visible` expressions and `requiredPermissions` on every item
|
|
363
|
+
*
|
|
364
|
+
* @example
|
|
365
|
+
* ```tsx
|
|
366
|
+
* <AppSchemaRenderer
|
|
367
|
+
* schema={appJson}
|
|
368
|
+
* basePath="/apps/sales"
|
|
369
|
+
* mobileNavMode="bottom_nav"
|
|
370
|
+
* evaluateVisibility={(expr) => evaluateVisibility(expr, evaluator)}
|
|
371
|
+
* checkPermission={(perms) => perms.every(p => can(p))}
|
|
372
|
+
* >
|
|
373
|
+
* <Outlet />
|
|
374
|
+
* </AppSchemaRenderer>
|
|
375
|
+
* ```
|
|
376
|
+
*/
|
|
377
|
+
export function AppSchemaRenderer({
|
|
378
|
+
schema,
|
|
379
|
+
basePath = '',
|
|
380
|
+
mobileNavMode = 'drawer',
|
|
381
|
+
evaluateVisibility: evalVisProp,
|
|
382
|
+
checkPermission: checkPermProp,
|
|
383
|
+
onAction,
|
|
384
|
+
navbar,
|
|
385
|
+
sidebarHeader,
|
|
386
|
+
sidebarFooter,
|
|
387
|
+
sidebarExtra,
|
|
388
|
+
children,
|
|
389
|
+
className,
|
|
390
|
+
defaultOpen = true,
|
|
391
|
+
enableSearch,
|
|
392
|
+
enablePinning,
|
|
393
|
+
onPinToggle,
|
|
394
|
+
enableReorder,
|
|
395
|
+
onReorder,
|
|
396
|
+
}: AppSchemaRendererProps) {
|
|
397
|
+
// Default evaluators
|
|
398
|
+
const evalVis: VisibilityEvaluator = evalVisProp ?? ((expr) => {
|
|
399
|
+
if (expr === false || expr === 'false') return false;
|
|
400
|
+
return true;
|
|
401
|
+
});
|
|
402
|
+
const checkPerm: PermissionChecker = checkPermProp ?? (() => true);
|
|
403
|
+
|
|
404
|
+
// --- Resolve navigation from legacy `menu` or modern `navigation`/`areas` ---
|
|
405
|
+
const legacyNavigation = useMemo(
|
|
406
|
+
() => (schema.menu ?? []).map((m, i) => menuItemToNavigationItem(m, i)),
|
|
407
|
+
[schema.menu],
|
|
408
|
+
);
|
|
409
|
+
const flatNavigation = schema.navigation ?? legacyNavigation;
|
|
410
|
+
|
|
411
|
+
// --- Area management ---
|
|
412
|
+
const areas = schema.areas ?? [];
|
|
413
|
+
const [activeAreaId, setActiveAreaId] = useState<string | null>(
|
|
414
|
+
() => areas.length > 0 ? areas[0].id : null,
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
const areaIds = areas.map((a) => a.id).join(',');
|
|
418
|
+
|
|
419
|
+
useEffect(() => {
|
|
420
|
+
if (areas.length > 0) {
|
|
421
|
+
setActiveAreaId((prev) =>
|
|
422
|
+
areas.some((a) => a.id === prev) ? prev : areas[0].id,
|
|
423
|
+
);
|
|
424
|
+
} else {
|
|
425
|
+
setActiveAreaId(null);
|
|
426
|
+
}
|
|
427
|
+
}, [schema.name, areaIds]);
|
|
428
|
+
|
|
429
|
+
const activeArea = areas.find((a) => a.id === activeAreaId);
|
|
430
|
+
const resolvedNavigation: NavigationItem[] = activeArea?.navigation ?? flatNavigation;
|
|
431
|
+
|
|
432
|
+
// --- Branding ---
|
|
433
|
+
const branding: AppShellBranding = {
|
|
434
|
+
title: schema.title,
|
|
435
|
+
favicon: schema.favicon,
|
|
436
|
+
logo: schema.logo,
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
// --- Build sidebar element ---
|
|
440
|
+
const sidebarElement = (
|
|
441
|
+
<InternalSidebar
|
|
442
|
+
schema={schema}
|
|
443
|
+
basePath={basePath}
|
|
444
|
+
evalVis={evalVis}
|
|
445
|
+
checkPerm={checkPerm}
|
|
446
|
+
onAction={onAction}
|
|
447
|
+
sidebarHeader={sidebarHeader}
|
|
448
|
+
sidebarFooter={sidebarFooter}
|
|
449
|
+
sidebarExtra={sidebarExtra}
|
|
450
|
+
activeAreaId={activeAreaId}
|
|
451
|
+
setActiveAreaId={setActiveAreaId}
|
|
452
|
+
resolvedNavigation={resolvedNavigation}
|
|
453
|
+
enableSearch={enableSearch}
|
|
454
|
+
enablePinning={enablePinning}
|
|
455
|
+
onPinToggle={onPinToggle}
|
|
456
|
+
enableReorder={enableReorder}
|
|
457
|
+
onReorder={onReorder}
|
|
458
|
+
/>
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
// --- Mobile bottom nav (shown alongside drawer sidebar on mobile) ---
|
|
462
|
+
const showBottomNav = mobileNavMode === 'bottom_nav';
|
|
463
|
+
|
|
464
|
+
return (
|
|
465
|
+
<>
|
|
466
|
+
<AppShell
|
|
467
|
+
sidebar={sidebarElement}
|
|
468
|
+
navbar={navbar}
|
|
469
|
+
className={className}
|
|
470
|
+
defaultOpen={defaultOpen}
|
|
471
|
+
branding={branding}
|
|
472
|
+
>
|
|
473
|
+
{children}
|
|
474
|
+
</AppShell>
|
|
475
|
+
{showBottomNav && (
|
|
476
|
+
<MobileBottomNav items={resolvedNavigation} basePath={basePath} />
|
|
477
|
+
)}
|
|
478
|
+
</>
|
|
479
|
+
);
|
|
480
|
+
}
|
package/src/AppShell.tsx
CHANGED
|
@@ -140,7 +140,7 @@ export function AppShell({
|
|
|
140
140
|
<div className="w-px h-4 bg-border mx-1 sm:mx-2" />
|
|
141
141
|
{navbar}
|
|
142
142
|
</header>
|
|
143
|
-
<main className={cn("flex-1 overflow-auto p-3 sm:p-4 md:p-6", className)}>
|
|
143
|
+
<main className={cn("flex-1 min-w-0 overflow-auto p-3 sm:p-4 md:p-6", className)}>
|
|
144
144
|
{children}
|
|
145
145
|
</main>
|
|
146
146
|
</SidebarInset>
|