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.
Files changed (129) hide show
  1. package/components/Sidebar.tsx +323 -57
  2. package/components/TemplateContent.tsx +130 -1
  3. package/components/ui/accordion.tsx +10 -0
  4. package/components/ui/alert-dialog.tsx +10 -0
  5. package/components/ui/alert.tsx +11 -0
  6. package/components/ui/aspect-ratio.tsx +10 -0
  7. package/components/ui/avatar.tsx +10 -0
  8. package/components/ui/badge.tsx +10 -0
  9. package/components/ui/breadcrumb.tsx +10 -0
  10. package/components/ui/button.tsx +34 -8
  11. package/components/ui/calendar.tsx +10 -0
  12. package/components/ui/card.tsx +11 -0
  13. package/components/ui/carousel.tsx +10 -0
  14. package/components/ui/chart.tsx +10 -0
  15. package/components/ui/checkbox.tsx +19 -0
  16. package/components/ui/collapsible.tsx +9 -0
  17. package/components/ui/command.tsx +10 -0
  18. package/components/ui/context-menu.tsx +10 -0
  19. package/components/ui/dialog.tsx +12 -0
  20. package/components/ui/drawer.tsx +10 -0
  21. package/components/ui/dropdown-menu.tsx +12 -0
  22. package/components/ui/empty.tsx +10 -0
  23. package/components/ui/file-upload.tsx +11 -0
  24. package/components/ui/form.tsx +12 -0
  25. package/components/ui/hover-card.tsx +9 -0
  26. package/components/ui/input-otp.tsx +10 -0
  27. package/components/ui/input.tsx +24 -1
  28. package/components/ui/label.tsx +10 -0
  29. package/components/ui/map.tsx +12 -0
  30. package/components/ui/menubar.tsx +10 -0
  31. package/components/ui/navigation-menu.tsx +10 -0
  32. package/components/ui/notification-badge.tsx +10 -0
  33. package/components/ui/page-header.tsx +10 -0
  34. package/components/ui/pagination.tsx +10 -0
  35. package/components/ui/popover.tsx +10 -0
  36. package/components/ui/progress.tsx +10 -0
  37. package/components/ui/radio-group.tsx +10 -0
  38. package/components/ui/rating.tsx +10 -0
  39. package/components/ui/resizable.tsx +10 -0
  40. package/components/ui/route-map.tsx +11 -0
  41. package/components/ui/scroll-area.tsx +10 -0
  42. package/components/ui/search.tsx +10 -0
  43. package/components/ui/select.tsx +9 -0
  44. package/components/ui/separator.tsx +9 -0
  45. package/components/ui/sheet.tsx +9 -0
  46. package/components/ui/skeleton.tsx +10 -0
  47. package/components/ui/slider.tsx +10 -0
  48. package/components/ui/sonner.tsx +7 -16
  49. package/components/ui/stats-card.tsx +10 -0
  50. package/components/ui/stepper.tsx +10 -0
  51. package/components/ui/switch.tsx +9 -0
  52. package/components/ui/table.tsx +11 -0
  53. package/components/ui/tabs.tsx +12 -0
  54. package/components/ui/textarea.tsx +10 -0
  55. package/components/ui/timeline.tsx +10 -0
  56. package/components/ui/toggle-group.tsx +10 -0
  57. package/components/ui/toggle.tsx +9 -0
  58. package/components/ui/tooltip.tsx +10 -0
  59. package/components/ui/tree-view.tsx +10 -0
  60. package/components/ui/xertica-assistant.tsx +12 -0
  61. package/dist/components/Sidebar.d.ts +51 -2
  62. package/dist/components/ui/accordion.d.ts +10 -0
  63. package/dist/components/ui/alert-dialog.d.ts +10 -0
  64. package/dist/components/ui/alert.d.ts +11 -0
  65. package/dist/components/ui/aspect-ratio.d.ts +10 -0
  66. package/dist/components/ui/avatar.d.ts +10 -0
  67. package/dist/components/ui/badge.d.ts +10 -0
  68. package/dist/components/ui/breadcrumb.d.ts +10 -0
  69. package/dist/components/ui/button.d.ts +29 -5
  70. package/dist/components/ui/calendar.d.ts +10 -0
  71. package/dist/components/ui/card.d.ts +11 -0
  72. package/dist/components/ui/carousel.d.ts +10 -0
  73. package/dist/components/ui/chart.d.ts +10 -0
  74. package/dist/components/ui/checkbox.d.ts +19 -0
  75. package/dist/components/ui/collapsible.d.ts +9 -0
  76. package/dist/components/ui/command.d.ts +10 -0
  77. package/dist/components/ui/context-menu.d.ts +10 -0
  78. package/dist/components/ui/dialog.d.ts +12 -0
  79. package/dist/components/ui/drawer.d.ts +10 -0
  80. package/dist/components/ui/dropdown-menu.d.ts +12 -0
  81. package/dist/components/ui/empty.d.ts +10 -0
  82. package/dist/components/ui/file-upload.d.ts +11 -0
  83. package/dist/components/ui/form.d.ts +12 -0
  84. package/dist/components/ui/hover-card.d.ts +9 -0
  85. package/dist/components/ui/input-otp.d.ts +10 -0
  86. package/dist/components/ui/input.d.ts +24 -1
  87. package/dist/components/ui/label.d.ts +10 -0
  88. package/dist/components/ui/map.d.ts +12 -0
  89. package/dist/components/ui/menubar.d.ts +10 -0
  90. package/dist/components/ui/navigation-menu.d.ts +10 -0
  91. package/dist/components/ui/notification-badge.d.ts +10 -0
  92. package/dist/components/ui/page-header.d.ts +10 -0
  93. package/dist/components/ui/pagination.d.ts +10 -0
  94. package/dist/components/ui/popover.d.ts +10 -0
  95. package/dist/components/ui/progress.d.ts +10 -0
  96. package/dist/components/ui/radio-group.d.ts +10 -0
  97. package/dist/components/ui/rating.d.ts +10 -0
  98. package/dist/components/ui/resizable.d.ts +10 -0
  99. package/dist/components/ui/route-map.d.ts +11 -0
  100. package/dist/components/ui/scroll-area.d.ts +10 -0
  101. package/dist/components/ui/search.d.ts +10 -0
  102. package/dist/components/ui/select.d.ts +8 -0
  103. package/dist/components/ui/separator.d.ts +9 -0
  104. package/dist/components/ui/sheet.d.ts +9 -0
  105. package/dist/components/ui/skeleton.d.ts +10 -0
  106. package/dist/components/ui/slider.d.ts +10 -0
  107. package/dist/components/ui/sonner.d.ts +7 -16
  108. package/dist/components/ui/stats-card.d.ts +10 -0
  109. package/dist/components/ui/stepper.d.ts +10 -0
  110. package/dist/components/ui/switch.d.ts +9 -0
  111. package/dist/components/ui/table.d.ts +11 -0
  112. package/dist/components/ui/tabs.d.ts +12 -0
  113. package/dist/components/ui/textarea.d.ts +10 -0
  114. package/dist/components/ui/timeline.d.ts +10 -0
  115. package/dist/components/ui/toggle-group.d.ts +10 -0
  116. package/dist/components/ui/toggle.d.ts +9 -0
  117. package/dist/components/ui/tooltip.d.ts +10 -0
  118. package/dist/components/ui/tree-view.d.ts +10 -0
  119. package/dist/components/ui/xertica-assistant.d.ts +12 -0
  120. package/dist/index.es.js +280 -38
  121. package/dist/index.umd.js +279 -37
  122. package/dist/xertica-ui.css +1 -1
  123. package/package.json +3 -1
  124. package/templates/package.json +1 -1
  125. package/templates/src/app/pages/CrudTemplate.tsx +106 -0
  126. package/templates/src/app/pages/DashboardTemplate.tsx +101 -0
  127. package/templates/src/app/pages/FormTemplate.tsx +109 -0
  128. package/templates/src/app/pages/LoginTemplate.tsx +52 -0
  129. package/templates/src/app/pages/Template/TemplateContent.tsx +139 -0
