@evolution-soft/ui 1.0.0 → 1.0.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.
Files changed (78) hide show
  1. package/cli/index.mjs +386 -0
  2. package/components/button-icon-lottie/index.tsx +46 -0
  3. package/components/fullscreen-mode/index.tsx +82 -0
  4. package/components/header/components/buttons.tsx +102 -0
  5. package/components/header/index.tsx +146 -0
  6. package/components/loading-default/index.tsx +90 -0
  7. package/components/lottie-icon/index.tsx +78 -0
  8. package/components/not-found-default/index.tsx +68 -0
  9. package/components/settings-modal/index.tsx +225 -0
  10. package/components/sidebar/index.tsx +645 -0
  11. package/components/subtitle/index.tsx +60 -0
  12. package/components/theme-transition/index.tsx +142 -0
  13. package/components/title/index.tsx +66 -0
  14. package/components/tooltip-indicator/index.tsx +30 -0
  15. package/components/ui/accordion.tsx +66 -0
  16. package/components/ui/alert-dialog.tsx +157 -0
  17. package/components/ui/alert.tsx +66 -0
  18. package/components/ui/aspect-ratio.tsx +11 -0
  19. package/components/ui/avatar.tsx +53 -0
  20. package/components/ui/badge.tsx +46 -0
  21. package/components/ui/breadcrumb.tsx +109 -0
  22. package/components/ui/button.tsx +58 -0
  23. package/components/ui/calendar.tsx +78 -0
  24. package/components/ui/card.tsx +92 -0
  25. package/components/ui/carousel.tsx +241 -0
  26. package/components/ui/chart.tsx +360 -0
  27. package/components/ui/checkbox.tsx +32 -0
  28. package/components/ui/collapsible.tsx +33 -0
  29. package/components/ui/command.tsx +177 -0
  30. package/components/ui/context-menu.tsx +252 -0
  31. package/components/ui/dialog.tsx +135 -0
  32. package/components/ui/divisor.tsx +9 -0
  33. package/components/ui/drawer.tsx +132 -0
  34. package/components/ui/dropdown-menu.tsx +257 -0
  35. package/components/ui/emoji-picker.tsx +76 -0
  36. package/components/ui/form.tsx +168 -0
  37. package/components/ui/hover-card.tsx +44 -0
  38. package/components/ui/input-mask.tsx +46 -0
  39. package/components/ui/input-otp.tsx +77 -0
  40. package/components/ui/input.tsx +61 -0
  41. package/components/ui/label.tsx +24 -0
  42. package/components/ui/menubar.tsx +276 -0
  43. package/components/ui/multiselect.tsx +105 -0
  44. package/components/ui/navigation-menu.tsx +168 -0
  45. package/components/ui/pagination.tsx +127 -0
  46. package/components/ui/popover.tsx +48 -0
  47. package/components/ui/progress.tsx +31 -0
  48. package/components/ui/radio-group.tsx +45 -0
  49. package/components/ui/resizable.tsx +65 -0
  50. package/components/ui/scroll-area.tsx +58 -0
  51. package/components/ui/searchable-select.tsx +211 -0
  52. package/components/ui/select.tsx +189 -0
  53. package/components/ui/separator.tsx +28 -0
  54. package/components/ui/sheet.tsx +139 -0
  55. package/components/ui/sidebar.tsx +727 -0
  56. package/components/ui/skeleton.tsx +144 -0
  57. package/components/ui/slider.tsx +63 -0
  58. package/components/ui/sonner.tsx +26 -0
  59. package/components/ui/switch.tsx +31 -0
  60. package/components/ui/table.tsx +116 -0
  61. package/components/ui/tabs.tsx +76 -0
  62. package/components/ui/textarea.tsx +18 -0
  63. package/components/ui/theme-toggle.tsx +89 -0
  64. package/components/ui/toggle-group.tsx +73 -0
  65. package/components/ui/toggle.tsx +47 -0
  66. package/components/ui/tooltip.tsx +61 -0
  67. package/components/ui/use-mobile.ts +21 -0
  68. package/components/ui/utils.ts +6 -0
  69. package/contexts/AnimationSettingsContext.tsx +85 -0
  70. package/contexts/AuthContext.tsx +80 -0
  71. package/contexts/ThemeContext.tsx +70 -0
  72. package/hooks/useAnimationSettings.ts +2 -0
  73. package/hooks/usePermissions.ts +4 -0
  74. package/lib/persistentFilters.ts +120 -0
  75. package/lib/utils.ts +2 -0
  76. package/package.json +11 -2
  77. package/stores/theme.ts +30 -0
  78. package/stores/useThemeStore.ts +32 -0
