@evolution-soft/ui 1.0.0 → 1.0.3
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/cli/index.js +386 -0
- package/components/button-icon-lottie/index.tsx +46 -0
- package/components/fullscreen-mode/index.tsx +82 -0
- package/components/header/components/buttons.tsx +102 -0
- package/components/header/index.tsx +146 -0
- package/components/loading-default/index.tsx +90 -0
- package/components/lottie-icon/index.tsx +78 -0
- package/components/not-found-default/index.tsx +68 -0
- package/components/settings-modal/index.tsx +225 -0
- package/components/sidebar/index.tsx +645 -0
- package/components/subtitle/index.tsx +60 -0
- package/components/theme-transition/index.tsx +142 -0
- package/components/title/index.tsx +66 -0
- package/components/tooltip-indicator/index.tsx +30 -0
- package/components/ui/accordion.tsx +66 -0
- package/components/ui/alert-dialog.tsx +157 -0
- package/components/ui/alert.tsx +66 -0
- package/components/ui/aspect-ratio.tsx +11 -0
- package/components/ui/avatar.tsx +53 -0
- package/components/ui/badge.tsx +46 -0
- package/components/ui/breadcrumb.tsx +109 -0
- package/components/ui/button.tsx +58 -0
- package/components/ui/calendar.tsx +78 -0
- package/components/ui/card.tsx +92 -0
- package/components/ui/carousel.tsx +241 -0
- package/components/ui/chart.tsx +360 -0
- package/components/ui/checkbox.tsx +32 -0
- package/components/ui/collapsible.tsx +33 -0
- package/components/ui/command.tsx +177 -0
- package/components/ui/context-menu.tsx +252 -0
- package/components/ui/dialog.tsx +135 -0
- package/components/ui/divisor.tsx +9 -0
- package/components/ui/drawer.tsx +132 -0
- package/components/ui/dropdown-menu.tsx +257 -0
- package/components/ui/emoji-picker.tsx +76 -0
- package/components/ui/form.tsx +168 -0
- package/components/ui/hover-card.tsx +44 -0
- package/components/ui/input-mask.tsx +46 -0
- package/components/ui/input-otp.tsx +77 -0
- package/components/ui/input.tsx +61 -0
- package/components/ui/label.tsx +24 -0
- package/components/ui/menubar.tsx +276 -0
- package/components/ui/multiselect.tsx +105 -0
- package/components/ui/navigation-menu.tsx +168 -0
- package/components/ui/pagination.tsx +127 -0
- package/components/ui/popover.tsx +48 -0
- package/components/ui/progress.tsx +31 -0
- package/components/ui/radio-group.tsx +45 -0
- package/components/ui/resizable.tsx +65 -0
- package/components/ui/scroll-area.tsx +58 -0
- package/components/ui/searchable-select.tsx +211 -0
- package/components/ui/select.tsx +189 -0
- package/components/ui/separator.tsx +28 -0
- package/components/ui/sheet.tsx +139 -0
- package/components/ui/sidebar.tsx +727 -0
- package/components/ui/skeleton.tsx +144 -0
- package/components/ui/slider.tsx +63 -0
- package/components/ui/sonner.tsx +26 -0
- package/components/ui/switch.tsx +31 -0
- package/components/ui/table.tsx +116 -0
- package/components/ui/tabs.tsx +76 -0
- package/components/ui/textarea.tsx +18 -0
- package/components/ui/theme-toggle.tsx +89 -0
- package/components/ui/toggle-group.tsx +73 -0
- package/components/ui/toggle.tsx +47 -0
- package/components/ui/tooltip.tsx +61 -0
- package/components/ui/use-mobile.ts +21 -0
- package/components/ui/utils.ts +6 -0
- package/contexts/AnimationSettingsContext.tsx +85 -0
- package/contexts/AuthContext.tsx +80 -0
- package/contexts/ThemeContext.tsx +70 -0
- package/hooks/useAnimationSettings.ts +2 -0
- package/hooks/usePermissions.ts +4 -0
- package/lib/persistentFilters.ts +120 -0
- package/lib/utils.ts +2 -0
- package/package.json +11 -2
- package/stores/theme.ts +30 -0
- 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
|
+
}
|