@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.
Files changed (159) hide show
  1. package/README.md +9 -9
  2. package/package.json +133 -4
  3. package/src/assets/index.ts +120 -0
  4. package/src/assets/media/avatars/300-1.png +0 -0
  5. package/src/assets/media/avatars/300-10.png +0 -0
  6. package/src/assets/media/avatars/300-11.png +0 -0
  7. package/src/assets/media/avatars/300-12.png +0 -0
  8. package/src/assets/media/avatars/300-13.png +0 -0
  9. package/src/assets/media/avatars/300-14.png +0 -0
  10. package/src/assets/media/avatars/300-15.png +0 -0
  11. package/src/assets/media/avatars/300-16.png +0 -0
  12. package/src/assets/media/avatars/300-17.png +0 -0
  13. package/src/assets/media/avatars/300-18.png +0 -0
  14. package/src/assets/media/avatars/300-19.png +0 -0
  15. package/src/assets/media/avatars/300-2.png +0 -0
  16. package/src/assets/media/avatars/300-20.png +0 -0
  17. package/src/assets/media/avatars/300-21.png +0 -0
  18. package/src/assets/media/avatars/300-22.png +0 -0
  19. package/src/assets/media/avatars/300-23.png +0 -0
  20. package/src/assets/media/avatars/300-24.png +0 -0
  21. package/src/assets/media/avatars/300-25.png +0 -0
  22. package/src/assets/media/avatars/300-26.png +0 -0
  23. package/src/assets/media/avatars/300-27.png +0 -0
  24. package/src/assets/media/avatars/300-28.png +0 -0
  25. package/src/assets/media/avatars/300-29.png +0 -0
  26. package/src/assets/media/avatars/300-3.png +0 -0
  27. package/src/assets/media/avatars/300-30.png +0 -0
  28. package/src/assets/media/avatars/300-31.png +0 -0
  29. package/src/assets/media/avatars/300-32.png +0 -0
  30. package/src/assets/media/avatars/300-33.png +0 -0
  31. package/src/assets/media/avatars/300-34.png +0 -0
  32. package/src/assets/media/avatars/300-4.png +0 -0
  33. package/src/assets/media/avatars/300-5.png +0 -0
  34. package/src/assets/media/avatars/300-6.png +0 -0
  35. package/src/assets/media/avatars/300-7.png +0 -0
  36. package/src/assets/media/avatars/300-8.png +0 -0
  37. package/src/assets/media/avatars/300-9.png +0 -0
  38. package/src/assets/media/avatars/blank.png +0 -0
  39. package/src/assets/media/avatars/gray/1.png +0 -0
  40. package/src/assets/media/avatars/gray/2.png +0 -0
  41. package/src/assets/media/avatars/gray/3.png +0 -0
  42. package/src/assets/media/avatars/gray/4.png +0 -0
  43. package/src/assets/media/avatars/gray/5.png +0 -0
  44. package/src/assets/media/illustrations/1-dark.svg +78 -0
  45. package/src/assets/media/illustrations/1.svg +78 -0
  46. package/src/assets/media/illustrations/10-dark.svg +148 -0
  47. package/src/assets/media/illustrations/10.svg +148 -0
  48. package/src/assets/media/illustrations/11-dark.svg +234 -0
  49. package/src/assets/media/illustrations/11.svg +234 -0
  50. package/src/assets/media/illustrations/12.svg +138 -0
  51. package/src/assets/media/illustrations/13.svg +205 -0
  52. package/src/assets/media/illustrations/14.svg +259 -0
  53. package/src/assets/media/illustrations/15.svg +242 -0
  54. package/src/assets/media/illustrations/16.svg +128 -0
  55. package/src/assets/media/illustrations/17.svg +180 -0
  56. package/src/assets/media/illustrations/18-dark.svg +6 -0
  57. package/src/assets/media/illustrations/18.svg +6 -0
  58. package/src/assets/media/illustrations/19-dark.svg +8 -0
  59. package/src/assets/media/illustrations/19.svg +8 -0
  60. package/src/assets/media/illustrations/2-dark.svg +78 -0
  61. package/src/assets/media/illustrations/2.svg +78 -0
  62. package/src/assets/media/illustrations/20-dark.svg +13 -0
  63. package/src/assets/media/illustrations/20.svg +13 -0
  64. package/src/assets/media/illustrations/21-dark.svg +9 -0
  65. package/src/assets/media/illustrations/21.svg +9 -0
  66. package/src/assets/media/illustrations/22-dark.svg +17 -0
  67. package/src/assets/media/illustrations/22.svg +17 -0
  68. package/src/assets/media/illustrations/23-dark.svg +13 -0
  69. package/src/assets/media/illustrations/23.svg +13 -0
  70. package/src/assets/media/illustrations/24.svg +6 -0
  71. package/src/assets/media/illustrations/25.svg +8 -0
  72. package/src/assets/media/illustrations/26.svg +8 -0
  73. package/src/assets/media/illustrations/27.svg +6 -0
  74. package/src/assets/media/illustrations/28-dark.svg +28 -0
  75. package/src/assets/media/illustrations/28.svg +14 -0
  76. package/src/assets/media/illustrations/29-dark.svg +6 -0
  77. package/src/assets/media/illustrations/29.svg +6 -0
  78. package/src/assets/media/illustrations/3-dark.svg +70 -0
  79. package/src/assets/media/illustrations/3.svg +70 -0
  80. package/src/assets/media/illustrations/30-dark.svg +8 -0
  81. package/src/assets/media/illustrations/30.svg +8 -0
  82. package/src/assets/media/illustrations/31-dark.svg +9 -0
  83. package/src/assets/media/illustrations/31.svg +9 -0
  84. package/src/assets/media/illustrations/32-dark.svg +10 -0
  85. package/src/assets/media/illustrations/32.svg +10 -0
  86. package/src/assets/media/illustrations/33-dark.svg +15 -0
  87. package/src/assets/media/illustrations/33.svg +15 -0
  88. package/src/assets/media/illustrations/34-dark.svg +5 -0
  89. package/src/assets/media/illustrations/34.svg +5 -0
  90. package/src/assets/media/illustrations/35-dark.svg +11 -0
  91. package/src/assets/media/illustrations/35.svg +4 -0
  92. package/src/assets/media/illustrations/4-dark.svg +51 -0
  93. package/src/assets/media/illustrations/4.svg +51 -0
  94. package/src/assets/media/illustrations/5-dark.svg +78 -0
  95. package/src/assets/media/illustrations/5.svg +78 -0
  96. package/src/assets/media/illustrations/6.svg +58 -0
  97. package/src/assets/media/illustrations/7.svg +49 -0
  98. package/src/assets/media/illustrations/8.svg +61 -0
  99. package/src/assets/media/illustrations/9.svg +57 -0
  100. package/src/assets/media/misc/placeholder.svg +15 -0
  101. package/src/components/devices/devices-mini-map.tsx +32 -26
  102. package/src/components/devices/map-marker.tsx +98 -0
  103. package/src/components/ui/checklist-item.tsx +55 -0
  104. package/src/components/ui/plan-card.tsx +68 -0
  105. package/src/components/ui/settings-row.tsx +32 -0
  106. package/src/components/ui/settings-section.tsx +22 -0
  107. package/src/components/ui/usage-meter.tsx +35 -0
  108. package/src/index.ts +12 -1
  109. package/src/layouts/LayoutSwitcher.tsx +220 -0
  110. package/src/layouts/app/MegaMenuLayout.tsx +69 -34
  111. package/src/layouts/app/MegaMenuNavbarLayout.tsx +73 -37
  112. package/src/layouts/app/NavbarCollapsibleLayout.tsx +53 -4
  113. package/src/layouts/app/NavbarSidebarLayout.tsx +74 -29
  114. package/src/layouts/app/SidebarDualMenuLayout.tsx +48 -5
  115. package/src/layouts/app/SidebarFixedLayout.tsx +15 -10
  116. package/src/layouts/app/SidebarMinimalLayout.tsx +51 -3
  117. package/src/layouts/app/SidebarTabsLayout.tsx +48 -2
  118. package/src/layouts/app/SplitSidebarLayout.tsx +91 -43
  119. package/src/layouts/app/TopNavLayout.tsx +7 -12
  120. package/src/layouts/app/WorkspaceSidebarLayout.tsx +103 -46
  121. package/src/layouts/app/partials/Navbar.tsx +61 -10
  122. package/src/layouts/app/partials/Toolbar.tsx +1 -1
  123. package/src/layouts/auth/AuthCenteredLayout.tsx +10 -4
  124. package/src/lib/map-markers.ts +21 -3
  125. package/src/pages/login/ConfirmPasswordPage.tsx +35 -0
  126. package/src/pages/login/ForgotPasswordPage.tsx +41 -0
  127. package/src/pages/login/LoginPage.tsx +50 -0
  128. package/src/pages/login/RegisterPage.tsx +41 -0
  129. package/src/pages/login/ResetPasswordPage.tsx +35 -0
  130. package/src/pages/login/TwoFactorChallengePage.tsx +41 -0
  131. package/src/pages/login/VerifyEmailPage.tsx +31 -0
  132. package/src/pages/my/ActivityPage.tsx +160 -0
  133. package/src/pages/my/GetStartedPage.tsx +221 -0
  134. package/src/pages/my/NotificationsPage.tsx +133 -0
  135. package/src/pages/my/ProfilePage.tsx +650 -0
  136. package/src/pages/my/TenantsPage.tsx +37 -0
  137. package/src/pages/tenant/AssigneesPage.tsx +155 -0
  138. package/src/pages/tenant/BeatsPage.tsx +403 -0
  139. package/src/pages/tenant/DashboardPage.tsx +195 -0
  140. package/src/pages/tenant/GeofencePage.tsx +422 -0
  141. package/src/pages/tenant/IncidentsPage.tsx +214 -0
  142. package/src/pages/tenant/IntegrationsPage.tsx +352 -0
  143. package/src/pages/tenant/InvitePage.tsx +153 -0
  144. package/src/pages/tenant/LiveStreamPage.tsx +141 -0
  145. package/src/pages/tenant/MembersPage.tsx +414 -0
  146. package/src/pages/tenant/TenantProfilePage.tsx +701 -0
  147. package/src/platform/adapters/default.tsx +1 -1
  148. package/src/platform/types.ts +2 -0
  149. package/src/styles/components/apexcharts.css +101 -0
  150. package/src/styles/components/image-input.css +51 -0
  151. package/src/styles/components/leaflet.css +25 -0
  152. package/src/styles/components/rating.css +89 -0
  153. package/src/styles/components/scrollable.css +119 -0
  154. package/src/styles/layout.css +24 -0
  155. package/src/styles/layouts/sidebar-fixed.css +93 -138
  156. package/src/styles/themes.css +5 -5
  157. package/src/vite-env.d.ts +21 -0
  158. package/src/layouts/SettingsLayout.tsx +0 -21
  159. 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 effectiveSidebarItems = sidebarItems.length > 0 ? sidebarItems : [];
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
- {effectiveSidebarItems.length > 0 && (
56
- <aside className={cn('shrink-0 border-e border-sidebar-border bg-sidebar transition-all', collapsed ? 'w-0 overflow-hidden' : 'w-60')}>
57
- <div className="flex items-center justify-between px-3 py-2 border-b border-sidebar-border">
58
- <Button variant="ghost" size="sm" className="size-8 p-0" onClick={() => setCollapsed((c) => !c)}>
59
- <Menu className="size-4" />
60
- </Button>
61
- </div>
62
- <ScrollArea className="py-2 px-2">
63
- <AccordionMenu type="single" collapsible matchPath={(href) => !!href && currentUrl.startsWith(href)} selectedValue={currentUrl}>
64
- <AccordionMenuGroup>
65
- {effectiveSidebarItems.map((item, i) => (
66
- item.items ? (
67
- <AccordionMenuSub key={i} value={item.title}>
68
- <AccordionMenuSubTrigger>{item.title}</AccordionMenuSubTrigger>
69
- <AccordionMenuSubContent type="single" collapsible parentValue={item.title}>
70
- {item.items.map((c, ci) => <AccordionMenuItem key={ci} value={c.href ?? c.title} asChild><a href={c.href}>{c.title}</a></AccordionMenuItem>)}
71
- </AccordionMenuSubContent>
72
- </AccordionMenuSub>
73
- ) : (
74
- <AccordionMenuItem key={i} value={item.href ?? item.title} asChild><a href={item.href}>{item.title}</a></AccordionMenuItem>
75
- )
76
- ))}
77
- </AccordionMenuGroup>
78
- </AccordionMenu>
79
- </ScrollArea>
80
- </aside>
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
- {/* Sidebar */}
41
- <aside className="flex shrink-0">
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
- </aside>
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, AppLayoutUser } from './layout-types';
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
- {/* Sidebar header / logo */}
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 overflow-hidden">
102
- {logo}
103
- {appName && <span className="text-sm font-semibold text-sidebar-foreground whitespace-nowrap">{appName}</span>}
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={cn('layout-wrapper flex flex-col flex-1 min-h-screen')}>
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 pt-5" role="content">
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 { cn } from '../../lib/utils';
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?: React.ReactNode;
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
- <aside className="w-64 shrink-0 flex flex-col border-e border-sidebar-border bg-sidebar">
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
- <aside className="flex shrink-0">
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
- </aside>
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 { ReactNode } from 'react';
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
- {/* Primary sidebar (icon strip) */}
39
- {primaryItems.length > 0 && (
40
- <aside className="w-16 shrink-0 border-e border-sidebar-border bg-sidebar flex flex-col items-center py-3 gap-1">
41
- {logo && <a href={logoHref} className="mb-3">{logo}</a>}
42
- {primaryItems.map((item, i) => (
43
- <button
44
- key={i}
45
- onClick={() => {}}
46
- 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')}
47
- title={item.title}
48
- >
49
- {item.title.slice(0, 2)}
50
- </button>
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
- {/* Secondary sidebar (full nav) */}
56
- <aside className="w-56 shrink-0 border-e border-sidebar-border bg-sidebar flex flex-col">
57
- {!primaryItems.length && logo && (
58
- <div className="flex items-center gap-2 px-4 h-[70px] border-b border-sidebar-border shrink-0">
59
- <a href={logoHref}>{logo}</a>
60
- {appName && <span className="text-sm font-semibold">{appName}</span>}
61
- </div>
62
- )}
63
- <ScrollArea className="flex-1 py-3 px-2">
64
- <AccordionMenu type="single" collapsible matchPath={(href) => !!href && currentUrl.startsWith(href)} selectedValue={currentUrl}>
65
- <AccordionMenuGroup>
66
- {sideItems.map((item, i) => (
67
- item.items ? (
68
- <AccordionMenuSub key={i} value={item.title}>
69
- <AccordionMenuSubTrigger>{item.title}</AccordionMenuSubTrigger>
70
- <AccordionMenuSubContent type="single" collapsible parentValue={item.title}>
71
- {item.items.map((c, ci) => <AccordionMenuItem key={ci} value={c.href ?? c.title} asChild><a href={c.href}>{c.title}</a></AccordionMenuItem>)}
72
- </AccordionMenuSubContent>
73
- </AccordionMenuSub>
74
- ) : (
75
- <AccordionMenuItem key={i} value={item.href ?? item.title} asChild><a href={item.href}>{item.title}</a></AccordionMenuItem>
76
- )
77
- ))}
78
- </AccordionMenuGroup>
79
- </AccordionMenu>
80
- </ScrollArea>
81
- </aside>
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>