xertica-ui 1.4.13 → 1.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/components/Sidebar.tsx +323 -57
- package/components/TemplateContent.tsx +130 -1
- package/components/ui/accordion.tsx +10 -0
- package/components/ui/alert-dialog.tsx +10 -0
- package/components/ui/alert.tsx +11 -0
- package/components/ui/aspect-ratio.tsx +10 -0
- package/components/ui/avatar.tsx +10 -0
- package/components/ui/badge.tsx +10 -0
- package/components/ui/breadcrumb.tsx +10 -0
- package/components/ui/button.tsx +34 -8
- package/components/ui/calendar.tsx +10 -0
- package/components/ui/card.tsx +11 -0
- package/components/ui/carousel.tsx +10 -0
- package/components/ui/chart.tsx +10 -0
- package/components/ui/checkbox.tsx +19 -0
- package/components/ui/collapsible.tsx +9 -0
- package/components/ui/command.tsx +10 -0
- package/components/ui/context-menu.tsx +10 -0
- package/components/ui/dialog.tsx +12 -0
- package/components/ui/drawer.tsx +10 -0
- package/components/ui/dropdown-menu.tsx +12 -0
- package/components/ui/empty.tsx +10 -0
- package/components/ui/file-upload.tsx +11 -0
- package/components/ui/form.tsx +12 -0
- package/components/ui/hover-card.tsx +9 -0
- package/components/ui/input-otp.tsx +10 -0
- package/components/ui/input.tsx +24 -1
- package/components/ui/label.tsx +10 -0
- package/components/ui/map.tsx +12 -0
- package/components/ui/menubar.tsx +10 -0
- package/components/ui/navigation-menu.tsx +10 -0
- package/components/ui/notification-badge.tsx +10 -0
- package/components/ui/page-header.tsx +10 -0
- package/components/ui/pagination.tsx +10 -0
- package/components/ui/popover.tsx +10 -0
- package/components/ui/progress.tsx +10 -0
- package/components/ui/radio-group.tsx +10 -0
- package/components/ui/rating.tsx +10 -0
- package/components/ui/resizable.tsx +10 -0
- package/components/ui/route-map.tsx +11 -0
- package/components/ui/scroll-area.tsx +10 -0
- package/components/ui/search.tsx +10 -0
- package/components/ui/select.tsx +9 -0
- package/components/ui/separator.tsx +9 -0
- package/components/ui/sheet.tsx +9 -0
- package/components/ui/skeleton.tsx +10 -0
- package/components/ui/slider.tsx +10 -0
- package/components/ui/sonner.tsx +7 -16
- package/components/ui/stats-card.tsx +10 -0
- package/components/ui/stepper.tsx +10 -0
- package/components/ui/switch.tsx +9 -0
- package/components/ui/table.tsx +11 -0
- package/components/ui/tabs.tsx +12 -0
- package/components/ui/textarea.tsx +10 -0
- package/components/ui/timeline.tsx +10 -0
- package/components/ui/toggle-group.tsx +10 -0
- package/components/ui/toggle.tsx +9 -0
- package/components/ui/tooltip.tsx +10 -0
- package/components/ui/tree-view.tsx +10 -0
- package/components/ui/xertica-assistant.tsx +12 -0
- package/dist/components/Sidebar.d.ts +51 -2
- package/dist/components/ui/accordion.d.ts +10 -0
- package/dist/components/ui/alert-dialog.d.ts +10 -0
- package/dist/components/ui/alert.d.ts +11 -0
- package/dist/components/ui/aspect-ratio.d.ts +10 -0
- package/dist/components/ui/avatar.d.ts +10 -0
- package/dist/components/ui/badge.d.ts +10 -0
- package/dist/components/ui/breadcrumb.d.ts +10 -0
- package/dist/components/ui/button.d.ts +29 -5
- package/dist/components/ui/calendar.d.ts +10 -0
- package/dist/components/ui/card.d.ts +11 -0
- package/dist/components/ui/carousel.d.ts +10 -0
- package/dist/components/ui/chart.d.ts +10 -0
- package/dist/components/ui/checkbox.d.ts +19 -0
- package/dist/components/ui/collapsible.d.ts +9 -0
- package/dist/components/ui/command.d.ts +10 -0
- package/dist/components/ui/context-menu.d.ts +10 -0
- package/dist/components/ui/dialog.d.ts +12 -0
- package/dist/components/ui/drawer.d.ts +10 -0
- package/dist/components/ui/dropdown-menu.d.ts +12 -0
- package/dist/components/ui/empty.d.ts +10 -0
- package/dist/components/ui/file-upload.d.ts +11 -0
- package/dist/components/ui/form.d.ts +12 -0
- package/dist/components/ui/hover-card.d.ts +9 -0
- package/dist/components/ui/input-otp.d.ts +10 -0
- package/dist/components/ui/input.d.ts +24 -1
- package/dist/components/ui/label.d.ts +10 -0
- package/dist/components/ui/map.d.ts +12 -0
- package/dist/components/ui/menubar.d.ts +10 -0
- package/dist/components/ui/navigation-menu.d.ts +10 -0
- package/dist/components/ui/notification-badge.d.ts +10 -0
- package/dist/components/ui/page-header.d.ts +10 -0
- package/dist/components/ui/pagination.d.ts +10 -0
- package/dist/components/ui/popover.d.ts +10 -0
- package/dist/components/ui/progress.d.ts +10 -0
- package/dist/components/ui/radio-group.d.ts +10 -0
- package/dist/components/ui/rating.d.ts +10 -0
- package/dist/components/ui/resizable.d.ts +10 -0
- package/dist/components/ui/route-map.d.ts +11 -0
- package/dist/components/ui/scroll-area.d.ts +10 -0
- package/dist/components/ui/search.d.ts +10 -0
- package/dist/components/ui/select.d.ts +8 -0
- package/dist/components/ui/separator.d.ts +9 -0
- package/dist/components/ui/sheet.d.ts +9 -0
- package/dist/components/ui/skeleton.d.ts +10 -0
- package/dist/components/ui/slider.d.ts +10 -0
- package/dist/components/ui/sonner.d.ts +7 -16
- package/dist/components/ui/stats-card.d.ts +10 -0
- package/dist/components/ui/stepper.d.ts +10 -0
- package/dist/components/ui/switch.d.ts +9 -0
- package/dist/components/ui/table.d.ts +11 -0
- package/dist/components/ui/tabs.d.ts +12 -0
- package/dist/components/ui/textarea.d.ts +10 -0
- package/dist/components/ui/timeline.d.ts +10 -0
- package/dist/components/ui/toggle-group.d.ts +10 -0
- package/dist/components/ui/toggle.d.ts +9 -0
- package/dist/components/ui/tooltip.d.ts +10 -0
- package/dist/components/ui/tree-view.d.ts +10 -0
- package/dist/components/ui/xertica-assistant.d.ts +12 -0
- package/dist/index.es.js +280 -38
- package/dist/index.umd.js +279 -37
- package/dist/xertica-ui.css +1 -1
- package/package.json +3 -1
- package/templates/package.json +1 -1
- package/templates/src/app/pages/CrudTemplate.tsx +106 -0
- package/templates/src/app/pages/DashboardTemplate.tsx +101 -0
- package/templates/src/app/pages/FormTemplate.tsx +109 -0
- package/templates/src/app/pages/LoginTemplate.tsx +52 -0
- package/templates/src/app/pages/Template/TemplateContent.tsx +139 -0
package/components/Sidebar.tsx
CHANGED
|
@@ -4,14 +4,28 @@ import {
|
|
|
4
4
|
ArrowLeft,
|
|
5
5
|
LogOut,
|
|
6
6
|
Settings,
|
|
7
|
-
|
|
7
|
+
MoreVertical,
|
|
8
8
|
ChevronRight,
|
|
9
|
+
Filter,
|
|
9
10
|
} from "lucide-react";
|
|
10
11
|
// routes import removed to make it reusable
|
|
11
12
|
import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar";
|
|
12
13
|
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
|
|
13
14
|
import { Tooltip, TooltipProvider, TooltipTrigger } from "./ui/tooltip";
|
|
14
15
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
|
16
|
+
import {
|
|
17
|
+
DropdownMenu,
|
|
18
|
+
DropdownMenuContent,
|
|
19
|
+
DropdownMenuItem,
|
|
20
|
+
DropdownMenuTrigger,
|
|
21
|
+
DropdownMenuSub,
|
|
22
|
+
DropdownMenuSubTrigger,
|
|
23
|
+
DropdownMenuSubContent,
|
|
24
|
+
DropdownMenuPortal,
|
|
25
|
+
} from "./ui/dropdown-menu";
|
|
26
|
+
import { ScrollArea } from "./ui/scroll-area";
|
|
27
|
+
import { Input } from "./ui/input";
|
|
28
|
+
import { Search as SearchIcon } from "lucide-react";
|
|
15
29
|
import { cn } from "./ui/utils";
|
|
16
30
|
import { XerticaLogo } from "./XerticaLogo";
|
|
17
31
|
import { XerticaXLogo } from "./XerticaXLogo";
|
|
@@ -45,17 +59,29 @@ function SidebarTooltipContent({
|
|
|
45
59
|
}
|
|
46
60
|
|
|
47
61
|
// Interface for Route Config (copied/imported type)
|
|
62
|
+
export interface ActionMenuItem {
|
|
63
|
+
label: string;
|
|
64
|
+
icon?: React.ComponentType<any>;
|
|
65
|
+
onClick?: (item: any) => void;
|
|
66
|
+
variant?: "default" | "destructive";
|
|
67
|
+
children?: ActionMenuItem[];
|
|
68
|
+
}
|
|
69
|
+
|
|
48
70
|
export interface RouteConfig {
|
|
49
71
|
path: string;
|
|
50
72
|
label: string;
|
|
51
73
|
icon: React.ComponentType<any>;
|
|
52
74
|
component?: React.ComponentType<any>;
|
|
75
|
+
actions?: ActionMenuItem[]; // Menus de hover (elipses)
|
|
76
|
+
description?: React.ReactNode; // Conteúdo auxiliar quando selecionado
|
|
53
77
|
}
|
|
54
78
|
|
|
55
79
|
interface SubMenuItem {
|
|
56
80
|
id: string;
|
|
57
81
|
label: string;
|
|
58
82
|
path: string;
|
|
83
|
+
icon?: React.ComponentType<any>;
|
|
84
|
+
actions?: ActionMenuItem[];
|
|
59
85
|
}
|
|
60
86
|
|
|
61
87
|
interface NavigationItem {
|
|
@@ -66,6 +92,33 @@ interface NavigationItem {
|
|
|
66
92
|
subItems?: SubMenuItem[];
|
|
67
93
|
}
|
|
68
94
|
|
|
95
|
+
export interface SidebarFilterConfig {
|
|
96
|
+
show: boolean;
|
|
97
|
+
content?: React.ReactNode;
|
|
98
|
+
icon?: React.ReactNode;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface SidebarSearchConfig {
|
|
102
|
+
show: boolean;
|
|
103
|
+
placeholder?: string;
|
|
104
|
+
value?: string;
|
|
105
|
+
onChange?: (value: string) => void;
|
|
106
|
+
filter?: SidebarFilterConfig;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface SidebarFixedAreaConfig {
|
|
110
|
+
show: boolean;
|
|
111
|
+
content?: React.ReactNode;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface RouteGroup {
|
|
115
|
+
id: string;
|
|
116
|
+
label?: string; // Título do grupo
|
|
117
|
+
icon?: React.ComponentType<any>; // Ícone do grupo
|
|
118
|
+
items: RouteConfig[];
|
|
119
|
+
actions?: ActionMenuItem[]; // Menu contextual para o grupo inteiro
|
|
120
|
+
}
|
|
121
|
+
|
|
69
122
|
interface SidebarProps {
|
|
70
123
|
expanded: boolean;
|
|
71
124
|
onToggle: () => void;
|
|
@@ -73,11 +126,32 @@ interface SidebarProps {
|
|
|
73
126
|
onLogout: () => void;
|
|
74
127
|
location: { pathname: string };
|
|
75
128
|
navigate: (path: string) => void;
|
|
76
|
-
routes
|
|
129
|
+
routes?: RouteConfig[]; // tornado opcional devido ao assistant
|
|
77
130
|
logo?: React.ReactNode;
|
|
78
131
|
logoCollapsed?: React.ReactNode;
|
|
132
|
+
|
|
133
|
+
// Prop para definir qual comportamento renderizar
|
|
134
|
+
variant?: "default" | "assistant";
|
|
135
|
+
|
|
136
|
+
// Específicos do Assistant
|
|
137
|
+
fixedArea?: SidebarFixedAreaConfig;
|
|
138
|
+
search?: SidebarSearchConfig;
|
|
139
|
+
navigationGroups?: RouteGroup[];
|
|
79
140
|
}
|
|
80
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Sidebar e Navegação Principal.
|
|
144
|
+
*
|
|
145
|
+
* @description
|
|
146
|
+
* Componente complexo que gerencia renderização em Desktop/Mobile com suporte a:
|
|
147
|
+
* - Modo Normal (Lista Simples)
|
|
148
|
+
* - Modo Assistent (Grupos, Pesquisa, Áreas Fixas)
|
|
149
|
+
*
|
|
150
|
+
* @ai-rules
|
|
151
|
+
* 1. NUNCA tente reinventar a Sidebar com classes tailwind. SEMPRE injete este componente no `layout` da aplicação (seja `App.tsx` ou em layout do Next.js).
|
|
152
|
+
* 2. Utilize a prop apropriada `variant="assistant"` ou `variant="default"`.
|
|
153
|
+
* 3. Garanta que o callback `navigate` manipule as rotas do seu roteador local.
|
|
154
|
+
*/
|
|
81
155
|
export function Sidebar({
|
|
82
156
|
expanded,
|
|
83
157
|
onToggle,
|
|
@@ -88,8 +162,13 @@ export function Sidebar({
|
|
|
88
162
|
routes,
|
|
89
163
|
logo,
|
|
90
164
|
logoCollapsed,
|
|
165
|
+
variant = "default",
|
|
166
|
+
fixedArea,
|
|
167
|
+
search,
|
|
168
|
+
navigationGroups = [],
|
|
91
169
|
}: SidebarProps) {
|
|
92
170
|
const navRef = useRef<HTMLDivElement>(null);
|
|
171
|
+
const [localActiveItem, setLocalActiveItem] = useState<string | null>(null);
|
|
93
172
|
const [hasOverflow, setHasOverflow] = useState(false);
|
|
94
173
|
const [visibleItems, setVisibleItems] = useState<NavigationItem[]>([]);
|
|
95
174
|
const [overflowItems, setOverflowItems] = useState<NavigationItem[]>([]);
|
|
@@ -111,7 +190,7 @@ export function Sidebar({
|
|
|
111
190
|
],
|
|
112
191
|
};
|
|
113
192
|
|
|
114
|
-
const navigationItems: NavigationItem[] = routes.map((route) => ({
|
|
193
|
+
const navigationItems: NavigationItem[] = (routes || []).map((route) => ({
|
|
115
194
|
...route,
|
|
116
195
|
label:
|
|
117
196
|
labelTranslations[route.label.toLowerCase()] ||
|
|
@@ -126,6 +205,7 @@ export function Sidebar({
|
|
|
126
205
|
useEffect(() => {
|
|
127
206
|
const checkOverflow = () => {
|
|
128
207
|
if (!navRef.current) return;
|
|
208
|
+
if (variant === "assistant") return;
|
|
129
209
|
|
|
130
210
|
const navHeight = navRef.current.clientHeight;
|
|
131
211
|
const itemHeight = 44; // h-10 + gap
|
|
@@ -148,6 +228,7 @@ export function Sidebar({
|
|
|
148
228
|
}, [navigationItems.length]);
|
|
149
229
|
|
|
150
230
|
const handleNavigate = (path: string) => {
|
|
231
|
+
setLocalActiveItem(path);
|
|
151
232
|
navigate(path);
|
|
152
233
|
setOpenSubmenu(null);
|
|
153
234
|
// Fecha o menu no mobile após navegar
|
|
@@ -181,7 +262,7 @@ export function Sidebar({
|
|
|
181
262
|
{item.label}
|
|
182
263
|
</span>
|
|
183
264
|
{hasSubItems && (
|
|
184
|
-
<
|
|
265
|
+
<MoreVertical className="w-4 h-4 flex-shrink-0 text-sidebar-foreground/60" />
|
|
185
266
|
)}
|
|
186
267
|
</>
|
|
187
268
|
)}
|
|
@@ -292,6 +373,132 @@ export function Sidebar({
|
|
|
292
373
|
return simpleButton;
|
|
293
374
|
};
|
|
294
375
|
|
|
376
|
+
const renderActionItems = (actions: ActionMenuItem[]) => {
|
|
377
|
+
return actions.map((action, idx) => {
|
|
378
|
+
const Icon = action.icon;
|
|
379
|
+
|
|
380
|
+
if (action.children && action.children.length > 0) {
|
|
381
|
+
return (
|
|
382
|
+
<DropdownMenuSub key={idx}>
|
|
383
|
+
<DropdownMenuSubTrigger>
|
|
384
|
+
{Icon && <Icon className="mr-2 h-4 w-4" />}
|
|
385
|
+
<span>{action.label}</span>
|
|
386
|
+
</DropdownMenuSubTrigger>
|
|
387
|
+
<DropdownMenuPortal>
|
|
388
|
+
<DropdownMenuSubContent className="w-48 bg-popover border-border">
|
|
389
|
+
{renderActionItems(action.children)}
|
|
390
|
+
</DropdownMenuSubContent>
|
|
391
|
+
</DropdownMenuPortal>
|
|
392
|
+
</DropdownMenuSub>
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return (
|
|
397
|
+
<DropdownMenuItem
|
|
398
|
+
key={idx}
|
|
399
|
+
className={cn(
|
|
400
|
+
"flex items-center gap-2",
|
|
401
|
+
action.variant === "destructive" ? "text-destructive focus:text-destructive" : ""
|
|
402
|
+
)}
|
|
403
|
+
onClick={(e) => {
|
|
404
|
+
e.stopPropagation();
|
|
405
|
+
action.onClick?.(null);
|
|
406
|
+
}}
|
|
407
|
+
>
|
|
408
|
+
{Icon && <Icon className="h-4 w-4 flex-shrink-0" />}
|
|
409
|
+
<span>{action.label}</span>
|
|
410
|
+
</DropdownMenuItem>
|
|
411
|
+
);
|
|
412
|
+
});
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const renderAssistantActionMenu = (actions?: ActionMenuItem[], isHeader: boolean = false) => {
|
|
416
|
+
if (!actions || actions.length === 0) return null;
|
|
417
|
+
|
|
418
|
+
return (
|
|
419
|
+
<DropdownMenu>
|
|
420
|
+
<DropdownMenuTrigger asChild>
|
|
421
|
+
<Button
|
|
422
|
+
variant="ghost"
|
|
423
|
+
size="icon"
|
|
424
|
+
className={cn(
|
|
425
|
+
"h-8 w-8 text-sidebar-foreground/60 hover:bg-sidebar-foreground/20 hover:text-sidebar-foreground rounded-full transition-all",
|
|
426
|
+
!isHeader && "opacity-0 group-hover/item:opacity-100"
|
|
427
|
+
)}
|
|
428
|
+
>
|
|
429
|
+
<MoreVertical className="h-4 w-4" />
|
|
430
|
+
</Button>
|
|
431
|
+
</DropdownMenuTrigger>
|
|
432
|
+
<DropdownMenuContent align="end" className="w-48 bg-popover border-border p-1">
|
|
433
|
+
{renderActionItems(actions)}
|
|
434
|
+
</DropdownMenuContent>
|
|
435
|
+
</DropdownMenu>
|
|
436
|
+
);
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
const renderAssistantGroup = (group: RouteGroup) => {
|
|
440
|
+
return (
|
|
441
|
+
<div key={group.id} className="py-2 group">
|
|
442
|
+
{(group.label || group.icon) && (
|
|
443
|
+
<div className="flex items-center justify-between px-3 mb-1">
|
|
444
|
+
<div className="flex items-center gap-2 text-sidebar-foreground/60 text-xs font-semibold uppercase tracking-wider">
|
|
445
|
+
{group.icon && <group.icon className="h-4 w-4" />}
|
|
446
|
+
{expanded && group.label && <span>{group.label}</span>}
|
|
447
|
+
</div>
|
|
448
|
+
{expanded && renderAssistantActionMenu(group.actions, true)}
|
|
449
|
+
</div>
|
|
450
|
+
)}
|
|
451
|
+
<div className="space-y-1">
|
|
452
|
+
{group.items.map(item => {
|
|
453
|
+
const isRouteActive = location.pathname === item.path || location.pathname.startsWith(item.path + "/");
|
|
454
|
+
const isActive = isRouteActive || localActiveItem === item.path;
|
|
455
|
+
const Icon = item.icon;
|
|
456
|
+
|
|
457
|
+
const simpleBtn = (
|
|
458
|
+
<div
|
|
459
|
+
className={expanded
|
|
460
|
+
? `group/item flex items-start justify-between px-3 min-h-[36px] py-2.5 rounded-[var(--radius-button)] cursor-pointer transition-all duration-200 ${isActive ? 'bg-sidebar-foreground/15 text-sidebar-foreground' : 'text-sidebar-foreground/80 hover:bg-sidebar-foreground/10 hover:text-sidebar-foreground'}`
|
|
461
|
+
: `group/item flex items-center justify-center h-10 px-0 rounded-[var(--radius-button)] cursor-pointer transition-all duration-200 ${isActive ? 'bg-sidebar-foreground/15 text-sidebar-foreground' : 'text-sidebar-foreground/80 hover:bg-sidebar-foreground/10 hover:text-sidebar-foreground'}`
|
|
462
|
+
}
|
|
463
|
+
onClick={() => handleNavigate(item.path)}
|
|
464
|
+
>
|
|
465
|
+
<div className="flex flex-col min-w-0 flex-1">
|
|
466
|
+
<div className="flex items-center gap-3 overflow-hidden h-5">
|
|
467
|
+
<Icon className="w-4 h-4 flex-shrink-0" />
|
|
468
|
+
{expanded && <span className="truncate text-sm font-medium leading-none">{item.label}</span>}
|
|
469
|
+
</div>
|
|
470
|
+
{expanded && isActive && item.description && (
|
|
471
|
+
<div className="ml-7 text-[11px] text-sidebar-foreground/60 mt-1.5 animate-in fade-in slide-in-from-top-1 duration-200">
|
|
472
|
+
{item.description}
|
|
473
|
+
</div>
|
|
474
|
+
)}
|
|
475
|
+
</div>
|
|
476
|
+
{expanded && (
|
|
477
|
+
<div className="h-5 flex items-center ml-1">
|
|
478
|
+
{renderAssistantActionMenu(item.actions)}
|
|
479
|
+
</div>
|
|
480
|
+
)}
|
|
481
|
+
</div>
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
return (
|
|
485
|
+
<div key={item.path}>
|
|
486
|
+
{!expanded ? (
|
|
487
|
+
<Tooltip>
|
|
488
|
+
<TooltipTrigger asChild>{simpleBtn}</TooltipTrigger>
|
|
489
|
+
<SidebarTooltipContent side="right" sideOffset={0}>
|
|
490
|
+
<p>{item.label}</p>
|
|
491
|
+
</SidebarTooltipContent>
|
|
492
|
+
</Tooltip>
|
|
493
|
+
) : simpleBtn}
|
|
494
|
+
</div>
|
|
495
|
+
);
|
|
496
|
+
})}
|
|
497
|
+
</div>
|
|
498
|
+
</div>
|
|
499
|
+
);
|
|
500
|
+
};
|
|
501
|
+
|
|
295
502
|
return (
|
|
296
503
|
<TooltipProvider delayDuration={300}>
|
|
297
504
|
{/* Sidebar */}
|
|
@@ -302,7 +509,7 @@ export function Sidebar({
|
|
|
302
509
|
}`}
|
|
303
510
|
>
|
|
304
511
|
{/* Botão Toggle Menu */}
|
|
305
|
-
<div className="p-[14px] pt-[13px] pr-[14px] pb-[12px] pl-[14px]">
|
|
512
|
+
<div className="flex-shrink-0 p-[14px] pt-[13px] pr-[14px] pb-[12px] pl-[14px]">
|
|
306
513
|
<button
|
|
307
514
|
onClick={onToggle}
|
|
308
515
|
className="w-full h-10 flex items-center gap-3 px-3 justify-center rounded-[var(--radius-button)] transition-all duration-200 text-sidebar-foreground/80 hover:bg-sidebar-foreground/15 hover:text-sidebar-foreground"
|
|
@@ -316,7 +523,7 @@ export function Sidebar({
|
|
|
316
523
|
</div>
|
|
317
524
|
|
|
318
525
|
{/* Logo */}
|
|
319
|
-
<div className="px-4 py-4">
|
|
526
|
+
<div className="flex-shrink-0 px-4 py-4">
|
|
320
527
|
<div
|
|
321
528
|
className={`flex items-center h-10 ${expanded ? "justify-center" : "justify-center"}`}
|
|
322
529
|
>
|
|
@@ -340,62 +547,121 @@ export function Sidebar({
|
|
|
340
547
|
</div>
|
|
341
548
|
</div>
|
|
342
549
|
|
|
343
|
-
{/*
|
|
344
|
-
|
|
345
|
-
<div className="space-y-
|
|
346
|
-
{
|
|
347
|
-
|
|
348
|
-
|
|
550
|
+
{/* Assistant-specific Header (Search/Fixed Area) - Always fixed when present */}
|
|
551
|
+
{variant === "assistant" && ((fixedArea && fixedArea.show) || (search && search.show)) && (
|
|
552
|
+
<div className="flex-shrink-0 px-4 pb-4 space-y-4 border-b border-sidebar-border/30 mb-2">
|
|
553
|
+
{fixedArea?.show && fixedArea.content && expanded && (
|
|
554
|
+
<div className="animate-in fade-in slide-in-from-top-1 duration-300">
|
|
555
|
+
{fixedArea.content}
|
|
556
|
+
</div>
|
|
349
557
|
)}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
})}
|
|
390
|
-
</div>
|
|
391
|
-
</PopoverContent>
|
|
392
|
-
</Popover>
|
|
558
|
+
{search?.show && expanded && (
|
|
559
|
+
<div className="flex items-center gap-2">
|
|
560
|
+
<div className="relative flex-1">
|
|
561
|
+
<SearchIcon className="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-sidebar-foreground/50" />
|
|
562
|
+
<Input
|
|
563
|
+
type="text"
|
|
564
|
+
placeholder={search.placeholder || "Buscar..."}
|
|
565
|
+
value={search.value}
|
|
566
|
+
onChange={(e) => search.onChange?.(e.target.value)}
|
|
567
|
+
className="w-full h-9 bg-sidebar-foreground/10 border-sidebar-border text-sidebar-foreground placeholder:text-sidebar-foreground/50 pl-9 focus-visible:ring-1 focus-visible:ring-sidebar-foreground/30 focus-visible:ring-offset-0"
|
|
568
|
+
/>
|
|
569
|
+
</div>
|
|
570
|
+
{search.filter?.show && search.filter.content && (
|
|
571
|
+
<Popover>
|
|
572
|
+
<PopoverTrigger asChild>
|
|
573
|
+
<Button variant="ghost" size="icon" className="h-9 w-9 text-sidebar-foreground hover:bg-sidebar-foreground/15 hover:text-sidebar-foreground">
|
|
574
|
+
{search.filter.icon || <Filter className="h-4 w-4" />}
|
|
575
|
+
</Button>
|
|
576
|
+
</PopoverTrigger>
|
|
577
|
+
<PopoverContent side="bottom" align="end" className="w-64 p-0 bg-popover border-border">
|
|
578
|
+
{search.filter.content}
|
|
579
|
+
</PopoverContent>
|
|
580
|
+
</Popover>
|
|
581
|
+
)}
|
|
582
|
+
</div>
|
|
583
|
+
)}
|
|
584
|
+
{(!expanded && (fixedArea?.show || search?.show)) && (
|
|
585
|
+
<div className="flex flex-col items-center gap-2">
|
|
586
|
+
{search?.show && (
|
|
587
|
+
<Tooltip>
|
|
588
|
+
<TooltipTrigger asChild>
|
|
589
|
+
<button className="h-10 w-10 flex items-center justify-center rounded-[var(--radius-button)] text-sidebar-foreground/80 hover:bg-sidebar-foreground/15 hover:text-sidebar-foreground">
|
|
590
|
+
<SearchIcon className="h-5 w-5" />
|
|
591
|
+
</button>
|
|
592
|
+
</TooltipTrigger>
|
|
593
|
+
<SidebarTooltipContent side="right">Buscar</SidebarTooltipContent>
|
|
594
|
+
</Tooltip>
|
|
595
|
+
)}
|
|
596
|
+
</div>
|
|
393
597
|
)}
|
|
394
598
|
</div>
|
|
395
|
-
|
|
599
|
+
)}
|
|
600
|
+
|
|
601
|
+
{/* Main Content Area - Scrollable */}
|
|
602
|
+
<div className="flex-1 min-h-0 overflow-hidden">
|
|
603
|
+
{variant === "default" ? (
|
|
604
|
+
<nav className="h-full px-4 py-4 overflow-hidden" ref={navRef}>
|
|
605
|
+
<div className="space-y-1">
|
|
606
|
+
{/* Itens visíveis */}
|
|
607
|
+
{(hasOverflow ? visibleItems : navigationItems).map((item) =>
|
|
608
|
+
renderMenuItem(item)
|
|
609
|
+
)}
|
|
610
|
+
|
|
611
|
+
{/* Botão de overflow (ellipsis) */}
|
|
612
|
+
{hasOverflow && (
|
|
613
|
+
<Popover>
|
|
614
|
+
<PopoverTrigger asChild>
|
|
615
|
+
<button
|
|
616
|
+
className={
|
|
617
|
+
expanded
|
|
618
|
+
? "w-full h-10 flex items-center gap-3 px-3 justify-start rounded-[var(--radius-button)] transition-all duration-200 text-sidebar-foreground/80 hover:bg-sidebar-foreground/15 hover:text-sidebar-foreground"
|
|
619
|
+
: "w-full h-10 flex items-center justify-center px-0 rounded-[var(--radius-button)] transition-all duration-200 text-sidebar-foreground/80 hover:bg-sidebar-foreground/15 hover:text-sidebar-foreground"
|
|
620
|
+
}
|
|
621
|
+
>
|
|
622
|
+
<MoreVertical className="w-5 h-5 flex-shrink-0" />
|
|
623
|
+
{expanded && (
|
|
624
|
+
<span className="truncate text-sidebar-foreground">
|
|
625
|
+
Mais opções
|
|
626
|
+
</span>
|
|
627
|
+
)}
|
|
628
|
+
</button>
|
|
629
|
+
</PopoverTrigger>
|
|
630
|
+
<PopoverContent
|
|
631
|
+
side="right"
|
|
632
|
+
align="start"
|
|
633
|
+
className="w-56 p-2 bg-popover border border-border rounded-[var(--radius-card)] shadow-lg"
|
|
634
|
+
sideOffset={8}
|
|
635
|
+
>
|
|
636
|
+
<div className="space-y-1">
|
|
637
|
+
{overflowItems.map((item) => {
|
|
638
|
+
const Icon = item.icon;
|
|
639
|
+
return (
|
|
640
|
+
<button
|
|
641
|
+
key={item.path}
|
|
642
|
+
onClick={() => handleNavigate(item.path)}
|
|
643
|
+
className="w-full h-9 flex items-center gap-2 px-3 rounded-[var(--radius-button)] transition-all duration-200 text-popover-foreground/80 hover:bg-accent hover:text-accent-foreground text-left"
|
|
644
|
+
>
|
|
645
|
+
<Icon className="w-4 h-4 flex-shrink-0" />
|
|
646
|
+
<span className="truncate">{item.label}</span>
|
|
647
|
+
</button>
|
|
648
|
+
);
|
|
649
|
+
})}
|
|
650
|
+
</div>
|
|
651
|
+
</PopoverContent>
|
|
652
|
+
</Popover>
|
|
653
|
+
)}
|
|
654
|
+
</div>
|
|
655
|
+
</nav>
|
|
656
|
+
) : (
|
|
657
|
+
<ScrollArea className="h-full px-4">
|
|
658
|
+
{navigationGroups.map(group => renderAssistantGroup(group))}
|
|
659
|
+
</ScrollArea>
|
|
660
|
+
)}
|
|
661
|
+
</div>
|
|
396
662
|
|
|
397
663
|
{/* Footer da sidebar - Usuário */}
|
|
398
|
-
<div className="p-4 space-y-2">
|
|
664
|
+
<div className="flex-shrink-0 p-4 space-y-2">
|
|
399
665
|
{/* Avatar do usuário */}
|
|
400
666
|
{!expanded ? (
|
|
401
667
|
<Tooltip>
|
|
@@ -14,7 +14,7 @@ import { Textarea } from './ui/textarea';
|
|
|
14
14
|
import { Progress } from './ui/progress';
|
|
15
15
|
import { Separator } from './ui/separator';
|
|
16
16
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './ui/table';
|
|
17
|
-
import { Settings, User, Mail, Phone, Calendar, Search, Menu, ChevronRight, Home, Users } from 'lucide-react';
|
|
17
|
+
import { Settings, User, Mail, Phone, Calendar, Search, Menu, ChevronRight, Home, Users, Plus, Trash2, Archive, ArrowRightLeft, History, FileEdit, Filter, Clock, Map } from 'lucide-react';
|
|
18
18
|
import { Slider } from './ui/slider';
|
|
19
19
|
import { toast } from 'sonner';
|
|
20
20
|
import { ScrollArea } from './ui/scroll-area';
|
|
@@ -22,6 +22,7 @@ import { ThemeToggle } from './ThemeToggle';
|
|
|
22
22
|
import { LanguageSelector } from './LanguageSelector';
|
|
23
23
|
import { MapShowcase } from './examples/MapShowcase';
|
|
24
24
|
import { Header } from './Header';
|
|
25
|
+
import { Sidebar } from './Sidebar';
|
|
25
26
|
import {
|
|
26
27
|
Dialog,
|
|
27
28
|
DialogTrigger,
|
|
@@ -713,6 +714,134 @@ export function TemplateContent({ }: TemplateContentProps) {
|
|
|
713
714
|
|
|
714
715
|
<Separator className="my-8" />
|
|
715
716
|
|
|
717
|
+
{/* Sidebar Variations */}
|
|
718
|
+
<section>
|
|
719
|
+
<h3 className="mb-4">Sidebar Variations</h3>
|
|
720
|
+
<Card>
|
|
721
|
+
<CardHeader>
|
|
722
|
+
<CardTitle>Sidebar Assistant Mode vs Default</CardTitle>
|
|
723
|
+
<CardDescription>
|
|
724
|
+
A Sidebar suporta flexibilidade com a propriedade variant.
|
|
725
|
+
</CardDescription>
|
|
726
|
+
</CardHeader>
|
|
727
|
+
<CardContent>
|
|
728
|
+
<Tabs defaultValue="assistant" className="w-full">
|
|
729
|
+
<TabsList className="mb-4">
|
|
730
|
+
<TabsTrigger value="assistant">Modo Assistant</TabsTrigger>
|
|
731
|
+
<TabsTrigger value="default">Modo Padrão</TabsTrigger>
|
|
732
|
+
</TabsList>
|
|
733
|
+
|
|
734
|
+
<TabsContent value="assistant">
|
|
735
|
+
<div className="relative h-[600px] border rounded-[var(--radius-lg)] bg-muted/20 overflow-hidden" style={{ transform: "translateZ(0)" }}>
|
|
736
|
+
<Sidebar
|
|
737
|
+
expanded={true}
|
|
738
|
+
onToggle={() => { }}
|
|
739
|
+
user={{ email: "admin@xertica.com" }}
|
|
740
|
+
onLogout={() => toast("Saiu")}
|
|
741
|
+
location={{ pathname: "/assistant/current" }}
|
|
742
|
+
navigate={() => { }}
|
|
743
|
+
variant="assistant"
|
|
744
|
+
search={{
|
|
745
|
+
show: true,
|
|
746
|
+
placeholder: "Pesquisar tópicos..."
|
|
747
|
+
}}
|
|
748
|
+
fixedArea={{
|
|
749
|
+
show: true,
|
|
750
|
+
content: (
|
|
751
|
+
<Button className="w-full bg-sidebar-primary hover:bg-sidebar-primary/90 text-sidebar-primary-foreground shadow-lg font-bold border-none transition-all duration-300 transform hover:scale-[1.02] active:scale-[0.98]">
|
|
752
|
+
<Plus className="w-4 h-4 mr-2" />
|
|
753
|
+
Nova Conversa
|
|
754
|
+
</Button>
|
|
755
|
+
)
|
|
756
|
+
}}
|
|
757
|
+
navigationGroups={[
|
|
758
|
+
{
|
|
759
|
+
id: "recent",
|
|
760
|
+
label: "Recentes",
|
|
761
|
+
icon: Clock,
|
|
762
|
+
items: [
|
|
763
|
+
{
|
|
764
|
+
path: "/assistant/refatoracao",
|
|
765
|
+
label: "Refatoração Sidebar",
|
|
766
|
+
icon: History,
|
|
767
|
+
description: "Ativa agora",
|
|
768
|
+
actions: [
|
|
769
|
+
{ label: "Renomear", icon: FileEdit, onClick: () => toast("Abrir renomear...") },
|
|
770
|
+
{
|
|
771
|
+
label: "Mover",
|
|
772
|
+
icon: ArrowRightLeft,
|
|
773
|
+
children: [
|
|
774
|
+
{ label: "Projetos Ativos", onClick: () => toast("Movido para Projetos Ativos") },
|
|
775
|
+
{ label: "Monitoramento", onClick: () => toast("Movido para Monitoramento") },
|
|
776
|
+
{ label: "Arquivo", onClick: () => toast("Movido para Arquivo") }
|
|
777
|
+
]
|
|
778
|
+
},
|
|
779
|
+
{ label: "Limpar", icon: Trash2, onClick: () => toast("Histórico Limpo!"), variant: "destructive" }
|
|
780
|
+
]
|
|
781
|
+
},
|
|
782
|
+
]
|
|
783
|
+
},
|
|
784
|
+
{
|
|
785
|
+
id: "projects",
|
|
786
|
+
label: "Monitoramento de Obras",
|
|
787
|
+
icon: Map,
|
|
788
|
+
actions: [
|
|
789
|
+
{ label: "Nova Categoria", icon: Plus, onClick: () => toast("Criar nova categoria...") },
|
|
790
|
+
{ label: "Arquivar Grupo", icon: Archive, onClick: () => toast("Arquivando grupo...") }
|
|
791
|
+
],
|
|
792
|
+
items: [
|
|
793
|
+
{
|
|
794
|
+
path: "/assistant/br163",
|
|
795
|
+
label: "Restauração BR-163",
|
|
796
|
+
icon: () => <div className="w-2 h-2 rounded-full bg-yellow-500" />,
|
|
797
|
+
description: (
|
|
798
|
+
<div className="space-y-1.5 min-w-[160px]">
|
|
799
|
+
<Progress value={67} className="h-1.5 bg-sidebar-foreground/10" />
|
|
800
|
+
<div className="flex justify-between items-center text-[10px] text-sidebar-foreground/60">
|
|
801
|
+
<span>Cuiabá, MT</span>
|
|
802
|
+
<span>67%</span>
|
|
803
|
+
</div>
|
|
804
|
+
</div>
|
|
805
|
+
)
|
|
806
|
+
}
|
|
807
|
+
]
|
|
808
|
+
}
|
|
809
|
+
]}
|
|
810
|
+
/>
|
|
811
|
+
<div className="absolute inset-y-0 right-0 left-64 p-8 flex items-center justify-center">
|
|
812
|
+
<p className="text-muted-foreground text-center">Conteúdo do Assistant Mode</p>
|
|
813
|
+
</div>
|
|
814
|
+
</div>
|
|
815
|
+
</TabsContent>
|
|
816
|
+
|
|
817
|
+
<TabsContent value="default">
|
|
818
|
+
<div className="relative h-[600px] border rounded-[var(--radius-lg)] bg-muted/20 overflow-hidden" style={{ transform: "translateZ(0)" }}>
|
|
819
|
+
<Sidebar
|
|
820
|
+
expanded={true}
|
|
821
|
+
onToggle={() => { }}
|
|
822
|
+
user={{ email: "admin@xertica.com" }}
|
|
823
|
+
onLogout={() => toast("Saiu")}
|
|
824
|
+
location={{ pathname: "/home" }}
|
|
825
|
+
navigate={() => { }}
|
|
826
|
+
variant="default"
|
|
827
|
+
routes={[
|
|
828
|
+
{ path: "/home", label: "Início", icon: Home },
|
|
829
|
+
{ path: "/dashboard", label: "Dashboard", icon: Users },
|
|
830
|
+
{ path: "/settings", label: "Configurações", icon: Settings },
|
|
831
|
+
]}
|
|
832
|
+
/>
|
|
833
|
+
<div className="absolute inset-y-0 right-0 left-64 p-8 flex items-center justify-center">
|
|
834
|
+
<p className="text-muted-foreground text-center">Navegação Tradicional do Sistema</p>
|
|
835
|
+
</div>
|
|
836
|
+
</div>
|
|
837
|
+
</TabsContent>
|
|
838
|
+
</Tabs>
|
|
839
|
+
</CardContent>
|
|
840
|
+
</Card>
|
|
841
|
+
</section>
|
|
842
|
+
|
|
843
|
+
<Separator className="my-8" />
|
|
844
|
+
|
|
716
845
|
{/* Footer Note */}
|
|
717
846
|
<Card className="mt-8">
|
|
718
847
|
<CardHeader>
|
|
@@ -6,6 +6,16 @@ import { ChevronDownIcon } from "lucide-react";
|
|
|
6
6
|
|
|
7
7
|
import { cn } from "./utils";
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Gerenciador de exibição oculta (collapse) vertical focada em empilhamento.
|
|
11
|
+
*
|
|
12
|
+
* @description
|
|
13
|
+
* Baseado em Radix UI Accordion para garantir acessibilidade via teclado.
|
|
14
|
+
*
|
|
15
|
+
* @ai-rules
|
|
16
|
+
* 1. `AccordionItem` REQUER OBRIGATORIAMENTE a prop `value`. Sem ela a lógica local quebra.
|
|
17
|
+
* 2. Em type="single", o accordion só abre um e fecha os outros. Requer prop `collapsible` para que possa fechar a si mesmo.
|
|
18
|
+
*/
|
|
9
19
|
function Accordion({
|
|
10
20
|
...props
|
|
11
21
|
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|