@@ -4,14 +4,28 @@ import {
4
4
  ArrowLeft,
5
5
  LogOut,
6
6
  Settings,
7
- MoreHorizontal,
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: RouteConfig[];
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
- <MoreHorizontal className="w-4 h-4 flex-shrink-0 text-sidebar-foreground/60" />
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
- {/* Navegação */}
344
- <nav className="flex-1 px-4 py-4 overflow-hidden" ref={navRef}>
345
- <div className="space-y-1">
346
- {/* Itens visíveis */}
347
- {(hasOverflow ? visibleItems : navigationItems).map((item) =>
348
- renderMenuItem(item)
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
- {/* Botão de overflow (ellipsis) */}
352
- {hasOverflow && (
353
- <Popover>
354
- <PopoverTrigger asChild>
355
- <button
356
- className={
357
- expanded
358
- ? "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"
359
- : "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"
360
- }
361
- >
362
- <MoreHorizontal className="w-5 h-5 flex-shrink-0" />
363
- {expanded && (
364
- <span className="truncate text-sidebar-foreground">
365
- Mais opções
366
- </span>
367
- )}
368
- </button>
369
- </PopoverTrigger>
370
- <PopoverContent
371
- side="right"
372
- align="start"
373
- className="w-56 p-2 bg-popover border border-border rounded-[var(--radius-card)] shadow-lg"
374
- sideOffset={8}
375
- >
376
- <div className="space-y-1">
377
- {overflowItems.map((item) => {
378
- const Icon = item.icon;
379
- return (
380
- <button
381
- key={item.path}
382
- onClick={() => handleNavigate(item.path)}
383
- 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"
384
- >
385
- <Icon className="w-4 h-4 flex-shrink-0" />
386
- <span className="truncate">{item.label}</span>
387
- </button>
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
- </nav>
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>) {