@moontra/moonui-pro 2.5.0 → 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.
@@ -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
@@ -30,11 +30,11 @@ function Calendar({
30
30
  ),
31
31
  nav_button_previous: "absolute left-1",
32
32
  nav_button_next: "absolute right-1",
33
- table: "w-full border-collapse space-y-1",
34
- head_row: "flex",
33
+ table: "w-full border-collapse",
34
+ head_row: "grid grid-cols-7",
35
35
  head_cell:
36
- "text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
37
- row: "flex w-full mt-2",
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
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
39
  day: cn(
40
40
  buttonVariants({ variant: "ghost" }),
@@ -2,4 +2,72 @@
2
2
  @import "./tailwind.css";
3
3
  @import "./tokens.css";
4
4
  @import "./design-system.css";
5
- @import "./advanced-chart.css";
5
+ @import "./advanced-chart.css";
6
+
7
+ /* React Grid Layout - Required for Dashboard component */
8
+ @import "react-grid-layout/css/styles.css";
9
+ @import "react-resizable/css/styles.css";
10
+
11
+ /* Dashboard Grid Layout Overrides */
12
+ .react-grid-layout {
13
+ position: relative !important;
14
+ transition: height 200ms ease;
15
+ }
16
+
17
+ .react-grid-item {
18
+ transition: all 200ms ease;
19
+ transition-property: left, top, width, height;
20
+ }
21
+
22
+ .react-grid-item > div {
23
+ height: 100%;
24
+ display: flex;
25
+ flex-direction: column;
26
+ }
27
+
28
+ .react-grid-item.cssTransforms {
29
+ transition-property: transform, width, height;
30
+ }
31
+
32
+ .react-grid-item.resizing {
33
+ transition: none;
34
+ z-index: 1;
35
+ will-change: width, height;
36
+ }
37
+
38
+ .react-grid-item.react-draggable-dragging {
39
+ transition: none;
40
+ z-index: 3;
41
+ will-change: transform;
42
+ }
43
+
44
+ /* Prevent overflow on small screens */
45
+ @media (max-width: 768px) {
46
+ .react-grid-layout {
47
+ overflow-x: hidden !important;
48
+ height: auto !important;
49
+ }
50
+
51
+ .react-grid-item {
52
+ position: relative !important;
53
+ transform: none !important;
54
+ margin-bottom: 24px !important;
55
+ width: 100% !important;
56
+ left: 0 !important;
57
+ right: 0 !important;
58
+ }
59
+
60
+ .react-grid-item:last-child {
61
+ margin-bottom: 0 !important;
62
+ }
63
+
64
+ /* React grid layout'u mobilde devre dışı bırak */
65
+ .react-grid-item.cssTransforms {
66
+ position: relative !important;
67
+ transform: none !important;
68
+ }
69
+
70
+ .react-grid-placeholder {
71
+ display: none !important;
72
+ }
73
+ }