@moontra/moonui-pro 2.4.6 → 2.5.1
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 +234 -16
- package/dist/index.mjs +2493 -187
- package/package.json +4 -1
- package/src/components/advanced-chart/index.tsx +11 -1
- package/src/components/dashboard/dashboard-grid.tsx +462 -0
- package/src/components/dashboard/demo.tsx +311 -0
- package/src/components/dashboard/index.tsx +691 -275
- package/src/components/dashboard/time-range-picker.tsx +269 -0
- package/src/components/dashboard/types.ts +222 -0
- package/src/components/dashboard/widgets/activity-feed.tsx +344 -0
- package/src/components/dashboard/widgets/chart-widget.tsx +418 -0
- package/src/components/dashboard/widgets/metric-card.tsx +343 -0
- package/src/components/index.ts +6 -3
- package/src/components/sidebar/index.tsx +579 -0
- package/src/components/ui/calendar.tsx +65 -0
- package/src/components/ui/index.ts +12 -0
- package/src/components/ui/scroll-area.tsx +47 -0
- package/src/components/ui/sheet.tsx +139 -0
- package/src/styles/index.css +69 -1
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect, useCallback, useRef } from 'react'
|
|
4
|
+
import { motion, AnimatePresence, useMotionValue, useSpring } from 'framer-motion'
|
|
5
|
+
import { cn } from '../../lib/utils'
|
|
6
|
+
import { Button } from '../ui/button'
|
|
7
|
+
import { ScrollArea } from '../ui/scroll-area'
|
|
8
|
+
import { Sheet, SheetContent, SheetTrigger } from '../ui/sheet'
|
|
9
|
+
import { Badge } from '../ui/badge'
|
|
10
|
+
import { Separator } from '../ui/separator'
|
|
11
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'
|
|
12
|
+
import {
|
|
13
|
+
Menu,
|
|
14
|
+
X,
|
|
15
|
+
ChevronRight,
|
|
16
|
+
ChevronLeft,
|
|
17
|
+
ChevronDown,
|
|
18
|
+
Home,
|
|
19
|
+
Search,
|
|
20
|
+
Settings,
|
|
21
|
+
HelpCircle,
|
|
22
|
+
Moon,
|
|
23
|
+
Sun,
|
|
24
|
+
Monitor,
|
|
25
|
+
MoreHorizontal,
|
|
26
|
+
Pin,
|
|
27
|
+
PinOff,
|
|
28
|
+
Sparkles,
|
|
29
|
+
Command
|
|
30
|
+
} from 'lucide-react'
|
|
31
|
+
import { Input } from '../ui/input'
|
|
32
|
+
import {
|
|
33
|
+
DropdownMenu,
|
|
34
|
+
DropdownMenuContent,
|
|
35
|
+
DropdownMenuItem,
|
|
36
|
+
DropdownMenuSeparator,
|
|
37
|
+
DropdownMenuTrigger,
|
|
38
|
+
} from '../ui/dropdown-menu'
|
|
39
|
+
|
|
40
|
+
export interface SidebarItem {
|
|
41
|
+
id: string
|
|
42
|
+
title: string
|
|
43
|
+
href?: string
|
|
44
|
+
icon?: React.ReactNode
|
|
45
|
+
badge?: string | number
|
|
46
|
+
badgeVariant?: 'secondary' | 'destructive' | 'outline'
|
|
47
|
+
disabled?: boolean
|
|
48
|
+
items?: SidebarItem[]
|
|
49
|
+
action?: () => void
|
|
50
|
+
tooltip?: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface SidebarSection {
|
|
54
|
+
id: string
|
|
55
|
+
title?: string
|
|
56
|
+
items: SidebarItem[]
|
|
57
|
+
collapsible?: boolean
|
|
58
|
+
defaultExpanded?: boolean
|
|
59
|
+
showDivider?: boolean
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface SidebarConfig {
|
|
63
|
+
sections: SidebarSection[]
|
|
64
|
+
footer?: SidebarSection
|
|
65
|
+
showSearch?: boolean
|
|
66
|
+
searchPlaceholder?: string
|
|
67
|
+
onSearchChange?: (value: string) => void
|
|
68
|
+
showThemeToggle?: boolean
|
|
69
|
+
theme?: 'light' | 'dark' | 'system'
|
|
70
|
+
onThemeChange?: (theme: 'light' | 'dark' | 'system') => void
|
|
71
|
+
branding?: {
|
|
72
|
+
logo?: React.ReactNode
|
|
73
|
+
title?: string
|
|
74
|
+
href?: string
|
|
75
|
+
}
|
|
76
|
+
collapsible?: boolean
|
|
77
|
+
defaultCollapsed?: boolean
|
|
78
|
+
floatingActionButton?: boolean
|
|
79
|
+
glassmorphism?: boolean
|
|
80
|
+
animatedBackground?: boolean
|
|
81
|
+
keyboardShortcuts?: boolean
|
|
82
|
+
persistState?: boolean
|
|
83
|
+
onStateChange?: (state: SidebarState) => void
|
|
84
|
+
customStyles?: {
|
|
85
|
+
background?: string
|
|
86
|
+
border?: string
|
|
87
|
+
text?: string
|
|
88
|
+
hover?: string
|
|
89
|
+
active?: string
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface SidebarState {
|
|
94
|
+
collapsed: boolean
|
|
95
|
+
expandedSections: string[]
|
|
96
|
+
searchQuery: string
|
|
97
|
+
pinnedItems: string[]
|
|
98
|
+
recentItems: string[]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
interface SidebarProps extends SidebarConfig {
|
|
102
|
+
className?: string
|
|
103
|
+
activePath?: string
|
|
104
|
+
onNavigate?: (href: string) => void
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function Sidebar({
|
|
108
|
+
sections,
|
|
109
|
+
footer,
|
|
110
|
+
showSearch = true,
|
|
111
|
+
searchPlaceholder = "Search...",
|
|
112
|
+
onSearchChange,
|
|
113
|
+
showThemeToggle = false,
|
|
114
|
+
theme = 'system',
|
|
115
|
+
onThemeChange,
|
|
116
|
+
branding,
|
|
117
|
+
collapsible = true,
|
|
118
|
+
defaultCollapsed = false,
|
|
119
|
+
floatingActionButton = true,
|
|
120
|
+
glassmorphism = false,
|
|
121
|
+
animatedBackground = false,
|
|
122
|
+
keyboardShortcuts = true,
|
|
123
|
+
persistState = true,
|
|
124
|
+
onStateChange,
|
|
125
|
+
customStyles,
|
|
126
|
+
className,
|
|
127
|
+
activePath,
|
|
128
|
+
onNavigate
|
|
129
|
+
}: SidebarProps) {
|
|
130
|
+
const [isMobile, setIsMobile] = useState(false)
|
|
131
|
+
const [isOpen, setIsOpen] = useState(false)
|
|
132
|
+
const [collapsed, setCollapsed] = useState(defaultCollapsed)
|
|
133
|
+
const [expandedSections, setExpandedSections] = useState<string[]>([])
|
|
134
|
+
const [searchQuery, setSearchQuery] = useState('')
|
|
135
|
+
const [pinnedItems, setPinnedItems] = useState<string[]>([])
|
|
136
|
+
const searchInputRef = useRef<HTMLInputElement>(null)
|
|
137
|
+
const mouseX = useMotionValue(0)
|
|
138
|
+
const mouseY = useMotionValue(0)
|
|
139
|
+
const springX = useSpring(mouseX, { stiffness: 300, damping: 30 })
|
|
140
|
+
const springY = useSpring(mouseY, { stiffness: 300, damping: 30 })
|
|
141
|
+
|
|
142
|
+
// Load persisted state
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
if (persistState && typeof window !== 'undefined') {
|
|
145
|
+
const savedState = localStorage.getItem('moonui-sidebar-state')
|
|
146
|
+
if (savedState) {
|
|
147
|
+
const state = JSON.parse(savedState) as SidebarState
|
|
148
|
+
setCollapsed(state.collapsed)
|
|
149
|
+
setExpandedSections(state.expandedSections)
|
|
150
|
+
setPinnedItems(state.pinnedItems || [])
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}, [persistState])
|
|
154
|
+
|
|
155
|
+
// Save state changes
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
if (persistState && typeof window !== 'undefined') {
|
|
158
|
+
const state: SidebarState = {
|
|
159
|
+
collapsed,
|
|
160
|
+
expandedSections,
|
|
161
|
+
searchQuery,
|
|
162
|
+
pinnedItems,
|
|
163
|
+
recentItems: []
|
|
164
|
+
}
|
|
165
|
+
localStorage.setItem('moonui-sidebar-state', JSON.stringify(state))
|
|
166
|
+
onStateChange?.(state)
|
|
167
|
+
}
|
|
168
|
+
}, [collapsed, expandedSections, searchQuery, pinnedItems, persistState, onStateChange])
|
|
169
|
+
|
|
170
|
+
// Check mobile
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
const checkMobile = () => {
|
|
173
|
+
setIsMobile(window.innerWidth < 768)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
checkMobile()
|
|
177
|
+
window.addEventListener('resize', checkMobile)
|
|
178
|
+
return () => window.removeEventListener('resize', checkMobile)
|
|
179
|
+
}, [])
|
|
180
|
+
|
|
181
|
+
// Initialize expanded sections
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
const sectionsToExpand = sections
|
|
184
|
+
.filter(section => section.defaultExpanded !== false)
|
|
185
|
+
.map(section => section.id)
|
|
186
|
+
setExpandedSections(sectionsToExpand)
|
|
187
|
+
}, [sections])
|
|
188
|
+
|
|
189
|
+
// Keyboard shortcuts
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
if (!keyboardShortcuts) return
|
|
192
|
+
|
|
193
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
194
|
+
// Cmd/Ctrl + K for search
|
|
195
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
196
|
+
e.preventDefault()
|
|
197
|
+
searchInputRef.current?.focus()
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Cmd/Ctrl + B to toggle sidebar
|
|
201
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'b') {
|
|
202
|
+
e.preventDefault()
|
|
203
|
+
if (isMobile) {
|
|
204
|
+
setIsOpen(!isOpen)
|
|
205
|
+
} else {
|
|
206
|
+
setCollapsed(!collapsed)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
document.addEventListener('keydown', handleKeyDown)
|
|
212
|
+
return () => document.removeEventListener('keydown', handleKeyDown)
|
|
213
|
+
}, [keyboardShortcuts, isMobile, isOpen, collapsed])
|
|
214
|
+
|
|
215
|
+
// Mouse tracking for animated background
|
|
216
|
+
useEffect(() => {
|
|
217
|
+
if (!animatedBackground) return
|
|
218
|
+
|
|
219
|
+
const handleMouseMove = (e: MouseEvent) => {
|
|
220
|
+
const rect = document.querySelector('.sidebar-container')?.getBoundingClientRect()
|
|
221
|
+
if (rect) {
|
|
222
|
+
mouseX.set(e.clientX - rect.left)
|
|
223
|
+
mouseY.set(e.clientY - rect.top)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
document.addEventListener('mousemove', handleMouseMove)
|
|
228
|
+
return () => document.removeEventListener('mousemove', handleMouseMove)
|
|
229
|
+
}, [animatedBackground, mouseX, mouseY])
|
|
230
|
+
|
|
231
|
+
const toggleSection = (sectionId: string) => {
|
|
232
|
+
setExpandedSections(prev =>
|
|
233
|
+
prev.includes(sectionId)
|
|
234
|
+
? prev.filter(id => id !== sectionId)
|
|
235
|
+
: [...prev, sectionId]
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const togglePinItem = (itemId: string) => {
|
|
240
|
+
setPinnedItems(prev =>
|
|
241
|
+
prev.includes(itemId)
|
|
242
|
+
? prev.filter(id => id !== itemId)
|
|
243
|
+
: [...prev, itemId]
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const handleItemClick = (item: SidebarItem) => {
|
|
248
|
+
if (item.action) {
|
|
249
|
+
item.action()
|
|
250
|
+
} else if (item.href && onNavigate) {
|
|
251
|
+
onNavigate(item.href)
|
|
252
|
+
if (isMobile) {
|
|
253
|
+
setIsOpen(false)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const filterItems = (items: SidebarItem[], query: string): SidebarItem[] => {
|
|
259
|
+
if (!query) return items
|
|
260
|
+
|
|
261
|
+
return items.filter(item => {
|
|
262
|
+
const matchesTitle = item.title.toLowerCase().includes(query.toLowerCase())
|
|
263
|
+
const hasMatchingChildren = item.items?.some(child =>
|
|
264
|
+
child.title.toLowerCase().includes(query.toLowerCase())
|
|
265
|
+
)
|
|
266
|
+
return matchesTitle || hasMatchingChildren
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const renderItem = (item: SidebarItem, depth = 0) => {
|
|
271
|
+
const isActive = item.href === activePath
|
|
272
|
+
const isPinned = pinnedItems.includes(item.id)
|
|
273
|
+
const hasChildren = item.items && item.items.length > 0
|
|
274
|
+
const isExpanded = expandedSections.includes(item.id)
|
|
275
|
+
|
|
276
|
+
const ItemWrapper = item.tooltip && !collapsed ? TooltipTrigger : React.Fragment
|
|
277
|
+
|
|
278
|
+
const itemContent = (
|
|
279
|
+
<button
|
|
280
|
+
onClick={() => {
|
|
281
|
+
if (hasChildren) {
|
|
282
|
+
toggleSection(item.id)
|
|
283
|
+
} else {
|
|
284
|
+
handleItemClick(item)
|
|
285
|
+
}
|
|
286
|
+
}}
|
|
287
|
+
className={cn(
|
|
288
|
+
"w-full flex items-center gap-3 rounded-lg px-3 py-2 text-sm transition-all",
|
|
289
|
+
"hover:bg-accent hover:text-accent-foreground",
|
|
290
|
+
isActive && "bg-primary/10 text-primary font-medium",
|
|
291
|
+
item.disabled && "opacity-50 cursor-not-allowed",
|
|
292
|
+
depth > 0 && "ml-6 text-xs",
|
|
293
|
+
collapsed && depth === 0 && "justify-center px-2",
|
|
294
|
+
customStyles?.hover
|
|
295
|
+
)}
|
|
296
|
+
disabled={item.disabled}
|
|
297
|
+
>
|
|
298
|
+
{item.icon && (
|
|
299
|
+
<span className={cn(
|
|
300
|
+
"flex-shrink-0",
|
|
301
|
+
collapsed && depth === 0 && "mx-auto"
|
|
302
|
+
)}>
|
|
303
|
+
{item.icon}
|
|
304
|
+
</span>
|
|
305
|
+
)}
|
|
306
|
+
|
|
307
|
+
{(!collapsed || depth > 0) && (
|
|
308
|
+
<>
|
|
309
|
+
<span className="flex-1 text-left truncate">{item.title}</span>
|
|
310
|
+
|
|
311
|
+
{item.badge && (
|
|
312
|
+
<Badge
|
|
313
|
+
variant={item.badgeVariant || 'secondary'}
|
|
314
|
+
className="ml-auto flex-shrink-0"
|
|
315
|
+
>
|
|
316
|
+
{item.badge}
|
|
317
|
+
</Badge>
|
|
318
|
+
)}
|
|
319
|
+
|
|
320
|
+
{hasChildren && depth === 0 && (
|
|
321
|
+
<ChevronDown
|
|
322
|
+
className={cn(
|
|
323
|
+
"h-4 w-4 flex-shrink-0 transition-transform",
|
|
324
|
+
isExpanded && "rotate-180"
|
|
325
|
+
)}
|
|
326
|
+
/>
|
|
327
|
+
)}
|
|
328
|
+
|
|
329
|
+
{isPinned && (
|
|
330
|
+
<Pin className="h-3 w-3 flex-shrink-0" />
|
|
331
|
+
)}
|
|
332
|
+
</>
|
|
333
|
+
)}
|
|
334
|
+
</button>
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
return (
|
|
338
|
+
<div key={item.id}>
|
|
339
|
+
{item.tooltip && !collapsed ? (
|
|
340
|
+
<TooltipProvider>
|
|
341
|
+
<Tooltip>
|
|
342
|
+
<ItemWrapper>
|
|
343
|
+
{itemContent}
|
|
344
|
+
</ItemWrapper>
|
|
345
|
+
<TooltipContent side="right">
|
|
346
|
+
<p>{item.tooltip}</p>
|
|
347
|
+
</TooltipContent>
|
|
348
|
+
</Tooltip>
|
|
349
|
+
</TooltipProvider>
|
|
350
|
+
) : (
|
|
351
|
+
itemContent
|
|
352
|
+
)}
|
|
353
|
+
|
|
354
|
+
{hasChildren && !collapsed && (
|
|
355
|
+
<AnimatePresence>
|
|
356
|
+
{isExpanded && (
|
|
357
|
+
<motion.div
|
|
358
|
+
initial={{ height: 0, opacity: 0 }}
|
|
359
|
+
animate={{ height: "auto", opacity: 1 }}
|
|
360
|
+
exit={{ height: 0, opacity: 0 }}
|
|
361
|
+
transition={{ duration: 0.2 }}
|
|
362
|
+
className="overflow-hidden"
|
|
363
|
+
>
|
|
364
|
+
<div className="pt-1 space-y-1">
|
|
365
|
+
{filterItems(item.items!, searchQuery).map(child =>
|
|
366
|
+
renderItem(child, depth + 1)
|
|
367
|
+
)}
|
|
368
|
+
</div>
|
|
369
|
+
</motion.div>
|
|
370
|
+
)}
|
|
371
|
+
</AnimatePresence>
|
|
372
|
+
)}
|
|
373
|
+
</div>
|
|
374
|
+
)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const SidebarContent = () => (
|
|
378
|
+
<>
|
|
379
|
+
{/* Header */}
|
|
380
|
+
<div className={cn(
|
|
381
|
+
"flex items-center gap-3 p-4 border-b",
|
|
382
|
+
collapsed && !isMobile && "justify-center px-2"
|
|
383
|
+
)}>
|
|
384
|
+
{branding && (
|
|
385
|
+
<>
|
|
386
|
+
{branding.logo}
|
|
387
|
+
{(!collapsed || isMobile) && branding.title && (
|
|
388
|
+
<span className="font-semibold text-lg">{branding.title}</span>
|
|
389
|
+
)}
|
|
390
|
+
</>
|
|
391
|
+
)}
|
|
392
|
+
|
|
393
|
+
{isMobile && (
|
|
394
|
+
<Button
|
|
395
|
+
variant="ghost"
|
|
396
|
+
size="sm"
|
|
397
|
+
className="ml-auto h-8 w-8 p-0"
|
|
398
|
+
onClick={() => setIsOpen(false)}
|
|
399
|
+
>
|
|
400
|
+
<X className="h-4 w-4" />
|
|
401
|
+
</Button>
|
|
402
|
+
)}
|
|
403
|
+
|
|
404
|
+
{!isMobile && collapsible && (
|
|
405
|
+
<Button
|
|
406
|
+
variant="ghost"
|
|
407
|
+
size="sm"
|
|
408
|
+
className={cn(
|
|
409
|
+
"h-8 w-8 p-0",
|
|
410
|
+
!collapsed && "ml-auto"
|
|
411
|
+
)}
|
|
412
|
+
onClick={() => setCollapsed(!collapsed)}
|
|
413
|
+
>
|
|
414
|
+
{collapsed ? <ChevronRight className="h-4 w-4" /> : <ChevronLeft className="h-4 w-4" />}
|
|
415
|
+
</Button>
|
|
416
|
+
)}
|
|
417
|
+
</div>
|
|
418
|
+
|
|
419
|
+
{/* Search */}
|
|
420
|
+
{showSearch && (!collapsed || isMobile) && (
|
|
421
|
+
<div className="p-4 border-b">
|
|
422
|
+
<div className="relative">
|
|
423
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
424
|
+
<Input
|
|
425
|
+
ref={searchInputRef}
|
|
426
|
+
type="search"
|
|
427
|
+
placeholder={searchPlaceholder}
|
|
428
|
+
value={searchQuery}
|
|
429
|
+
onChange={(e) => {
|
|
430
|
+
setSearchQuery(e.target.value)
|
|
431
|
+
onSearchChange?.(e.target.value)
|
|
432
|
+
}}
|
|
433
|
+
className="pl-9 pr-9"
|
|
434
|
+
/>
|
|
435
|
+
{keyboardShortcuts && (
|
|
436
|
+
<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">
|
|
437
|
+
<span className="text-xs">⌘</span>K
|
|
438
|
+
</kbd>
|
|
439
|
+
)}
|
|
440
|
+
</div>
|
|
441
|
+
</div>
|
|
442
|
+
)}
|
|
443
|
+
|
|
444
|
+
{/* Pinned Items */}
|
|
445
|
+
{pinnedItems.length > 0 && (!collapsed || isMobile) && (
|
|
446
|
+
<div className="p-4 border-b">
|
|
447
|
+
<h4 className="text-xs font-medium text-muted-foreground mb-2">Pinned</h4>
|
|
448
|
+
<div className="space-y-1">
|
|
449
|
+
{sections.flatMap(section =>
|
|
450
|
+
section.items.filter(item => pinnedItems.includes(item.id))
|
|
451
|
+
).map(item => renderItem(item))}
|
|
452
|
+
</div>
|
|
453
|
+
</div>
|
|
454
|
+
)}
|
|
455
|
+
|
|
456
|
+
{/* Main Content */}
|
|
457
|
+
<ScrollArea className="flex-1">
|
|
458
|
+
<div className="p-4 space-y-6">
|
|
459
|
+
{sections.map((section, index) => {
|
|
460
|
+
const filteredItems = filterItems(section.items, searchQuery)
|
|
461
|
+
if (filteredItems.length === 0) return null
|
|
462
|
+
|
|
463
|
+
return (
|
|
464
|
+
<div key={section.id}>
|
|
465
|
+
{section.title && (!collapsed || isMobile) && (
|
|
466
|
+
<h4 className="text-xs font-medium text-muted-foreground mb-2">
|
|
467
|
+
{section.title}
|
|
468
|
+
</h4>
|
|
469
|
+
)}
|
|
470
|
+
<div className="space-y-1">
|
|
471
|
+
{filteredItems.map(item => renderItem(item))}
|
|
472
|
+
</div>
|
|
473
|
+
{section.showDivider && index < sections.length - 1 && (
|
|
474
|
+
<Separator className="mt-4" />
|
|
475
|
+
)}
|
|
476
|
+
</div>
|
|
477
|
+
)
|
|
478
|
+
})}
|
|
479
|
+
</div>
|
|
480
|
+
</ScrollArea>
|
|
481
|
+
|
|
482
|
+
{/* Footer */}
|
|
483
|
+
{footer && (
|
|
484
|
+
<div className="border-t p-4">
|
|
485
|
+
<div className="space-y-1">
|
|
486
|
+
{footer.items.map(item => renderItem(item))}
|
|
487
|
+
</div>
|
|
488
|
+
|
|
489
|
+
{showThemeToggle && (!collapsed || isMobile) && (
|
|
490
|
+
<div className="mt-3 flex items-center justify-between">
|
|
491
|
+
<span className="text-xs text-muted-foreground">Theme</span>
|
|
492
|
+
<DropdownMenu>
|
|
493
|
+
<DropdownMenuTrigger asChild>
|
|
494
|
+
<Button variant="ghost" size="sm" className="h-7 px-2">
|
|
495
|
+
{theme === 'light' && <Sun className="h-3 w-3" />}
|
|
496
|
+
{theme === 'dark' && <Moon className="h-3 w-3" />}
|
|
497
|
+
{theme === 'system' && <Monitor className="h-3 w-3" />}
|
|
498
|
+
</Button>
|
|
499
|
+
</DropdownMenuTrigger>
|
|
500
|
+
<DropdownMenuContent align="end">
|
|
501
|
+
<DropdownMenuItem onClick={() => onThemeChange?.('light')}>
|
|
502
|
+
<Sun className="mr-2 h-4 w-4" />
|
|
503
|
+
Light
|
|
504
|
+
</DropdownMenuItem>
|
|
505
|
+
<DropdownMenuItem onClick={() => onThemeChange?.('dark')}>
|
|
506
|
+
<Moon className="mr-2 h-4 w-4" />
|
|
507
|
+
Dark
|
|
508
|
+
</DropdownMenuItem>
|
|
509
|
+
<DropdownMenuItem onClick={() => onThemeChange?.('system')}>
|
|
510
|
+
<Monitor className="mr-2 h-4 w-4" />
|
|
511
|
+
System
|
|
512
|
+
</DropdownMenuItem>
|
|
513
|
+
</DropdownMenuContent>
|
|
514
|
+
</DropdownMenu>
|
|
515
|
+
</div>
|
|
516
|
+
)}
|
|
517
|
+
</div>
|
|
518
|
+
)}
|
|
519
|
+
</>
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
const sidebarClasses = cn(
|
|
523
|
+
"flex h-screen flex-col bg-background border-r sticky top-0",
|
|
524
|
+
glassmorphism && "bg-background/80 backdrop-blur-xl border-white/10",
|
|
525
|
+
collapsed && !isMobile && "w-16",
|
|
526
|
+
!collapsed && !isMobile && "w-64",
|
|
527
|
+
customStyles?.background,
|
|
528
|
+
customStyles?.border,
|
|
529
|
+
className
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
if (isMobile) {
|
|
533
|
+
return (
|
|
534
|
+
<>
|
|
535
|
+
{floatingActionButton && (
|
|
536
|
+
<Button
|
|
537
|
+
onClick={() => setIsOpen(true)}
|
|
538
|
+
className="fixed bottom-4 left-4 z-40 h-12 w-12 rounded-full shadow-lg md:hidden"
|
|
539
|
+
size="icon"
|
|
540
|
+
>
|
|
541
|
+
<Menu className="h-5 w-5" />
|
|
542
|
+
</Button>
|
|
543
|
+
)}
|
|
544
|
+
|
|
545
|
+
<Sheet open={isOpen} onOpenChange={setIsOpen}>
|
|
546
|
+
<SheetContent side="left" className="w-80 p-0">
|
|
547
|
+
<div className={cn(sidebarClasses, "sidebar-container")}>
|
|
548
|
+
{animatedBackground && (
|
|
549
|
+
<motion.div
|
|
550
|
+
className="absolute inset-0 opacity-30"
|
|
551
|
+
style={{
|
|
552
|
+
background: `radial-gradient(circle at ${springX}px ${springY}px, rgba(var(--primary-rgb), 0.15), transparent 50%)`,
|
|
553
|
+
}}
|
|
554
|
+
/>
|
|
555
|
+
)}
|
|
556
|
+
<SidebarContent />
|
|
557
|
+
</div>
|
|
558
|
+
</SheetContent>
|
|
559
|
+
</Sheet>
|
|
560
|
+
</>
|
|
561
|
+
)
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return (
|
|
565
|
+
<aside className={cn(sidebarClasses, "sidebar-container hidden md:flex")}>
|
|
566
|
+
{animatedBackground && (
|
|
567
|
+
<motion.div
|
|
568
|
+
className="absolute inset-0 opacity-30"
|
|
569
|
+
style={{
|
|
570
|
+
background: `radial-gradient(circle at ${springX}px ${springY}px, rgba(var(--primary-rgb), 0.15), transparent 50%)`,
|
|
571
|
+
}}
|
|
572
|
+
/>
|
|
573
|
+
)}
|
|
574
|
+
<SidebarContent />
|
|
575
|
+
</aside>
|
|
576
|
+
)
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
export default Sidebar
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { ChevronLeft, ChevronRight } from "lucide-react"
|
|
5
|
+
import { DayPicker } from "react-day-picker"
|
|
6
|
+
import { cn } from "../../lib/utils"
|
|
7
|
+
import { buttonVariants } from "./button"
|
|
8
|
+
|
|
9
|
+
export type CalendarProps = React.ComponentProps<typeof DayPicker>
|
|
10
|
+
|
|
11
|
+
function Calendar({
|
|
12
|
+
className,
|
|
13
|
+
classNames,
|
|
14
|
+
showOutsideDays = true,
|
|
15
|
+
...props
|
|
16
|
+
}: CalendarProps) {
|
|
17
|
+
return (
|
|
18
|
+
<DayPicker
|
|
19
|
+
showOutsideDays={showOutsideDays}
|
|
20
|
+
className={cn("p-3", className)}
|
|
21
|
+
classNames={{
|
|
22
|
+
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
|
|
23
|
+
month: "space-y-4",
|
|
24
|
+
caption: "flex justify-center pt-1 relative items-center",
|
|
25
|
+
caption_label: "text-sm font-medium",
|
|
26
|
+
nav: "space-x-1 flex items-center",
|
|
27
|
+
nav_button: cn(
|
|
28
|
+
buttonVariants({ variant: "outline" }),
|
|
29
|
+
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
|
|
30
|
+
),
|
|
31
|
+
nav_button_previous: "absolute left-1",
|
|
32
|
+
nav_button_next: "absolute right-1",
|
|
33
|
+
table: "w-full border-collapse",
|
|
34
|
+
head_row: "grid grid-cols-7",
|
|
35
|
+
head_cell:
|
|
36
|
+
"text-muted-foreground rounded-md w-9 h-9 font-normal text-[0.8rem] flex items-center justify-center",
|
|
37
|
+
row: "grid grid-cols-7 gap-1 mt-1",
|
|
38
|
+
cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
|
|
39
|
+
day: cn(
|
|
40
|
+
buttonVariants({ variant: "ghost" }),
|
|
41
|
+
"h-9 w-9 p-0 font-normal aria-selected:opacity-100"
|
|
42
|
+
),
|
|
43
|
+
day_range_end: "day-range-end",
|
|
44
|
+
day_selected:
|
|
45
|
+
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
|
46
|
+
day_today: "bg-accent text-accent-foreground",
|
|
47
|
+
day_outside:
|
|
48
|
+
"day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
|
|
49
|
+
day_disabled: "text-muted-foreground opacity-50",
|
|
50
|
+
day_range_middle:
|
|
51
|
+
"aria-selected:bg-accent aria-selected:text-accent-foreground",
|
|
52
|
+
day_hidden: "invisible",
|
|
53
|
+
...classNames,
|
|
54
|
+
}}
|
|
55
|
+
components={{
|
|
56
|
+
// IconLeft: () => <ChevronLeft className="h-4 w-4" />,
|
|
57
|
+
// IconRight: () => <ChevronRight className="h-4 w-4" />,
|
|
58
|
+
}}
|
|
59
|
+
{...props}
|
|
60
|
+
/>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
Calendar.displayName = "Calendar"
|
|
64
|
+
|
|
65
|
+
export { Calendar }
|
|
@@ -34,6 +34,10 @@ export {
|
|
|
34
34
|
Button, buttonVariants
|
|
35
35
|
} from './button';
|
|
36
36
|
|
|
37
|
+
export {
|
|
38
|
+
Calendar
|
|
39
|
+
} from './calendar';
|
|
40
|
+
|
|
37
41
|
export {
|
|
38
42
|
MoonUICardPro, MoonUICardHeaderPro, MoonUICardFooterPro, MoonUICardTitlePro, MoonUICardDescriptionPro, MoonUICardContentPro,
|
|
39
43
|
Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent
|
|
@@ -99,6 +103,10 @@ export {
|
|
|
99
103
|
radioGroupItemVariants, RadioGroupContext, RadioGroup, RadioGroupItem, RadioLabel, RadioItemWithLabel
|
|
100
104
|
} from './radio-group';
|
|
101
105
|
|
|
106
|
+
export {
|
|
107
|
+
ScrollArea, ScrollBar
|
|
108
|
+
} from './scroll-area';
|
|
109
|
+
|
|
102
110
|
export {
|
|
103
111
|
MoonUISelectPro, MoonUISelectTriggerPro, MoonUISelectContentPro, MoonUISelectItemPro, MoonUISelectValuePro, MoonUISelectGroupPro, MoonUISelectLabelPro, MoonUISelectSeparatorPro,
|
|
104
112
|
Select, SelectTrigger, SelectContent, SelectItem, SelectValue, SelectGroup, SelectLabel, SelectSeparator
|
|
@@ -109,6 +117,10 @@ export {
|
|
|
109
117
|
Separator, separatorVariants
|
|
110
118
|
} from './separator';
|
|
111
119
|
|
|
120
|
+
export {
|
|
121
|
+
Sheet, SheetTrigger, SheetContent, SheetHeader, SheetFooter, SheetTitle, SheetDescription, SheetClose, SheetPortal, SheetOverlay
|
|
122
|
+
} from './sheet';
|
|
123
|
+
|
|
112
124
|
export {
|
|
113
125
|
MoonUISkeletonPro,
|
|
114
126
|
Skeleton
|