@moontra/moonui-pro 2.20.1 → 2.20.2
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/dist/index.d.ts +691 -261
- package/dist/index.mjs +7418 -4934
- package/package.json +4 -3
- package/scripts/postbuild.js +27 -0
- package/src/components/advanced-chart/index.tsx +5 -1
- package/src/components/advanced-forms/index.tsx +175 -16
- package/src/components/calendar/event-dialog.tsx +18 -13
- package/src/components/calendar/index.tsx +197 -50
- package/src/components/dashboard/dashboard-grid.tsx +21 -3
- package/src/components/dashboard/types.ts +3 -0
- package/src/components/dashboard/widgets/activity-feed.tsx +6 -1
- package/src/components/dashboard/widgets/comparison-widget.tsx +177 -0
- package/src/components/dashboard/widgets/index.ts +5 -0
- package/src/components/dashboard/widgets/metric-card.tsx +21 -1
- package/src/components/dashboard/widgets/progress-widget.tsx +113 -0
- package/src/components/error-boundary/index.tsx +160 -37
- package/src/components/form-wizard/form-wizard-context.tsx +54 -26
- package/src/components/form-wizard/form-wizard-progress.tsx +33 -2
- package/src/components/form-wizard/types.ts +2 -1
- package/src/components/github-stars/hooks.ts +1 -0
- package/src/components/github-stars/variants.tsx +3 -1
- package/src/components/health-check/index.tsx +14 -14
- package/src/components/hover-card-3d/index.tsx +2 -3
- package/src/components/index.ts +5 -3
- package/src/components/kanban/kanban.tsx +23 -18
- package/src/components/license-error/index.tsx +2 -0
- package/src/components/magnetic-button/index.tsx +56 -7
- package/src/components/memory-efficient-data/index.tsx +117 -115
- package/src/components/navbar/index.tsx +781 -0
- package/src/components/performance-debugger/index.tsx +62 -38
- package/src/components/performance-monitor/index.tsx +47 -33
- package/src/components/phone-number-input/index.tsx +32 -27
- package/src/components/phone-number-input/phone-number-input-simple.tsx +167 -0
- package/src/components/rich-text-editor/index.tsx +26 -28
- package/src/components/rich-text-editor/slash-commands-extension.ts +15 -5
- package/src/components/sidebar/index.tsx +32 -13
- package/src/components/timeline/index.tsx +84 -49
- package/src/components/ui/accordion.tsx +550 -42
- package/src/components/ui/avatar.tsx +2 -0
- package/src/components/ui/badge.tsx +2 -0
- package/src/components/ui/breadcrumb.tsx +2 -0
- package/src/components/ui/button.tsx +39 -33
- package/src/components/ui/card.tsx +2 -0
- package/src/components/ui/collapsible.tsx +546 -50
- package/src/components/ui/command.tsx +790 -67
- package/src/components/ui/dialog.tsx +510 -92
- package/src/components/ui/dropdown-menu.tsx +540 -52
- package/src/components/ui/index.ts +37 -5
- package/src/components/ui/input.tsx +2 -0
- package/src/components/ui/magnetic-button.tsx +1 -1
- package/src/components/ui/media-gallery.tsx +1 -2
- package/src/components/ui/navigation-menu.tsx +130 -0
- package/src/components/ui/pagination.tsx +2 -0
- package/src/components/ui/select.tsx +6 -2
- package/src/components/ui/spotlight-card.tsx +1 -1
- package/src/components/ui/table.tsx +2 -0
- package/src/components/ui/tabs-pro.tsx +542 -0
- package/src/components/ui/tabs.tsx +23 -167
- package/src/components/ui/toggle.tsx +12 -12
- package/src/index.ts +11 -3
- package/src/styles/index.css +596 -0
- package/src/use-performance-optimizer.ts +1 -1
- package/src/utils/chart-helpers.ts +1 -1
- package/src/__tests__/use-intersection-observer.test.tsx +0 -216
- package/src/__tests__/use-local-storage.test.tsx +0 -174
- package/src/__tests__/use-pro-access.test.tsx +0 -183
- package/src/components/advanced-chart/advanced-chart.test.tsx +0 -281
- package/src/components/data-table/data-table.test.tsx +0 -187
- package/src/components/enhanced/badge.tsx +0 -191
- package/src/components/enhanced/button.tsx +0 -362
- package/src/components/enhanced/card.tsx +0 -266
- package/src/components/enhanced/dialog.tsx +0 -246
- package/src/components/enhanced/index.ts +0 -4
- package/src/components/file-upload/file-upload.test.tsx +0 -243
- package/src/components/rich-text-editor/index-old-backup.tsx +0 -437
- package/src/types/moonui.d.ts +0 -22
|
@@ -0,0 +1,781 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect, useCallback, useRef } from 'react'
|
|
4
|
+
import { motion, AnimatePresence } from 'framer-motion'
|
|
5
|
+
import { cn } from '../../lib/utils'
|
|
6
|
+
import { Button } from '../ui/button'
|
|
7
|
+
import { Badge } from '../ui/badge'
|
|
8
|
+
import { Separator } from '../ui/separator'
|
|
9
|
+
import { Sheet, SheetContent, SheetTrigger } from '../ui/sheet'
|
|
10
|
+
import {
|
|
11
|
+
DropdownMenu,
|
|
12
|
+
DropdownMenuContent,
|
|
13
|
+
DropdownMenuItem,
|
|
14
|
+
DropdownMenuSeparator,
|
|
15
|
+
DropdownMenuTrigger,
|
|
16
|
+
DropdownMenuSub,
|
|
17
|
+
DropdownMenuSubContent,
|
|
18
|
+
DropdownMenuSubTrigger,
|
|
19
|
+
} from '../ui/dropdown-menu'
|
|
20
|
+
import {
|
|
21
|
+
NavigationMenu,
|
|
22
|
+
NavigationMenuContent,
|
|
23
|
+
NavigationMenuItem,
|
|
24
|
+
NavigationMenuLink,
|
|
25
|
+
NavigationMenuList,
|
|
26
|
+
NavigationMenuTrigger,
|
|
27
|
+
navigationMenuTriggerStyle,
|
|
28
|
+
} from '../ui/navigation-menu'
|
|
29
|
+
import {
|
|
30
|
+
Menu,
|
|
31
|
+
X,
|
|
32
|
+
ChevronDown,
|
|
33
|
+
Search,
|
|
34
|
+
Bell,
|
|
35
|
+
User,
|
|
36
|
+
Settings,
|
|
37
|
+
LogOut,
|
|
38
|
+
Moon,
|
|
39
|
+
Sun,
|
|
40
|
+
Monitor,
|
|
41
|
+
Command,
|
|
42
|
+
Sparkles
|
|
43
|
+
} from 'lucide-react'
|
|
44
|
+
import { Avatar, AvatarFallback, AvatarImage } from '../ui/avatar'
|
|
45
|
+
import { Input } from '../ui/input'
|
|
46
|
+
import {
|
|
47
|
+
CommandDialog,
|
|
48
|
+
CommandEmpty,
|
|
49
|
+
CommandGroup,
|
|
50
|
+
CommandInput,
|
|
51
|
+
CommandItem,
|
|
52
|
+
CommandList,
|
|
53
|
+
} from '../ui/command'
|
|
54
|
+
|
|
55
|
+
export interface NavbarItem {
|
|
56
|
+
id: string
|
|
57
|
+
title: string
|
|
58
|
+
href?: string
|
|
59
|
+
icon?: React.ReactNode
|
|
60
|
+
badge?: string | number
|
|
61
|
+
badgeVariant?: 'primary' | 'secondary' | 'destructive' | 'outline'
|
|
62
|
+
disabled?: boolean
|
|
63
|
+
items?: NavbarItem[]
|
|
64
|
+
action?: () => void
|
|
65
|
+
description?: string
|
|
66
|
+
thumbnail?: string
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface NavbarSection {
|
|
70
|
+
id: string
|
|
71
|
+
items: NavbarItem[]
|
|
72
|
+
showInMobile?: boolean
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface NavbarConfig {
|
|
76
|
+
sections: NavbarSection[]
|
|
77
|
+
branding?: {
|
|
78
|
+
logo?: React.ReactNode
|
|
79
|
+
title?: string
|
|
80
|
+
href?: string
|
|
81
|
+
}
|
|
82
|
+
showSearch?: boolean
|
|
83
|
+
searchPlaceholder?: string
|
|
84
|
+
onSearch?: (value: string) => void
|
|
85
|
+
showThemeToggle?: boolean
|
|
86
|
+
theme?: 'light' | 'dark' | 'system'
|
|
87
|
+
onThemeChange?: (theme: 'light' | 'dark' | 'system') => void
|
|
88
|
+
userMenu?: {
|
|
89
|
+
user?: {
|
|
90
|
+
name?: string
|
|
91
|
+
email?: string
|
|
92
|
+
avatar?: string
|
|
93
|
+
}
|
|
94
|
+
items?: NavbarItem[]
|
|
95
|
+
}
|
|
96
|
+
notifications?: {
|
|
97
|
+
count?: number
|
|
98
|
+
items?: Array<{
|
|
99
|
+
id: string
|
|
100
|
+
title: string
|
|
101
|
+
description?: string
|
|
102
|
+
time?: string
|
|
103
|
+
read?: boolean
|
|
104
|
+
}>
|
|
105
|
+
onNotificationClick?: (id: string) => void
|
|
106
|
+
}
|
|
107
|
+
cta?: {
|
|
108
|
+
text: string
|
|
109
|
+
href?: string
|
|
110
|
+
action?: () => void
|
|
111
|
+
variant?: 'primary' | 'secondary' | 'outline' | 'ghost'
|
|
112
|
+
}
|
|
113
|
+
sticky?: boolean
|
|
114
|
+
glassmorphism?: boolean
|
|
115
|
+
animatedBackground?: boolean
|
|
116
|
+
keyboardShortcuts?: boolean
|
|
117
|
+
mobileBreakpoint?: number
|
|
118
|
+
customStyles?: {
|
|
119
|
+
background?: string
|
|
120
|
+
border?: string
|
|
121
|
+
text?: string
|
|
122
|
+
hover?: string
|
|
123
|
+
}
|
|
124
|
+
variant?: 'default' | 'floating' | 'minimal' | 'transparent'
|
|
125
|
+
size?: 'sm' | 'md' | 'lg'
|
|
126
|
+
blur?: boolean
|
|
127
|
+
shadow?: boolean
|
|
128
|
+
rounded?: boolean
|
|
129
|
+
logoPosition?: 'left' | 'center'
|
|
130
|
+
mobileMenuPosition?: 'left' | 'right'
|
|
131
|
+
hideOnScroll?: boolean
|
|
132
|
+
showScrollProgress?: boolean
|
|
133
|
+
maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full'
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
interface NavbarProps extends NavbarConfig {
|
|
137
|
+
className?: string
|
|
138
|
+
activePath?: string
|
|
139
|
+
onNavigate?: (href: string) => void
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function Navbar({
|
|
143
|
+
sections,
|
|
144
|
+
branding,
|
|
145
|
+
showSearch = true,
|
|
146
|
+
searchPlaceholder = "Search...",
|
|
147
|
+
onSearch,
|
|
148
|
+
showThemeToggle = false,
|
|
149
|
+
theme = 'system',
|
|
150
|
+
onThemeChange,
|
|
151
|
+
userMenu,
|
|
152
|
+
notifications,
|
|
153
|
+
cta,
|
|
154
|
+
sticky = true,
|
|
155
|
+
glassmorphism = false,
|
|
156
|
+
animatedBackground = false,
|
|
157
|
+
keyboardShortcuts = true,
|
|
158
|
+
mobileBreakpoint = 768,
|
|
159
|
+
customStyles,
|
|
160
|
+
variant = 'default',
|
|
161
|
+
size = 'md',
|
|
162
|
+
blur = true,
|
|
163
|
+
shadow = false,
|
|
164
|
+
rounded = false,
|
|
165
|
+
logoPosition = 'left',
|
|
166
|
+
mobileMenuPosition = 'left',
|
|
167
|
+
hideOnScroll = false,
|
|
168
|
+
showScrollProgress = false,
|
|
169
|
+
maxWidth = '2xl',
|
|
170
|
+
className,
|
|
171
|
+
activePath,
|
|
172
|
+
onNavigate
|
|
173
|
+
}: NavbarProps) {
|
|
174
|
+
const [isMobile, setIsMobile] = useState(false)
|
|
175
|
+
const [isOpen, setIsOpen] = useState(false)
|
|
176
|
+
const [isSearchOpen, setIsSearchOpen] = useState(false)
|
|
177
|
+
const [isCommandOpen, setIsCommandOpen] = useState(false)
|
|
178
|
+
const [searchValue, setSearchValue] = useState('')
|
|
179
|
+
const [scrolled, setScrolled] = useState(false)
|
|
180
|
+
const [hidden, setHidden] = useState(false)
|
|
181
|
+
const [scrollProgress, setScrollProgress] = useState(0)
|
|
182
|
+
const lastScrollY = useRef(0)
|
|
183
|
+
const navRef = useRef<HTMLElement>(null)
|
|
184
|
+
|
|
185
|
+
// Check mobile
|
|
186
|
+
useEffect(() => {
|
|
187
|
+
const checkMobile = () => {
|
|
188
|
+
setIsMobile(window.innerWidth < mobileBreakpoint)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
checkMobile()
|
|
192
|
+
window.addEventListener('resize', checkMobile)
|
|
193
|
+
return () => window.removeEventListener('resize', checkMobile)
|
|
194
|
+
}, [mobileBreakpoint])
|
|
195
|
+
|
|
196
|
+
// Handle scroll for sticky navbar and hide on scroll
|
|
197
|
+
useEffect(() => {
|
|
198
|
+
if (!sticky && !hideOnScroll && !showScrollProgress) return
|
|
199
|
+
|
|
200
|
+
const handleScroll = () => {
|
|
201
|
+
const currentScrollY = window.scrollY
|
|
202
|
+
|
|
203
|
+
// Set scrolled state
|
|
204
|
+
setScrolled(currentScrollY > 10)
|
|
205
|
+
|
|
206
|
+
// Hide on scroll
|
|
207
|
+
if (hideOnScroll) {
|
|
208
|
+
if (currentScrollY > lastScrollY.current && currentScrollY > 100) {
|
|
209
|
+
setHidden(true)
|
|
210
|
+
} else {
|
|
211
|
+
setHidden(false)
|
|
212
|
+
}
|
|
213
|
+
lastScrollY.current = currentScrollY
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Scroll progress
|
|
217
|
+
if (showScrollProgress) {
|
|
218
|
+
const winScroll = window.scrollY
|
|
219
|
+
const height = document.documentElement.scrollHeight - document.documentElement.clientHeight
|
|
220
|
+
const scrolled = (winScroll / height) * 100
|
|
221
|
+
setScrollProgress(scrolled)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
window.addEventListener('scroll', handleScroll, { passive: true })
|
|
226
|
+
return () => window.removeEventListener('scroll', handleScroll)
|
|
227
|
+
}, [sticky, hideOnScroll, showScrollProgress])
|
|
228
|
+
|
|
229
|
+
// Keyboard shortcuts
|
|
230
|
+
useEffect(() => {
|
|
231
|
+
if (!keyboardShortcuts) return
|
|
232
|
+
|
|
233
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
234
|
+
// Cmd/Ctrl + K for search
|
|
235
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
236
|
+
e.preventDefault()
|
|
237
|
+
setIsCommandOpen(true)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
document.addEventListener('keydown', handleKeyDown)
|
|
242
|
+
return () => document.removeEventListener('keydown', handleKeyDown)
|
|
243
|
+
}, [keyboardShortcuts])
|
|
244
|
+
|
|
245
|
+
const handleItemClick = useCallback((item: NavbarItem) => {
|
|
246
|
+
if (item.action) {
|
|
247
|
+
item.action()
|
|
248
|
+
} else if (item.href && onNavigate) {
|
|
249
|
+
onNavigate(item.href)
|
|
250
|
+
if (isMobile) {
|
|
251
|
+
setIsOpen(false)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}, [onNavigate, isMobile])
|
|
255
|
+
|
|
256
|
+
const handleSearch = useCallback((value: string) => {
|
|
257
|
+
setSearchValue(value)
|
|
258
|
+
onSearch?.(value)
|
|
259
|
+
}, [onSearch])
|
|
260
|
+
|
|
261
|
+
// Render mega menu content
|
|
262
|
+
const renderMegaMenuContent = (items: NavbarItem[]) => (
|
|
263
|
+
<div className="grid gap-3 p-6 md:w-[400px] lg:w-[500px] lg:grid-cols-[.75fr_1fr]">
|
|
264
|
+
{items.map(item => (
|
|
265
|
+
<NavigationMenuLink
|
|
266
|
+
key={item.id}
|
|
267
|
+
className={cn(
|
|
268
|
+
"block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground",
|
|
269
|
+
item.disabled && "opacity-50 cursor-not-allowed"
|
|
270
|
+
)}
|
|
271
|
+
onClick={() => !item.disabled && handleItemClick(item)}
|
|
272
|
+
>
|
|
273
|
+
<div className="flex items-center gap-2">
|
|
274
|
+
{item.icon && <span className="flex-shrink-0">{item.icon}</span>}
|
|
275
|
+
<div className="text-sm font-medium leading-none">{item.title}</div>
|
|
276
|
+
{item.badge && (
|
|
277
|
+
<Badge variant={item.badgeVariant || 'secondary'} className="ml-auto">
|
|
278
|
+
{item.badge}
|
|
279
|
+
</Badge>
|
|
280
|
+
)}
|
|
281
|
+
</div>
|
|
282
|
+
{item.description && (
|
|
283
|
+
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
|
|
284
|
+
{item.description}
|
|
285
|
+
</p>
|
|
286
|
+
)}
|
|
287
|
+
</NavigationMenuLink>
|
|
288
|
+
))}
|
|
289
|
+
</div>
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
// Render dropdown menu
|
|
293
|
+
const renderDropdownMenu = (items: NavbarItem[]) => (
|
|
294
|
+
<>
|
|
295
|
+
{items.map(item => {
|
|
296
|
+
if (item.items && item.items.length > 0) {
|
|
297
|
+
return (
|
|
298
|
+
<DropdownMenuSub key={item.id}>
|
|
299
|
+
<DropdownMenuSubTrigger disabled={item.disabled}>
|
|
300
|
+
<span className="flex items-center gap-2">
|
|
301
|
+
{item.icon && <span>{item.icon}</span>}
|
|
302
|
+
{item.title}
|
|
303
|
+
</span>
|
|
304
|
+
</DropdownMenuSubTrigger>
|
|
305
|
+
<DropdownMenuSubContent>
|
|
306
|
+
{renderDropdownMenu(item.items)}
|
|
307
|
+
</DropdownMenuSubContent>
|
|
308
|
+
</DropdownMenuSub>
|
|
309
|
+
)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return (
|
|
313
|
+
<DropdownMenuItem
|
|
314
|
+
key={item.id}
|
|
315
|
+
disabled={item.disabled}
|
|
316
|
+
onClick={() => handleItemClick(item)}
|
|
317
|
+
>
|
|
318
|
+
<span className="flex items-center gap-2">
|
|
319
|
+
{item.icon && <span>{item.icon}</span>}
|
|
320
|
+
{item.title}
|
|
321
|
+
</span>
|
|
322
|
+
{item.badge && (
|
|
323
|
+
<Badge variant={item.badgeVariant || 'secondary'} className="ml-auto">
|
|
324
|
+
{item.badge}
|
|
325
|
+
</Badge>
|
|
326
|
+
)}
|
|
327
|
+
</DropdownMenuItem>
|
|
328
|
+
)
|
|
329
|
+
})}
|
|
330
|
+
</>
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
// Mobile menu
|
|
334
|
+
const MobileMenu = () => (
|
|
335
|
+
<Sheet open={isOpen} onOpenChange={setIsOpen}>
|
|
336
|
+
<SheetTrigger asChild>
|
|
337
|
+
<Button variant="ghost" size="icon" className="md:hidden">
|
|
338
|
+
<Menu className="h-5 w-5" />
|
|
339
|
+
</Button>
|
|
340
|
+
</SheetTrigger>
|
|
341
|
+
<SheetContent side={mobileMenuPosition} className="w-80 p-0">
|
|
342
|
+
<div className="flex h-full flex-col">
|
|
343
|
+
<div className="flex items-center justify-between p-4 border-b">
|
|
344
|
+
{branding && (
|
|
345
|
+
<div className="flex items-center gap-2">
|
|
346
|
+
{branding.logo}
|
|
347
|
+
{branding.title && (
|
|
348
|
+
<span className="font-semibold text-lg">{branding.title}</span>
|
|
349
|
+
)}
|
|
350
|
+
</div>
|
|
351
|
+
)}
|
|
352
|
+
<Button
|
|
353
|
+
variant="ghost"
|
|
354
|
+
size="icon"
|
|
355
|
+
onClick={() => setIsOpen(false)}
|
|
356
|
+
className="h-8 w-8"
|
|
357
|
+
>
|
|
358
|
+
<X className="h-4 w-4" />
|
|
359
|
+
</Button>
|
|
360
|
+
</div>
|
|
361
|
+
|
|
362
|
+
<div className="flex-1 overflow-y-auto p-4">
|
|
363
|
+
{sections.map(section => (
|
|
364
|
+
<div key={section.id} className="space-y-1">
|
|
365
|
+
{section.items.map(item => (
|
|
366
|
+
<div key={item.id}>
|
|
367
|
+
{item.items && item.items.length > 0 ? (
|
|
368
|
+
<DropdownMenu>
|
|
369
|
+
<DropdownMenuTrigger asChild>
|
|
370
|
+
<Button
|
|
371
|
+
variant="ghost"
|
|
372
|
+
className="w-full justify-between"
|
|
373
|
+
disabled={item.disabled}
|
|
374
|
+
>
|
|
375
|
+
<span className="flex items-center gap-2">
|
|
376
|
+
{item.icon && <span>{item.icon}</span>}
|
|
377
|
+
{item.title}
|
|
378
|
+
</span>
|
|
379
|
+
<ChevronDown className="h-4 w-4" />
|
|
380
|
+
</Button>
|
|
381
|
+
</DropdownMenuTrigger>
|
|
382
|
+
<DropdownMenuContent align="start" className="w-56">
|
|
383
|
+
{item.items && renderDropdownMenu(item.items)}
|
|
384
|
+
</DropdownMenuContent>
|
|
385
|
+
</DropdownMenu>
|
|
386
|
+
) : (
|
|
387
|
+
<Button
|
|
388
|
+
variant="ghost"
|
|
389
|
+
className="w-full justify-start"
|
|
390
|
+
disabled={item.disabled}
|
|
391
|
+
onClick={() => handleItemClick(item)}
|
|
392
|
+
>
|
|
393
|
+
<span className="flex items-center gap-2">
|
|
394
|
+
{item.icon && <span>{item.icon}</span>}
|
|
395
|
+
{item.title}
|
|
396
|
+
</span>
|
|
397
|
+
{item.badge && (
|
|
398
|
+
<Badge variant={item.badgeVariant || 'secondary'} className="ml-auto">
|
|
399
|
+
{item.badge}
|
|
400
|
+
</Badge>
|
|
401
|
+
)}
|
|
402
|
+
</Button>
|
|
403
|
+
)}
|
|
404
|
+
</div>
|
|
405
|
+
))}
|
|
406
|
+
<Separator className="my-2" />
|
|
407
|
+
</div>
|
|
408
|
+
))}
|
|
409
|
+
</div>
|
|
410
|
+
</div>
|
|
411
|
+
</SheetContent>
|
|
412
|
+
</Sheet>
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
// Get height based on size
|
|
416
|
+
const getHeight = () => {
|
|
417
|
+
switch (size) {
|
|
418
|
+
case 'sm': return 'h-14'
|
|
419
|
+
case 'lg': return 'h-20'
|
|
420
|
+
default: return 'h-16'
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const navClasses = cn(
|
|
425
|
+
"moonui-pro w-full transition-all duration-300",
|
|
426
|
+
// Base variant styles
|
|
427
|
+
variant === 'default' && "border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60",
|
|
428
|
+
variant === 'floating' && "mx-4 mt-4 rounded-lg border bg-background/95 backdrop-blur shadow-lg",
|
|
429
|
+
variant === 'minimal' && "bg-background",
|
|
430
|
+
variant === 'transparent' && "bg-transparent",
|
|
431
|
+
// Sticky behavior
|
|
432
|
+
sticky && "sticky top-0 z-50",
|
|
433
|
+
// Scroll states
|
|
434
|
+
scrolled && sticky && variant !== 'transparent' && "shadow-sm",
|
|
435
|
+
scrolled && variant === 'transparent' && "bg-background/95 backdrop-blur border-b",
|
|
436
|
+
hidden && "-translate-y-full",
|
|
437
|
+
// Glass effect
|
|
438
|
+
glassmorphism && "bg-background/80 backdrop-blur-xl border-white/10",
|
|
439
|
+
blur && "backdrop-blur-md",
|
|
440
|
+
shadow && "shadow-lg",
|
|
441
|
+
rounded && "rounded-lg",
|
|
442
|
+
// Custom styles
|
|
443
|
+
customStyles?.background,
|
|
444
|
+
customStyles?.border,
|
|
445
|
+
className
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
return (
|
|
449
|
+
<>
|
|
450
|
+
<nav ref={navRef} className={navClasses}>
|
|
451
|
+
<div className={cn(
|
|
452
|
+
"flex items-center",
|
|
453
|
+
getHeight(),
|
|
454
|
+
maxWidth === 'sm' && "container max-w-screen-sm",
|
|
455
|
+
maxWidth === 'md' && "container max-w-screen-md",
|
|
456
|
+
maxWidth === 'lg' && "container max-w-screen-lg",
|
|
457
|
+
maxWidth === 'xl' && "container max-w-screen-xl",
|
|
458
|
+
maxWidth === '2xl' && "container max-w-screen-2xl",
|
|
459
|
+
maxWidth === 'full' && "w-full px-4 sm:px-6 lg:px-8"
|
|
460
|
+
)}>
|
|
461
|
+
{/* Branding */}
|
|
462
|
+
{branding && (
|
|
463
|
+
<div className={cn(
|
|
464
|
+
"flex items-center gap-2",
|
|
465
|
+
logoPosition === 'center' && !isMobile ? "absolute left-1/2 -translate-x-1/2" : "mr-4"
|
|
466
|
+
)}>
|
|
467
|
+
{logoPosition === 'left' && mobileMenuPosition === 'left' && <MobileMenu />}
|
|
468
|
+
<a
|
|
469
|
+
href={branding.href || '/'}
|
|
470
|
+
onClick={(e) => {
|
|
471
|
+
if (branding.href && onNavigate) {
|
|
472
|
+
e.preventDefault()
|
|
473
|
+
onNavigate(branding.href)
|
|
474
|
+
}
|
|
475
|
+
}}
|
|
476
|
+
className="flex items-center gap-2"
|
|
477
|
+
>
|
|
478
|
+
{branding.logo}
|
|
479
|
+
{branding.title && !isMobile && (
|
|
480
|
+
<span className="font-semibold text-lg">{branding.title}</span>
|
|
481
|
+
)}
|
|
482
|
+
</a>
|
|
483
|
+
</div>
|
|
484
|
+
)}
|
|
485
|
+
|
|
486
|
+
{/* Desktop Navigation */}
|
|
487
|
+
{!isMobile && (
|
|
488
|
+
<NavigationMenu className="hidden md:flex">
|
|
489
|
+
<NavigationMenuList>
|
|
490
|
+
{sections.map(section => (
|
|
491
|
+
<React.Fragment key={section.id}>
|
|
492
|
+
{section.items.map(item => {
|
|
493
|
+
const hasChildren = item.items && item.items.length > 0
|
|
494
|
+
const isActive = item.href === activePath
|
|
495
|
+
|
|
496
|
+
if (hasChildren) {
|
|
497
|
+
return (
|
|
498
|
+
<NavigationMenuItem key={item.id}>
|
|
499
|
+
<NavigationMenuTrigger
|
|
500
|
+
className={cn(
|
|
501
|
+
isActive && "bg-accent text-accent-foreground",
|
|
502
|
+
item.disabled && "opacity-50 cursor-not-allowed"
|
|
503
|
+
)}
|
|
504
|
+
disabled={item.disabled}
|
|
505
|
+
>
|
|
506
|
+
<span className="flex items-center gap-2">
|
|
507
|
+
{item.icon && <span>{item.icon}</span>}
|
|
508
|
+
{item.title}
|
|
509
|
+
</span>
|
|
510
|
+
</NavigationMenuTrigger>
|
|
511
|
+
<NavigationMenuContent>
|
|
512
|
+
{item.items && renderMegaMenuContent(item.items)}
|
|
513
|
+
</NavigationMenuContent>
|
|
514
|
+
</NavigationMenuItem>
|
|
515
|
+
)
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return (
|
|
519
|
+
<NavigationMenuItem key={item.id}>
|
|
520
|
+
<NavigationMenuLink
|
|
521
|
+
className={cn(
|
|
522
|
+
navigationMenuTriggerStyle(),
|
|
523
|
+
isActive && "bg-accent text-accent-foreground",
|
|
524
|
+
item.disabled && "opacity-50 cursor-not-allowed"
|
|
525
|
+
)}
|
|
526
|
+
onClick={() => !item.disabled && handleItemClick(item)}
|
|
527
|
+
>
|
|
528
|
+
<span className="flex items-center gap-2">
|
|
529
|
+
{item.icon && <span>{item.icon}</span>}
|
|
530
|
+
{item.title}
|
|
531
|
+
</span>
|
|
532
|
+
{item.badge && (
|
|
533
|
+
<Badge variant={item.badgeVariant || 'secondary'} className="ml-2">
|
|
534
|
+
{item.badge}
|
|
535
|
+
</Badge>
|
|
536
|
+
)}
|
|
537
|
+
</NavigationMenuLink>
|
|
538
|
+
</NavigationMenuItem>
|
|
539
|
+
)
|
|
540
|
+
})}
|
|
541
|
+
</React.Fragment>
|
|
542
|
+
))}
|
|
543
|
+
</NavigationMenuList>
|
|
544
|
+
</NavigationMenu>
|
|
545
|
+
)}
|
|
546
|
+
|
|
547
|
+
{/* Right side actions */}
|
|
548
|
+
<div className="ml-auto flex items-center gap-2">
|
|
549
|
+
{/* Search */}
|
|
550
|
+
{showSearch && (
|
|
551
|
+
<>
|
|
552
|
+
{isMobile ? (
|
|
553
|
+
<Button
|
|
554
|
+
variant="ghost"
|
|
555
|
+
size="icon"
|
|
556
|
+
onClick={() => setIsCommandOpen(true)}
|
|
557
|
+
>
|
|
558
|
+
<Search className="h-5 w-5" />
|
|
559
|
+
</Button>
|
|
560
|
+
) : (
|
|
561
|
+
<div className="relative">
|
|
562
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
563
|
+
<Input
|
|
564
|
+
type="search"
|
|
565
|
+
placeholder={searchPlaceholder}
|
|
566
|
+
className="w-64 pl-9 pr-4 h-9"
|
|
567
|
+
value={searchValue}
|
|
568
|
+
onChange={(e) => handleSearch(e.target.value)}
|
|
569
|
+
/>
|
|
570
|
+
{keyboardShortcuts && (
|
|
571
|
+
<kbd className="absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium opacity-100 hidden sm:flex">
|
|
572
|
+
<span className="text-xs">⌘</span>K
|
|
573
|
+
</kbd>
|
|
574
|
+
)}
|
|
575
|
+
</div>
|
|
576
|
+
)}
|
|
577
|
+
</>
|
|
578
|
+
)}
|
|
579
|
+
|
|
580
|
+
{/* Theme Toggle */}
|
|
581
|
+
{showThemeToggle && (
|
|
582
|
+
<DropdownMenu>
|
|
583
|
+
<DropdownMenuTrigger asChild>
|
|
584
|
+
<Button variant="ghost" size="icon">
|
|
585
|
+
{theme === 'light' && <Sun className="h-5 w-5" />}
|
|
586
|
+
{theme === 'dark' && <Moon className="h-5 w-5" />}
|
|
587
|
+
{theme === 'system' && <Monitor className="h-5 w-5" />}
|
|
588
|
+
</Button>
|
|
589
|
+
</DropdownMenuTrigger>
|
|
590
|
+
<DropdownMenuContent align="end">
|
|
591
|
+
<DropdownMenuItem onClick={() => onThemeChange?.('light')}>
|
|
592
|
+
<Sun className="mr-2 h-4 w-4" />
|
|
593
|
+
Light
|
|
594
|
+
</DropdownMenuItem>
|
|
595
|
+
<DropdownMenuItem onClick={() => onThemeChange?.('dark')}>
|
|
596
|
+
<Moon className="mr-2 h-4 w-4" />
|
|
597
|
+
Dark
|
|
598
|
+
</DropdownMenuItem>
|
|
599
|
+
<DropdownMenuItem onClick={() => onThemeChange?.('system')}>
|
|
600
|
+
<Monitor className="mr-2 h-4 w-4" />
|
|
601
|
+
System
|
|
602
|
+
</DropdownMenuItem>
|
|
603
|
+
</DropdownMenuContent>
|
|
604
|
+
</DropdownMenu>
|
|
605
|
+
)}
|
|
606
|
+
|
|
607
|
+
{/* Notifications */}
|
|
608
|
+
{notifications && (
|
|
609
|
+
<DropdownMenu>
|
|
610
|
+
<DropdownMenuTrigger asChild>
|
|
611
|
+
<Button variant="ghost" size="icon" className="relative">
|
|
612
|
+
<Bell className="h-5 w-5" />
|
|
613
|
+
{notifications.count && notifications.count > 0 && (
|
|
614
|
+
<span className="absolute -top-1 -right-1 h-4 w-4 rounded-full bg-destructive text-[10px] font-medium text-destructive-foreground flex items-center justify-center">
|
|
615
|
+
{notifications.count}
|
|
616
|
+
</span>
|
|
617
|
+
)}
|
|
618
|
+
</Button>
|
|
619
|
+
</DropdownMenuTrigger>
|
|
620
|
+
<DropdownMenuContent align="end" className="w-80">
|
|
621
|
+
<div className="flex items-center justify-between p-4">
|
|
622
|
+
<h4 className="text-sm font-semibold">Notifications</h4>
|
|
623
|
+
{notifications.count && notifications.count > 0 && (
|
|
624
|
+
<Badge variant="secondary">{notifications.count} new</Badge>
|
|
625
|
+
)}
|
|
626
|
+
</div>
|
|
627
|
+
<DropdownMenuSeparator />
|
|
628
|
+
<div className="max-h-[300px] overflow-y-auto">
|
|
629
|
+
{notifications.items && notifications.items.length > 0 ? (
|
|
630
|
+
notifications.items.map(notification => (
|
|
631
|
+
<DropdownMenuItem
|
|
632
|
+
key={notification.id}
|
|
633
|
+
className="flex flex-col items-start p-4 cursor-pointer"
|
|
634
|
+
onClick={() => notifications.onNotificationClick?.(notification.id)}
|
|
635
|
+
>
|
|
636
|
+
<div className="flex w-full items-start justify-between">
|
|
637
|
+
<p className={cn(
|
|
638
|
+
"text-sm font-medium",
|
|
639
|
+
!notification.read && "text-primary"
|
|
640
|
+
)}>
|
|
641
|
+
{notification.title}
|
|
642
|
+
</p>
|
|
643
|
+
{!notification.read && (
|
|
644
|
+
<span className="h-2 w-2 rounded-full bg-primary" />
|
|
645
|
+
)}
|
|
646
|
+
</div>
|
|
647
|
+
{notification.description && (
|
|
648
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
649
|
+
{notification.description}
|
|
650
|
+
</p>
|
|
651
|
+
)}
|
|
652
|
+
{notification.time && (
|
|
653
|
+
<p className="text-xs text-muted-foreground mt-2">
|
|
654
|
+
{notification.time}
|
|
655
|
+
</p>
|
|
656
|
+
)}
|
|
657
|
+
</DropdownMenuItem>
|
|
658
|
+
))
|
|
659
|
+
) : (
|
|
660
|
+
<div className="p-4 text-center text-sm text-muted-foreground">
|
|
661
|
+
No notifications
|
|
662
|
+
</div>
|
|
663
|
+
)}
|
|
664
|
+
</div>
|
|
665
|
+
</DropdownMenuContent>
|
|
666
|
+
</DropdownMenu>
|
|
667
|
+
)}
|
|
668
|
+
|
|
669
|
+
{/* CTA Button */}
|
|
670
|
+
{cta && (
|
|
671
|
+
<Button
|
|
672
|
+
variant={cta.variant || 'primary'}
|
|
673
|
+
size="sm"
|
|
674
|
+
onClick={() => {
|
|
675
|
+
if (cta.action) {
|
|
676
|
+
cta.action()
|
|
677
|
+
} else if (cta.href && onNavigate) {
|
|
678
|
+
onNavigate(cta.href)
|
|
679
|
+
}
|
|
680
|
+
}}
|
|
681
|
+
className={!isMobile ? 'ml-2' : ''}
|
|
682
|
+
>
|
|
683
|
+
{cta.text}
|
|
684
|
+
</Button>
|
|
685
|
+
)}
|
|
686
|
+
|
|
687
|
+
{/* User Menu */}
|
|
688
|
+
{userMenu && (
|
|
689
|
+
<DropdownMenu>
|
|
690
|
+
<DropdownMenuTrigger asChild>
|
|
691
|
+
<Button variant="ghost" size="icon" className="relative">
|
|
692
|
+
<Avatar className="h-8 w-8">
|
|
693
|
+
<AvatarImage src={userMenu.user?.avatar} alt={userMenu.user?.name} />
|
|
694
|
+
<AvatarFallback>
|
|
695
|
+
{userMenu.user?.name?.charAt(0) || <User className="h-4 w-4" />}
|
|
696
|
+
</AvatarFallback>
|
|
697
|
+
</Avatar>
|
|
698
|
+
</Button>
|
|
699
|
+
</DropdownMenuTrigger>
|
|
700
|
+
<DropdownMenuContent align="end" className="w-56">
|
|
701
|
+
{userMenu.user && (
|
|
702
|
+
<>
|
|
703
|
+
<div className="p-2">
|
|
704
|
+
<p className="text-sm font-medium">{userMenu.user.name}</p>
|
|
705
|
+
<p className="text-xs text-muted-foreground">{userMenu.user.email}</p>
|
|
706
|
+
</div>
|
|
707
|
+
<DropdownMenuSeparator />
|
|
708
|
+
</>
|
|
709
|
+
)}
|
|
710
|
+
{userMenu.items && renderDropdownMenu(userMenu.items)}
|
|
711
|
+
</DropdownMenuContent>
|
|
712
|
+
</DropdownMenu>
|
|
713
|
+
)}
|
|
714
|
+
</div>
|
|
715
|
+
</div>
|
|
716
|
+
|
|
717
|
+
{/* Animated background */}
|
|
718
|
+
{animatedBackground && (
|
|
719
|
+
<motion.div
|
|
720
|
+
className="absolute inset-0 -z-10 opacity-30"
|
|
721
|
+
animate={{
|
|
722
|
+
background: [
|
|
723
|
+
'radial-gradient(circle at 20% 50%, rgba(120, 119, 198, 0.3), transparent 50%)',
|
|
724
|
+
'radial-gradient(circle at 80% 50%, rgba(120, 119, 198, 0.3), transparent 50%)',
|
|
725
|
+
'radial-gradient(circle at 20% 50%, rgba(120, 119, 198, 0.3), transparent 50%)',
|
|
726
|
+
],
|
|
727
|
+
}}
|
|
728
|
+
transition={{
|
|
729
|
+
duration: 10,
|
|
730
|
+
repeat: Infinity,
|
|
731
|
+
ease: "linear"
|
|
732
|
+
}}
|
|
733
|
+
/>
|
|
734
|
+
)}
|
|
735
|
+
|
|
736
|
+
{/* Scroll Progress Bar */}
|
|
737
|
+
{showScrollProgress && (
|
|
738
|
+
<div className="absolute bottom-0 left-0 w-full h-0.5 bg-muted">
|
|
739
|
+
<div
|
|
740
|
+
className="h-full bg-primary transition-all duration-150"
|
|
741
|
+
style={{ width: `${scrollProgress}%` }}
|
|
742
|
+
/>
|
|
743
|
+
</div>
|
|
744
|
+
)}
|
|
745
|
+
</nav>
|
|
746
|
+
|
|
747
|
+
{/* Command Dialog for search */}
|
|
748
|
+
<CommandDialog open={isCommandOpen} onOpenChange={setIsCommandOpen}>
|
|
749
|
+
<CommandInput
|
|
750
|
+
placeholder={searchPlaceholder}
|
|
751
|
+
value={searchValue}
|
|
752
|
+
onValueChange={handleSearch}
|
|
753
|
+
/>
|
|
754
|
+
<CommandList>
|
|
755
|
+
<CommandEmpty>No results found.</CommandEmpty>
|
|
756
|
+
{sections.map(section => (
|
|
757
|
+
<CommandGroup key={section.id}>
|
|
758
|
+
{section.items.map(item => (
|
|
759
|
+
<CommandItem
|
|
760
|
+
key={item.id}
|
|
761
|
+
disabled={item.disabled}
|
|
762
|
+
onSelect={() => {
|
|
763
|
+
handleItemClick(item)
|
|
764
|
+
setIsCommandOpen(false)
|
|
765
|
+
}}
|
|
766
|
+
>
|
|
767
|
+
<span className="flex items-center gap-2">
|
|
768
|
+
{item.icon && <span>{item.icon}</span>}
|
|
769
|
+
{item.title}
|
|
770
|
+
</span>
|
|
771
|
+
</CommandItem>
|
|
772
|
+
))}
|
|
773
|
+
</CommandGroup>
|
|
774
|
+
))}
|
|
775
|
+
</CommandList>
|
|
776
|
+
</CommandDialog>
|
|
777
|
+
</>
|
|
778
|
+
)
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
export default Navbar
|