@trackany-device/components 1.0.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/package.json +185 -0
- package/src/assets/logo.png +0 -0
- package/src/assets/map/arrows/map-arrow-blue.png +0 -0
- package/src/assets/map/arrows/map-arrow-green.png +0 -0
- package/src/assets/map/arrows/map-arrow-purple.png +0 -0
- package/src/assets/map/arrows/map-arrow-red.png +0 -0
- package/src/assets/map/flags/flag-blue.png +0 -0
- package/src/assets/map/flags/flag-green.png +0 -0
- package/src/assets/map/flags/flag-red.png +0 -0
- package/src/assets/map/flags/flag-yellow.png +0 -0
- package/src/assets/map/pins/map-pin-blue.png +0 -0
- package/src/assets/map/pins/map-pin-green.png +0 -0
- package/src/assets/map/pins/map-pin-purple.png +0 -0
- package/src/assets/map/pins/map-pin-red.png +0 -0
- package/src/components/Card.tsx +9 -0
- package/src/components/alert-error.tsx +24 -0
- package/src/components/app-content.tsx +22 -0
- package/src/components/app-header.tsx +153 -0
- package/src/components/app-logo-icon.tsx +13 -0
- package/src/components/app-logo.tsx +21 -0
- package/src/components/app-shell.tsx +19 -0
- package/src/components/app-sidebar-header.tsx +68 -0
- package/src/components/app-sidebar.tsx +106 -0
- package/src/components/appearance-tabs.tsx +46 -0
- package/src/components/breadcrumbs.tsx +50 -0
- package/src/components/cms/blurred-image.tsx +111 -0
- package/src/components/cms/section-bg.tsx +473 -0
- package/src/components/cms/section-button.tsx +127 -0
- package/src/components/cms/sections/banner-5050-section.tsx +135 -0
- package/src/components/cms/sections/blogs-listing-section.tsx +270 -0
- package/src/components/cms/sections/cards-grid-section.tsx +185 -0
- package/src/components/cms/sections/contact-form-section.tsx +157 -0
- package/src/components/cms/sections/cta-section.tsx +101 -0
- package/src/components/cms/sections/featured-blog-slider-section.tsx +256 -0
- package/src/components/cms/sections/featured-products-grid-section.tsx +173 -0
- package/src/components/cms/sections/featured-solutions-grid-section.tsx +183 -0
- package/src/components/cms/sections/hero-section.tsx +180 -0
- package/src/components/cms/sections/solutions-with-filter-section.tsx +234 -0
- package/src/components/cms/sections/text-section.tsx +77 -0
- package/src/components/cutout-image.tsx +228 -0
- package/src/components/devices/devices-mini-map.tsx +275 -0
- package/src/components/docs/docs-shell.tsx +280 -0
- package/src/components/fleet-hero-animated.tsx +383 -0
- package/src/components/input-error.tsx +17 -0
- package/src/components/keenicons/assets/duotone/Read Me.txt +7 -0
- package/src/components/keenicons/assets/duotone/demo-files/demo.css +160 -0
- package/src/components/keenicons/assets/duotone/demo-files/demo.js +32 -0
- package/src/components/keenicons/assets/duotone/demo.html +12424 -0
- package/src/components/keenicons/assets/duotone/fonts/keenicons-duotone.svg +1109 -0
- package/src/components/keenicons/assets/duotone/fonts/keenicons-duotone.ttf +0 -0
- package/src/components/keenicons/assets/duotone/fonts/keenicons-duotone.woff +0 -0
- package/src/components/keenicons/assets/duotone/selection.json +17313 -0
- package/src/components/keenicons/assets/duotone/style.css +4931 -0
- package/src/components/keenicons/assets/filled/Read Me.txt +7 -0
- package/src/components/keenicons/assets/filled/demo-files/demo.css +160 -0
- package/src/components/keenicons/assets/filled/demo-files/demo.js +32 -0
- package/src/components/keenicons/assets/filled/demo.html +12370 -0
- package/src/components/keenicons/assets/filled/fonts/keenicons-filled.svg +1082 -0
- package/src/components/keenicons/assets/filled/fonts/keenicons-filled.ttf +0 -0
- package/src/components/keenicons/assets/filled/fonts/keenicons-filled.woff +0 -0
- package/src/components/keenicons/assets/filled/selection.json +17096 -0
- package/src/components/keenicons/assets/filled/style.css +4769 -0
- package/src/components/keenicons/assets/outline/Read Me.txt +7 -0
- package/src/components/keenicons/assets/outline/demo-files/demo.css +160 -0
- package/src/components/keenicons/assets/outline/demo-files/demo.js +32 -0
- package/src/components/keenicons/assets/outline/demo.html +11356 -0
- package/src/components/keenicons/assets/outline/fonts/keenicons-outline.svg +575 -0
- package/src/components/keenicons/assets/outline/fonts/keenicons-outline.ttf +0 -0
- package/src/components/keenicons/assets/outline/fonts/keenicons-outline.woff +0 -0
- package/src/components/keenicons/assets/outline/selection.json +13054 -0
- package/src/components/keenicons/assets/outline/style.css +1721 -0
- package/src/components/keenicons/assets/solid/Read Me.txt +7 -0
- package/src/components/keenicons/assets/solid/demo-files/demo.css +160 -0
- package/src/components/keenicons/assets/solid/demo-files/demo.js +32 -0
- package/src/components/keenicons/assets/solid/demo.html +11356 -0
- package/src/components/keenicons/assets/solid/fonts/keenicons-solid.svg +575 -0
- package/src/components/keenicons/assets/solid/fonts/keenicons-solid.ttf +0 -0
- package/src/components/keenicons/assets/solid/fonts/keenicons-solid.woff +0 -0
- package/src/components/keenicons/assets/solid/selection.json +13048 -0
- package/src/components/keenicons/assets/solid/style.css +1721 -0
- package/src/components/keenicons/assets/styles.css +4 -0
- package/src/components/keenicons/index.ts +2 -0
- package/src/components/keenicons/keenicons.tsx +16 -0
- package/src/components/keenicons/types.ts +7 -0
- package/src/components/nav-footer.tsx +49 -0
- package/src/components/nav-main.tsx +53 -0
- package/src/components/nav-user.tsx +59 -0
- package/src/components/notification-bell.tsx +190 -0
- package/src/components/products/product-card.tsx +159 -0
- package/src/components/text-link.tsx +23 -0
- package/src/components/ui/accordion-menu.tsx +322 -0
- package/src/components/ui/accordion.tsx +133 -0
- package/src/components/ui/alert-dialog.tsx +82 -0
- package/src/components/ui/alert.tsx +63 -0
- package/src/components/ui/avatar-group.tsx +129 -0
- package/src/components/ui/avatar.tsx +67 -0
- package/src/components/ui/badge.tsx +230 -0
- package/src/components/ui/breadcrumb.tsx +88 -0
- package/src/components/ui/button.tsx +412 -0
- package/src/components/ui/calendar.tsx +56 -0
- package/src/components/ui/card.tsx +147 -0
- package/src/components/ui/chart.tsx +290 -0
- package/src/components/ui/checkbox.tsx +47 -0
- package/src/components/ui/code.tsx +45 -0
- package/src/components/ui/collapsible.tsx +31 -0
- package/src/components/ui/command-palette.tsx +189 -0
- package/src/components/ui/command.tsx +138 -0
- package/src/components/ui/cookie-banner.tsx +220 -0
- package/src/components/ui/copy-button.tsx +60 -0
- package/src/components/ui/data-grid-column-filter.tsx +124 -0
- package/src/components/ui/data-grid-column-header.tsx +284 -0
- package/src/components/ui/data-grid-column-visibility.tsx +38 -0
- package/src/components/ui/data-grid-pagination.tsx +206 -0
- package/src/components/ui/data-grid-table-dnd-rows.tsx +147 -0
- package/src/components/ui/data-grid-table-dnd.tsx +175 -0
- package/src/components/ui/data-grid-table.tsx +500 -0
- package/src/components/ui/data-grid.tsx +193 -0
- package/src/components/ui/data-list.tsx +76 -0
- package/src/components/ui/datefield.tsx +91 -0
- package/src/components/ui/dialog.tsx +139 -0
- package/src/components/ui/divider.tsx +41 -0
- package/src/components/ui/drawer.tsx +59 -0
- package/src/components/ui/dropdown-menu.tsx +224 -0
- package/src/components/ui/empty-state.tsx +54 -0
- package/src/components/ui/file-upload.tsx +152 -0
- package/src/components/ui/form.tsx +88 -0
- package/src/components/ui/icon.tsx +14 -0
- package/src/components/ui/input-otp.tsx +71 -0
- package/src/components/ui/input.tsx +155 -0
- package/src/components/ui/kbd.tsx +26 -0
- package/src/components/ui/label.tsx +31 -0
- package/src/components/ui/navigation-menu.tsx +168 -0
- package/src/components/ui/pagination.tsx +37 -0
- package/src/components/ui/placeholder-pattern.tsx +21 -0
- package/src/components/ui/popover.tsx +50 -0
- package/src/components/ui/progress.tsx +65 -0
- package/src/components/ui/radio-group.tsx +73 -0
- package/src/components/ui/resizable.tsx +39 -0
- package/src/components/ui/scroll-area.tsx +50 -0
- package/src/components/ui/select.tsx +234 -0
- package/src/components/ui/separator.tsx +24 -0
- package/src/components/ui/sheet.tsx +147 -0
- package/src/components/ui/sidebar.tsx +721 -0
- package/src/components/ui/skeleton.tsx +15 -0
- package/src/components/ui/slider.tsx +35 -0
- package/src/components/ui/sonner.tsx +28 -0
- package/src/components/ui/sortable.tsx +724 -0
- package/src/components/ui/spinner.tsx +17 -0
- package/src/components/ui/stat-card.tsx +82 -0
- package/src/components/ui/stepper.tsx +410 -0
- package/src/components/ui/switch.tsx +68 -0
- package/src/components/ui/table.tsx +42 -0
- package/src/components/ui/tabs.tsx +196 -0
- package/src/components/ui/timeline.tsx +90 -0
- package/src/components/ui/toggle-group.tsx +73 -0
- package/src/components/ui/toggle.tsx +45 -0
- package/src/components/ui/tooltip.tsx +55 -0
- package/src/components/user-info.tsx +33 -0
- package/src/components/user-menu-content.tsx +53 -0
- package/src/components/web/SiteFooter.tsx +154 -0
- package/src/components/web/SiteHeader.tsx +159 -0
- package/src/components/workflows/workflow-canvas.tsx +321 -0
- package/src/controls/Blockquote.tsx +25 -0
- package/src/controls/Button.tsx +101 -0
- package/src/controls/Checkbox.tsx +29 -0
- package/src/controls/DateField.tsx +37 -0
- package/src/controls/FormField.tsx +20 -0
- package/src/controls/Heading.tsx +28 -0
- package/src/controls/Input.tsx +21 -0
- package/src/controls/Label.tsx +18 -0
- package/src/controls/Paragraph.tsx +39 -0
- package/src/controls/PasswordInput.tsx +40 -0
- package/src/controls/RadioGroup.tsx +70 -0
- package/src/controls/Select.tsx +24 -0
- package/src/controls/Slider.tsx +33 -0
- package/src/controls/Switch.tsx +31 -0
- package/src/controls/Textarea.tsx +22 -0
- package/src/elements/ConfirmPasswordForm.tsx +43 -0
- package/src/elements/DeviceStatusBadge.tsx +38 -0
- package/src/elements/DriverCard.tsx +67 -0
- package/src/elements/ForgotPasswordForm.tsx +64 -0
- package/src/elements/IncidentCard.tsx +67 -0
- package/src/elements/LoginForm.tsx +100 -0
- package/src/elements/OtpForm.tsx +71 -0
- package/src/elements/RegisterForm.tsx +150 -0
- package/src/elements/ResetPasswordForm.tsx +72 -0
- package/src/elements/SmsChallengeForm.tsx +104 -0
- package/src/elements/VehicleCard.tsx +73 -0
- package/src/elements/VerifyEmailForm.tsx +39 -0
- package/src/hooks/use-appearance.tsx +117 -0
- package/src/hooks/use-applied-theme.ts +98 -0
- package/src/hooks/use-clipboard.ts +34 -0
- package/src/hooks/use-current-url.ts +83 -0
- package/src/hooks/use-dark-mode.ts +48 -0
- package/src/hooks/use-flash-toast.ts +29 -0
- package/src/hooks/use-initials.tsx +24 -0
- package/src/hooks/use-mobile-navigation.ts +12 -0
- package/src/hooks/use-mobile.tsx +38 -0
- package/src/index.ts +408 -0
- package/src/layouts/AppLayout.tsx +60 -0
- package/src/layouts/AuthLayout.tsx +32 -0
- package/src/layouts/SettingsLayout.tsx +21 -0
- package/src/layouts/app/AIChatLayout.tsx +73 -0
- package/src/layouts/app/AsideSidebarLayout.tsx +3 -0
- package/src/layouts/app/CalendarSidebarLayout.tsx +69 -0
- package/src/layouts/app/CommunitiesNavbarLayout.tsx +3 -0
- package/src/layouts/app/DualNavbarSidebarLayout.tsx +3 -0
- package/src/layouts/app/FocusSidebarLayout.tsx +75 -0
- package/src/layouts/app/MailLayout.tsx +69 -0
- package/src/layouts/app/MegaMenuHeaderLayout.tsx +3 -0
- package/src/layouts/app/MegaMenuLayout.tsx +81 -0
- package/src/layouts/app/MegaMenuNavbarLayout.tsx +88 -0
- package/src/layouts/app/MegaMenuSearchNavbarLayout.tsx +3 -0
- package/src/layouts/app/NavbarCollapsibleLayout.tsx +88 -0
- package/src/layouts/app/NavbarCollapsibleLinksLayout.tsx +3 -0
- package/src/layouts/app/NavbarMinimalLayout.tsx +3 -0
- package/src/layouts/app/NavbarMinimalSidebarLayout.tsx +3 -0
- package/src/layouts/app/NavbarSidebarDashboardLayout.tsx +3 -0
- package/src/layouts/app/NavbarSidebarLayout.tsx +92 -0
- package/src/layouts/app/NavbarSimpleSidebarLayout.tsx +3 -0
- package/src/layouts/app/NavbarTitledSidebarLayout.tsx +3 -0
- package/src/layouts/app/PanelSidebarLayout.tsx +3 -0
- package/src/layouts/app/SearchNavbarSidebarLayout.tsx +3 -0
- package/src/layouts/app/SidebarBreadcrumbLayout.tsx +3 -0
- package/src/layouts/app/SidebarCleanLayout.tsx +3 -0
- package/src/layouts/app/SidebarCommunitiesLayout.tsx +3 -0
- package/src/layouts/app/SidebarContentLayout.tsx +3 -0
- package/src/layouts/app/SidebarDualMenuLayout.tsx +104 -0
- package/src/layouts/app/SidebarFixedLayout.tsx +166 -0
- package/src/layouts/app/SidebarFooterNavbarLayout.tsx +3 -0
- package/src/layouts/app/SidebarHeaderMenuLayout.tsx +3 -0
- package/src/layouts/app/SidebarMegaMenuLayout.tsx +4 -0
- package/src/layouts/app/SidebarMinimalLayout.tsx +70 -0
- package/src/layouts/app/SidebarMobileSearchLayout.tsx +3 -0
- package/src/layouts/app/SidebarMultiPanelLayout.tsx +3 -0
- package/src/layouts/app/SidebarPrimarySecondaryLayout.tsx +3 -0
- package/src/layouts/app/SidebarSearchHeaderLayout.tsx +103 -0
- package/src/layouts/app/SidebarSearchToolbarLayout.tsx +3 -0
- package/src/layouts/app/SidebarTabsDualLayout.tsx +3 -0
- package/src/layouts/app/SidebarTabsLayout.tsx +98 -0
- package/src/layouts/app/SidebarTreeLayout.tsx +3 -0
- package/src/layouts/app/SplitNavbarLayout.tsx +3 -0
- package/src/layouts/app/SplitSidebarDashboardLayout.tsx +3 -0
- package/src/layouts/app/SplitSidebarLayout.tsx +99 -0
- package/src/layouts/app/TopNavLayout.tsx +105 -0
- package/src/layouts/app/TopNavLinksLayout.tsx +3 -0
- package/src/layouts/app/WorkspaceBreadcrumbLayout.tsx +3 -0
- package/src/layouts/app/WorkspaceCommunitiesLayout.tsx +3 -0
- package/src/layouts/app/WorkspaceNavbarLayout.tsx +3 -0
- package/src/layouts/app/WorkspaceSidebarLayout.tsx +98 -0
- package/src/layouts/app/WorkspaceSidebarTitleLayout.tsx +3 -0
- package/src/layouts/app/app-header-layout.tsx +45 -0
- package/src/layouts/app/app-sidebar-layout.tsx +56 -0
- package/src/layouts/app/layout-context.tsx +44 -0
- package/src/layouts/app/layout-types.ts +47 -0
- package/src/layouts/app/partials/Footer.tsx +35 -0
- package/src/layouts/app/partials/HeaderTopbar.tsx +96 -0
- package/src/layouts/app/partials/Navbar.tsx +85 -0
- package/src/layouts/app/partials/Toolbar.tsx +47 -0
- package/src/layouts/app-layout.tsx +29 -0
- package/src/layouts/auth/AuthBrandedLayout.tsx +58 -0
- package/src/layouts/auth/AuthCardLayout.tsx +31 -0
- package/src/layouts/auth/AuthCenteredLayout.tsx +41 -0
- package/src/layouts/auth/AuthClassicLayout.tsx +41 -0
- package/src/layouts/auth/AuthSimpleLayout.tsx +33 -0
- package/src/layouts/auth/AuthSplitLayout.tsx +89 -0
- package/src/layouts/web-app-layout.tsx +162 -0
- package/src/layouts/web-layout.tsx +23 -0
- package/src/lib/datetime.ts +188 -0
- package/src/lib/google-maps-loader.ts +99 -0
- package/src/lib/location.ts +127 -0
- package/src/lib/lucide-icon-map.ts +132 -0
- package/src/lib/map-markers.ts +124 -0
- package/src/lib/map-styles.ts +351 -0
- package/src/lib/utils.ts +11 -0
- package/src/platform/adapters/default.tsx +156 -0
- package/src/platform/adapters/inertia.tsx +88 -0
- package/src/platform/adapters/nextjs.ts +86 -0
- package/src/platform/context.tsx +106 -0
- package/src/platform/index.ts +27 -0
- package/src/platform/types.ts +105 -0
- package/src/styles/layouts/sidebar-fixed.css +161 -0
- package/src/styles/themes.css +583 -0
- package/src/types/assets.d.ts +5 -0
- package/src/types/auth.ts +25 -0
- package/src/types/global.d.ts +13 -0
- package/src/types/index.ts +9 -0
- package/src/types/navigation.ts +15 -0
- package/src/types/ui.ts +32 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, type ReactNode } from 'react';
|
|
4
|
+
import { cn } from '../../lib/utils';
|
|
5
|
+
import type { BaseAppLayoutProps } from './layout-types';
|
|
6
|
+
import type { NavItem } from '../../types/navigation';
|
|
7
|
+
import { Toolbar } from './partials/Toolbar';
|
|
8
|
+
import { Footer } from './partials/Footer';
|
|
9
|
+
import { HeaderTopbar } from './partials/HeaderTopbar';
|
|
10
|
+
import { AccordionMenu, AccordionMenuGroup, AccordionMenuItem, AccordionMenuSub, AccordionMenuSubContent, AccordionMenuSubTrigger } from '../../components/ui/accordion-menu';
|
|
11
|
+
import { ScrollArea } from '../../components/ui/scroll-area';
|
|
12
|
+
import { Button } from '../../controls/Button';
|
|
13
|
+
import { Menu } from 'lucide-react';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* SidebarDualMenuLayout (demo10)
|
|
17
|
+
* Sidebar with primary icon-strip + secondary full-label panel.
|
|
18
|
+
* Same dual structure as SidebarTabsLayout but toolbar is in the sidebar header.
|
|
19
|
+
*/
|
|
20
|
+
interface SidebarDualMenuLayoutProps extends BaseAppLayoutProps {
|
|
21
|
+
primaryNavItems?: NavItem[];
|
|
22
|
+
sidebarFooter?: ReactNode;
|
|
23
|
+
showToolbar?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function SidebarDualMenuLayout({
|
|
27
|
+
children, navItems = [], primaryNavItems = [], currentUrl = '',
|
|
28
|
+
logo, logoHref = '/', appName, user, sidebarFooter,
|
|
29
|
+
title, breadcrumbs = [], toolbarActions,
|
|
30
|
+
onLogout, settingsUrl, logoutUrl, unreadCount = 0,
|
|
31
|
+
footerLinks = [], copyright, showToolbar = true,
|
|
32
|
+
defaultSidebarCollapsed = false,
|
|
33
|
+
}: SidebarDualMenuLayoutProps) {
|
|
34
|
+
const [collapsed, setCollapsed] = useState(defaultSidebarCollapsed);
|
|
35
|
+
const [activeSection, setActiveSection] = useState(primaryNavItems[0]?.title ?? '');
|
|
36
|
+
const sectionItems = primaryNavItems.find((p) => p.title === activeSection)?.items ?? navItems;
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className="flex min-h-screen">
|
|
40
|
+
{/* Sidebar */}
|
|
41
|
+
<aside className="flex shrink-0">
|
|
42
|
+
{/* Primary icon strip */}
|
|
43
|
+
{primaryNavItems.length > 0 && (
|
|
44
|
+
<div className="w-[70px] flex flex-col items-center py-4 gap-1 border-e border-sidebar-border bg-sidebar">
|
|
45
|
+
{logo && <a href={logoHref} className="mb-4 shrink-0">{logo}</a>}
|
|
46
|
+
{primaryNavItems.map((item, i) => (
|
|
47
|
+
<button key={i} onClick={() => setActiveSection(item.title)}
|
|
48
|
+
className={cn('w-10 h-10 rounded-lg flex items-center justify-center text-xs font-semibold transition-colors', activeSection === item.title ? 'bg-sidebar-primary text-sidebar-primary-foreground' : 'text-sidebar-foreground hover:bg-sidebar-accent/70')}
|
|
49
|
+
title={item.title}
|
|
50
|
+
>
|
|
51
|
+
{item.title.slice(0, 2)}
|
|
52
|
+
</button>
|
|
53
|
+
))}
|
|
54
|
+
</div>
|
|
55
|
+
)}
|
|
56
|
+
|
|
57
|
+
{/* Secondary full nav */}
|
|
58
|
+
<div className={cn('flex flex-col border-e border-sidebar-border bg-sidebar transition-all', collapsed ? 'w-0 overflow-hidden' : 'w-56')}>
|
|
59
|
+
<div className="flex items-center justify-between px-3 h-[70px] border-b border-sidebar-border shrink-0">
|
|
60
|
+
{!primaryNavItems.length && logo && <a href={logoHref}>{logo}</a>}
|
|
61
|
+
{activeSection && <span className="text-sm font-semibold text-sidebar-foreground truncate">{activeSection || appName}</span>}
|
|
62
|
+
<Button variant="ghost" size="sm" className="size-8 p-0 ml-auto" onClick={() => setCollapsed((c) => !c)}>
|
|
63
|
+
<Menu className="size-4" />
|
|
64
|
+
</Button>
|
|
65
|
+
</div>
|
|
66
|
+
<ScrollArea className="flex-1 py-2 px-2">
|
|
67
|
+
<AccordionMenu type="single" collapsible matchPath={(href) => !!href && currentUrl.startsWith(href)} selectedValue={currentUrl}>
|
|
68
|
+
<AccordionMenuGroup>
|
|
69
|
+
{sectionItems.map((item, i) => (
|
|
70
|
+
item.items ? (
|
|
71
|
+
<AccordionMenuSub key={i} value={item.title}>
|
|
72
|
+
<AccordionMenuSubTrigger>{item.title}</AccordionMenuSubTrigger>
|
|
73
|
+
<AccordionMenuSubContent type="single" collapsible parentValue={item.title}>
|
|
74
|
+
{item.items.map((c, ci) => <AccordionMenuItem key={ci} value={c.href ?? c.title} asChild><a href={c.href}>{c.title}</a></AccordionMenuItem>)}
|
|
75
|
+
</AccordionMenuSubContent>
|
|
76
|
+
</AccordionMenuSub>
|
|
77
|
+
) : (
|
|
78
|
+
<AccordionMenuItem key={i} value={item.href ?? item.title} asChild><a href={item.href}>{item.title}</a></AccordionMenuItem>
|
|
79
|
+
)
|
|
80
|
+
))}
|
|
81
|
+
</AccordionMenuGroup>
|
|
82
|
+
</AccordionMenu>
|
|
83
|
+
</ScrollArea>
|
|
84
|
+
{sidebarFooter && <div className="p-3 border-t border-sidebar-border">{sidebarFooter}</div>}
|
|
85
|
+
</div>
|
|
86
|
+
</aside>
|
|
87
|
+
|
|
88
|
+
{/* Main */}
|
|
89
|
+
<div className="flex flex-col flex-1 min-w-0">
|
|
90
|
+
<header className="flex items-center h-[70px] border-b border-border bg-background px-4 shrink-0">
|
|
91
|
+
<div className="flex-1" />
|
|
92
|
+
<HeaderTopbar user={user} unreadCount={unreadCount} settingsUrl={settingsUrl} logoutUrl={logoutUrl} onLogout={onLogout} />
|
|
93
|
+
</header>
|
|
94
|
+
<main className="flex-1" role="content">
|
|
95
|
+
{showToolbar && (title || breadcrumbs.length > 0 || toolbarActions) && (
|
|
96
|
+
<Toolbar title={title} breadcrumbs={breadcrumbs} actions={toolbarActions} currentUrl={currentUrl} />
|
|
97
|
+
)}
|
|
98
|
+
{children}
|
|
99
|
+
</main>
|
|
100
|
+
<Footer links={footerLinks} copyright={copyright} />
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, type ReactNode } from 'react';
|
|
4
|
+
import { cn } from '../../lib/utils';
|
|
5
|
+
import type { BaseAppLayoutProps, AppLayoutUser } from './layout-types';
|
|
6
|
+
import type { NavItem } from '../../types/navigation';
|
|
7
|
+
import { Toolbar } from './partials/Toolbar';
|
|
8
|
+
import { Footer } from './partials/Footer';
|
|
9
|
+
import { HeaderTopbar } from './partials/HeaderTopbar';
|
|
10
|
+
import { AccordionMenu, AccordionMenuGroup, AccordionMenuItem, AccordionMenuSub, AccordionMenuSubContent, AccordionMenuSubTrigger, AccordionMenuIndicator } from '../../components/ui/accordion-menu';
|
|
11
|
+
import { ScrollArea } from '../../components/ui/scroll-area';
|
|
12
|
+
import { Menu, X } from 'lucide-react';
|
|
13
|
+
import { Button } from '../../controls/Button';
|
|
14
|
+
|
|
15
|
+
interface SidebarFixedLayoutProps extends BaseAppLayoutProps {
|
|
16
|
+
showToolbar?: boolean;
|
|
17
|
+
headerRightSlot?: ReactNode;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function SidebarNav({ navItems, currentUrl }: { navItems: NavItem[]; currentUrl: string }) {
|
|
21
|
+
function isActive(url: string): boolean {
|
|
22
|
+
return !!url && url !== '#' && currentUrl.startsWith(url);
|
|
23
|
+
}
|
|
24
|
+
function hasActive(items: NavItem[]): boolean {
|
|
25
|
+
return items.some((i) => isActive(i.href ?? '') || (i.items ? hasActive(i.items) : false));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<AccordionMenu type="single" collapsible matchPath={(href) => isActive(href)} selectedValue={currentUrl}>
|
|
30
|
+
<AccordionMenuGroup>
|
|
31
|
+
{navItems.map((item, i) => {
|
|
32
|
+
if (item.items && item.items.length > 0) {
|
|
33
|
+
return (
|
|
34
|
+
<AccordionMenuSub key={i} value={item.title}>
|
|
35
|
+
<AccordionMenuSubTrigger>
|
|
36
|
+
{item.title}
|
|
37
|
+
{hasActive(item.items) && <AccordionMenuIndicator className="size-1.5 rounded-full bg-primary" />}
|
|
38
|
+
</AccordionMenuSubTrigger>
|
|
39
|
+
<AccordionMenuSubContent type="single" collapsible parentValue={item.title}>
|
|
40
|
+
{item.items.map((child, ci) => (
|
|
41
|
+
<AccordionMenuItem key={ci} value={child.href ?? child.title} asChild>
|
|
42
|
+
<a href={child.href}>{child.title}</a>
|
|
43
|
+
</AccordionMenuItem>
|
|
44
|
+
))}
|
|
45
|
+
</AccordionMenuSubContent>
|
|
46
|
+
</AccordionMenuSub>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return (
|
|
50
|
+
<AccordionMenuItem key={i} value={item.href ?? item.title} asChild>
|
|
51
|
+
<a href={item.href}>{item.title}</a>
|
|
52
|
+
</AccordionMenuItem>
|
|
53
|
+
);
|
|
54
|
+
})}
|
|
55
|
+
</AccordionMenuGroup>
|
|
56
|
+
</AccordionMenu>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function SidebarFixedLayout({
|
|
61
|
+
children,
|
|
62
|
+
navItems = [],
|
|
63
|
+
currentUrl = '',
|
|
64
|
+
logo,
|
|
65
|
+
logoHref = '/',
|
|
66
|
+
appName,
|
|
67
|
+
user,
|
|
68
|
+
title,
|
|
69
|
+
breadcrumbs = [],
|
|
70
|
+
toolbarActions,
|
|
71
|
+
defaultSidebarCollapsed = false,
|
|
72
|
+
onLogout,
|
|
73
|
+
settingsUrl,
|
|
74
|
+
logoutUrl,
|
|
75
|
+
unreadCount = 0,
|
|
76
|
+
footerLinks = [],
|
|
77
|
+
copyright,
|
|
78
|
+
showToolbar = true,
|
|
79
|
+
headerRightSlot,
|
|
80
|
+
}: SidebarFixedLayoutProps) {
|
|
81
|
+
const [collapsed, setCollapsed] = useState(defaultSidebarCollapsed);
|
|
82
|
+
const [mobileOpen, setMobileOpen] = useState(false);
|
|
83
|
+
const [initialized, setInitialized] = useState(false);
|
|
84
|
+
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
const timer = setTimeout(() => setInitialized(true), 100);
|
|
87
|
+
return () => clearTimeout(timer);
|
|
88
|
+
}, []);
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div className={cn('sidebar-fixed-layout header-fixed flex min-h-screen', collapsed && 'sidebar-collapse', initialized && 'layout-initialized')}>
|
|
92
|
+
{/* Sidebar */}
|
|
93
|
+
<aside className={cn(
|
|
94
|
+
'layout-sidebar fixed inset-y-0 start-0 z-40 flex flex-col bg-sidebar border-e border-sidebar-border',
|
|
95
|
+
'transition-transform lg:translate-x-0',
|
|
96
|
+
mobileOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0',
|
|
97
|
+
)}>
|
|
98
|
+
{/* Sidebar header / logo */}
|
|
99
|
+
<div className="sidebar-header flex items-center gap-3 px-4 shrink-0 border-b border-sidebar-border">
|
|
100
|
+
{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>}
|
|
104
|
+
</a>
|
|
105
|
+
)}
|
|
106
|
+
</div>
|
|
107
|
+
{/* Nav */}
|
|
108
|
+
<ScrollArea className="flex-1 py-3 px-2">
|
|
109
|
+
<SidebarNav navItems={navItems} currentUrl={currentUrl} />
|
|
110
|
+
</ScrollArea>
|
|
111
|
+
</aside>
|
|
112
|
+
|
|
113
|
+
{/* Mobile overlay */}
|
|
114
|
+
{mobileOpen && (
|
|
115
|
+
<div className="fixed inset-0 z-30 bg-black/50 lg:hidden" onClick={() => setMobileOpen(false)} />
|
|
116
|
+
)}
|
|
117
|
+
|
|
118
|
+
{/* 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">
|
|
122
|
+
<Button
|
|
123
|
+
variant="ghost" size="sm"
|
|
124
|
+
className="size-9 p-0 rounded-full lg:hidden"
|
|
125
|
+
onClick={() => setMobileOpen((o) => !o)}
|
|
126
|
+
aria-label="Toggle menu"
|
|
127
|
+
>
|
|
128
|
+
{mobileOpen ? <X className="size-4" /> : <Menu className="size-4" />}
|
|
129
|
+
</Button>
|
|
130
|
+
<Button
|
|
131
|
+
variant="ghost" size="sm"
|
|
132
|
+
className="size-9 p-0 rounded-full hidden lg:flex"
|
|
133
|
+
onClick={() => setCollapsed((c) => !c)}
|
|
134
|
+
aria-label="Collapse sidebar"
|
|
135
|
+
>
|
|
136
|
+
<Menu className="size-4" />
|
|
137
|
+
</Button>
|
|
138
|
+
<div className="flex-1" />
|
|
139
|
+
<HeaderTopbar
|
|
140
|
+
user={user}
|
|
141
|
+
unreadCount={unreadCount}
|
|
142
|
+
settingsUrl={settingsUrl}
|
|
143
|
+
logoutUrl={logoutUrl}
|
|
144
|
+
onLogout={onLogout}
|
|
145
|
+
extraSlot={headerRightSlot}
|
|
146
|
+
/>
|
|
147
|
+
</header>
|
|
148
|
+
|
|
149
|
+
{/* Main content */}
|
|
150
|
+
<main className="grow pt-5" role="content">
|
|
151
|
+
{showToolbar && (title || breadcrumbs.length > 0 || toolbarActions) && (
|
|
152
|
+
<Toolbar
|
|
153
|
+
title={title}
|
|
154
|
+
breadcrumbs={breadcrumbs}
|
|
155
|
+
actions={toolbarActions}
|
|
156
|
+
currentUrl={currentUrl}
|
|
157
|
+
/>
|
|
158
|
+
)}
|
|
159
|
+
{children}
|
|
160
|
+
</main>
|
|
161
|
+
|
|
162
|
+
<Footer links={footerLinks} copyright={copyright} />
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { cn } from '../../lib/utils';
|
|
4
|
+
import type { BaseAppLayoutProps } from './layout-types';
|
|
5
|
+
import { Toolbar } from './partials/Toolbar';
|
|
6
|
+
import { Footer } from './partials/Footer';
|
|
7
|
+
import { HeaderTopbar } from './partials/HeaderTopbar';
|
|
8
|
+
import { AccordionMenu, AccordionMenuGroup, AccordionMenuItem, AccordionMenuSub, AccordionMenuSubContent, AccordionMenuSubTrigger } from '../../components/ui/accordion-menu';
|
|
9
|
+
import { ScrollArea } from '../../components/ui/scroll-area';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* SidebarMinimalLayout (demo8)
|
|
13
|
+
* Clean single sidebar with no secondary panel. Sidebar has header + menu + footer area.
|
|
14
|
+
*/
|
|
15
|
+
interface SidebarMinimalLayoutProps extends BaseAppLayoutProps {
|
|
16
|
+
sidebarFooter?: React.ReactNode;
|
|
17
|
+
showToolbar?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function SidebarMinimalLayout({
|
|
21
|
+
children, navItems = [], currentUrl = '',
|
|
22
|
+
logo, logoHref = '/', appName, user,
|
|
23
|
+
title, breadcrumbs = [], toolbarActions, sidebarFooter,
|
|
24
|
+
onLogout, settingsUrl, logoutUrl, unreadCount = 0,
|
|
25
|
+
footerLinks = [], copyright, showToolbar = true,
|
|
26
|
+
}: SidebarMinimalLayoutProps) {
|
|
27
|
+
return (
|
|
28
|
+
<div className="flex min-h-screen">
|
|
29
|
+
<aside className="w-64 shrink-0 flex flex-col border-e border-sidebar-border bg-sidebar">
|
|
30
|
+
<div className="flex items-center gap-2 px-4 h-[70px] border-b border-sidebar-border shrink-0">
|
|
31
|
+
{logo && <a href={logoHref}>{logo}</a>}
|
|
32
|
+
{appName && <span className="text-sm font-semibold text-sidebar-foreground">{appName}</span>}
|
|
33
|
+
</div>
|
|
34
|
+
<ScrollArea className="flex-1 py-3 px-2">
|
|
35
|
+
<AccordionMenu type="single" collapsible matchPath={(href) => !!href && currentUrl.startsWith(href)} selectedValue={currentUrl}>
|
|
36
|
+
<AccordionMenuGroup>
|
|
37
|
+
{navItems.map((item, i) => (
|
|
38
|
+
item.items ? (
|
|
39
|
+
<AccordionMenuSub key={i} value={item.title}>
|
|
40
|
+
<AccordionMenuSubTrigger>{item.title}</AccordionMenuSubTrigger>
|
|
41
|
+
<AccordionMenuSubContent type="single" collapsible parentValue={item.title}>
|
|
42
|
+
{item.items.map((c, ci) => <AccordionMenuItem key={ci} value={c.href ?? c.title} asChild><a href={c.href}>{c.title}</a></AccordionMenuItem>)}
|
|
43
|
+
</AccordionMenuSubContent>
|
|
44
|
+
</AccordionMenuSub>
|
|
45
|
+
) : (
|
|
46
|
+
<AccordionMenuItem key={i} value={item.href ?? item.title} asChild><a href={item.href}>{item.title}</a></AccordionMenuItem>
|
|
47
|
+
)
|
|
48
|
+
))}
|
|
49
|
+
</AccordionMenuGroup>
|
|
50
|
+
</AccordionMenu>
|
|
51
|
+
</ScrollArea>
|
|
52
|
+
{sidebarFooter && <div className="p-3 border-t border-sidebar-border">{sidebarFooter}</div>}
|
|
53
|
+
</aside>
|
|
54
|
+
|
|
55
|
+
<div className="flex flex-col flex-1 min-w-0">
|
|
56
|
+
<header className="flex items-center h-[70px] border-b border-border bg-background px-4 shrink-0">
|
|
57
|
+
<div className="flex-1" />
|
|
58
|
+
<HeaderTopbar user={user} unreadCount={unreadCount} settingsUrl={settingsUrl} logoutUrl={logoutUrl} onLogout={onLogout} />
|
|
59
|
+
</header>
|
|
60
|
+
<main className="flex-1" role="content">
|
|
61
|
+
{showToolbar && (title || breadcrumbs.length > 0 || toolbarActions) && (
|
|
62
|
+
<Toolbar title={title} breadcrumbs={breadcrumbs} actions={toolbarActions} currentUrl={currentUrl} />
|
|
63
|
+
)}
|
|
64
|
+
{children}
|
|
65
|
+
</main>
|
|
66
|
+
<Footer links={footerLinks} copyright={copyright} />
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
import { cn } from '../../lib/utils';
|
|
5
|
+
import type { BaseAppLayoutProps } from './layout-types';
|
|
6
|
+
import { Toolbar } from './partials/Toolbar';
|
|
7
|
+
import { Footer } from './partials/Footer';
|
|
8
|
+
import { HeaderTopbar } from './partials/HeaderTopbar';
|
|
9
|
+
import { AccordionMenu, AccordionMenuGroup, AccordionMenuItem, AccordionMenuSub, AccordionMenuSubContent, AccordionMenuSubTrigger } from '../../components/ui/accordion-menu';
|
|
10
|
+
import { ScrollArea } from '../../components/ui/scroll-area';
|
|
11
|
+
import { Search } from 'lucide-react';
|
|
12
|
+
import { Input } from './../../controls/Input';
|
|
13
|
+
import { useState } from 'react';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* SidebarSearchHeaderLayout (layout-11)
|
|
17
|
+
* Sidebar with inline search + header with horizontal menu.
|
|
18
|
+
*/
|
|
19
|
+
interface SidebarSearchHeaderLayoutProps extends BaseAppLayoutProps {
|
|
20
|
+
headerMenuItems?: Array<{ label: string; href: string }>;
|
|
21
|
+
showToolbar?: boolean;
|
|
22
|
+
sidebarSearchPlaceholder?: string;
|
|
23
|
+
headerActions?: ReactNode;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function SidebarSearchHeaderLayout({
|
|
27
|
+
children, navItems = [], currentUrl = '',
|
|
28
|
+
logo, logoHref = '/', appName, user,
|
|
29
|
+
title, breadcrumbs = [], toolbarActions,
|
|
30
|
+
onLogout, settingsUrl, logoutUrl, unreadCount = 0,
|
|
31
|
+
footerLinks = [], copyright, showToolbar = true,
|
|
32
|
+
headerMenuItems = [], sidebarSearchPlaceholder = 'Search…', headerActions,
|
|
33
|
+
}: SidebarSearchHeaderLayoutProps) {
|
|
34
|
+
const [query, setQuery] = useState('');
|
|
35
|
+
const filtered = query
|
|
36
|
+
? navItems.filter((i) => i.title.toLowerCase().includes(query.toLowerCase()))
|
|
37
|
+
: navItems;
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className="flex min-h-screen">
|
|
41
|
+
<aside className="w-64 shrink-0 flex flex-col border-e border-sidebar-border bg-sidebar">
|
|
42
|
+
<div className="flex items-center gap-2 px-4 h-[70px] border-b border-sidebar-border shrink-0">
|
|
43
|
+
{logo && <a href={logoHref}>{logo}</a>}
|
|
44
|
+
{appName && <span className="text-sm font-semibold text-sidebar-foreground">{appName}</span>}
|
|
45
|
+
</div>
|
|
46
|
+
<div className="px-3 py-2 border-b border-sidebar-border">
|
|
47
|
+
<div className="relative">
|
|
48
|
+
<Search className="absolute start-2.5 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
|
|
49
|
+
<Input
|
|
50
|
+
type="search"
|
|
51
|
+
placeholder={sidebarSearchPlaceholder}
|
|
52
|
+
className="ps-8 h-8 text-sm"
|
|
53
|
+
value={query}
|
|
54
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
<ScrollArea className="flex-1 py-2 px-2">
|
|
59
|
+
<AccordionMenu type="single" collapsible matchPath={(href) => !!href && currentUrl.startsWith(href)} selectedValue={currentUrl}>
|
|
60
|
+
<AccordionMenuGroup>
|
|
61
|
+
{filtered.map((item, i) => (
|
|
62
|
+
item.items ? (
|
|
63
|
+
<AccordionMenuSub key={i} value={item.title}>
|
|
64
|
+
<AccordionMenuSubTrigger>{item.title}</AccordionMenuSubTrigger>
|
|
65
|
+
<AccordionMenuSubContent type="single" collapsible parentValue={item.title}>
|
|
66
|
+
{item.items.map((c, ci) => <AccordionMenuItem key={ci} value={c.href ?? c.title} asChild><a href={c.href}>{c.title}</a></AccordionMenuItem>)}
|
|
67
|
+
</AccordionMenuSubContent>
|
|
68
|
+
</AccordionMenuSub>
|
|
69
|
+
) : (
|
|
70
|
+
<AccordionMenuItem key={i} value={item.href ?? item.title} asChild><a href={item.href}>{item.title}</a></AccordionMenuItem>
|
|
71
|
+
)
|
|
72
|
+
))}
|
|
73
|
+
</AccordionMenuGroup>
|
|
74
|
+
</AccordionMenu>
|
|
75
|
+
</ScrollArea>
|
|
76
|
+
</aside>
|
|
77
|
+
|
|
78
|
+
<div className="flex flex-col flex-1 min-w-0">
|
|
79
|
+
<header className="flex items-center h-[70px] border-b border-border bg-background px-4 shrink-0 gap-4">
|
|
80
|
+
{headerMenuItems.length > 0 && (
|
|
81
|
+
<nav className="flex items-center gap-1 flex-1">
|
|
82
|
+
{headerMenuItems.map((item, i) => (
|
|
83
|
+
<a key={i} href={item.href} className={cn('px-3 py-2 text-sm rounded-md transition-colors hover:bg-accent', currentUrl.startsWith(item.href) && 'bg-accent text-accent-foreground')}>
|
|
84
|
+
{item.label}
|
|
85
|
+
</a>
|
|
86
|
+
))}
|
|
87
|
+
</nav>
|
|
88
|
+
)}
|
|
89
|
+
<div className="flex-1" />
|
|
90
|
+
{headerActions}
|
|
91
|
+
<HeaderTopbar user={user} unreadCount={unreadCount} settingsUrl={settingsUrl} logoutUrl={logoutUrl} onLogout={onLogout} />
|
|
92
|
+
</header>
|
|
93
|
+
<main className="flex-1" role="content">
|
|
94
|
+
{showToolbar && (title || breadcrumbs.length > 0 || toolbarActions) && (
|
|
95
|
+
<Toolbar title={title} breadcrumbs={breadcrumbs} actions={toolbarActions} currentUrl={currentUrl} />
|
|
96
|
+
)}
|
|
97
|
+
{children}
|
|
98
|
+
</main>
|
|
99
|
+
<Footer links={footerLinks} copyright={copyright} />
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { cn } from '../../lib/utils';
|
|
5
|
+
import type { BaseAppLayoutProps } from './layout-types';
|
|
6
|
+
import type { NavItem } from '../../types/navigation';
|
|
7
|
+
import { Toolbar } from './partials/Toolbar';
|
|
8
|
+
import { Footer } from './partials/Footer';
|
|
9
|
+
import { HeaderTopbar } from './partials/HeaderTopbar';
|
|
10
|
+
import { AccordionMenu, AccordionMenuGroup, AccordionMenuItem, AccordionMenuSub, AccordionMenuSubContent, AccordionMenuSubTrigger } from '../../components/ui/accordion-menu';
|
|
11
|
+
import { ScrollArea } from '../../components/ui/scroll-area';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* SidebarTabsLayout (demo6)
|
|
15
|
+
* Sidebar with primary tabs (section switcher) and secondary menu per tab.
|
|
16
|
+
* No top horizontal navbar.
|
|
17
|
+
*/
|
|
18
|
+
interface SidebarTabsLayoutProps extends BaseAppLayoutProps {
|
|
19
|
+
primaryNavItems?: NavItem[];
|
|
20
|
+
showToolbar?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function SidebarTabsLayout({
|
|
24
|
+
children, navItems = [], primaryNavItems = [], currentUrl = '',
|
|
25
|
+
logo, logoHref = '/', appName, user,
|
|
26
|
+
title, breadcrumbs = [], toolbarActions,
|
|
27
|
+
onLogout, settingsUrl, logoutUrl, unreadCount = 0,
|
|
28
|
+
footerLinks = [], copyright, showToolbar = true,
|
|
29
|
+
}: SidebarTabsLayoutProps) {
|
|
30
|
+
const [activeTab, setActiveTab] = useState(primaryNavItems[0]?.title ?? '');
|
|
31
|
+
const activeSection = primaryNavItems.find((p) => p.title === activeTab);
|
|
32
|
+
const sideItems = activeSection?.items ?? navItems;
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="flex min-h-screen">
|
|
36
|
+
<aside className="flex shrink-0">
|
|
37
|
+
{/* Tab strip */}
|
|
38
|
+
{primaryNavItems.length > 0 && (
|
|
39
|
+
<div className="w-16 flex flex-col items-center py-3 gap-1 border-e border-sidebar-border bg-sidebar">
|
|
40
|
+
{logo && <a href={logoHref} className="mb-3 shrink-0">{logo}</a>}
|
|
41
|
+
{primaryNavItems.map((item, i) => (
|
|
42
|
+
<button
|
|
43
|
+
key={i}
|
|
44
|
+
onClick={() => setActiveTab(item.title)}
|
|
45
|
+
className={cn('w-10 h-10 rounded-lg flex items-center justify-center text-xs font-medium transition-colors', activeTab === item.title ? 'bg-sidebar-accent text-sidebar-primary' : 'text-sidebar-foreground hover:bg-sidebar-accent/60')}
|
|
46
|
+
title={item.title}
|
|
47
|
+
>
|
|
48
|
+
{item.title.slice(0, 2)}
|
|
49
|
+
</button>
|
|
50
|
+
))}
|
|
51
|
+
</div>
|
|
52
|
+
)}
|
|
53
|
+
{/* Secondary nav */}
|
|
54
|
+
<div className="w-52 flex flex-col border-e border-sidebar-border bg-sidebar">
|
|
55
|
+
{!primaryNavItems.length && logo && (
|
|
56
|
+
<div className="flex items-center gap-2 px-4 h-[70px] border-b border-sidebar-border shrink-0">
|
|
57
|
+
<a href={logoHref}>{logo}</a>
|
|
58
|
+
{appName && <span className="text-sm font-semibold">{appName}</span>}
|
|
59
|
+
</div>
|
|
60
|
+
)}
|
|
61
|
+
{activeTab && <div className="px-4 py-2 text-xs font-semibold text-sidebar-foreground border-b border-sidebar-border">{activeTab}</div>}
|
|
62
|
+
<ScrollArea className="flex-1 py-2 px-2">
|
|
63
|
+
<AccordionMenu type="single" collapsible matchPath={(href) => !!href && currentUrl.startsWith(href)} selectedValue={currentUrl}>
|
|
64
|
+
<AccordionMenuGroup>
|
|
65
|
+
{sideItems.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
|
+
</div>
|
|
81
|
+
</aside>
|
|
82
|
+
|
|
83
|
+
<div className="flex flex-col flex-1 min-w-0">
|
|
84
|
+
<header className="flex items-center h-[70px] border-b border-border bg-background px-4 shrink-0">
|
|
85
|
+
<div className="flex-1" />
|
|
86
|
+
<HeaderTopbar user={user} unreadCount={unreadCount} settingsUrl={settingsUrl} logoutUrl={logoutUrl} onLogout={onLogout} />
|
|
87
|
+
</header>
|
|
88
|
+
<main className="flex-1" role="content">
|
|
89
|
+
{showToolbar && (title || breadcrumbs.length > 0 || toolbarActions) && (
|
|
90
|
+
<Toolbar title={title} breadcrumbs={breadcrumbs} actions={toolbarActions} currentUrl={currentUrl} />
|
|
91
|
+
)}
|
|
92
|
+
{children}
|
|
93
|
+
</main>
|
|
94
|
+
<Footer links={footerLinks} copyright={copyright} />
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|