@trackany-device/components 1.0.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 +216 -0
- 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
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared layout switcher for app stories.
|
|
3
|
+
*
|
|
4
|
+
* App layouts: add `args: { layout: 'SidebarFixed' }` + `argTypes: LAYOUT_ARG_TYPE`,
|
|
5
|
+
* then wrap content with <LayoutResolved {...args}>.
|
|
6
|
+
*
|
|
7
|
+
* Auth layouts: add `args: { authLayout: 'split' }` + `argTypes: AUTH_LAYOUT_ARG_TYPE`,
|
|
8
|
+
* then wrap content with <AuthLayoutResolved variant={args.authLayout} ...>.
|
|
9
|
+
*/
|
|
10
|
+
import React from 'react';
|
|
11
|
+
import logoUrl from '../assets/logo.png';
|
|
12
|
+
import { AuthLayout } from '@trackany-device/components';
|
|
13
|
+
import type { AuthLayoutVariant } from '@trackany-device/components';
|
|
14
|
+
import {
|
|
15
|
+
TopNavLayout,
|
|
16
|
+
SidebarFixedLayout,
|
|
17
|
+
NavbarCollapsibleLayout,
|
|
18
|
+
SplitSidebarLayout,
|
|
19
|
+
NavbarSidebarLayout,
|
|
20
|
+
SidebarTabsLayout,
|
|
21
|
+
MegaMenuLayout,
|
|
22
|
+
SidebarMinimalLayout,
|
|
23
|
+
MegaMenuNavbarLayout,
|
|
24
|
+
SidebarDualMenuLayout,
|
|
25
|
+
WorkspaceSidebarLayout,
|
|
26
|
+
} from '@trackany-device/components';
|
|
27
|
+
import type { NavItem } from '@trackany-device/components';
|
|
28
|
+
|
|
29
|
+
export type LayoutName =
|
|
30
|
+
| 'SidebarFixed'
|
|
31
|
+
| 'TopNav'
|
|
32
|
+
| 'NavbarCollapsible'
|
|
33
|
+
| 'SplitSidebar'
|
|
34
|
+
| 'NavbarSidebar'
|
|
35
|
+
| 'SidebarTabs'
|
|
36
|
+
| 'MegaMenu'
|
|
37
|
+
| 'SidebarMinimal'
|
|
38
|
+
| 'MegaMenuNavbar'
|
|
39
|
+
| 'SidebarDualMenu'
|
|
40
|
+
| 'WorkspaceSidebar';
|
|
41
|
+
|
|
42
|
+
export const LAYOUT_OPTIONS: LayoutName[] = [
|
|
43
|
+
'SidebarFixed',
|
|
44
|
+
'TopNav',
|
|
45
|
+
'NavbarCollapsible',
|
|
46
|
+
'SplitSidebar',
|
|
47
|
+
'NavbarSidebar',
|
|
48
|
+
'SidebarTabs',
|
|
49
|
+
'MegaMenu',
|
|
50
|
+
'SidebarMinimal',
|
|
51
|
+
'MegaMenuNavbar',
|
|
52
|
+
'SidebarDualMenu',
|
|
53
|
+
'WorkspaceSidebar',
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
export const LAYOUT_ARG_TYPE = {
|
|
57
|
+
layout: {
|
|
58
|
+
control: 'select' as const,
|
|
59
|
+
options: LAYOUT_OPTIONS,
|
|
60
|
+
description: 'Swap the surrounding app layout',
|
|
61
|
+
table: { category: 'Layout' },
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const DEFAULT_NAV: NavItem[] = [
|
|
66
|
+
{ title: 'Dashboard', href: '/dashboard' },
|
|
67
|
+
{ title: 'Vehicles', href: '/vehicles', items: [{ title: 'All Vehicles', href: '/vehicles' }, { title: 'Live Map', href: '/vehicles/map' }] },
|
|
68
|
+
{ title: 'Drivers', href: '/drivers' },
|
|
69
|
+
{ title: 'Trips', href: '/trips' },
|
|
70
|
+
{ title: 'Incidents', href: '/incidents' },
|
|
71
|
+
{ title: 'Alerts', href: '/alerts' },
|
|
72
|
+
{ title: 'Reports', href: '/reports' },
|
|
73
|
+
{ title: 'Settings', href: '/settings' },
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
const DEFAULT_USER = { name: 'Ahmad Faryab', email: 'ahmad@tad.io', avatar: '' };
|
|
77
|
+
|
|
78
|
+
const DEFAULT_LOGO = <img src={logoUrl} alt="Logo" className="h-8 w-auto" />;
|
|
79
|
+
|
|
80
|
+
type LayoutResolvedProps = {
|
|
81
|
+
layout?: LayoutName;
|
|
82
|
+
navItems?: NavItem[];
|
|
83
|
+
user?: typeof DEFAULT_USER;
|
|
84
|
+
title?: string;
|
|
85
|
+
currentUrl?: string;
|
|
86
|
+
logo?: React.ReactNode;
|
|
87
|
+
children: React.ReactNode;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export function LayoutResolved({
|
|
91
|
+
layout = 'SidebarFixed',
|
|
92
|
+
navItems = DEFAULT_NAV,
|
|
93
|
+
user = DEFAULT_USER,
|
|
94
|
+
title = 'Dashboard',
|
|
95
|
+
currentUrl = '/dashboard',
|
|
96
|
+
logo = DEFAULT_LOGO,
|
|
97
|
+
children,
|
|
98
|
+
}: LayoutResolvedProps) {
|
|
99
|
+
const shared = { navItems, user, title, currentUrl, appName: 'Track Any Device', logo };
|
|
100
|
+
|
|
101
|
+
switch (layout) {
|
|
102
|
+
case 'TopNav':
|
|
103
|
+
return (
|
|
104
|
+
<TopNavLayout {...shared} logoHref="/" copyright="© 2026 Track Any Device" footerLinks={[{ label: 'Support', href: '#' }]}>
|
|
105
|
+
{children}
|
|
106
|
+
</TopNavLayout>
|
|
107
|
+
);
|
|
108
|
+
case 'NavbarCollapsible':
|
|
109
|
+
return <NavbarCollapsibleLayout {...shared}>{children}</NavbarCollapsibleLayout>;
|
|
110
|
+
case 'SplitSidebar':
|
|
111
|
+
return <SplitSidebarLayout {...shared}>{children}</SplitSidebarLayout>;
|
|
112
|
+
case 'NavbarSidebar':
|
|
113
|
+
return <NavbarSidebarLayout {...shared}>{children}</NavbarSidebarLayout>;
|
|
114
|
+
case 'SidebarTabs':
|
|
115
|
+
return (
|
|
116
|
+
<SidebarTabsLayout
|
|
117
|
+
{...shared}
|
|
118
|
+
primaryNavItems={[
|
|
119
|
+
{ title: 'Fleet', href: '/fleet', items: navItems.slice(0, 4) },
|
|
120
|
+
{ title: 'Admin', href: '/admin', items: navItems.slice(4) },
|
|
121
|
+
]}
|
|
122
|
+
>
|
|
123
|
+
{children}
|
|
124
|
+
</SidebarTabsLayout>
|
|
125
|
+
);
|
|
126
|
+
case 'MegaMenu':
|
|
127
|
+
return <MegaMenuLayout {...shared}>{children}</MegaMenuLayout>;
|
|
128
|
+
case 'SidebarMinimal':
|
|
129
|
+
return <SidebarMinimalLayout {...shared}>{children}</SidebarMinimalLayout>;
|
|
130
|
+
case 'MegaMenuNavbar':
|
|
131
|
+
return <MegaMenuNavbarLayout {...shared}>{children}</MegaMenuNavbarLayout>;
|
|
132
|
+
case 'SidebarDualMenu':
|
|
133
|
+
return (
|
|
134
|
+
<SidebarDualMenuLayout
|
|
135
|
+
{...shared}
|
|
136
|
+
primaryNavItems={[
|
|
137
|
+
{ title: 'Fleet', href: '/fleet', items: navItems.slice(0, 4) },
|
|
138
|
+
{ title: 'Admin', href: '/admin', items: navItems.slice(4) },
|
|
139
|
+
]}
|
|
140
|
+
>
|
|
141
|
+
{children}
|
|
142
|
+
</SidebarDualMenuLayout>
|
|
143
|
+
);
|
|
144
|
+
case 'WorkspaceSidebar':
|
|
145
|
+
return (
|
|
146
|
+
<WorkspaceSidebarLayout
|
|
147
|
+
{...shared}
|
|
148
|
+
workspaces={[{ id: 'fleet', name: 'Track Any Device', href: '/' }, { id: 'admin', name: 'Admin', href: '/admin' }]}
|
|
149
|
+
activeWorkspace="fleet"
|
|
150
|
+
>
|
|
151
|
+
{children}
|
|
152
|
+
</WorkspaceSidebarLayout>
|
|
153
|
+
);
|
|
154
|
+
default:
|
|
155
|
+
return <SidebarFixedLayout {...shared}>{children}</SidebarFixedLayout>;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** @deprecated Use LayoutResolved */
|
|
160
|
+
export const StoryLayout = LayoutResolved;
|
|
161
|
+
|
|
162
|
+
// ── Auth layout switcher ──────────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
export const AUTH_LAYOUT_OPTIONS: AuthLayoutVariant[] = [
|
|
165
|
+
'split', 'branded', 'classic', 'centered', 'simple', 'card',
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
export const AUTH_LAYOUT_ARG_TYPE = {
|
|
169
|
+
authLayout: {
|
|
170
|
+
control: 'select' as const,
|
|
171
|
+
options: AUTH_LAYOUT_OPTIONS,
|
|
172
|
+
description: 'Swap the surrounding auth layout',
|
|
173
|
+
table: { category: 'Layout' },
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
type AuthLayoutResolvedProps = {
|
|
178
|
+
variant?: AuthLayoutVariant;
|
|
179
|
+
title?: string;
|
|
180
|
+
description?: string;
|
|
181
|
+
logo?: React.ReactNode;
|
|
182
|
+
appName?: string;
|
|
183
|
+
backgroundImage?: string;
|
|
184
|
+
backgroundImageDark?: string;
|
|
185
|
+
brandImage?: string;
|
|
186
|
+
brandImageDark?: string;
|
|
187
|
+
children: React.ReactNode;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
export function AuthLayoutResolved({
|
|
191
|
+
variant = 'split',
|
|
192
|
+
title,
|
|
193
|
+
description,
|
|
194
|
+
logo = DEFAULT_LOGO,
|
|
195
|
+
appName = 'Track Any Device',
|
|
196
|
+
backgroundImage,
|
|
197
|
+
backgroundImageDark,
|
|
198
|
+
brandImage,
|
|
199
|
+
brandImageDark,
|
|
200
|
+
children,
|
|
201
|
+
}: AuthLayoutResolvedProps) {
|
|
202
|
+
return (
|
|
203
|
+
<AuthLayout
|
|
204
|
+
variant={variant}
|
|
205
|
+
title={title}
|
|
206
|
+
description={description}
|
|
207
|
+
logo={logo}
|
|
208
|
+
appName={appName}
|
|
209
|
+
backgroundImage={backgroundImage}
|
|
210
|
+
backgroundImageDark={backgroundImageDark}
|
|
211
|
+
brandImage={brandImage}
|
|
212
|
+
brandImageDark={brandImageDark}
|
|
213
|
+
>
|
|
214
|
+
{children}
|
|
215
|
+
</AuthLayout>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** @deprecated Use AuthLayoutResolved */
|
|
220
|
+
export const AuthStoryLayout = AuthLayoutResolved;
|
|
@@ -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';
|
|
@@ -8,7 +8,7 @@ import { Toolbar } from './partials/Toolbar';
|
|
|
8
8
|
import { Footer } from './partials/Footer';
|
|
9
9
|
import { HeaderTopbar } from './partials/HeaderTopbar';
|
|
10
10
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '../../components/ui/dropdown-menu';
|
|
11
|
-
import { ChevronDown } from 'lucide-react';
|
|
11
|
+
import { ChevronDown, Menu, X } from 'lucide-react';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* MegaMenuLayout (demo7)
|
|
@@ -25,48 +25,83 @@ export function MegaMenuLayout({
|
|
|
25
25
|
onLogout, settingsUrl, logoutUrl, unreadCount = 0,
|
|
26
26
|
footerLinks = [], copyright, showToolbar = true,
|
|
27
27
|
}: MegaMenuLayoutProps) {
|
|
28
|
+
const [mobileOpen, setMobileOpen] = useState(false);
|
|
29
|
+
|
|
28
30
|
function isActive(url: string) { return !!url && url !== '#' && currentUrl.startsWith(url); }
|
|
29
31
|
function hasActiveChild(items: NavItem[]): boolean { return items.some((i) => isActive(i.href ?? '') || (i.items ? hasActiveChild(i.items) : false)); }
|
|
30
32
|
|
|
31
33
|
return (
|
|
32
34
|
<div className="flex flex-col min-h-screen">
|
|
33
|
-
<header className="sticky top-0 z-20
|
|
34
|
-
<div className="
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
<header className="sticky top-0 z-20 border-b border-border bg-background/95 backdrop-blur-sm shrink-0 relative">
|
|
36
|
+
<div className="flex items-center h-[70px] px-4">
|
|
37
|
+
<div className="container mx-auto flex items-center gap-6">
|
|
38
|
+
{logo && <a href={logoHref} className="shrink-0">{logo}</a>}
|
|
39
|
+
{appName && <span className="text-sm font-medium hidden md:inline">{appName}</span>}
|
|
37
40
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
{/* Desktop mega menu */}
|
|
42
|
+
<nav className="hidden lg:flex items-stretch flex-1 overflow-x-auto">
|
|
43
|
+
{navItems.map((item, i) => {
|
|
44
|
+
const active = isActive(item.href ?? '') || (item.items ? hasActiveChild(item.items) : false);
|
|
45
|
+
if (item.items && item.items.length > 0) {
|
|
46
|
+
return (
|
|
47
|
+
<DropdownMenu key={i}>
|
|
48
|
+
<DropdownMenuTrigger asChild>
|
|
49
|
+
<button className={cn('flex items-center gap-1.5 px-3 py-3.5 text-sm text-nowrap border-b-2 transition-colors hover:text-mono bg-transparent', active ? 'text-mono border-mono' : 'text-secondary-foreground border-transparent')}>
|
|
50
|
+
{item.title} <ChevronDown className="size-3.5" />
|
|
51
|
+
</button>
|
|
52
|
+
</DropdownMenuTrigger>
|
|
53
|
+
<DropdownMenuContent className="min-w-[200px]">
|
|
54
|
+
{item.items.map((child, ci) => (
|
|
55
|
+
<DropdownMenuItem key={ci} asChild>
|
|
56
|
+
<a href={child.href} className={cn(isActive(child.href ?? '') && 'bg-accent')}>{child.title}</a>
|
|
57
|
+
</DropdownMenuItem>
|
|
58
|
+
))}
|
|
59
|
+
</DropdownMenuContent>
|
|
60
|
+
</DropdownMenu>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
43
63
|
return (
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
{item.title} <ChevronDown className="size-3.5" />
|
|
48
|
-
</button>
|
|
49
|
-
</DropdownMenuTrigger>
|
|
50
|
-
<DropdownMenuContent className="min-w-[200px]">
|
|
51
|
-
{item.items.map((child, ci) => (
|
|
52
|
-
<DropdownMenuItem key={ci} asChild>
|
|
53
|
-
<a href={child.href} className={cn(isActive(child.href ?? '') && 'bg-accent')}>{child.title}</a>
|
|
54
|
-
</DropdownMenuItem>
|
|
55
|
-
))}
|
|
56
|
-
</DropdownMenuContent>
|
|
57
|
-
</DropdownMenu>
|
|
64
|
+
<a key={i} href={item.href} className={cn('flex items-center px-3 py-3.5 text-sm text-nowrap border-b-2 transition-colors hover:text-mono', active ? 'text-mono border-mono' : 'text-secondary-foreground border-transparent')}>
|
|
65
|
+
{item.title}
|
|
66
|
+
</a>
|
|
58
67
|
);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
})}
|
|
69
|
+
</nav>
|
|
70
|
+
|
|
71
|
+
{/* Mobile hamburger */}
|
|
72
|
+
<button
|
|
73
|
+
className="lg:hidden flex items-center justify-center size-9 rounded-md hover:bg-accent transition-colors ml-auto mr-2"
|
|
74
|
+
onClick={() => setMobileOpen((o) => !o)}
|
|
75
|
+
aria-label="Toggle navigation"
|
|
76
|
+
>
|
|
77
|
+
{mobileOpen ? <X className="size-4" /> : <Menu className="size-4" />}
|
|
78
|
+
</button>
|
|
67
79
|
|
|
68
|
-
|
|
80
|
+
<HeaderTopbar user={user} unreadCount={unreadCount} settingsUrl={settingsUrl} logoutUrl={logoutUrl} onLogout={onLogout} />
|
|
81
|
+
</div>
|
|
69
82
|
</div>
|
|
83
|
+
|
|
84
|
+
{/* Mobile menu dropdown — absolute so it overlays content without shifting layout */}
|
|
85
|
+
{mobileOpen && (
|
|
86
|
+
<div className="lg:hidden absolute top-full left-0 right-0 z-50 border-b border-border bg-background shadow-md pb-2">
|
|
87
|
+
{navItems.map((item, i) =>
|
|
88
|
+
item.items && item.items.length > 0 ? (
|
|
89
|
+
<div key={i}>
|
|
90
|
+
<div className="px-4 pt-3 pb-1 text-xs font-semibold text-muted-foreground uppercase tracking-wide">{item.title}</div>
|
|
91
|
+
{item.items.map((child, ci) => (
|
|
92
|
+
<a key={ci} href={child.href} className={cn('block px-6 py-2 text-sm hover:bg-accent transition-colors', isActive(child.href ?? '') && 'text-primary font-medium')} onClick={() => setMobileOpen(false)}>
|
|
93
|
+
{child.title}
|
|
94
|
+
</a>
|
|
95
|
+
))}
|
|
96
|
+
</div>
|
|
97
|
+
) : (
|
|
98
|
+
<a key={i} href={item.href} className={cn('block px-4 py-2.5 text-sm hover:bg-accent transition-colors', isActive(item.href ?? '') && 'text-primary font-medium')} onClick={() => setMobileOpen(false)}>
|
|
99
|
+
{item.title}
|
|
100
|
+
</a>
|
|
101
|
+
),
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
70
105
|
</header>
|
|
71
106
|
|
|
72
107
|
<main className="flex-1" role="content">
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { useState } from 'react';
|
|
3
4
|
import type { BaseAppLayoutProps } from './layout-types';
|
|
4
5
|
import type { NavItem } from '../../types/navigation';
|
|
5
6
|
import { Navbar } from './partials/Navbar';
|
|
@@ -7,7 +8,7 @@ import { Toolbar } from './partials/Toolbar';
|
|
|
7
8
|
import { Footer } from './partials/Footer';
|
|
8
9
|
import { HeaderTopbar } from './partials/HeaderTopbar';
|
|
9
10
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '../../components/ui/dropdown-menu';
|
|
10
|
-
import { ChevronDown, Search } from 'lucide-react';
|
|
11
|
+
import { ChevronDown, Menu, Search, X } from 'lucide-react';
|
|
11
12
|
import { cn } from '../../lib/utils';
|
|
12
13
|
import { Button } from '../../controls/Button';
|
|
13
14
|
|
|
@@ -27,55 +28,90 @@ export function MegaMenuNavbarLayout({
|
|
|
27
28
|
onLogout, settingsUrl, logoutUrl, unreadCount = 0,
|
|
28
29
|
footerLinks = [], copyright, showToolbar = true, onSearchOpen,
|
|
29
30
|
}: MegaMenuNavbarLayoutProps) {
|
|
31
|
+
const [mobileOpen, setMobileOpen] = useState(false);
|
|
32
|
+
|
|
30
33
|
function isActive(url: string) { return !!url && url !== '#' && currentUrl.startsWith(url); }
|
|
31
34
|
function hasActiveChild(items: NavItem[]): boolean { return items.some((i) => isActive(i.href ?? '') || (i.items ? hasActiveChild(i.items) : false)); }
|
|
32
35
|
|
|
33
|
-
// Split navItems: top-level as mega menu triggers, sub-items go in dropdown
|
|
34
|
-
const megaItems = navItems;
|
|
35
|
-
|
|
36
36
|
return (
|
|
37
37
|
<div className="flex flex-col min-h-screen">
|
|
38
38
|
{/* Header with mega menu */}
|
|
39
|
-
<header className="sticky top-0 z-20
|
|
40
|
-
<div className="
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
<header className="sticky top-0 z-20 border-b border-border bg-background/95 backdrop-blur-sm shrink-0 relative">
|
|
40
|
+
<div className="flex items-center h-[70px] px-4">
|
|
41
|
+
<div className="container mx-auto flex items-center gap-4">
|
|
42
|
+
{logo && <a href={logoHref} className="shrink-0">{logo}</a>}
|
|
43
|
+
{appName && <span className="text-sm font-medium hidden md:inline">{appName}</span>}
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
{/* Desktop mega menu */}
|
|
46
|
+
<nav className="hidden lg:flex items-stretch flex-1 overflow-x-auto">
|
|
47
|
+
{navItems.map((item, i) => {
|
|
48
|
+
const active = isActive(item.href ?? '') || (item.items ? hasActiveChild(item.items) : false);
|
|
49
|
+
if (item.items && item.items.length > 0) {
|
|
50
|
+
return (
|
|
51
|
+
<DropdownMenu key={i}>
|
|
52
|
+
<DropdownMenuTrigger asChild>
|
|
53
|
+
<button className={cn('flex items-center gap-1 px-3 py-3.5 text-sm border-b-2 transition-colors hover:text-mono bg-transparent text-nowrap', active ? 'text-mono border-mono' : 'text-secondary-foreground border-transparent')}>
|
|
54
|
+
{item.title} <ChevronDown className="size-3" />
|
|
55
|
+
</button>
|
|
56
|
+
</DropdownMenuTrigger>
|
|
57
|
+
<DropdownMenuContent className="min-w-[180px]">
|
|
58
|
+
{item.items.map((c, ci) => <DropdownMenuItem key={ci} asChild><a href={c.href}>{c.title}</a></DropdownMenuItem>)}
|
|
59
|
+
</DropdownMenuContent>
|
|
60
|
+
</DropdownMenu>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
49
63
|
return (
|
|
50
|
-
<
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
{item.title} <ChevronDown className="size-3" />
|
|
54
|
-
</button>
|
|
55
|
-
</DropdownMenuTrigger>
|
|
56
|
-
<DropdownMenuContent className="min-w-[180px]">
|
|
57
|
-
{item.items.map((c, ci) => <DropdownMenuItem key={ci} asChild><a href={c.href}>{c.title}</a></DropdownMenuItem>)}
|
|
58
|
-
</DropdownMenuContent>
|
|
59
|
-
</DropdownMenu>
|
|
64
|
+
<a key={i} href={item.href} className={cn('flex items-center px-3 py-3.5 text-sm border-b-2 transition-colors hover:text-mono text-nowrap', active ? 'text-mono border-mono' : 'text-secondary-foreground border-transparent')}>
|
|
65
|
+
{item.title}
|
|
66
|
+
</a>
|
|
60
67
|
);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
})}
|
|
69
|
+
</nav>
|
|
70
|
+
|
|
71
|
+
{/* Mobile hamburger */}
|
|
72
|
+
<button
|
|
73
|
+
className="lg:hidden flex items-center justify-center size-9 rounded-md hover:bg-accent transition-colors ml-auto mr-1"
|
|
74
|
+
onClick={() => setMobileOpen((o) => !o)}
|
|
75
|
+
aria-label="Toggle navigation"
|
|
76
|
+
>
|
|
77
|
+
{mobileOpen ? <X className="size-4" /> : <Menu className="size-4" />}
|
|
78
|
+
</button>
|
|
69
79
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
80
|
+
{onSearchOpen && (
|
|
81
|
+
<Button variant="ghost" size="sm" className="size-9 p-0 rounded-full hidden lg:flex" onClick={onSearchOpen}>
|
|
82
|
+
<Search className="size-4" />
|
|
83
|
+
</Button>
|
|
84
|
+
)}
|
|
85
|
+
<HeaderTopbar user={user} unreadCount={unreadCount} settingsUrl={settingsUrl} logoutUrl={logoutUrl} onLogout={onLogout} />
|
|
86
|
+
</div>
|
|
76
87
|
</div>
|
|
88
|
+
|
|
89
|
+
{/* Mobile menu dropdown — absolute so it overlays content without shifting layout */}
|
|
90
|
+
{mobileOpen && (
|
|
91
|
+
<div className="lg:hidden absolute top-full left-0 right-0 z-50 border-b border-border bg-background shadow-md pb-2">
|
|
92
|
+
{navItems.map((item, i) =>
|
|
93
|
+
item.items && item.items.length > 0 ? (
|
|
94
|
+
<div key={i}>
|
|
95
|
+
<div className="px-4 pt-3 pb-1 text-xs font-semibold text-muted-foreground uppercase tracking-wide">{item.title}</div>
|
|
96
|
+
{item.items.map((child, ci) => (
|
|
97
|
+
<a key={ci} href={child.href} className={cn('block px-6 py-2 text-sm hover:bg-accent transition-colors', isActive(child.href ?? '') && 'text-primary font-medium')} onClick={() => setMobileOpen(false)}>
|
|
98
|
+
{child.title}
|
|
99
|
+
</a>
|
|
100
|
+
))}
|
|
101
|
+
</div>
|
|
102
|
+
) : (
|
|
103
|
+
<a key={i} href={item.href} className={cn('block px-4 py-2.5 text-sm hover:bg-accent transition-colors', isActive(item.href ?? '') && 'text-primary font-medium')} onClick={() => setMobileOpen(false)}>
|
|
104
|
+
{item.title}
|
|
105
|
+
</a>
|
|
106
|
+
),
|
|
107
|
+
)}
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
77
110
|
</header>
|
|
78
111
|
|
|
112
|
+
{/* Secondary navbar (desktop only — already hidden on mobile via Navbar itself) */}
|
|
113
|
+
<Navbar navItems={navItems} currentUrl={currentUrl} className="hidden lg:block" />
|
|
114
|
+
|
|
79
115
|
<main className="flex-1" role="content">
|
|
80
116
|
{showToolbar && (title || breadcrumbs.length > 0 || toolbarActions) && (
|
|
81
117
|
<Toolbar title={title} breadcrumbs={breadcrumbs} actions={toolbarActions} currentUrl={currentUrl} />
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState
|
|
3
|
+
import { useState } from 'react';
|
|
4
4
|
import { cn } from '../../lib/utils';
|
|
5
5
|
import type { BaseAppLayoutProps } from './layout-types';
|
|
6
6
|
import { Navbar } from './partials/Navbar';
|
|
@@ -16,7 +16,6 @@ import type { NavItem } from '../../types/navigation';
|
|
|
16
16
|
/**
|
|
17
17
|
* NavbarCollapsibleLayout (demo3)
|
|
18
18
|
* Header (logo + topbar) + horizontal navbar + optional collapsible sidebar.
|
|
19
|
-
* Sidebar contains section-specific sub-navigation driven by the active navbar item.
|
|
20
19
|
*/
|
|
21
20
|
interface NavbarCollapsibleLayoutProps extends BaseAppLayoutProps {
|
|
22
21
|
sidebarItems?: NavItem[];
|
|
@@ -32,6 +31,8 @@ export function NavbarCollapsibleLayout({
|
|
|
32
31
|
defaultSidebarCollapsed = false,
|
|
33
32
|
}: NavbarCollapsibleLayoutProps) {
|
|
34
33
|
const [sidebarOpen, setSidebarOpen] = useState(!defaultSidebarCollapsed);
|
|
34
|
+
const [mobileOpen, setMobileOpen] = useState(false);
|
|
35
|
+
const hasSidebar = sidebarItems.length > 0;
|
|
35
36
|
|
|
36
37
|
return (
|
|
37
38
|
<div className="flex flex-col min-h-screen">
|
|
@@ -39,6 +40,17 @@ export function NavbarCollapsibleLayout({
|
|
|
39
40
|
<header className="flex items-center h-[70px] border-b border-border bg-background px-4 shrink-0">
|
|
40
41
|
<div className="container mx-auto flex justify-between items-center gap-4">
|
|
41
42
|
<div className="flex items-center gap-3">
|
|
43
|
+
{/* Mobile hamburger — only when sidebar is present */}
|
|
44
|
+
{hasSidebar && (
|
|
45
|
+
<Button
|
|
46
|
+
variant="ghost" size="sm"
|
|
47
|
+
className="size-9 p-0 rounded-full lg:hidden"
|
|
48
|
+
onClick={() => setMobileOpen((o) => !o)}
|
|
49
|
+
aria-label="Toggle menu"
|
|
50
|
+
>
|
|
51
|
+
{mobileOpen ? <X className="size-4" /> : <Menu className="size-4" />}
|
|
52
|
+
</Button>
|
|
53
|
+
)}
|
|
42
54
|
{logo && <a href={logoHref}>{logo}</a>}
|
|
43
55
|
{appName && <span className="text-sm font-medium hidden md:inline">{appName}</span>}
|
|
44
56
|
</div>
|
|
@@ -51,9 +63,13 @@ export function NavbarCollapsibleLayout({
|
|
|
51
63
|
|
|
52
64
|
{/* Body: optional sidebar + content */}
|
|
53
65
|
<div className="flex flex-1">
|
|
54
|
-
{
|
|
66
|
+
{hasSidebar && (
|
|
55
67
|
<>
|
|
56
|
-
|
|
68
|
+
{/* Desktop sidebar */}
|
|
69
|
+
<aside className={cn(
|
|
70
|
+
'w-64 shrink-0 border-e border-border bg-sidebar hidden lg:block',
|
|
71
|
+
!sidebarOpen && 'hidden',
|
|
72
|
+
)}>
|
|
57
73
|
<ScrollArea className="h-full py-3 px-2">
|
|
58
74
|
<AccordionMenu type="single" collapsible matchPath={(href) => !!href && currentUrl.startsWith(href)} selectedValue={currentUrl}>
|
|
59
75
|
<AccordionMenuGroup>
|
|
@@ -73,6 +89,39 @@ export function NavbarCollapsibleLayout({
|
|
|
73
89
|
</AccordionMenu>
|
|
74
90
|
</ScrollArea>
|
|
75
91
|
</aside>
|
|
92
|
+
|
|
93
|
+
{/* Mobile sidebar overlay */}
|
|
94
|
+
{mobileOpen && (
|
|
95
|
+
<>
|
|
96
|
+
<div className="fixed inset-0 z-30 bg-black/50 lg:hidden" onClick={() => setMobileOpen(false)} />
|
|
97
|
+
<aside className="fixed inset-y-0 start-0 z-40 w-72 flex flex-col border-e border-sidebar-border bg-sidebar lg:hidden">
|
|
98
|
+
<div className="flex items-center justify-between px-4 h-[70px] border-b border-sidebar-border shrink-0">
|
|
99
|
+
{logo && <a href={logoHref}>{logo}</a>}
|
|
100
|
+
<Button variant="ghost" size="sm" className="size-8 p-0 ml-auto" onClick={() => setMobileOpen(false)}>
|
|
101
|
+
<X className="size-4" />
|
|
102
|
+
</Button>
|
|
103
|
+
</div>
|
|
104
|
+
<ScrollArea className="flex-1 py-3 px-2">
|
|
105
|
+
<AccordionMenu type="single" collapsible matchPath={(href) => !!href && currentUrl.startsWith(href)} selectedValue={currentUrl}>
|
|
106
|
+
<AccordionMenuGroup>
|
|
107
|
+
{sidebarItems.map((item, i) => (
|
|
108
|
+
item.items ? (
|
|
109
|
+
<AccordionMenuSub key={i} value={item.title}>
|
|
110
|
+
<AccordionMenuSubTrigger>{item.title}</AccordionMenuSubTrigger>
|
|
111
|
+
<AccordionMenuSubContent type="single" collapsible parentValue={item.title}>
|
|
112
|
+
{item.items.map((c, ci) => <AccordionMenuItem key={ci} value={c.href ?? c.title} asChild><a href={c.href}>{c.title}</a></AccordionMenuItem>)}
|
|
113
|
+
</AccordionMenuSubContent>
|
|
114
|
+
</AccordionMenuSub>
|
|
115
|
+
) : (
|
|
116
|
+
<AccordionMenuItem key={i} value={item.href ?? item.title} asChild><a href={item.href}>{item.title}</a></AccordionMenuItem>
|
|
117
|
+
)
|
|
118
|
+
))}
|
|
119
|
+
</AccordionMenuGroup>
|
|
120
|
+
</AccordionMenu>
|
|
121
|
+
</ScrollArea>
|
|
122
|
+
</aside>
|
|
123
|
+
</>
|
|
124
|
+
)}
|
|
76
125
|
</>
|
|
77
126
|
)}
|
|
78
127
|
<main className="flex-1 min-w-0" role="content">
|