@@ -0,0 +1,645 @@
1
+ import { useState, useEffect, useRef, useMemo } from 'react';
2
+
3
+ import Link from 'next/link';
4
+ import { usePathname } from 'next/navigation';
5
+
6
+ import { useAuth } from '@/contexts/AuthContext';
7
+ import { usePermissions } from '@/hooks/usePermissions';
8
+ import { useThemeStore } from '@/stores/useThemeStore';
9
+
10
+ import { cn } from '@/components/ui/utils';
11
+ import { Badge } from '@/components/ui/badge';
12
+ import { Button } from '@/components/ui/button';
13
+ import { Separator } from '@/components/ui/separator';
14
+ import { ThemeToggle } from '@/components/ui/theme-toggle';
15
+ import { SettingsModal } from '@/components/settings-modal';
16
+ import { TooltipIndicator } from '@/components/tooltip-indicator';
17
+ import { ToggleFullScreenModeButton } from '@/components/fullscreen-mode';
18
+
19
+ import {
20
+ AlertDialog,
21
+ AlertDialogAction,
22
+ AlertDialogCancel,
23
+ AlertDialogContent,
24
+ AlertDialogDescription,
25
+ AlertDialogFooter,
26
+ AlertDialogHeader,
27
+ AlertDialogTitle,
28
+ } from '@/components/ui/alert-dialog';
29
+
30
+ import {
31
+ LogOut,
32
+ Menu,
33
+ X,
34
+ ChevronLeft,
35
+ ChevronRight,
36
+ ChevronDown
37
+ } from 'lucide-react';
38
+
39
+
40
+ import { LottieIcon } from '../lottie-icon';
41
+
42
+ import homeIcon from '@/public/animations/icons-sidebar/home-icon.json';
43
+ import agrupamentosIcon from '@/public/animations/icons-sidebar/sidebar-agrupamentos-icon.json';
44
+ import timeIcon from '@/public/animations/icons-sidebar/sidebar-time-icon.json';
45
+ import basepatanIcon from '@/public/animations/icons-sidebar/sidebar-basepatan-icon.json';
46
+ import dashboardIcon from '@/public/animations/icons-sidebar/sidebar-dashboard-icon.json';
47
+ import kanbanIcon from '@/public/animations/icons-sidebar/sidebar-kanban-icon.json';
48
+ import rotaIcon from '@/public/animations/icons-sidebar/sidebar-rota-icon.json';
49
+ import heijunkaIcon from '@/public/animations/icons-sidebar/sidebar-heijunka-icon.json';
50
+
51
+ import homeIconDark from '@/public/animations/icons-sidebar-dark/home-icon-dark.json';
52
+ import agrupamentosIconDark from '@/public/animations/icons-sidebar-dark/sidebar-agrupamentos-icon-dark.json';
53
+ import timeIconDark from '@/public/animations/icons-sidebar-dark/sidebar-time-icon-dark.json';
54
+ import basepatanIconDark from '@/public/animations/icons-sidebar-dark/sidebar-basepatan-icon-dark.json';
55
+ import dashboardIconDark from '@/public/animations/icons-sidebar-dark/sidebar-dashboard-icon-dark.json';
56
+ import kanbanIconDark from '@/public/animations/icons-sidebar-dark/sidebar-kanban-icon-dark.json';
57
+ import rotaIconDark from '@/public/animations/icons-sidebar-dark/sidebar-rota-icon-dark.json';
58
+ import heijunkaIconDark from '@/public/animations/icons-sidebar-dark/sidebar-heijunka-icon-dark.json';
59
+
60
+ import logo from '@/public/logo.png';
61
+ import Image from 'next/image';
62
+
63
+ interface SubMenuItem {
64
+ label: string;
65
+ href: string;
66
+ functionality?: string;
67
+ development?: boolean;
68
+ analysis?: boolean;
69
+ alert?: string;
70
+ }
71
+
72
+ interface MenuItem {
73
+ label: string;
74
+ href: string;
75
+ lightAnimationData: any;
76
+ darkAnimationData: any;
77
+ color?: string;
78
+ badge?: string;
79
+ description?: string;
80
+ functionality?: string;
81
+ subItems?: SubMenuItem[];
82
+ }
83
+
84
+ interface MenuItemComponentProps {
85
+ item: MenuItem;
86
+ isActive: boolean;
87
+ collapsed: boolean;
88
+ hasSubItems?: boolean;
89
+ onToggle?: () => void;
90
+ isOpen?: boolean;
91
+ }
92
+
93
+ const colorMapping = {
94
+ blue: 'border-blue-500',
95
+ green: 'border-green-500',
96
+ yellow: 'border-yellow-500',
97
+ red: 'border-red-500',
98
+ purple: 'border-purple-500',
99
+ teal: 'border-teal-500',
100
+ orange: 'border-orange-500',
101
+ pink: 'border-pink-500',
102
+ indigo: 'border-indigo-500',
103
+ cyan: 'border-cyan-500',
104
+ rose: 'border-rose-500',
105
+ } as const;
106
+
107
+ const SubMenuItemComponent = ({ item, isActive, collapsed }: { item: SubMenuItem; isActive: boolean; collapsed: boolean }) => {
108
+ const [isHovered, setIsHovered] = useState(false);
109
+
110
+ return (
111
+ <Link href={item.href}>
112
+ <div
113
+ className={cn(
114
+ "flex items-center gap-2 rounded-lg px-3 py-2 text-xs transition-colors ml-6",
115
+ "hover:bg-accent hover:text-accent-foreground",
116
+ isActive && "bg-accent text-accent-foreground font-medium",
117
+ collapsed && "justify-center ml-0"
118
+ )}
119
+ title={collapsed ? item.label : undefined}
120
+ onMouseEnter={() => setIsHovered(true)}
121
+ onMouseLeave={() => setIsHovered(false)}
122
+ >
123
+ {!collapsed && (
124
+ <>
125
+ <span className="transition-opacity duration-300">{item.label}</span>
126
+ {item.development && (
127
+ <TooltipIndicator type="development" />
128
+ )}
129
+ {item.analysis && (
130
+ <TooltipIndicator type="analysis" message={item.alert} />
131
+ )}
132
+ </>
133
+ )}
134
+ </div>
135
+ </Link>
136
+ );
137
+ };
138
+
139
+ const MenuItemComponent = ({ item, isActive, collapsed, hasSubItems, onToggle, isOpen }: MenuItemComponentProps) => {
140
+ const [isHovered, setIsHovered] = useState(false);
141
+ const [isDark, setIsDark] = useState(false);
142
+
143
+ useEffect(() => {
144
+ const checkDarkMode = () => {
145
+ setIsDark(document.documentElement.classList.contains('dark'));
146
+ };
147
+
148
+ checkDarkMode();
149
+
150
+ const observer = new MutationObserver(checkDarkMode);
151
+ observer.observe(document.documentElement, {
152
+ attributes: true,
153
+ attributeFilter: ['class']
154
+ });
155
+
156
+ return () => observer.disconnect();
157
+ }, []);
158
+
159
+ const handleClick = (e: React.MouseEvent) => {
160
+ if (hasSubItems && onToggle) {
161
+ e.preventDefault();
162
+ onToggle();
163
+ }
164
+ };
165
+
166
+ // Extraímos o conteúdo visual para não repetir código
167
+ const Content = (
168
+ <div
169
+ className={cn(
170
+ "flex items-center gap-2 rounded-lg px-2 py-1.5 text-sm transition-colors cursor-pointer", // Adicionado cursor-pointer
171
+ "hover:bg-accent hover:text-accent-foreground",
172
+ (isActive || isOpen) && `bg-accent text-accent-foreground font-medium border-l-6 ${item.color && colorMapping[item.color as keyof typeof colorMapping] || 'border-blue-500'}`,
173
+ collapsed && (isActive || isOpen) && `border-r-0 border-t-0 border-l-0 border-b-3 ${item.color && colorMapping[item.color as keyof typeof colorMapping] || 'border-blue-500'}`,
174
+ collapsed && `justify-center `
175
+ )}
176
+ title={collapsed ? item.label : undefined}
177
+ onMouseEnter={() => setIsHovered(true)}
178
+ onMouseLeave={() => setIsHovered(false)}
179
+ >
180
+ <LottieIcon
181
+ lightAnimationData={item.lightAnimationData}
182
+ darkAnimationData={item.darkAnimationData}
183
+ className="h-5 w-5 shrink-0"
184
+ isActive={isActive || isOpen}
185
+ shouldPlay={isHovered}
186
+ isDark={isDark}
187
+ />
188
+ {!collapsed && (
189
+ <div className="flex flex-col flex-1 transition-all duration-300 ease-in-out">
190
+ <span className="transition-opacity duration-300">{item.label}</span>
191
+ {item.description && (
192
+ <span className="text-xs text-muted-foreground transition-opacity duration-300">{item.description}</span>
193
+ )}
194
+ </div>
195
+ )}
196
+ {!collapsed && item.badge && (
197
+ <Badge variant="secondary" className="ml-auto text-xs h-5 px-1.5">
198
+ {item.badge}
199
+ </Badge>
200
+ )}
201
+ {!collapsed && hasSubItems && (
202
+ <ChevronDown
203
+ className={cn(
204
+ "h-3.5 w-3.5 shrink-0 transition-transform duration-200",
205
+ isOpen && "transform rotate-180"
206
+ )}
207
+ />
208
+ )}
209
+ </div>
210
+ );
211
+
212
+ // Se tiver sub-itens, NÃO renderiza o Link do Next, apenas uma div clicável
213
+ if (hasSubItems) {
214
+ return <div onClick={handleClick}>{Content}</div>;
215
+ }
216
+
217
+ // Se for um link direto (sem filhos), renderiza o Link normalmente
218
+ return (
219
+ <Link href={item.href} onClick={handleClick}>
220
+ {Content}
221
+ </Link>
222
+ );
223
+ };
224
+
225
+ const menuItems: MenuItem[] = [
226
+ {
227
+ label: 'Home',
228
+ href: '/bem-vindo',
229
+ color: 'rose',
230
+ lightAnimationData: homeIcon,
231
+ darkAnimationData: homeIconDark,
232
+ description: 'Página inicial'
233
+ },
234
+ {
235
+ label: 'Pessoas',
236
+ href: '#',
237
+ color: 'green',
238
+ lightAnimationData: timeIcon,
239
+ darkAnimationData: timeIconDark,
240
+ description: 'Gestão de colaboradores',
241
+ functionality: 'DASHBOARD',
242
+ subItems: [
243
+ {
244
+ label: 'Colaboradores',
245
+ href: '/colaboradores',
246
+ functionality: 'COLLABORATOR'
247
+ },
248
+ {
249
+ label: 'Controle de Acesso',
250
+ href: '/controle-de-acesso',
251
+ functionality: 'AUTH_ROLE'
252
+ }
253
+ ]
254
+ },
255
+ ];
256
+
257
+ interface AppSidebarProps {
258
+ children: React.ReactNode;
259
+ }
260
+
261
+ export function AppSidebar({ children }: AppSidebarProps) {
262
+ const [collapsed, setCollapsed] = useState(false);
263
+ const [mobileOpen, setMobileOpen] = useState(false);
264
+ const [showLogoutDialog, setShowLogoutDialog] = useState(false);
265
+ const [dynamicSidebarEnabled, setDynamicSidebarEnabled] = useState(false);
266
+ const [isDynamicallyExpanded, setIsDynamicallyExpanded] = useState(false);
267
+ const [openSubmenus, setOpenSubmenus] = useState<Set<string>>(new Set());
268
+
269
+ const hoverTimeoutRef = useRef<NodeJS.Timeout | null>(null);
270
+ const { user, logout } = useAuth();
271
+ const pathname = usePathname() ?? '';
272
+
273
+ useEffect(() => {
274
+ // Abre automaticamente apenas quando pathname muda, não quando filteredMenuItems muda
275
+ // Isso previne scroll voltando ao topo
276
+ menuItems.forEach(item => {
277
+ if (item.subItems) {
278
+ const hasActiveSubItem = item.subItems.some(subItem =>
279
+ pathname === subItem.href || pathname.startsWith(subItem.href + '/')
280
+ );
281
+
282
+ if (hasActiveSubItem) {
283
+ setOpenSubmenus(prev => {
284
+ if (prev.has(item.label)) {
285
+ return prev;
286
+ }
287
+ const newSet = new Set(prev);
288
+ newSet.add(item.label);
289
+ return newSet;
290
+ });
291
+ }
292
+ }
293
+ });
294
+ }, [pathname]);
295
+
296
+ const toggleSubmenu = (itemLabel: string) => {
297
+ setOpenSubmenus(prev => {
298
+ const newSet = new Set(prev);
299
+ if (newSet.has(itemLabel)) {
300
+ newSet.delete(itemLabel);
301
+ } else {
302
+ newSet.add(itemLabel);
303
+ }
304
+ return newSet;
305
+ });
306
+ };
307
+
308
+ useEffect(() => {
309
+ const checkDynamicSidebarSettings = () => {
310
+ const saved = localStorage.getItem('dynamic-sidebar-enabled');
311
+ const isDynamicEnabled = saved !== null ? JSON.parse(saved) : false;
312
+ setDynamicSidebarEnabled(isDynamicEnabled);
313
+
314
+ if (isDynamicEnabled) {
315
+ setCollapsed(true);
316
+ setIsDynamicallyExpanded(false);
317
+ }
318
+ };
319
+
320
+ checkDynamicSidebarSettings();
321
+
322
+ const handleSettingsChange = (event: CustomEvent) => {
323
+ setDynamicSidebarEnabled(event.detail.enabled);
324
+ if (!event.detail.enabled) {
325
+ setIsDynamicallyExpanded(false);
326
+ }
327
+ };
328
+
329
+ const handleResetState = () => {
330
+ setIsDynamicallyExpanded(false);
331
+ };
332
+
333
+ const handleAutoCollapse = () => {
334
+ setCollapsed(true);
335
+ setIsDynamicallyExpanded(false);
336
+ };
337
+
338
+ window.addEventListener('dynamic-sidebar-settings-changed', handleSettingsChange as EventListener);
339
+ window.addEventListener('reset-dynamic-sidebar-state', handleResetState as EventListener);
340
+ window.addEventListener('auto-collapse-sidebar', handleAutoCollapse as EventListener);
341
+
342
+ return () => {
343
+ window.removeEventListener('dynamic-sidebar-settings-changed', handleSettingsChange as EventListener);
344
+ window.removeEventListener('reset-dynamic-sidebar-state', handleResetState as EventListener);
345
+ window.removeEventListener('auto-collapse-sidebar', handleAutoCollapse as EventListener);
346
+ };
347
+ }, []);
348
+
349
+ const handleSidebarMouseEnter = () => {
350
+ if (dynamicSidebarEnabled && collapsed && window.innerWidth >= 768) {
351
+ if (hoverTimeoutRef.current) {
352
+ clearTimeout(hoverTimeoutRef.current);
353
+ }
354
+
355
+ setIsDynamicallyExpanded(true);
356
+ }
357
+ };
358
+
359
+ const handleSidebarMouseLeave = () => {
360
+ if (dynamicSidebarEnabled && collapsed && isDynamicallyExpanded) {
361
+ if (hoverTimeoutRef.current) {
362
+ clearTimeout(hoverTimeoutRef.current);
363
+ }
364
+
365
+ setIsDynamicallyExpanded(false);
366
+ }
367
+ };
368
+
369
+ useEffect(() => {
370
+ if (!collapsed) {
371
+ setIsDynamicallyExpanded(false);
372
+ }
373
+ }, [collapsed]);
374
+
375
+ useEffect(() => {
376
+ if (!dynamicSidebarEnabled) {
377
+ setIsDynamicallyExpanded(false);
378
+ } else {
379
+ setCollapsed(true);
380
+ setIsDynamicallyExpanded(false);
381
+ }
382
+ }, [dynamicSidebarEnabled]);
383
+
384
+ useEffect(() => {
385
+ return () => {
386
+ if (hoverTimeoutRef.current) {
387
+ clearTimeout(hoverTimeoutRef.current);
388
+ }
389
+ };
390
+ }, []);
391
+
392
+ const handleLogout = async () => {
393
+ await logout();
394
+ setShowLogoutDialog(false);
395
+ };
396
+
397
+ const SidebarContent = ({ collapsed: sidebarCollapsed = collapsed }: { collapsed?: boolean } = {}) => (
398
+ <div className="flex h-full flex-col">
399
+ {/* Header */}
400
+
401
+ {!sidebarCollapsed && (
402
+ <div className={`flex h-16 items-center justify-center border-b `}>
403
+ <div className="flex items-center px-3 py-2 rounded">
404
+ <div className="flex items-center px-5 py-2">
405
+ <img
406
+ src="/logo.png"
407
+ alt="E-collab Logo"
408
+ className="h-6 w-auto dark:invert"
409
+ />
410
+ </div>
411
+
412
+ </div>
413
+ </div>
414
+ )}
415
+
416
+ {/* User Info */}
417
+ <div className="border-b p-2 px-3">
418
+ <div className={`flex items-center gap-2 ${sidebarCollapsed ? 'justify-center' : ''}`}>
419
+ <div className="flex h-7 w-7 items-center justify-center rounded-full bg-blue-100 text-blue-600 text-sm font-medium overflow-hidden">
420
+ {user?.avatar && typeof user.avatar === 'string' ? (
421
+ <img src={user.avatar} alt={user.name} className="w-full h-full object-cover" />
422
+ ) : (
423
+ user?.name?.charAt(0) || 'U'
424
+ )}
425
+ </div>
426
+ {!sidebarCollapsed && (
427
+ <div className="flex flex-col flex-1 transition-all duration-300 ease-in-out">
428
+ <div className="flex items-center justify-between transition-opacity duration-300">
429
+ <span className="text-sm font-medium">{user?.name}</span>
430
+ <SettingsModal collapsed={sidebarCollapsed} />
431
+ </div>
432
+ <div className="flex items-center gap-1 transition-opacity duration-300">
433
+ <Badge variant={user?.roleName === 'Admin' ? 'default' : 'secondary'} className="text-xs h-5 px-1.5">
434
+ {user?.roleName === 'Admin' ? 'Admin' : typeof user?.roleName === 'string' ? user.roleName.toUpperCase() : 'Usuário'}
435
+ </Badge>
436
+ </div>
437
+ </div>
438
+ )}
439
+ </div>
440
+ </div>
441
+
442
+ {/* Navigation */}
443
+ <nav className="flex-1 p-3 overflow-y-auto max-h-[calc(100vh-280px)] scroll-smooth">
444
+ <div className="space-y-1 flex flex-col gap-1">
445
+ {menuItems.map((item) => {
446
+ const isActive = pathname === item.href;
447
+ const hasSubItems = item.subItems && item.subItems.length > 0;
448
+ const isOpen = openSubmenus.has(item.label);
449
+ const hasActiveSubItem = item.subItems?.some(subItem =>
450
+ pathname === subItem.href || pathname.startsWith(subItem.href + '/')
451
+ ) || false;
452
+
453
+ return (
454
+ <div key={item.href + item.label}>
455
+ <MenuItemComponent
456
+ item={item}
457
+ isActive={isActive || hasActiveSubItem}
458
+ collapsed={sidebarCollapsed}
459
+ hasSubItems={hasSubItems}
460
+ isOpen={isOpen}
461
+ onToggle={() => toggleSubmenu(item.label)}
462
+ />
463
+ {/* Render sub-items com Animação Suave */}
464
+ {hasSubItems && !sidebarCollapsed && (
465
+ <div
466
+ className={cn(
467
+ "grid transition-all duration-300 ease-in-out",
468
+ isOpen ? "grid-rows-[1fr] opacity-100" : "grid-rows-[0fr] opacity-0"
469
+ )}
470
+ style={{
471
+ transition: 'grid-template-rows 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease-in-out'
472
+ }}
473
+ >
474
+ <div className="overflow-hidden flex flex-col gap-1 pt-1">
475
+ {item.subItems!.map((subItem) => {
476
+ const isSubItemActive = pathname === subItem.href || pathname.startsWith(subItem.href + '/');
477
+ return (
478
+ <SubMenuItemComponent
479
+ key={subItem.href}
480
+ item={subItem}
481
+ isActive={isSubItemActive}
482
+ collapsed={sidebarCollapsed}
483
+ />
484
+ );
485
+ })}
486
+ </div>
487
+ </div>
488
+ )}
489
+ </div>
490
+ );
491
+ })}
492
+ </div>
493
+ </nav>
494
+
495
+ <Separator />
496
+
497
+ {/* Footer */}
498
+ <div className="p-3">
499
+ <div className="space-y-2">
500
+ {/* Fullscreen Toggle */}
501
+ <ToggleFullScreenModeButton collapsed={sidebarCollapsed} />
502
+
503
+ {/* Theme Toggle */}
504
+ <ThemeToggle collapsed={sidebarCollapsed} />
505
+
506
+ {/* Logout Button */}
507
+ <Button
508
+ variant="ghost"
509
+ className={cn(
510
+ "w-full justify-start gap-2 px-2 py-2 h-auto text-sm",
511
+ sidebarCollapsed && "px-2"
512
+ )}
513
+ onClick={() => setShowLogoutDialog(true)}
514
+ title={sidebarCollapsed ? 'Sair' : undefined}
515
+ >
516
+ <LogOut className="h-3.5 w-3.5 shrink-0" />
517
+ {!sidebarCollapsed && <span className="transition-opacity duration-300">Sair</span>}
518
+ </Button>
519
+
520
+ {!sidebarCollapsed && (
521
+ <div className="mt-5 pt-3 border-t border-gray-100 dark:border-gray-800 transition-all duration-300 ease-in-out">
522
+ <div className="space-y-1.5 transition-opacity duration-300">
523
+ <div className="flex items-center gap-1 text-xs text-gray-500 dark:text-gray-400">
524
+ <span className="font-medium">E-collab - Versão 2.0.0</span>
525
+ </div>
526
+
527
+ <a
528
+ href="https://lordicon.com/"
529
+ target="_blank"
530
+ rel="noopener noreferrer"
531
+ className="group flex items-center gap-1 text-[9px] text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 transition-colors duration-200"
532
+ >
533
+ <span>Ícones por Lordicon</span>
534
+ </a>
535
+ </div>
536
+ </div>
537
+ )}
538
+ </div>
539
+ </div>
540
+ </div>
541
+ );
542
+
543
+ return (
544
+ <div className="flex h-screen bg-background">
545
+ {/* Desktop Sidebar */}
546
+ <div
547
+ className={cn(
548
+ "hidden md:flex border-r bg-card relative",
549
+ collapsed && !isDynamicallyExpanded ? "w-16" : "w-64",
550
+ isDynamicallyExpanded && "shadow-xl z-10"
551
+ )}
552
+ style={{
553
+ transition: isDynamicallyExpanded
554
+ ? 'width 0.4s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s ease-in-out'
555
+ : 'width 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.2s ease-in-out'
556
+ }}
557
+ onMouseEnter={handleSidebarMouseEnter}
558
+ onMouseLeave={handleSidebarMouseLeave}
559
+ >
560
+ <div className="flex h-full w-full flex-col relative">
561
+ <div
562
+ className={cn(
563
+ "h-full w-full transition-opacity duration-300",
564
+ isDynamicallyExpanded ? "opacity-100" : "opacity-100"
565
+ )}
566
+ >
567
+ <SidebarContent collapsed={collapsed && !isDynamicallyExpanded} />
568
+ </div>
569
+
570
+ {!dynamicSidebarEnabled && (
571
+ <Button
572
+ variant="outline"
573
+ size="icon"
574
+ className="absolute -right-4 top-6 h-8 w-8 rounded-full border bg-background shadow-md"
575
+ onClick={() => {
576
+ setCollapsed(!collapsed);
577
+ setIsDynamicallyExpanded(false);
578
+ }}
579
+ >
580
+ {collapsed ? (
581
+ <ChevronRight className="h-4 w-4" />
582
+ ) : (
583
+ <ChevronLeft className="h-4 w-4" />
584
+ )}
585
+ </Button>
586
+ )}
587
+ </div>
588
+ </div>
589
+
590
+ {/* Mobile Sidebar */}
591
+ {mobileOpen && (
592
+ <div className="fixed inset-0 z-50 md:hidden">
593
+ <div className="fixed inset-0 bg-background/80 backdrop-blur-sm" onClick={() => setMobileOpen(false)} />
594
+ <div className="fixed left-0 top-0 h-full w-64 border-r bg-card overflow-y-auto">
595
+ <SidebarContent collapsed={false} />
596
+ </div>
597
+ </div>
598
+ )}
599
+
600
+ {/* Main Content */}
601
+ <div className="flex flex-1 flex-col overflow-hidden">
602
+ {/* Mobile Header */}
603
+ <div className="flex h-16 items-center border-b px-4 md:hidden">
604
+ <Button
605
+ variant="outline"
606
+ size="icon"
607
+ onClick={() => setMobileOpen(true)}
608
+ >
609
+ <Menu className="h-6 w-6" />
610
+ </Button>
611
+ <div className="ml-4 flex items-center gap-2">
612
+ <img
613
+ src="/logo.png"
614
+ alt="E-collab Logo"
615
+ className="h-6 w-auto dark:invert"
616
+ />
617
+ </div>
618
+ </div>
619
+
620
+ {/* Page Content */}
621
+ <main className="flex-1 overflow-auto">
622
+ {children}
623
+ </main>
624
+ </div>
625
+
626
+ {/* Logout Confirmation Dialog */}
627
+ <AlertDialog open={showLogoutDialog} onOpenChange={setShowLogoutDialog}>
628
+ <AlertDialogContent>
629
+ <AlertDialogHeader>
630
+ <AlertDialogTitle>Confirmar Saída</AlertDialogTitle>
631
+ <AlertDialogDescription>
632
+ Tem certeza que deseja sair do sistema? Você precisará fazer login novamente para acessar.
633
+ </AlertDialogDescription>
634
+ </AlertDialogHeader>
635
+ <AlertDialogFooter>
636
+ <AlertDialogCancel>Cancelar</AlertDialogCancel>
637
+ <AlertDialogAction onClick={handleLogout}>
638
+ Sair
639
+ </AlertDialogAction>
640
+ </AlertDialogFooter>
641
+ </AlertDialogContent>
642
+ </AlertDialog>
643
+ </div>
644
+ );
645
+ }