@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,99 @@
|
|
|
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 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
|
+
* SplitSidebarLayout (demo4)
|
|
15
|
+
* No horizontal navbar. Left sidebar split into:
|
|
16
|
+
* - primary panel: icon/section switcher
|
|
17
|
+
* - secondary panel: full nav items for the active section
|
|
18
|
+
*/
|
|
19
|
+
interface SplitSidebarLayoutProps extends BaseAppLayoutProps {
|
|
20
|
+
primaryItems?: NavItem[];
|
|
21
|
+
secondaryItems?: NavItem[];
|
|
22
|
+
activePrimary?: string;
|
|
23
|
+
onPrimaryChange?: (value: string) => void;
|
|
24
|
+
showToolbar?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function SplitSidebarLayout({
|
|
28
|
+
children, navItems = [], primaryItems = [], secondaryItems = [],
|
|
29
|
+
currentUrl = '', logo, logoHref = '/', appName, user,
|
|
30
|
+
title, breadcrumbs = [], toolbarActions, activePrimary,
|
|
31
|
+
onLogout, settingsUrl, logoutUrl, unreadCount = 0,
|
|
32
|
+
footerLinks = [], copyright, showToolbar = true,
|
|
33
|
+
}: SplitSidebarLayoutProps) {
|
|
34
|
+
const sideItems = secondaryItems.length > 0 ? secondaryItems : navItems;
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<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
|
+
))}
|
|
52
|
+
</aside>
|
|
53
|
+
)}
|
|
54
|
+
|
|
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>
|
|
82
|
+
|
|
83
|
+
{/* Main area */}
|
|
84
|
+
<div className="flex flex-col flex-1 min-w-0">
|
|
85
|
+
<header className="flex items-center h-[70px] border-b border-border bg-background px-4 shrink-0">
|
|
86
|
+
<div className="flex-1" />
|
|
87
|
+
<HeaderTopbar user={user} unreadCount={unreadCount} settingsUrl={settingsUrl} logoutUrl={logoutUrl} onLogout={onLogout} />
|
|
88
|
+
</header>
|
|
89
|
+
<main className="flex-1" role="content">
|
|
90
|
+
{showToolbar && (title || breadcrumbs.length > 0 || toolbarActions) && (
|
|
91
|
+
<Toolbar title={title} breadcrumbs={breadcrumbs} actions={toolbarActions} currentUrl={currentUrl} />
|
|
92
|
+
)}
|
|
93
|
+
{children}
|
|
94
|
+
</main>
|
|
95
|
+
<Footer links={footerLinks} copyright={copyright} />
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, type ReactNode } from 'react';
|
|
4
|
+
import { cn } from '../../lib/utils';
|
|
5
|
+
import type { BaseAppLayoutProps } from './layout-types';
|
|
6
|
+
import { Navbar } from './partials/Navbar';
|
|
7
|
+
import { Toolbar } from './partials/Toolbar';
|
|
8
|
+
import { Footer } from './partials/Footer';
|
|
9
|
+
import { HeaderTopbar } from './partials/HeaderTopbar';
|
|
10
|
+
|
|
11
|
+
interface TopNavLayoutProps extends BaseAppLayoutProps {
|
|
12
|
+
headerRightSlot?: ReactNode;
|
|
13
|
+
navRightSlot?: ReactNode;
|
|
14
|
+
showToolbar?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function TopNavLayout({
|
|
18
|
+
children,
|
|
19
|
+
navItems = [],
|
|
20
|
+
currentUrl = '',
|
|
21
|
+
logo,
|
|
22
|
+
logoHref = '/',
|
|
23
|
+
appName,
|
|
24
|
+
user,
|
|
25
|
+
title,
|
|
26
|
+
breadcrumbs = [],
|
|
27
|
+
toolbarActions,
|
|
28
|
+
stickyHeader = true,
|
|
29
|
+
stickyOffset = 100,
|
|
30
|
+
onLogout,
|
|
31
|
+
settingsUrl,
|
|
32
|
+
logoutUrl,
|
|
33
|
+
unreadCount = 0,
|
|
34
|
+
footerLinks = [],
|
|
35
|
+
copyright,
|
|
36
|
+
headerRightSlot,
|
|
37
|
+
navRightSlot,
|
|
38
|
+
showToolbar = true,
|
|
39
|
+
}: TopNavLayoutProps) {
|
|
40
|
+
const [isSticky, setIsSticky] = useState(false);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (!stickyHeader) return;
|
|
44
|
+
const handleScroll = () => setIsSticky(window.scrollY > stickyOffset);
|
|
45
|
+
window.addEventListener('scroll', handleScroll, { passive: true });
|
|
46
|
+
return () => window.removeEventListener('scroll', handleScroll);
|
|
47
|
+
}, [stickyHeader, stickyOffset]);
|
|
48
|
+
|
|
49
|
+
const headerHeight = isSticky ? '60px' : '100px';
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div
|
|
53
|
+
className="flex grow flex-col min-h-screen"
|
|
54
|
+
style={{ '--header-height': headerHeight } as React.CSSProperties}
|
|
55
|
+
>
|
|
56
|
+
{/* Header */}
|
|
57
|
+
<header
|
|
58
|
+
className={cn(
|
|
59
|
+
'flex items-center shrink-0 transition-[height] border-b border-border',
|
|
60
|
+
'h-(--header-height)',
|
|
61
|
+
stickyHeader && isSticky && 'fixed z-10 top-0 inset-x-0 shadow-xs backdrop-blur-md bg-background/80',
|
|
62
|
+
)}
|
|
63
|
+
style={{ height: headerHeight }}
|
|
64
|
+
>
|
|
65
|
+
<div className="container mx-auto px-4 flex justify-between items-center gap-4">
|
|
66
|
+
{/* Logo */}
|
|
67
|
+
<div className="flex items-center gap-3 shrink-0">
|
|
68
|
+
{logo && <a href={logoHref} className="shrink-0">{logo}</a>}
|
|
69
|
+
{appName && <span className="text-sm font-medium text-foreground hidden md:inline">{appName}</span>}
|
|
70
|
+
</div>
|
|
71
|
+
{/* Right topbar */}
|
|
72
|
+
<HeaderTopbar
|
|
73
|
+
user={user}
|
|
74
|
+
unreadCount={unreadCount}
|
|
75
|
+
settingsUrl={settingsUrl}
|
|
76
|
+
logoutUrl={logoutUrl}
|
|
77
|
+
onLogout={onLogout}
|
|
78
|
+
extraSlot={headerRightSlot}
|
|
79
|
+
/>
|
|
80
|
+
</div>
|
|
81
|
+
</header>
|
|
82
|
+
|
|
83
|
+
{/* Spacer when header is sticky */}
|
|
84
|
+
{stickyHeader && isSticky && <div style={{ height: '100px' }} aria-hidden="true" />}
|
|
85
|
+
|
|
86
|
+
{/* Horizontal navbar */}
|
|
87
|
+
<Navbar navItems={navItems} currentUrl={currentUrl} rightSlot={navRightSlot} />
|
|
88
|
+
|
|
89
|
+
{/* Content */}
|
|
90
|
+
<main className="grow" role="content">
|
|
91
|
+
{showToolbar && (title || breadcrumbs.length > 0 || toolbarActions) && (
|
|
92
|
+
<Toolbar
|
|
93
|
+
title={title}
|
|
94
|
+
breadcrumbs={breadcrumbs}
|
|
95
|
+
actions={toolbarActions}
|
|
96
|
+
currentUrl={currentUrl}
|
|
97
|
+
/>
|
|
98
|
+
)}
|
|
99
|
+
{children}
|
|
100
|
+
</main>
|
|
101
|
+
|
|
102
|
+
<Footer links={footerLinks} copyright={copyright} />
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
// layout-14/20: workspace sidebar — primary nav + workspace switcher + secondary panel
|
|
3
|
+
// Used for multi-workspace apps (similar to Notion, Linear, Slack)
|
|
4
|
+
import { type ReactNode } from 'react';
|
|
5
|
+
import { cn } from '../../lib/utils';
|
|
6
|
+
import type { BaseAppLayoutProps } from './layout-types';
|
|
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
|
+
interface WorkspaceSidebarLayoutProps extends BaseAppLayoutProps {
|
|
14
|
+
workspaces?: Array<{ id: string; name: string; icon?: string; href: string }>;
|
|
15
|
+
activeWorkspace?: string;
|
|
16
|
+
communities?: Array<{ name: string; href: string }>;
|
|
17
|
+
secondaryItems?: import('../../types/navigation').NavItem[];
|
|
18
|
+
sidebarFooter?: ReactNode;
|
|
19
|
+
showToolbar?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function WorkspaceSidebarLayout({
|
|
23
|
+
children, navItems = [], currentUrl = '',
|
|
24
|
+
logo, logoHref = '/', appName, user,
|
|
25
|
+
title, breadcrumbs = [], toolbarActions,
|
|
26
|
+
workspaces = [], activeWorkspace, communities = [],
|
|
27
|
+
secondaryItems = [], sidebarFooter,
|
|
28
|
+
onLogout, settingsUrl, logoutUrl, unreadCount = 0,
|
|
29
|
+
footerLinks = [], copyright, showToolbar = true,
|
|
30
|
+
}: WorkspaceSidebarLayoutProps) {
|
|
31
|
+
const allNavItems = [...navItems, ...secondaryItems];
|
|
32
|
+
return (
|
|
33
|
+
<div className="flex min-h-screen">
|
|
34
|
+
{/* Workspace strip */}
|
|
35
|
+
{workspaces.length > 0 && (
|
|
36
|
+
<div className="w-[52px] flex flex-col items-center py-3 gap-1.5 border-e border-sidebar-border bg-sidebar shrink-0">
|
|
37
|
+
{logo && <a href={logoHref} className="mb-2">{logo}</a>}
|
|
38
|
+
{workspaces.map((ws, i) => (
|
|
39
|
+
<a key={i} href={ws.href} className={cn('w-8 h-8 rounded-lg flex items-center justify-center text-xs font-bold transition-colors', activeWorkspace === ws.id ? 'bg-sidebar-primary text-sidebar-primary-foreground' : 'bg-sidebar-accent text-sidebar-foreground hover:bg-sidebar-accent/80')}>
|
|
40
|
+
{ws.icon ?? ws.name.slice(0, 2)}
|
|
41
|
+
</a>
|
|
42
|
+
))}
|
|
43
|
+
</div>
|
|
44
|
+
)}
|
|
45
|
+
|
|
46
|
+
{/* Main sidebar */}
|
|
47
|
+
<aside className="w-56 shrink-0 flex flex-col border-e border-sidebar-border bg-sidebar">
|
|
48
|
+
{!workspaces.length && logo && (
|
|
49
|
+
<div className="flex items-center gap-2 px-4 h-[70px] border-b border-sidebar-border shrink-0">
|
|
50
|
+
<a href={logoHref}>{logo}</a>
|
|
51
|
+
{appName && <span className="text-sm font-semibold">{appName}</span>}
|
|
52
|
+
</div>
|
|
53
|
+
)}
|
|
54
|
+
<ScrollArea className="flex-1 py-2 px-2">
|
|
55
|
+
<AccordionMenu type="single" collapsible matchPath={(href) => !!href && currentUrl.startsWith(href)} selectedValue={currentUrl}>
|
|
56
|
+
<AccordionMenuGroup>
|
|
57
|
+
{allNavItems.map((item, i) => (
|
|
58
|
+
item.items ? (
|
|
59
|
+
<AccordionMenuSub key={i} value={item.title}>
|
|
60
|
+
<AccordionMenuSubTrigger>{item.title}</AccordionMenuSubTrigger>
|
|
61
|
+
<AccordionMenuSubContent type="single" collapsible parentValue={item.title}>
|
|
62
|
+
{item.items.map((c, ci) => <AccordionMenuItem key={ci} value={c.href ?? c.title} asChild><a href={c.href}>{c.title}</a></AccordionMenuItem>)}
|
|
63
|
+
</AccordionMenuSubContent>
|
|
64
|
+
</AccordionMenuSub>
|
|
65
|
+
) : (
|
|
66
|
+
<AccordionMenuItem key={i} value={item.href ?? item.title} asChild><a href={item.href}>{item.title}</a></AccordionMenuItem>
|
|
67
|
+
)
|
|
68
|
+
))}
|
|
69
|
+
</AccordionMenuGroup>
|
|
70
|
+
{communities.length > 0 && (
|
|
71
|
+
<AccordionMenuGroup>
|
|
72
|
+
<div className="px-2 py-1.5 text-xs font-medium text-muted-foreground">Communities</div>
|
|
73
|
+
{communities.map((c, i) => (
|
|
74
|
+
<AccordionMenuItem key={i} value={c.href} asChild><a href={c.href}>{c.name}</a></AccordionMenuItem>
|
|
75
|
+
))}
|
|
76
|
+
</AccordionMenuGroup>
|
|
77
|
+
)}
|
|
78
|
+
</AccordionMenu>
|
|
79
|
+
</ScrollArea>
|
|
80
|
+
{sidebarFooter && <div className="p-3 border-t border-sidebar-border">{sidebarFooter}</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
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { AppShell } from '../../components/app-shell';
|
|
3
|
+
import { AppContent } from '../../components/app-content';
|
|
4
|
+
import { AppHeader } from '../../components/app-header';
|
|
5
|
+
import type { NavItem, BreadcrumbItem } from '../../types/navigation';
|
|
6
|
+
import type { User } from '../../types/auth';
|
|
7
|
+
|
|
8
|
+
export type AppHeaderLayoutProps = {
|
|
9
|
+
breadcrumbs?: BreadcrumbItem[];
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
user?: User | null;
|
|
12
|
+
navItems?: NavItem[];
|
|
13
|
+
unreadCount?: number;
|
|
14
|
+
dashboardHref?: string;
|
|
15
|
+
settingsUrl?: string;
|
|
16
|
+
logoutUrl?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default function AppHeaderLayout({
|
|
20
|
+
breadcrumbs = [],
|
|
21
|
+
children,
|
|
22
|
+
user,
|
|
23
|
+
navItems = [],
|
|
24
|
+
unreadCount = 0,
|
|
25
|
+
dashboardHref = '/dashboard',
|
|
26
|
+
settingsUrl = '/settings/profile',
|
|
27
|
+
logoutUrl = '/logout',
|
|
28
|
+
}: AppHeaderLayoutProps) {
|
|
29
|
+
return (
|
|
30
|
+
<AppShell variant="header">
|
|
31
|
+
<AppHeader
|
|
32
|
+
breadcrumbs={breadcrumbs}
|
|
33
|
+
user={user}
|
|
34
|
+
navItems={navItems}
|
|
35
|
+
unreadCount={unreadCount}
|
|
36
|
+
dashboardHref={dashboardHref}
|
|
37
|
+
settingsUrl={settingsUrl}
|
|
38
|
+
logoutUrl={logoutUrl}
|
|
39
|
+
/>
|
|
40
|
+
<AppContent variant="header">
|
|
41
|
+
{children}
|
|
42
|
+
</AppContent>
|
|
43
|
+
</AppShell>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { AppShell } from '../../components/app-shell';
|
|
3
|
+
import { AppContent } from '../../components/app-content';
|
|
4
|
+
import { AppSidebar } from '../../components/app-sidebar';
|
|
5
|
+
import { AppSidebarHeader } from '../../components/app-sidebar-header';
|
|
6
|
+
import type { NavItem, BreadcrumbItem } from '../../types/navigation';
|
|
7
|
+
import type { User } from '../../types/auth';
|
|
8
|
+
|
|
9
|
+
export type AppSidebarLayoutProps = {
|
|
10
|
+
breadcrumbs?: BreadcrumbItem[];
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
user?: User | null;
|
|
13
|
+
navItems?: NavItem[];
|
|
14
|
+
tenant?: { display_name?: string | null; sub_brand?: string | null } | null;
|
|
15
|
+
unreadCount?: number;
|
|
16
|
+
dashboardHref?: string;
|
|
17
|
+
settingsUrl?: string;
|
|
18
|
+
logoutUrl?: string;
|
|
19
|
+
defaultOpen?: boolean;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default function AppSidebarLayout({
|
|
23
|
+
breadcrumbs = [],
|
|
24
|
+
children,
|
|
25
|
+
user,
|
|
26
|
+
navItems,
|
|
27
|
+
tenant,
|
|
28
|
+
unreadCount = 0,
|
|
29
|
+
dashboardHref = '/dashboard',
|
|
30
|
+
settingsUrl = '/settings/profile',
|
|
31
|
+
logoutUrl = '/logout',
|
|
32
|
+
defaultOpen = true,
|
|
33
|
+
}: AppSidebarLayoutProps) {
|
|
34
|
+
return (
|
|
35
|
+
<AppShell variant="sidebar" defaultOpen={defaultOpen}>
|
|
36
|
+
<AppSidebar
|
|
37
|
+
navItems={navItems}
|
|
38
|
+
user={user}
|
|
39
|
+
tenant={tenant}
|
|
40
|
+
dashboardHref={dashboardHref}
|
|
41
|
+
settingsUrl={settingsUrl}
|
|
42
|
+
logoutUrl={logoutUrl}
|
|
43
|
+
/>
|
|
44
|
+
<AppContent variant="sidebar">
|
|
45
|
+
<AppSidebarHeader
|
|
46
|
+
breadcrumbs={breadcrumbs}
|
|
47
|
+
user={user}
|
|
48
|
+
unreadCount={unreadCount}
|
|
49
|
+
settingsUrl={settingsUrl}
|
|
50
|
+
logoutUrl={logoutUrl}
|
|
51
|
+
/>
|
|
52
|
+
{children}
|
|
53
|
+
</AppContent>
|
|
54
|
+
</AppShell>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext, useState, type ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
interface LayoutContextValue {
|
|
6
|
+
sidebarCollapsed: boolean;
|
|
7
|
+
setSidebarCollapsed: (collapsed: boolean) => void;
|
|
8
|
+
toggleSidebar: () => void;
|
|
9
|
+
stickyHeader: boolean;
|
|
10
|
+
setStickyHeader: (sticky: boolean) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const LayoutContext = createContext<LayoutContextValue | null>(null);
|
|
14
|
+
|
|
15
|
+
export function LayoutProvider({
|
|
16
|
+
children,
|
|
17
|
+
defaultSidebarCollapsed = false,
|
|
18
|
+
defaultStickyHeader = true,
|
|
19
|
+
}: {
|
|
20
|
+
children: ReactNode;
|
|
21
|
+
defaultSidebarCollapsed?: boolean;
|
|
22
|
+
defaultStickyHeader?: boolean;
|
|
23
|
+
}) {
|
|
24
|
+
const [sidebarCollapsed, setSidebarCollapsed] = useState(defaultSidebarCollapsed);
|
|
25
|
+
const [stickyHeader, setStickyHeader] = useState(defaultStickyHeader);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<LayoutContext.Provider value={{
|
|
29
|
+
sidebarCollapsed,
|
|
30
|
+
setSidebarCollapsed,
|
|
31
|
+
toggleSidebar: () => setSidebarCollapsed((c) => !c),
|
|
32
|
+
stickyHeader,
|
|
33
|
+
setStickyHeader,
|
|
34
|
+
}}>
|
|
35
|
+
{children}
|
|
36
|
+
</LayoutContext.Provider>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function useLayout() {
|
|
41
|
+
const ctx = useContext(LayoutContext);
|
|
42
|
+
if (!ctx) throw new Error('useLayout must be used within a LayoutProvider');
|
|
43
|
+
return ctx;
|
|
44
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { NavItem, BreadcrumbItem } from '../../types/navigation';
|
|
3
|
+
|
|
4
|
+
export interface AppLayoutUser {
|
|
5
|
+
name: string;
|
|
6
|
+
email?: string;
|
|
7
|
+
avatar?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface AppLayoutFooterLink {
|
|
11
|
+
label: string;
|
|
12
|
+
href: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface BaseAppLayoutProps {
|
|
16
|
+
children: ReactNode;
|
|
17
|
+
|
|
18
|
+
// Navigation
|
|
19
|
+
navItems?: NavItem[];
|
|
20
|
+
currentUrl?: string;
|
|
21
|
+
|
|
22
|
+
// Identity
|
|
23
|
+
logo?: ReactNode;
|
|
24
|
+
logoHref?: string;
|
|
25
|
+
appName?: string;
|
|
26
|
+
user?: AppLayoutUser | null;
|
|
27
|
+
|
|
28
|
+
// Page context
|
|
29
|
+
title?: string;
|
|
30
|
+
breadcrumbs?: BreadcrumbItem[];
|
|
31
|
+
toolbarActions?: ReactNode;
|
|
32
|
+
|
|
33
|
+
// Behaviour
|
|
34
|
+
stickyHeader?: boolean;
|
|
35
|
+
stickyOffset?: number;
|
|
36
|
+
defaultSidebarCollapsed?: boolean;
|
|
37
|
+
|
|
38
|
+
// Actions
|
|
39
|
+
onLogout?: () => void;
|
|
40
|
+
settingsUrl?: string;
|
|
41
|
+
logoutUrl?: string;
|
|
42
|
+
unreadCount?: number;
|
|
43
|
+
|
|
44
|
+
// Footer
|
|
45
|
+
footerLinks?: AppLayoutFooterLink[];
|
|
46
|
+
copyright?: string;
|
|
47
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { AppLayoutFooterLink } from '../layout-types';
|
|
2
|
+
import { cn } from '../../../lib/utils';
|
|
3
|
+
|
|
4
|
+
interface FooterProps {
|
|
5
|
+
links?: AppLayoutFooterLink[];
|
|
6
|
+
copyright?: string;
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function Footer({ links = [], copyright, className }: FooterProps) {
|
|
11
|
+
const year = new Date().getFullYear();
|
|
12
|
+
return (
|
|
13
|
+
<footer className={cn('footer border-t border-border', className)}>
|
|
14
|
+
<div className="container mx-auto px-4">
|
|
15
|
+
<div className="flex flex-col md:flex-row justify-center md:justify-between items-center gap-3 py-5">
|
|
16
|
+
<div className="flex order-2 md:order-1 gap-2 font-normal text-sm">
|
|
17
|
+
<span className="text-muted-foreground">{year} ©</span>
|
|
18
|
+
{copyright && (
|
|
19
|
+
<span className="text-secondary-foreground">{copyright}</span>
|
|
20
|
+
)}
|
|
21
|
+
</div>
|
|
22
|
+
{links.length > 0 && (
|
|
23
|
+
<nav className="flex order-1 md:order-2 gap-4 font-normal text-sm text-muted-foreground">
|
|
24
|
+
{links.map((link) => (
|
|
25
|
+
<a key={link.href} href={link.href} className="hover:text-primary transition-colors">
|
|
26
|
+
{link.label}
|
|
27
|
+
</a>
|
|
28
|
+
))}
|
|
29
|
+
</nav>
|
|
30
|
+
)}
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</footer>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
import type { AppLayoutUser } from '../layout-types';
|
|
5
|
+
import { Bell, Search } from 'lucide-react';
|
|
6
|
+
import { cn } from '../../../lib/utils';
|
|
7
|
+
import { Button } from '../../../controls/Button';
|
|
8
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '../../../components/ui/dropdown-menu';
|
|
9
|
+
|
|
10
|
+
interface HeaderTopbarProps {
|
|
11
|
+
user?: AppLayoutUser | null;
|
|
12
|
+
unreadCount?: number;
|
|
13
|
+
onSearchOpen?: () => void;
|
|
14
|
+
settingsUrl?: string;
|
|
15
|
+
logoutUrl?: string;
|
|
16
|
+
onLogout?: () => void;
|
|
17
|
+
className?: string;
|
|
18
|
+
extraSlot?: ReactNode;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getInitials(name: string): string {
|
|
22
|
+
return name
|
|
23
|
+
.split(' ')
|
|
24
|
+
.map((n) => n[0])
|
|
25
|
+
.slice(0, 2)
|
|
26
|
+
.join('')
|
|
27
|
+
.toUpperCase();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function HeaderTopbar({
|
|
31
|
+
user,
|
|
32
|
+
unreadCount = 0,
|
|
33
|
+
onSearchOpen,
|
|
34
|
+
settingsUrl,
|
|
35
|
+
logoutUrl,
|
|
36
|
+
onLogout,
|
|
37
|
+
className,
|
|
38
|
+
extraSlot,
|
|
39
|
+
}: HeaderTopbarProps) {
|
|
40
|
+
return (
|
|
41
|
+
<div className={cn('flex items-center gap-3', className)}>
|
|
42
|
+
{extraSlot}
|
|
43
|
+
|
|
44
|
+
{onSearchOpen && (
|
|
45
|
+
<Button variant="ghost" size="sm" className="size-9 p-0 rounded-full" onClick={onSearchOpen} aria-label="Search">
|
|
46
|
+
<Search className="size-4" />
|
|
47
|
+
</Button>
|
|
48
|
+
)}
|
|
49
|
+
|
|
50
|
+
<Button variant="ghost" size="sm" className="size-9 p-0 rounded-full relative" aria-label="Notifications">
|
|
51
|
+
<Bell className="size-4" />
|
|
52
|
+
{unreadCount > 0 && (
|
|
53
|
+
<span className="absolute top-0.5 right-0.5 size-2 rounded-full bg-primary" />
|
|
54
|
+
)}
|
|
55
|
+
</Button>
|
|
56
|
+
|
|
57
|
+
{user && (
|
|
58
|
+
<DropdownMenu>
|
|
59
|
+
<DropdownMenuTrigger asChild>
|
|
60
|
+
<button className="size-9 rounded-full border border-border overflow-hidden cursor-pointer shrink-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
|
|
61
|
+
{user.avatar ? (
|
|
62
|
+
<img src={user.avatar} alt={user.name} className="size-full object-cover" />
|
|
63
|
+
) : (
|
|
64
|
+
<span className="size-full flex items-center justify-center bg-muted text-xs font-medium text-muted-foreground">
|
|
65
|
+
{getInitials(user.name)}
|
|
66
|
+
</span>
|
|
67
|
+
)}
|
|
68
|
+
</button>
|
|
69
|
+
</DropdownMenuTrigger>
|
|
70
|
+
<DropdownMenuContent align="end" className="w-52">
|
|
71
|
+
<div className="px-2 py-1.5">
|
|
72
|
+
<p className="text-sm font-medium text-foreground truncate">{user.name}</p>
|
|
73
|
+
{user.email && <p className="text-xs text-muted-foreground truncate">{user.email}</p>}
|
|
74
|
+
</div>
|
|
75
|
+
<DropdownMenuSeparator />
|
|
76
|
+
{settingsUrl && (
|
|
77
|
+
<DropdownMenuItem asChild>
|
|
78
|
+
<a href={settingsUrl}>Settings</a>
|
|
79
|
+
</DropdownMenuItem>
|
|
80
|
+
)}
|
|
81
|
+
<DropdownMenuSeparator />
|
|
82
|
+
{logoutUrl ? (
|
|
83
|
+
<DropdownMenuItem asChild>
|
|
84
|
+
<a href={logoutUrl} className="text-destructive focus:text-destructive">Sign out</a>
|
|
85
|
+
</DropdownMenuItem>
|
|
86
|
+
) : onLogout ? (
|
|
87
|
+
<DropdownMenuItem onClick={onLogout} className="text-destructive focus:text-destructive">
|
|
88
|
+
Sign out
|
|
89
|
+
</DropdownMenuItem>
|
|
90
|
+
) : null}
|
|
91
|
+
</DropdownMenuContent>
|
|
92
|
+
</DropdownMenu>
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
}
|