@trackany-device/components 1.1.0 → 1.2.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/README.md +9 -9
- package/package.json +133 -4
- package/src/assets/index.ts +120 -0
- package/src/assets/media/avatars/300-1.png +0 -0
- package/src/assets/media/avatars/300-10.png +0 -0
- package/src/assets/media/avatars/300-11.png +0 -0
- package/src/assets/media/avatars/300-12.png +0 -0
- package/src/assets/media/avatars/300-13.png +0 -0
- package/src/assets/media/avatars/300-14.png +0 -0
- package/src/assets/media/avatars/300-15.png +0 -0
- package/src/assets/media/avatars/300-16.png +0 -0
- package/src/assets/media/avatars/300-17.png +0 -0
- package/src/assets/media/avatars/300-18.png +0 -0
- package/src/assets/media/avatars/300-19.png +0 -0
- package/src/assets/media/avatars/300-2.png +0 -0
- package/src/assets/media/avatars/300-20.png +0 -0
- package/src/assets/media/avatars/300-21.png +0 -0
- package/src/assets/media/avatars/300-22.png +0 -0
- package/src/assets/media/avatars/300-23.png +0 -0
- package/src/assets/media/avatars/300-24.png +0 -0
- package/src/assets/media/avatars/300-25.png +0 -0
- package/src/assets/media/avatars/300-26.png +0 -0
- package/src/assets/media/avatars/300-27.png +0 -0
- package/src/assets/media/avatars/300-28.png +0 -0
- package/src/assets/media/avatars/300-29.png +0 -0
- package/src/assets/media/avatars/300-3.png +0 -0
- package/src/assets/media/avatars/300-30.png +0 -0
- package/src/assets/media/avatars/300-31.png +0 -0
- package/src/assets/media/avatars/300-32.png +0 -0
- package/src/assets/media/avatars/300-33.png +0 -0
- package/src/assets/media/avatars/300-34.png +0 -0
- package/src/assets/media/avatars/300-4.png +0 -0
- package/src/assets/media/avatars/300-5.png +0 -0
- package/src/assets/media/avatars/300-6.png +0 -0
- package/src/assets/media/avatars/300-7.png +0 -0
- package/src/assets/media/avatars/300-8.png +0 -0
- package/src/assets/media/avatars/300-9.png +0 -0
- package/src/assets/media/avatars/blank.png +0 -0
- package/src/assets/media/avatars/gray/1.png +0 -0
- package/src/assets/media/avatars/gray/2.png +0 -0
- package/src/assets/media/avatars/gray/3.png +0 -0
- package/src/assets/media/avatars/gray/4.png +0 -0
- package/src/assets/media/avatars/gray/5.png +0 -0
- package/src/assets/media/illustrations/1-dark.svg +78 -0
- package/src/assets/media/illustrations/1.svg +78 -0
- package/src/assets/media/illustrations/10-dark.svg +148 -0
- package/src/assets/media/illustrations/10.svg +148 -0
- package/src/assets/media/illustrations/11-dark.svg +234 -0
- package/src/assets/media/illustrations/11.svg +234 -0
- package/src/assets/media/illustrations/12.svg +138 -0
- package/src/assets/media/illustrations/13.svg +205 -0
- package/src/assets/media/illustrations/14.svg +259 -0
- package/src/assets/media/illustrations/15.svg +242 -0
- package/src/assets/media/illustrations/16.svg +128 -0
- package/src/assets/media/illustrations/17.svg +180 -0
- package/src/assets/media/illustrations/18-dark.svg +6 -0
- package/src/assets/media/illustrations/18.svg +6 -0
- package/src/assets/media/illustrations/19-dark.svg +8 -0
- package/src/assets/media/illustrations/19.svg +8 -0
- package/src/assets/media/illustrations/2-dark.svg +78 -0
- package/src/assets/media/illustrations/2.svg +78 -0
- package/src/assets/media/illustrations/20-dark.svg +13 -0
- package/src/assets/media/illustrations/20.svg +13 -0
- package/src/assets/media/illustrations/21-dark.svg +9 -0
- package/src/assets/media/illustrations/21.svg +9 -0
- package/src/assets/media/illustrations/22-dark.svg +17 -0
- package/src/assets/media/illustrations/22.svg +17 -0
- package/src/assets/media/illustrations/23-dark.svg +13 -0
- package/src/assets/media/illustrations/23.svg +13 -0
- package/src/assets/media/illustrations/24.svg +6 -0
- package/src/assets/media/illustrations/25.svg +8 -0
- package/src/assets/media/illustrations/26.svg +8 -0
- package/src/assets/media/illustrations/27.svg +6 -0
- package/src/assets/media/illustrations/28-dark.svg +28 -0
- package/src/assets/media/illustrations/28.svg +14 -0
- package/src/assets/media/illustrations/29-dark.svg +6 -0
- package/src/assets/media/illustrations/29.svg +6 -0
- package/src/assets/media/illustrations/3-dark.svg +70 -0
- package/src/assets/media/illustrations/3.svg +70 -0
- package/src/assets/media/illustrations/30-dark.svg +8 -0
- package/src/assets/media/illustrations/30.svg +8 -0
- package/src/assets/media/illustrations/31-dark.svg +9 -0
- package/src/assets/media/illustrations/31.svg +9 -0
- package/src/assets/media/illustrations/32-dark.svg +10 -0
- package/src/assets/media/illustrations/32.svg +10 -0
- package/src/assets/media/illustrations/33-dark.svg +15 -0
- package/src/assets/media/illustrations/33.svg +15 -0
- package/src/assets/media/illustrations/34-dark.svg +5 -0
- package/src/assets/media/illustrations/34.svg +5 -0
- package/src/assets/media/illustrations/35-dark.svg +11 -0
- package/src/assets/media/illustrations/35.svg +4 -0
- package/src/assets/media/illustrations/4-dark.svg +51 -0
- package/src/assets/media/illustrations/4.svg +51 -0
- package/src/assets/media/illustrations/5-dark.svg +78 -0
- package/src/assets/media/illustrations/5.svg +78 -0
- package/src/assets/media/illustrations/6.svg +58 -0
- package/src/assets/media/illustrations/7.svg +49 -0
- package/src/assets/media/illustrations/8.svg +61 -0
- package/src/assets/media/illustrations/9.svg +57 -0
- package/src/assets/media/misc/placeholder.svg +15 -0
- package/src/components/devices/devices-mini-map.tsx +32 -26
- package/src/components/devices/map-marker.tsx +98 -0
- package/src/components/ui/checklist-item.tsx +55 -0
- package/src/components/ui/plan-card.tsx +68 -0
- package/src/components/ui/settings-row.tsx +32 -0
- package/src/components/ui/settings-section.tsx +22 -0
- package/src/components/ui/usage-meter.tsx +35 -0
- package/src/index.ts +12 -1
- package/src/layouts/LayoutSwitcher.tsx +220 -0
- package/src/layouts/app/MegaMenuLayout.tsx +69 -34
- package/src/layouts/app/MegaMenuNavbarLayout.tsx +73 -37
- package/src/layouts/app/NavbarCollapsibleLayout.tsx +53 -4
- package/src/layouts/app/NavbarSidebarLayout.tsx +74 -29
- package/src/layouts/app/SidebarDualMenuLayout.tsx +48 -5
- package/src/layouts/app/SidebarFixedLayout.tsx +15 -10
- package/src/layouts/app/SidebarMinimalLayout.tsx +51 -3
- package/src/layouts/app/SidebarTabsLayout.tsx +48 -2
- package/src/layouts/app/SplitSidebarLayout.tsx +91 -43
- package/src/layouts/app/TopNavLayout.tsx +7 -12
- package/src/layouts/app/WorkspaceSidebarLayout.tsx +103 -46
- package/src/layouts/app/partials/Navbar.tsx +61 -10
- package/src/layouts/app/partials/Toolbar.tsx +1 -1
- package/src/layouts/auth/AuthCenteredLayout.tsx +10 -4
- package/src/lib/map-markers.ts +21 -3
- package/src/pages/login/ConfirmPasswordPage.tsx +35 -0
- package/src/pages/login/ForgotPasswordPage.tsx +41 -0
- package/src/pages/login/LoginPage.tsx +50 -0
- package/src/pages/login/RegisterPage.tsx +41 -0
- package/src/pages/login/ResetPasswordPage.tsx +35 -0
- package/src/pages/login/TwoFactorChallengePage.tsx +41 -0
- package/src/pages/login/VerifyEmailPage.tsx +31 -0
- package/src/pages/my/ActivityPage.tsx +160 -0
- package/src/pages/my/GetStartedPage.tsx +221 -0
- package/src/pages/my/NotificationsPage.tsx +133 -0
- package/src/pages/my/ProfilePage.tsx +650 -0
- package/src/pages/my/TenantsPage.tsx +37 -0
- package/src/pages/tenant/AssigneesPage.tsx +155 -0
- package/src/pages/tenant/BeatsPage.tsx +403 -0
- package/src/pages/tenant/DashboardPage.tsx +195 -0
- package/src/pages/tenant/GeofencePage.tsx +422 -0
- package/src/pages/tenant/IncidentsPage.tsx +214 -0
- package/src/pages/tenant/IntegrationsPage.tsx +352 -0
- package/src/pages/tenant/InvitePage.tsx +153 -0
- package/src/pages/tenant/LiveStreamPage.tsx +141 -0
- package/src/pages/tenant/MembersPage.tsx +414 -0
- package/src/pages/tenant/TenantProfilePage.tsx +701 -0
- package/src/platform/adapters/default.tsx +1 -1
- package/src/platform/types.ts +2 -0
- package/src/styles/components/apexcharts.css +101 -0
- package/src/styles/components/image-input.css +51 -0
- package/src/styles/components/leaflet.css +25 -0
- package/src/styles/components/rating.css +89 -0
- package/src/styles/components/scrollable.css +119 -0
- package/src/styles/layout.css +24 -0
- package/src/styles/layouts/sidebar-fixed.css +93 -138
- package/src/styles/themes.css +5 -5
- package/src/vite-env.d.ts +21 -0
- package/src/layouts/SettingsLayout.tsx +0 -21
- package/src/layouts/app-layout.tsx +0 -29
|
@@ -11,12 +11,11 @@ import { HeaderTopbar } from './partials/HeaderTopbar';
|
|
|
11
11
|
import { AccordionMenu, AccordionMenuGroup, AccordionMenuItem, AccordionMenuSub, AccordionMenuSubContent, AccordionMenuSubTrigger } from '../../components/ui/accordion-menu';
|
|
12
12
|
import { ScrollArea } from '../../components/ui/scroll-area';
|
|
13
13
|
import { Button } from '../../controls/Button';
|
|
14
|
-
import { Menu } from 'lucide-react';
|
|
14
|
+
import { Menu, X } from 'lucide-react';
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* NavbarSidebarLayout (demo5)
|
|
18
18
|
* Header (logo + topbar) + horizontal navbar + collapsible sidebar.
|
|
19
|
-
* Sidebar nav changes based on the active navbar section.
|
|
20
19
|
*/
|
|
21
20
|
interface NavbarSidebarLayoutProps extends BaseAppLayoutProps {
|
|
22
21
|
sidebarItems?: NavItem[];
|
|
@@ -32,7 +31,8 @@ export function NavbarSidebarLayout({
|
|
|
32
31
|
defaultSidebarCollapsed = false,
|
|
33
32
|
}: NavbarSidebarLayoutProps) {
|
|
34
33
|
const [collapsed, setCollapsed] = useState(defaultSidebarCollapsed);
|
|
35
|
-
const
|
|
34
|
+
const [mobileOpen, setMobileOpen] = useState(false);
|
|
35
|
+
const effectiveSidebarItems = sidebarItems.length > 0 ? sidebarItems : navItems;
|
|
36
36
|
|
|
37
37
|
return (
|
|
38
38
|
<div className="flex flex-col min-h-screen">
|
|
@@ -40,6 +40,15 @@ export function NavbarSidebarLayout({
|
|
|
40
40
|
<header className="flex items-center h-[70px] border-b border-border bg-background px-4 shrink-0">
|
|
41
41
|
<div className="container mx-auto flex justify-between items-center gap-4">
|
|
42
42
|
<div className="flex items-center gap-3">
|
|
43
|
+
{/* Mobile hamburger */}
|
|
44
|
+
<Button
|
|
45
|
+
variant="ghost" size="sm"
|
|
46
|
+
className="size-9 p-0 rounded-full lg:hidden"
|
|
47
|
+
onClick={() => setMobileOpen((o) => !o)}
|
|
48
|
+
aria-label="Toggle menu"
|
|
49
|
+
>
|
|
50
|
+
{mobileOpen ? <X className="size-4" /> : <Menu className="size-4" />}
|
|
51
|
+
</Button>
|
|
43
52
|
{logo && <a href={logoHref}>{logo}</a>}
|
|
44
53
|
{appName && <span className="text-sm font-medium hidden md:inline">{appName}</span>}
|
|
45
54
|
</div>
|
|
@@ -52,33 +61,69 @@ export function NavbarSidebarLayout({
|
|
|
52
61
|
|
|
53
62
|
{/* Body */}
|
|
54
63
|
<div className="flex flex-1">
|
|
55
|
-
{
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
64
|
+
{/* Desktop sidebar */}
|
|
65
|
+
<aside className={cn(
|
|
66
|
+
'shrink-0 border-e border-sidebar-border bg-sidebar transition-all hidden lg:block',
|
|
67
|
+
collapsed ? 'w-0 overflow-hidden' : 'w-60',
|
|
68
|
+
)}>
|
|
69
|
+
<div className="flex items-center justify-between px-3 py-2 border-b border-sidebar-border">
|
|
70
|
+
<Button variant="ghost" size="sm" className="size-8 p-0" onClick={() => setCollapsed((c) => !c)}>
|
|
71
|
+
<Menu className="size-4" />
|
|
72
|
+
</Button>
|
|
73
|
+
</div>
|
|
74
|
+
<ScrollArea className="py-2 px-2">
|
|
75
|
+
<AccordionMenu type="single" collapsible matchPath={(href) => !!href && currentUrl.startsWith(href)} selectedValue={currentUrl}>
|
|
76
|
+
<AccordionMenuGroup>
|
|
77
|
+
{effectiveSidebarItems.map((item, i) => (
|
|
78
|
+
item.items ? (
|
|
79
|
+
<AccordionMenuSub key={i} value={item.title}>
|
|
80
|
+
<AccordionMenuSubTrigger>{item.title}</AccordionMenuSubTrigger>
|
|
81
|
+
<AccordionMenuSubContent type="single" collapsible parentValue={item.title}>
|
|
82
|
+
{item.items.map((c, ci) => <AccordionMenuItem key={ci} value={c.href ?? c.title} asChild><a href={c.href}>{c.title}</a></AccordionMenuItem>)}
|
|
83
|
+
</AccordionMenuSubContent>
|
|
84
|
+
</AccordionMenuSub>
|
|
85
|
+
) : (
|
|
86
|
+
<AccordionMenuItem key={i} value={item.href ?? item.title} asChild><a href={item.href}>{item.title}</a></AccordionMenuItem>
|
|
87
|
+
)
|
|
88
|
+
))}
|
|
89
|
+
</AccordionMenuGroup>
|
|
90
|
+
</AccordionMenu>
|
|
91
|
+
</ScrollArea>
|
|
92
|
+
</aside>
|
|
93
|
+
|
|
94
|
+
{/* Mobile sidebar overlay */}
|
|
95
|
+
{mobileOpen && (
|
|
96
|
+
<>
|
|
97
|
+
<div className="fixed inset-0 z-30 bg-black/50 lg:hidden" onClick={() => setMobileOpen(false)} />
|
|
98
|
+
<aside className="fixed inset-y-0 start-0 z-40 w-72 flex flex-col border-e border-sidebar-border bg-sidebar lg:hidden">
|
|
99
|
+
<div className="flex items-center justify-between px-4 h-[70px] border-b border-sidebar-border shrink-0">
|
|
100
|
+
{logo && <a href={logoHref}>{logo}</a>}
|
|
101
|
+
<Button variant="ghost" size="sm" className="size-8 p-0 ml-auto" onClick={() => setMobileOpen(false)}>
|
|
102
|
+
<X className="size-4" />
|
|
103
|
+
</Button>
|
|
104
|
+
</div>
|
|
105
|
+
<ScrollArea className="flex-1 py-3 px-2">
|
|
106
|
+
<AccordionMenu type="single" collapsible matchPath={(href) => !!href && currentUrl.startsWith(href)} selectedValue={currentUrl}>
|
|
107
|
+
<AccordionMenuGroup>
|
|
108
|
+
{effectiveSidebarItems.map((item, i) => (
|
|
109
|
+
item.items ? (
|
|
110
|
+
<AccordionMenuSub key={i} value={item.title}>
|
|
111
|
+
<AccordionMenuSubTrigger>{item.title}</AccordionMenuSubTrigger>
|
|
112
|
+
<AccordionMenuSubContent type="single" collapsible parentValue={item.title}>
|
|
113
|
+
{item.items.map((c, ci) => <AccordionMenuItem key={ci} value={c.href ?? c.title} asChild><a href={c.href}>{c.title}</a></AccordionMenuItem>)}
|
|
114
|
+
</AccordionMenuSubContent>
|
|
115
|
+
</AccordionMenuSub>
|
|
116
|
+
) : (
|
|
117
|
+
<AccordionMenuItem key={i} value={item.href ?? item.title} asChild><a href={item.href}>{item.title}</a></AccordionMenuItem>
|
|
118
|
+
)
|
|
119
|
+
))}
|
|
120
|
+
</AccordionMenuGroup>
|
|
121
|
+
</AccordionMenu>
|
|
122
|
+
</ScrollArea>
|
|
123
|
+
</aside>
|
|
124
|
+
</>
|
|
81
125
|
)}
|
|
126
|
+
|
|
82
127
|
<main className="flex-1 min-w-0" role="content">
|
|
83
128
|
{showToolbar && (title || breadcrumbs.length > 0 || toolbarActions) && (
|
|
84
129
|
<Toolbar title={title} breadcrumbs={breadcrumbs} actions={toolbarActions} currentUrl={currentUrl} />
|
|
@@ -10,12 +10,11 @@ import { HeaderTopbar } from './partials/HeaderTopbar';
|
|
|
10
10
|
import { AccordionMenu, AccordionMenuGroup, AccordionMenuItem, AccordionMenuSub, AccordionMenuSubContent, AccordionMenuSubTrigger } from '../../components/ui/accordion-menu';
|
|
11
11
|
import { ScrollArea } from '../../components/ui/scroll-area';
|
|
12
12
|
import { Button } from '../../controls/Button';
|
|
13
|
-
import { Menu } from 'lucide-react';
|
|
13
|
+
import { Menu, X } from 'lucide-react';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* SidebarDualMenuLayout (demo10)
|
|
17
17
|
* Sidebar with primary icon-strip + secondary full-label panel.
|
|
18
|
-
* Same dual structure as SidebarTabsLayout but toolbar is in the sidebar header.
|
|
19
18
|
*/
|
|
20
19
|
interface SidebarDualMenuLayoutProps extends BaseAppLayoutProps {
|
|
21
20
|
primaryNavItems?: NavItem[];
|
|
@@ -33,12 +32,13 @@ export function SidebarDualMenuLayout({
|
|
|
33
32
|
}: SidebarDualMenuLayoutProps) {
|
|
34
33
|
const [collapsed, setCollapsed] = useState(defaultSidebarCollapsed);
|
|
35
34
|
const [activeSection, setActiveSection] = useState(primaryNavItems[0]?.title ?? '');
|
|
35
|
+
const [mobileOpen, setMobileOpen] = useState(false);
|
|
36
36
|
const sectionItems = primaryNavItems.find((p) => p.title === activeSection)?.items ?? navItems;
|
|
37
37
|
|
|
38
38
|
return (
|
|
39
39
|
<div className="flex min-h-screen">
|
|
40
|
-
{/*
|
|
41
|
-
<
|
|
40
|
+
{/* Desktop sidebar — always in-flow, hidden on mobile */}
|
|
41
|
+
<div className="hidden lg:flex shrink-0">
|
|
42
42
|
{/* Primary icon strip */}
|
|
43
43
|
{primaryNavItems.length > 0 && (
|
|
44
44
|
<div className="w-[70px] flex flex-col items-center py-4 gap-1 border-e border-sidebar-border bg-sidebar">
|
|
@@ -83,11 +83,54 @@ export function SidebarDualMenuLayout({
|
|
|
83
83
|
</ScrollArea>
|
|
84
84
|
{sidebarFooter && <div className="p-3 border-t border-sidebar-border">{sidebarFooter}</div>}
|
|
85
85
|
</div>
|
|
86
|
-
</
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
{/* Mobile sidebar overlay — only rendered when open */}
|
|
89
|
+
{mobileOpen && (
|
|
90
|
+
<>
|
|
91
|
+
<div className="fixed inset-0 z-30 bg-black/50 lg:hidden" onClick={() => setMobileOpen(false)} />
|
|
92
|
+
<aside className="fixed inset-y-0 start-0 z-40 w-[280px] flex flex-col border-e border-sidebar-border bg-sidebar lg:hidden">
|
|
93
|
+
<div className="flex items-center gap-2 px-4 h-[70px] border-b border-sidebar-border shrink-0">
|
|
94
|
+
{logo && <a href={logoHref}>{logo}</a>}
|
|
95
|
+
{appName && <span className="text-sm font-semibold text-sidebar-foreground">{appName}</span>}
|
|
96
|
+
<Button variant="ghost" size="sm" className="size-8 p-0 ml-auto" onClick={() => setMobileOpen(false)}>
|
|
97
|
+
<X className="size-4" />
|
|
98
|
+
</Button>
|
|
99
|
+
</div>
|
|
100
|
+
<ScrollArea className="flex-1 py-2 px-2">
|
|
101
|
+
<AccordionMenu type="single" collapsible matchPath={(href) => !!href && currentUrl.startsWith(href)} selectedValue={currentUrl}>
|
|
102
|
+
<AccordionMenuGroup>
|
|
103
|
+
{sectionItems.map((item, i) => (
|
|
104
|
+
item.items ? (
|
|
105
|
+
<AccordionMenuSub key={i} value={item.title}>
|
|
106
|
+
<AccordionMenuSubTrigger>{item.title}</AccordionMenuSubTrigger>
|
|
107
|
+
<AccordionMenuSubContent type="single" collapsible parentValue={item.title}>
|
|
108
|
+
{item.items.map((c, ci) => <AccordionMenuItem key={ci} value={c.href ?? c.title} asChild><a href={c.href}>{c.title}</a></AccordionMenuItem>)}
|
|
109
|
+
</AccordionMenuSubContent>
|
|
110
|
+
</AccordionMenuSub>
|
|
111
|
+
) : (
|
|
112
|
+
<AccordionMenuItem key={i} value={item.href ?? item.title} asChild><a href={item.href}>{item.title}</a></AccordionMenuItem>
|
|
113
|
+
)
|
|
114
|
+
))}
|
|
115
|
+
</AccordionMenuGroup>
|
|
116
|
+
</AccordionMenu>
|
|
117
|
+
</ScrollArea>
|
|
118
|
+
{sidebarFooter && <div className="p-3 border-t border-sidebar-border">{sidebarFooter}</div>}
|
|
119
|
+
</aside>
|
|
120
|
+
</>
|
|
121
|
+
)}
|
|
87
122
|
|
|
88
123
|
{/* Main */}
|
|
89
124
|
<div className="flex flex-col flex-1 min-w-0">
|
|
90
125
|
<header className="flex items-center h-[70px] border-b border-border bg-background px-4 shrink-0">
|
|
126
|
+
<Button
|
|
127
|
+
variant="ghost" size="sm"
|
|
128
|
+
className="size-9 p-0 rounded-full lg:hidden mr-2"
|
|
129
|
+
onClick={() => setMobileOpen((o) => !o)}
|
|
130
|
+
aria-label="Toggle menu"
|
|
131
|
+
>
|
|
132
|
+
{mobileOpen ? <X className="size-4" /> : <Menu className="size-4" />}
|
|
133
|
+
</Button>
|
|
91
134
|
<div className="flex-1" />
|
|
92
135
|
<HeaderTopbar user={user} unreadCount={unreadCount} settingsUrl={settingsUrl} logoutUrl={logoutUrl} onLogout={onLogout} />
|
|
93
136
|
</header>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState, type ReactNode } from 'react';
|
|
4
4
|
import { cn } from '../../lib/utils';
|
|
5
|
-
import type { BaseAppLayoutProps
|
|
5
|
+
import type { BaseAppLayoutProps } from './layout-types';
|
|
6
6
|
import type { NavItem } from '../../types/navigation';
|
|
7
7
|
import { Toolbar } from './partials/Toolbar';
|
|
8
8
|
import { Footer } from './partials/Footer';
|
|
@@ -11,6 +11,7 @@ import { AccordionMenu, AccordionMenuGroup, AccordionMenuItem, AccordionMenuSub,
|
|
|
11
11
|
import { ScrollArea } from '../../components/ui/scroll-area';
|
|
12
12
|
import { Menu, X } from 'lucide-react';
|
|
13
13
|
import { Button } from '../../controls/Button';
|
|
14
|
+
import '../../styles/layouts/sidebar-fixed.css';
|
|
14
15
|
|
|
15
16
|
interface SidebarFixedLayoutProps extends BaseAppLayoutProps {
|
|
16
17
|
showToolbar?: boolean;
|
|
@@ -95,12 +96,16 @@ export function SidebarFixedLayout({
|
|
|
95
96
|
'transition-transform lg:translate-x-0',
|
|
96
97
|
mobileOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0',
|
|
97
98
|
)}>
|
|
98
|
-
{/*
|
|
99
|
-
<div className="sidebar-header flex items-center gap-3 px-4 shrink-0 border-b border-sidebar-border">
|
|
99
|
+
{/* Logo area — same height as the fixed header */}
|
|
100
|
+
<div className="sidebar-header h-[70px] flex items-center gap-3 px-4 shrink-0 border-b border-sidebar-border overflow-hidden">
|
|
100
101
|
{logo && (
|
|
101
|
-
<a href={logoHref} className="default-logo flex items-center gap-2
|
|
102
|
-
{logo}
|
|
103
|
-
{appName &&
|
|
102
|
+
<a href={logoHref} className="default-logo flex items-center gap-2 min-w-0">
|
|
103
|
+
<span className="shrink-0">{logo}</span>
|
|
104
|
+
{appName && (
|
|
105
|
+
<span className="sidebar-app-name text-sm font-semibold text-sidebar-foreground whitespace-nowrap">
|
|
106
|
+
{appName}
|
|
107
|
+
</span>
|
|
108
|
+
)}
|
|
104
109
|
</a>
|
|
105
110
|
)}
|
|
106
111
|
</div>
|
|
@@ -116,9 +121,9 @@ export function SidebarFixedLayout({
|
|
|
116
121
|
)}
|
|
117
122
|
|
|
118
123
|
{/* Wrapper (header + content + footer) */}
|
|
119
|
-
<div className=
|
|
120
|
-
{/* Fixed header */}
|
|
121
|
-
<header className="layout-header fixed top-0 inset-x-0 z-20 flex items-center border-b border-border bg-background/95 backdrop-blur-sm px-4 gap-4">
|
|
124
|
+
<div className="layout-wrapper flex flex-col flex-1 min-h-screen">
|
|
125
|
+
{/* Fixed header — h-[70px] is the Tailwind fallback; CSS var overrides at breakpoints */}
|
|
126
|
+
<header className="layout-header h-[70px] fixed top-0 inset-x-0 z-20 flex items-center border-b border-border bg-background/95 backdrop-blur-sm px-4 gap-4">
|
|
122
127
|
<Button
|
|
123
128
|
variant="ghost" size="sm"
|
|
124
129
|
className="size-9 p-0 rounded-full lg:hidden"
|
|
@@ -147,7 +152,7 @@ export function SidebarFixedLayout({
|
|
|
147
152
|
</header>
|
|
148
153
|
|
|
149
154
|
{/* Main content */}
|
|
150
|
-
<main className="grow
|
|
155
|
+
<main className="grow" role="content">
|
|
151
156
|
{showToolbar && (title || breadcrumbs.length > 0 || toolbarActions) && (
|
|
152
157
|
<Toolbar
|
|
153
158
|
title={title}
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useState, type ReactNode } from 'react';
|
|
4
4
|
import type { BaseAppLayoutProps } from './layout-types';
|
|
5
5
|
import { Toolbar } from './partials/Toolbar';
|
|
6
6
|
import { Footer } from './partials/Footer';
|
|
7
7
|
import { HeaderTopbar } from './partials/HeaderTopbar';
|
|
8
8
|
import { AccordionMenu, AccordionMenuGroup, AccordionMenuItem, AccordionMenuSub, AccordionMenuSubContent, AccordionMenuSubTrigger } from '../../components/ui/accordion-menu';
|
|
9
9
|
import { ScrollArea } from '../../components/ui/scroll-area';
|
|
10
|
+
import { Menu, X } from 'lucide-react';
|
|
11
|
+
import { Button } from '../../controls/Button';
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* SidebarMinimalLayout (demo8)
|
|
13
15
|
* Clean single sidebar with no secondary panel. Sidebar has header + menu + footer area.
|
|
14
16
|
*/
|
|
15
17
|
interface SidebarMinimalLayoutProps extends BaseAppLayoutProps {
|
|
16
|
-
sidebarFooter?:
|
|
18
|
+
sidebarFooter?: ReactNode;
|
|
17
19
|
showToolbar?: boolean;
|
|
18
20
|
}
|
|
19
21
|
|
|
@@ -24,9 +26,12 @@ export function SidebarMinimalLayout({
|
|
|
24
26
|
onLogout, settingsUrl, logoutUrl, unreadCount = 0,
|
|
25
27
|
footerLinks = [], copyright, showToolbar = true,
|
|
26
28
|
}: SidebarMinimalLayoutProps) {
|
|
29
|
+
const [mobileOpen, setMobileOpen] = useState(false);
|
|
30
|
+
|
|
27
31
|
return (
|
|
28
32
|
<div className="flex min-h-screen">
|
|
29
|
-
|
|
33
|
+
{/* Desktop sidebar — always in-flow, hidden on mobile */}
|
|
34
|
+
<aside className="hidden lg:flex lg:w-64 flex-col border-e border-sidebar-border bg-sidebar shrink-0">
|
|
30
35
|
<div className="flex items-center gap-2 px-4 h-[70px] border-b border-sidebar-border shrink-0">
|
|
31
36
|
{logo && <a href={logoHref}>{logo}</a>}
|
|
32
37
|
{appName && <span className="text-sm font-semibold text-sidebar-foreground">{appName}</span>}
|
|
@@ -52,8 +57,51 @@ export function SidebarMinimalLayout({
|
|
|
52
57
|
{sidebarFooter && <div className="p-3 border-t border-sidebar-border">{sidebarFooter}</div>}
|
|
53
58
|
</aside>
|
|
54
59
|
|
|
60
|
+
{/* Mobile sidebar overlay — only rendered when open */}
|
|
61
|
+
{mobileOpen && (
|
|
62
|
+
<>
|
|
63
|
+
<div className="fixed inset-0 z-30 bg-black/50 lg:hidden" onClick={() => setMobileOpen(false)} />
|
|
64
|
+
<aside className="fixed inset-y-0 start-0 z-40 w-[280px] flex flex-col border-e border-sidebar-border bg-sidebar lg:hidden">
|
|
65
|
+
<div className="flex items-center gap-2 px-4 h-[70px] border-b border-sidebar-border shrink-0">
|
|
66
|
+
{logo && <a href={logoHref}>{logo}</a>}
|
|
67
|
+
{appName && <span className="text-sm font-semibold text-sidebar-foreground">{appName}</span>}
|
|
68
|
+
<Button variant="ghost" size="sm" className="size-8 p-0 ml-auto" onClick={() => setMobileOpen(false)}>
|
|
69
|
+
<X className="size-4" />
|
|
70
|
+
</Button>
|
|
71
|
+
</div>
|
|
72
|
+
<ScrollArea className="flex-1 py-3 px-2">
|
|
73
|
+
<AccordionMenu type="single" collapsible matchPath={(href) => !!href && currentUrl.startsWith(href)} selectedValue={currentUrl}>
|
|
74
|
+
<AccordionMenuGroup>
|
|
75
|
+
{navItems.map((item, i) => (
|
|
76
|
+
item.items ? (
|
|
77
|
+
<AccordionMenuSub key={i} value={item.title}>
|
|
78
|
+
<AccordionMenuSubTrigger>{item.title}</AccordionMenuSubTrigger>
|
|
79
|
+
<AccordionMenuSubContent type="single" collapsible parentValue={item.title}>
|
|
80
|
+
{item.items.map((c, ci) => <AccordionMenuItem key={ci} value={c.href ?? c.title} asChild><a href={c.href}>{c.title}</a></AccordionMenuItem>)}
|
|
81
|
+
</AccordionMenuSubContent>
|
|
82
|
+
</AccordionMenuSub>
|
|
83
|
+
) : (
|
|
84
|
+
<AccordionMenuItem key={i} value={item.href ?? item.title} asChild><a href={item.href}>{item.title}</a></AccordionMenuItem>
|
|
85
|
+
)
|
|
86
|
+
))}
|
|
87
|
+
</AccordionMenuGroup>
|
|
88
|
+
</AccordionMenu>
|
|
89
|
+
</ScrollArea>
|
|
90
|
+
{sidebarFooter && <div className="p-3 border-t border-sidebar-border">{sidebarFooter}</div>}
|
|
91
|
+
</aside>
|
|
92
|
+
</>
|
|
93
|
+
)}
|
|
94
|
+
|
|
55
95
|
<div className="flex flex-col flex-1 min-w-0">
|
|
56
96
|
<header className="flex items-center h-[70px] border-b border-border bg-background px-4 shrink-0">
|
|
97
|
+
<Button
|
|
98
|
+
variant="ghost" size="sm"
|
|
99
|
+
className="size-9 p-0 rounded-full lg:hidden mr-2"
|
|
100
|
+
onClick={() => setMobileOpen((o) => !o)}
|
|
101
|
+
aria-label="Toggle menu"
|
|
102
|
+
>
|
|
103
|
+
{mobileOpen ? <X className="size-4" /> : <Menu className="size-4" />}
|
|
104
|
+
</Button>
|
|
57
105
|
<div className="flex-1" />
|
|
58
106
|
<HeaderTopbar user={user} unreadCount={unreadCount} settingsUrl={settingsUrl} logoutUrl={logoutUrl} onLogout={onLogout} />
|
|
59
107
|
</header>
|
|
@@ -9,6 +9,8 @@ import { Footer } from './partials/Footer';
|
|
|
9
9
|
import { HeaderTopbar } from './partials/HeaderTopbar';
|
|
10
10
|
import { AccordionMenu, AccordionMenuGroup, AccordionMenuItem, AccordionMenuSub, AccordionMenuSubContent, AccordionMenuSubTrigger } from '../../components/ui/accordion-menu';
|
|
11
11
|
import { ScrollArea } from '../../components/ui/scroll-area';
|
|
12
|
+
import { Menu, X } from 'lucide-react';
|
|
13
|
+
import { Button } from '../../controls/Button';
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* SidebarTabsLayout (demo6)
|
|
@@ -28,12 +30,14 @@ export function SidebarTabsLayout({
|
|
|
28
30
|
footerLinks = [], copyright, showToolbar = true,
|
|
29
31
|
}: SidebarTabsLayoutProps) {
|
|
30
32
|
const [activeTab, setActiveTab] = useState(primaryNavItems[0]?.title ?? '');
|
|
33
|
+
const [mobileOpen, setMobileOpen] = useState(false);
|
|
31
34
|
const activeSection = primaryNavItems.find((p) => p.title === activeTab);
|
|
32
35
|
const sideItems = activeSection?.items ?? navItems;
|
|
33
36
|
|
|
34
37
|
return (
|
|
35
38
|
<div className="flex min-h-screen">
|
|
36
|
-
|
|
39
|
+
{/* Desktop sidebar — always in-flow, hidden on mobile */}
|
|
40
|
+
<div className="hidden lg:flex shrink-0">
|
|
37
41
|
{/* Tab strip */}
|
|
38
42
|
{primaryNavItems.length > 0 && (
|
|
39
43
|
<div className="w-16 flex flex-col items-center py-3 gap-1 border-e border-sidebar-border bg-sidebar">
|
|
@@ -78,10 +82,52 @@ export function SidebarTabsLayout({
|
|
|
78
82
|
</AccordionMenu>
|
|
79
83
|
</ScrollArea>
|
|
80
84
|
</div>
|
|
81
|
-
</
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
{/* Mobile sidebar overlay — only rendered when open */}
|
|
88
|
+
{mobileOpen && (
|
|
89
|
+
<>
|
|
90
|
+
<div className="fixed inset-0 z-30 bg-black/50 lg:hidden" onClick={() => setMobileOpen(false)} />
|
|
91
|
+
<aside className="fixed inset-y-0 start-0 z-40 w-[280px] flex flex-col border-e border-sidebar-border bg-sidebar lg:hidden">
|
|
92
|
+
<div className="flex items-center gap-2 px-4 h-[70px] border-b border-sidebar-border shrink-0">
|
|
93
|
+
{logo && <a href={logoHref}>{logo}</a>}
|
|
94
|
+
{appName && <span className="text-sm font-semibold">{appName}</span>}
|
|
95
|
+
<Button variant="ghost" size="sm" className="size-8 p-0 ml-auto" onClick={() => setMobileOpen(false)}>
|
|
96
|
+
<X className="size-4" />
|
|
97
|
+
</Button>
|
|
98
|
+
</div>
|
|
99
|
+
<ScrollArea className="flex-1 py-3 px-2">
|
|
100
|
+
<AccordionMenu type="single" collapsible matchPath={(href) => !!href && currentUrl.startsWith(href)} selectedValue={currentUrl}>
|
|
101
|
+
<AccordionMenuGroup>
|
|
102
|
+
{sideItems.map((item, i) => (
|
|
103
|
+
item.items ? (
|
|
104
|
+
<AccordionMenuSub key={i} value={item.title}>
|
|
105
|
+
<AccordionMenuSubTrigger>{item.title}</AccordionMenuSubTrigger>
|
|
106
|
+
<AccordionMenuSubContent type="single" collapsible parentValue={item.title}>
|
|
107
|
+
{item.items.map((c, ci) => <AccordionMenuItem key={ci} value={c.href ?? c.title} asChild><a href={c.href}>{c.title}</a></AccordionMenuItem>)}
|
|
108
|
+
</AccordionMenuSubContent>
|
|
109
|
+
</AccordionMenuSub>
|
|
110
|
+
) : (
|
|
111
|
+
<AccordionMenuItem key={i} value={item.href ?? item.title} asChild><a href={item.href}>{item.title}</a></AccordionMenuItem>
|
|
112
|
+
)
|
|
113
|
+
))}
|
|
114
|
+
</AccordionMenuGroup>
|
|
115
|
+
</AccordionMenu>
|
|
116
|
+
</ScrollArea>
|
|
117
|
+
</aside>
|
|
118
|
+
</>
|
|
119
|
+
)}
|
|
82
120
|
|
|
83
121
|
<div className="flex flex-col flex-1 min-w-0">
|
|
84
122
|
<header className="flex items-center h-[70px] border-b border-border bg-background px-4 shrink-0">
|
|
123
|
+
<Button
|
|
124
|
+
variant="ghost" size="sm"
|
|
125
|
+
className="size-9 p-0 rounded-full lg:hidden mr-2"
|
|
126
|
+
onClick={() => setMobileOpen((o) => !o)}
|
|
127
|
+
aria-label="Toggle menu"
|
|
128
|
+
>
|
|
129
|
+
{mobileOpen ? <X className="size-4" /> : <Menu className="size-4" />}
|
|
130
|
+
</Button>
|
|
85
131
|
<div className="flex-1" />
|
|
86
132
|
<HeaderTopbar user={user} unreadCount={unreadCount} settingsUrl={settingsUrl} logoutUrl={logoutUrl} onLogout={onLogout} />
|
|
87
133
|
</header>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import type
|
|
3
|
+
import { useState, type ReactNode } from 'react';
|
|
4
4
|
import { cn } from '../../lib/utils';
|
|
5
5
|
import type { BaseAppLayoutProps } from './layout-types';
|
|
6
6
|
import type { NavItem } from '../../types/navigation';
|
|
@@ -9,6 +9,8 @@ import { Footer } from './partials/Footer';
|
|
|
9
9
|
import { HeaderTopbar } from './partials/HeaderTopbar';
|
|
10
10
|
import { AccordionMenu, AccordionMenuGroup, AccordionMenuItem, AccordionMenuSub, AccordionMenuSubContent, AccordionMenuSubTrigger } from '../../components/ui/accordion-menu';
|
|
11
11
|
import { ScrollArea } from '../../components/ui/scroll-area';
|
|
12
|
+
import { Menu, X } from 'lucide-react';
|
|
13
|
+
import { Button } from '../../controls/Button';
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* SplitSidebarLayout (demo4)
|
|
@@ -31,58 +33,104 @@ export function SplitSidebarLayout({
|
|
|
31
33
|
onLogout, settingsUrl, logoutUrl, unreadCount = 0,
|
|
32
34
|
footerLinks = [], copyright, showToolbar = true,
|
|
33
35
|
}: SplitSidebarLayoutProps) {
|
|
36
|
+
const [mobileOpen, setMobileOpen] = useState(false);
|
|
34
37
|
const sideItems = secondaryItems.length > 0 ? secondaryItems : navItems;
|
|
35
38
|
|
|
36
39
|
return (
|
|
37
40
|
<div className="flex min-h-screen">
|
|
38
|
-
{/*
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
41
|
+
{/* Desktop sidebar — always in-flow, hidden on mobile */}
|
|
42
|
+
<div className="hidden lg:flex shrink-0">
|
|
43
|
+
{/* Primary sidebar (icon strip) */}
|
|
44
|
+
{primaryItems.length > 0 && (
|
|
45
|
+
<aside className="w-16 shrink-0 border-e border-sidebar-border bg-sidebar flex flex-col items-center py-3 gap-1">
|
|
46
|
+
{logo && <a href={logoHref} className="mb-3">{logo}</a>}
|
|
47
|
+
{primaryItems.map((item, i) => (
|
|
48
|
+
<button
|
|
49
|
+
key={i}
|
|
50
|
+
onClick={() => {}}
|
|
51
|
+
className={cn('w-10 h-10 flex items-center justify-center rounded-lg text-sidebar-foreground hover:bg-sidebar-accent transition-colors text-xs', activePrimary === item.title && 'bg-sidebar-accent')}
|
|
52
|
+
title={item.title}
|
|
53
|
+
>
|
|
54
|
+
{item.title.slice(0, 2)}
|
|
55
|
+
</button>
|
|
56
|
+
))}
|
|
57
|
+
</aside>
|
|
58
|
+
)}
|
|
59
|
+
|
|
60
|
+
{/* Secondary sidebar (full nav) */}
|
|
61
|
+
<aside className="w-56 shrink-0 border-e border-sidebar-border bg-sidebar flex flex-col">
|
|
62
|
+
{!primaryItems.length && logo && (
|
|
63
|
+
<div className="flex items-center gap-2 px-4 h-[70px] border-b border-sidebar-border shrink-0">
|
|
64
|
+
<a href={logoHref}>{logo}</a>
|
|
65
|
+
{appName && <span className="text-sm font-semibold">{appName}</span>}
|
|
66
|
+
</div>
|
|
67
|
+
)}
|
|
68
|
+
<ScrollArea className="flex-1 py-3 px-2">
|
|
69
|
+
<AccordionMenu type="single" collapsible matchPath={(href) => !!href && currentUrl.startsWith(href)} selectedValue={currentUrl}>
|
|
70
|
+
<AccordionMenuGroup>
|
|
71
|
+
{sideItems.map((item, i) => (
|
|
72
|
+
item.items ? (
|
|
73
|
+
<AccordionMenuSub key={i} value={item.title}>
|
|
74
|
+
<AccordionMenuSubTrigger>{item.title}</AccordionMenuSubTrigger>
|
|
75
|
+
<AccordionMenuSubContent type="single" collapsible parentValue={item.title}>
|
|
76
|
+
{item.items.map((c, ci) => <AccordionMenuItem key={ci} value={c.href ?? c.title} asChild><a href={c.href}>{c.title}</a></AccordionMenuItem>)}
|
|
77
|
+
</AccordionMenuSubContent>
|
|
78
|
+
</AccordionMenuSub>
|
|
79
|
+
) : (
|
|
80
|
+
<AccordionMenuItem key={i} value={item.href ?? item.title} asChild><a href={item.href}>{item.title}</a></AccordionMenuItem>
|
|
81
|
+
)
|
|
82
|
+
))}
|
|
83
|
+
</AccordionMenuGroup>
|
|
84
|
+
</AccordionMenu>
|
|
85
|
+
</ScrollArea>
|
|
52
86
|
</aside>
|
|
53
|
-
|
|
87
|
+
</div>
|
|
54
88
|
|
|
55
|
-
{/*
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
<div className="
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
89
|
+
{/* Mobile sidebar overlay — only rendered when open */}
|
|
90
|
+
{mobileOpen && (
|
|
91
|
+
<>
|
|
92
|
+
<div className="fixed inset-0 z-30 bg-black/50 lg:hidden" onClick={() => setMobileOpen(false)} />
|
|
93
|
+
<aside className="fixed inset-y-0 start-0 z-40 w-[280px] flex flex-col border-e border-sidebar-border bg-sidebar lg:hidden">
|
|
94
|
+
<div className="flex items-center gap-2 px-4 h-[70px] border-b border-sidebar-border shrink-0">
|
|
95
|
+
{logo && <a href={logoHref}>{logo}</a>}
|
|
96
|
+
{appName && <span className="text-sm font-semibold">{appName}</span>}
|
|
97
|
+
<Button variant="ghost" size="sm" className="size-8 p-0 ml-auto" onClick={() => setMobileOpen(false)}>
|
|
98
|
+
<X className="size-4" />
|
|
99
|
+
</Button>
|
|
100
|
+
</div>
|
|
101
|
+
<ScrollArea className="flex-1 py-3 px-2">
|
|
102
|
+
<AccordionMenu type="single" collapsible matchPath={(href) => !!href && currentUrl.startsWith(href)} selectedValue={currentUrl}>
|
|
103
|
+
<AccordionMenuGroup>
|
|
104
|
+
{sideItems.map((item, i) => (
|
|
105
|
+
item.items ? (
|
|
106
|
+
<AccordionMenuSub key={i} value={item.title}>
|
|
107
|
+
<AccordionMenuSubTrigger>{item.title}</AccordionMenuSubTrigger>
|
|
108
|
+
<AccordionMenuSubContent type="single" collapsible parentValue={item.title}>
|
|
109
|
+
{item.items.map((c, ci) => <AccordionMenuItem key={ci} value={c.href ?? c.title} asChild><a href={c.href}>{c.title}</a></AccordionMenuItem>)}
|
|
110
|
+
</AccordionMenuSubContent>
|
|
111
|
+
</AccordionMenuSub>
|
|
112
|
+
) : (
|
|
113
|
+
<AccordionMenuItem key={i} value={item.href ?? item.title} asChild><a href={item.href}>{item.title}</a></AccordionMenuItem>
|
|
114
|
+
)
|
|
115
|
+
))}
|
|
116
|
+
</AccordionMenuGroup>
|
|
117
|
+
</AccordionMenu>
|
|
118
|
+
</ScrollArea>
|
|
119
|
+
</aside>
|
|
120
|
+
</>
|
|
121
|
+
)}
|
|
82
122
|
|
|
83
123
|
{/* Main area */}
|
|
84
124
|
<div className="flex flex-col flex-1 min-w-0">
|
|
85
125
|
<header className="flex items-center h-[70px] border-b border-border bg-background px-4 shrink-0">
|
|
126
|
+
<Button
|
|
127
|
+
variant="ghost" size="sm"
|
|
128
|
+
className="size-9 p-0 rounded-full lg:hidden mr-2"
|
|
129
|
+
onClick={() => setMobileOpen((o) => !o)}
|
|
130
|
+
aria-label="Toggle menu"
|
|
131
|
+
>
|
|
132
|
+
{mobileOpen ? <X className="size-4" /> : <Menu className="size-4" />}
|
|
133
|
+
</Button>
|
|
86
134
|
<div className="flex-1" />
|
|
87
135
|
<HeaderTopbar user={user} unreadCount={unreadCount} settingsUrl={settingsUrl} logoutUrl={logoutUrl} onLogout={onLogout} />
|
|
88
136
|
</header>
|