xertica-ui 2.1.2 → 2.1.4

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 (181) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +1 -1
  3. package/bin/cli.ts +1 -1
  4. package/bin/generate-tokens.ts +13 -7
  5. package/components/assistant/xertica-assistant/index.ts +2 -0
  6. package/components/assistant/xertica-assistant/parts/AssistantCollapsedView.tsx +97 -0
  7. package/components/assistant/xertica-assistant/parts/AssistantConversationList.tsx +104 -0
  8. package/components/assistant/xertica-assistant/parts/AssistantDocumentEditor.tsx +81 -0
  9. package/components/assistant/xertica-assistant/parts/AssistantFeedbackDialog.tsx +86 -0
  10. package/components/assistant/xertica-assistant/parts/AssistantHeader.tsx +77 -0
  11. package/components/assistant/xertica-assistant/parts/AssistantMessageBubble.tsx +573 -0
  12. package/components/assistant/xertica-assistant/parts/AssistantTabBar.tsx +65 -0
  13. package/components/assistant/xertica-assistant/parts/AssistantTypingIndicator.tsx +41 -0
  14. package/components/assistant/xertica-assistant/parts/AssistantWelcomeScreen.tsx +98 -0
  15. package/components/assistant/xertica-assistant/parts/index.ts +16 -0
  16. package/components/assistant/xertica-assistant/types.ts +139 -0
  17. package/components/assistant/xertica-assistant/use-assistant.ts +559 -0
  18. package/components/assistant/xertica-assistant/xertica-assistant.stories.tsx +200 -0
  19. package/components/assistant/xertica-assistant/xertica-assistant.tsx +198 -1460
  20. package/components/brand/theme-toggle/ThemeToggle.tsx +8 -27
  21. package/components/hooks/index.ts +3 -0
  22. package/components/hooks/use-layout-shortcuts.ts +46 -0
  23. package/components/layout/sidebar/index.ts +2 -0
  24. package/components/layout/sidebar/sidebar.stories.tsx +160 -8
  25. package/components/layout/sidebar/sidebar.tsx +606 -497
  26. package/components/layout/sidebar/use-sidebar.ts +104 -0
  27. package/components/media/audio-player/AudioPlayer.tsx +131 -206
  28. package/components/media/audio-player/use-audio-player.ts +298 -0
  29. package/components/pages/home-page/HomePage.tsx +1 -1
  30. package/components/pages/template-content/TemplateContent.tsx +5 -5
  31. package/components/pages/template-page/TemplatePage.tsx +5 -5
  32. package/components/shared/CustomTooltipContent.tsx +52 -0
  33. package/components/shared/layout-constants.ts +1 -1
  34. package/components/ui/chart/chart.stories.tsx +966 -7
  35. package/components/ui/chart/chart.tsx +918 -45
  36. package/components/ui/file-upload/file-upload.stories.tsx +100 -0
  37. package/components/ui/file-upload/file-upload.tsx +14 -74
  38. package/components/ui/file-upload/index.ts +1 -0
  39. package/components/ui/file-upload/use-file-upload.ts +181 -0
  40. package/components/ui/pagination/index.ts +2 -0
  41. package/components/ui/pagination/pagination.stories.tsx +94 -0
  42. package/components/ui/pagination/use-pagination.ts +194 -0
  43. package/components/ui/rich-text-editor/index.ts +2 -0
  44. package/components/ui/rich-text-editor/rich-text-editor.stories.tsx +129 -1
  45. package/components/ui/rich-text-editor/rich-text-editor.tsx +86 -305
  46. package/components/ui/rich-text-editor/use-rich-text-editor.ts +439 -0
  47. package/components/ui/stepper/index.ts +3 -1
  48. package/components/ui/stepper/stepper.stories.tsx +116 -0
  49. package/components/ui/stepper/stepper.tsx +4 -4
  50. package/components/ui/stepper/use-stepper.ts +137 -0
  51. package/components/ui/tree-view/index.ts +4 -1
  52. package/components/ui/tree-view/tree-view.stories.tsx +110 -4
  53. package/components/ui/tree-view/tree-view.tsx +17 -125
  54. package/components/ui/tree-view/use-tree-view.ts +229 -0
  55. package/contexts/AssistenteContext.tsx +17 -54
  56. package/contexts/BrandColorsContext.tsx +6 -17
  57. package/contexts/LayoutContext.tsx +5 -31
  58. package/dist/AssistantChart-BAudAfne.cjs +3591 -0
  59. package/dist/AssistantChart-BP8upjMk.js +3565 -0
  60. package/dist/AudioPlayer-1ypwE2Wh.cjs +936 -0
  61. package/dist/AudioPlayer-DuKXrCfy.js +937 -0
  62. package/dist/CustomTooltipContent-DHjkY0ww.js +40 -0
  63. package/dist/CustomTooltipContent-c_K-DWRr.cjs +56 -0
  64. package/dist/LanguageContext-BwhwC3G2.js +657 -0
  65. package/dist/LanguageContext-DvUt5jBg.cjs +656 -0
  66. package/dist/LayoutContext-BDmcZfMH.cjs +84 -0
  67. package/dist/LayoutContext-dbQvdC4O.js +85 -0
  68. package/dist/ThemeContext-RTy1m2Uq.js +82 -0
  69. package/dist/ThemeContext-bSzuOit2.cjs +81 -0
  70. package/dist/VerifyEmailPage-C_ihbcth.js +2828 -0
  71. package/dist/VerifyEmailPage-Dt7zgA4w.cjs +2827 -0
  72. package/dist/XerticaProvider-CW9hpCdF.cjs +39 -0
  73. package/dist/XerticaProvider-siSt9uG2.js +40 -0
  74. package/dist/XerticaXLogo-D8jf0SNv.cjs +214 -0
  75. package/dist/XerticaXLogo-fAJMy3H4.js +215 -0
  76. package/dist/assistant.cjs.js +2 -1
  77. package/dist/assistant.es.js +3 -2
  78. package/dist/brand.cjs.js +2 -2
  79. package/dist/brand.es.js +2 -2
  80. package/dist/cli.js +14 -8
  81. package/dist/components/assistant/xertica-assistant/index.d.ts +2 -0
  82. package/dist/components/assistant/xertica-assistant/parts/AssistantCollapsedView.d.ts +13 -0
  83. package/dist/components/assistant/xertica-assistant/parts/AssistantConversationList.d.ts +16 -0
  84. package/dist/components/assistant/xertica-assistant/parts/AssistantDocumentEditor.d.ts +17 -0
  85. package/dist/components/assistant/xertica-assistant/parts/AssistantFeedbackDialog.d.ts +19 -0
  86. package/dist/components/assistant/xertica-assistant/parts/AssistantHeader.d.ts +11 -0
  87. package/dist/components/assistant/xertica-assistant/parts/AssistantMessageBubble.d.ts +29 -0
  88. package/dist/components/assistant/xertica-assistant/parts/AssistantTabBar.d.ts +13 -0
  89. package/dist/components/assistant/xertica-assistant/parts/AssistantTypingIndicator.d.ts +4 -0
  90. package/dist/components/assistant/xertica-assistant/parts/AssistantWelcomeScreen.d.ts +17 -0
  91. package/dist/components/assistant/xertica-assistant/parts/index.d.ts +16 -0
  92. package/dist/components/assistant/xertica-assistant/types.d.ts +106 -0
  93. package/dist/components/assistant/xertica-assistant/use-assistant.d.ts +125 -0
  94. package/dist/components/assistant/xertica-assistant/xertica-assistant.d.ts +8 -97
  95. package/dist/components/hooks/index.d.ts +3 -0
  96. package/dist/components/hooks/use-layout-shortcuts.d.ts +22 -0
  97. package/dist/components/layout/sidebar/index.d.ts +2 -0
  98. package/dist/components/layout/sidebar/sidebar.d.ts +80 -0
  99. package/dist/components/layout/sidebar/use-sidebar.d.ts +22 -0
  100. package/dist/components/media/audio-player/AudioPlayer.d.ts +4 -1
  101. package/dist/components/media/audio-player/use-audio-player.d.ts +72 -0
  102. package/dist/components/shared/CustomTooltipContent.d.ts +20 -0
  103. package/dist/components/shared/layout-constants.d.ts +1 -1
  104. package/dist/components/ui/alert/alert.d.ts +1 -1
  105. package/dist/components/ui/badge/badge.d.ts +1 -1
  106. package/dist/components/ui/button/button.d.ts +2 -2
  107. package/dist/components/ui/chart/chart.d.ts +162 -5
  108. package/dist/components/ui/file-upload/file-upload.d.ts +2 -0
  109. package/dist/components/ui/file-upload/index.d.ts +1 -0
  110. package/dist/components/ui/file-upload/use-file-upload.d.ts +49 -0
  111. package/dist/components/ui/pagination/index.d.ts +2 -0
  112. package/dist/components/ui/pagination/use-pagination.d.ts +78 -0
  113. package/dist/components/ui/rich-text-editor/index.d.ts +2 -0
  114. package/dist/components/ui/rich-text-editor/use-rich-text-editor.d.ts +107 -0
  115. package/dist/components/ui/stepper/index.d.ts +3 -1
  116. package/dist/components/ui/stepper/stepper.d.ts +2 -2
  117. package/dist/components/ui/stepper/use-stepper.d.ts +60 -0
  118. package/dist/components/ui/tree-view/index.d.ts +4 -1
  119. package/dist/components/ui/tree-view/tree-view.d.ts +4 -6
  120. package/dist/components/ui/tree-view/use-tree-view.d.ts +60 -0
  121. package/dist/contexts/AssistenteContext.d.ts +10 -49
  122. package/dist/hooks.cjs.js +30 -10
  123. package/dist/hooks.es.js +25 -4
  124. package/dist/index.cjs.js +20 -9
  125. package/dist/index.es.js +38 -27
  126. package/dist/layout.cjs.js +82 -1
  127. package/dist/layout.es.js +83 -2
  128. package/dist/media.cjs.js +1 -1
  129. package/dist/media.es.js +1 -1
  130. package/dist/pages.cjs.js +1 -1
  131. package/dist/pages.es.js +1 -1
  132. package/dist/rich-text-editor-BmsjY03B.js +2949 -0
  133. package/dist/rich-text-editor-GS2kpTAK.cjs +2966 -0
  134. package/dist/sidebar-CVUGHOS_.cjs +756 -0
  135. package/dist/sidebar-CmvwjnVb.js +757 -0
  136. package/dist/ui.cjs.js +12 -2
  137. package/dist/ui.es.js +24 -14
  138. package/dist/use-audio-player-Bkh23vQ3.js +177 -0
  139. package/dist/use-audio-player-Dn1NR9xN.cjs +176 -0
  140. package/dist/utils/color-utils.d.ts +51 -0
  141. package/dist/xertica-assistant-BMqdyRVi.js +2082 -0
  142. package/dist/xertica-assistant-Bj3vBCq_.cjs +2081 -0
  143. package/dist/xertica-ui.css +1 -1
  144. package/docs/ai-usage.md +28 -10
  145. package/docs/architecture-improvements.md +463 -0
  146. package/docs/architecture.md +77 -1
  147. package/docs/components/assistant-chart.md +1 -1
  148. package/docs/components/assistant.md +159 -0
  149. package/docs/components/audio-player.md +46 -0
  150. package/docs/components/branding.md +251 -0
  151. package/docs/components/chart.md +354 -39
  152. package/docs/components/code-block.md +108 -0
  153. package/docs/components/file-upload.md +119 -2
  154. package/docs/components/formatted-document.md +113 -0
  155. package/docs/components/hooks.md +430 -0
  156. package/docs/components/image-with-fallback.md +106 -0
  157. package/docs/components/map-layers.md +140 -0
  158. package/docs/components/modern-chat-input.md +163 -0
  159. package/docs/components/pages.md +351 -0
  160. package/docs/components/pagination.md +187 -0
  161. package/docs/components/rich-text-editor.md +164 -0
  162. package/docs/components/sidebar.md +153 -4
  163. package/docs/components/stepper.md +157 -12
  164. package/docs/components/tree-view.md +164 -6
  165. package/docs/doc-audit.md +223 -0
  166. package/docs/getting-started.md +155 -1
  167. package/docs/guidelines.md +14 -8
  168. package/docs/layout.md +2 -2
  169. package/docs/llms.md +29 -9
  170. package/docs/patterns/detail-page.md +276 -0
  171. package/docs/patterns/settings.md +346 -0
  172. package/docs/patterns/wizard.md +217 -0
  173. package/guidelines/Guidelines.md +5 -3
  174. package/llms.txt +1 -1
  175. package/package.json +10 -10
  176. package/styles/xertica/tokens.css +41 -12
  177. package/templates/CLAUDE.md +16 -6
  178. package/templates/guidelines/Guidelines.md +16 -4
  179. package/templates/package.json +3 -3
  180. package/templates/src/styles/xertica/tokens.css +39 -10
  181. package/utils/color-utils.ts +72 -0
@@ -1,4 +1,4 @@
1
- import React, { useState, useRef, useEffect } from "react";
1
+ import React, { useState, useRef, useEffect, createContext, useContext } from "react";
2
2
  import { motion, AnimatePresence } from "framer-motion";
3
3
  import {
4
4
  Menu,
@@ -12,7 +12,6 @@ import {
12
12
  import { Avatar, AvatarFallback, AvatarImage } from '../../ui/avatar';
13
13
  import { Popover, PopoverContent, PopoverTrigger } from '../../ui/popover';
14
14
  import { Tooltip, TooltipProvider, TooltipTrigger } from '../../ui/tooltip';
15
- import * as TooltipPrimitive from "@radix-ui/react-tooltip";
16
15
  import {
17
16
  DropdownMenu,
18
17
  DropdownMenuContent,
@@ -31,37 +30,13 @@ import { XerticaLogo } from '../../brand/xertica-logo';
31
30
  import { XerticaXLogo } from '../../brand/xertica-xlogo';
32
31
  import { Button } from '../../ui/button';
33
32
  import { useOptionalLayout } from "../../../contexts/LayoutContext";
33
+ import { useSidebar } from "./use-sidebar";
34
+ import { CustomTooltipContent as SidebarTooltipContent } from "../../shared/CustomTooltipContent";
34
35
 
35
- /**
36
- * Tooltip content styled for the sidebar.
37
- */
38
- function SidebarTooltipContent({
39
- className,
40
- sideOffset = 0,
41
- children,
42
- ...props
43
- }: React.ComponentProps<typeof TooltipPrimitive.Content>) {
44
- return (
45
- <TooltipPrimitive.Content
46
- data-slot="tooltip-content"
47
- sideOffset={sideOffset}
48
- className={cn(
49
- "bg-popover text-popover-foreground shadow-lg animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
50
- className,
51
- )}
52
- {...props}
53
- >
54
- {children}
55
- <TooltipPrimitive.Arrow
56
- className="fill-popover z-50 drop-shadow-sm"
57
- width={8}
58
- height={4}
59
- />
60
- </TooltipPrimitive.Content>
61
- );
62
- }
36
+ // ─────────────────────────────────────────────────────────────────────────────
37
+ // Public interfaces
38
+ // ─────────────────────────────────────────────────────────────────────────────
63
39
 
64
- // Interface for Route Config (copied/imported type)
65
40
  /**
66
41
  * Contextual action menu item for routes and groups.
67
42
  */
@@ -98,7 +73,7 @@ export interface RouteConfig {
98
73
  children?: RouteConfig[];
99
74
  }
100
75
 
101
- interface NavigationItem {
76
+ export interface NavigationItem {
102
77
  path: string;
103
78
  label: string;
104
79
  icon?: any;
@@ -159,9 +134,9 @@ export interface SidebarProps {
159
134
  /** Callback to toggle expansion state (defaults to LayoutContext toggle if available) */
160
135
  onToggle?: () => void;
161
136
  /** Authenticated user info */
162
- user?: {
163
- name?: string;
164
- email?: string;
137
+ user?: {
138
+ name?: string;
139
+ email?: string;
165
140
  avatar?: string;
166
141
  } | null;
167
142
  /** Logout callback */
@@ -194,73 +169,173 @@ export interface SidebarProps {
194
169
  width?: number;
195
170
  }
196
171
 
172
+ // ─────────────────────────────────────────────────────────────────────────────
173
+ // Compound Component Context
174
+ // ─────────────────────────────────────────────────────────────────────────────
175
+
176
+ interface SidebarContextValue {
177
+ expanded: boolean;
178
+ isMobileViewport: boolean;
179
+ onToggle: () => void;
180
+ navigate: (path: string) => void;
181
+ location: { pathname: string };
182
+ width: number;
183
+ }
184
+
185
+ const SidebarContext = createContext<SidebarContextValue | null>(null);
186
+
187
+ function useSidebarContext() {
188
+ const ctx = useContext(SidebarContext);
189
+ if (!ctx) {
190
+ throw new Error("Sidebar compound components must be used within <Sidebar.Root>");
191
+ }
192
+ return ctx;
193
+ }
194
+
195
+ // ─────────────────────────────────────────────────────────────────────────────
196
+ // Compound Sub-components
197
+ // ─────────────────────────────────────────────────────────────────────────────
198
+
197
199
  /**
198
- * Primary navigation sidebar component.
199
- *
200
- * @description
201
- * Manages desktop/mobile responsive navigation rendering with two variants:
202
- * - `"default"` — simple flat or grouped route list.
203
- * - `"assistant"` — advanced variant with fixed areas, search, filters, and grouped navigation.
200
+ * Root container for the Sidebar. Provides context to all sub-components.
201
+ * Use this when building a fully custom sidebar layout.
204
202
  *
205
- * This component is autonomous: it works out-of-the-box using local state or
206
- * integrates automatically with `LayoutContext` if wrapped in `LayoutProvider`.
207
- *
208
- * @ai-rules
209
- * 1. NEVER recreate the sidebar with raw Tailwind classes — always use this component.
210
- * 2. Use `variant="assistant"` for AI/tool sidebars; use `variant="default"` for standard navigation.
211
- * 3. Supports `Ctrl+B` keyboard shortcut automatically via `LayoutProvider`.
203
+ * @example
204
+ * <Sidebar.Root expanded={expanded} onToggle={toggle} width={280}>
205
+ * <Sidebar.Header logo={<MyLogo />} />
206
+ * <Sidebar.Nav navigationGroups={groups} />
207
+ * <Sidebar.Footer user={user} onLogout={logout} />
208
+ * </Sidebar.Root>
212
209
  */
213
- export function Sidebar({
210
+ function SidebarRoot({
214
211
  expanded: expandedProp,
215
212
  onToggle: onToggleProp,
216
- user,
217
- onLogout = () => {},
218
- onSettingsClick,
219
- location: locationProp,
220
213
  navigate: navigateProp,
221
- routes,
222
- logo,
223
- logoCollapsed,
224
- variant = "default",
225
- fixedArea,
226
- search,
227
- navigationGroups = [],
228
- footer,
229
- showFooter,
214
+ location: locationProp,
230
215
  width: widthProp,
231
- }: SidebarProps) {
216
+ children,
217
+ className,
218
+ }: {
219
+ expanded?: boolean;
220
+ onToggle?: () => void;
221
+ navigate?: (path: string) => void;
222
+ location?: { pathname: string };
223
+ width?: number;
224
+ children: React.ReactNode;
225
+ className?: string;
226
+ }) {
232
227
  const layoutContext = useOptionalLayout();
233
-
234
228
  const [localExpanded, setLocalExpanded] = useState(false);
235
229
  const [isMobileViewport, setIsMobileViewport] = useState(false);
236
-
230
+
237
231
  const expanded = expandedProp !== undefined ? expandedProp : (layoutContext?.sidebarExpanded ?? localExpanded);
238
232
  const onToggle = onToggleProp || (layoutContext?.toggleSidebar || (() => setLocalExpanded(prev => !prev)));
239
- const width = widthProp !== undefined ? widthProp : (layoutContext?.sidebarWidth ?? 256);
240
-
241
- // Safe navigation fallback
233
+ const width = widthProp !== undefined ? widthProp : (layoutContext?.sidebarWidth ?? 280);
242
234
  const navigate = navigateProp || ((path: string) => {
243
235
  if (typeof window !== 'undefined') window.location.href = path;
244
236
  });
245
-
246
- // Safe location fallback
247
237
  const location = locationProp || (typeof window !== 'undefined' ? window.location : { pathname: '/' });
248
- const {
249
- showUser = true,
250
- showSettings = true,
251
- showLogout = true,
252
- } = footer || {};
253
238
 
254
- const displayFooter = showFooter !== undefined ? showFooter : variant === "default";
239
+ useEffect(() => {
240
+ const checkViewport = () => setIsMobileViewport(window.innerWidth < 768);
241
+ checkViewport();
242
+ window.addEventListener("resize", checkViewport);
243
+ return () => window.removeEventListener("resize", checkViewport);
244
+ }, []);
245
+
246
+ return (
247
+ <SidebarContext.Provider value={{ expanded, isMobileViewport, onToggle, navigate, location, width }}>
248
+ <TooltipProvider delayDuration={300}>
249
+ <style>{`
250
+ @media (max-width: 767px) {
251
+ [style*="padding-left"].flex-1,
252
+ [style*="paddingLeft"].flex-1 {
253
+ padding-left: 0 !important;
254
+ }
255
+ }
256
+ `}</style>
257
+ <div
258
+ className={cn(
259
+ "bg-sidebar text-sidebar-foreground transition-all duration-300 ease-in-out flex flex-col z-50",
260
+ expanded
261
+ ? "fixed inset-0 md:fixed md:inset-y-0 md:left-0"
262
+ : "fixed inset-y-0 left-0 w-20 -translate-x-full md:translate-x-0",
263
+ className
264
+ )}
265
+ style={expanded && !isMobileViewport ? { width: `${width}px` } : undefined}
266
+ >
267
+ {children}
268
+ </div>
269
+ </TooltipProvider>
270
+ </SidebarContext.Provider>
271
+ );
272
+ }
273
+
274
+ /**
275
+ * Toggle button + logo header area for the Sidebar.
276
+ */
277
+ function SidebarHeader({
278
+ logo,
279
+ logoCollapsed,
280
+ }: {
281
+ logo?: React.ReactNode;
282
+ logoCollapsed?: React.ReactNode;
283
+ }) {
284
+ const { expanded, onToggle } = useSidebarContext();
285
+
286
+ return (
287
+ <>
288
+ {/* Menu Toggle Button */}
289
+ <div className="flex-shrink-0 p-[14px] pt-[13px] pr-[14px] pb-[12px] pl-[14px]">
290
+ <button
291
+ onClick={onToggle}
292
+ 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"
293
+ aria-label={expanded ? "Recolher menu" : "Expandir menu"}
294
+ >
295
+ {expanded ? (
296
+ <ArrowLeft className="w-5 h-5" />
297
+ ) : (
298
+ <Menu className="w-5 h-5" />
299
+ )}
300
+ </button>
301
+ </div>
302
+
303
+ {/* Logo */}
304
+ <div className="flex-shrink-0 px-4 py-4">
305
+ <div className="flex items-center h-10 justify-center">
306
+ <div className="flex items-center justify-center flex-shrink-0">
307
+ {expanded ? (
308
+ logo || <XerticaLogo className="h-5 w-auto" variant="white" />
309
+ ) : (
310
+ logoCollapsed || <XerticaXLogo className="h-5 w-auto" variant="white" />
311
+ )}
312
+ </div>
313
+ </div>
314
+ </div>
315
+ </>
316
+ );
317
+ }
318
+
319
+ /**
320
+ * Navigation area for the Sidebar (default variant).
321
+ * Renders grouped or flat navigation items with overflow handling.
322
+ */
323
+ function SidebarNav({
324
+ navigationGroups = [],
325
+ routes = [],
326
+ variant = "default",
327
+ }: {
328
+ navigationGroups?: RouteGroup[];
329
+ routes?: RouteConfig[];
330
+ variant?: "default" | "assistant";
331
+ }) {
332
+ const { expanded, navigate, location, onToggle } = useSidebarContext();
255
333
  const navRef = useRef<HTMLDivElement>(null);
256
334
  const [localActiveItem, setLocalActiveItem] = useState<string | null>(null);
257
335
  const [hasOverflow, setHasOverflow] = useState(false);
258
336
  const [visibleItems, setVisibleItems] = useState<NavigationItem[]>([]);
259
337
  const [overflowItems, setOverflowItems] = useState<NavigationItem[]>([]);
260
- const [openSubmenu, setOpenSubmenu] = useState<string | null>(null);
261
- const [isFilterOpen, setIsFilterOpen] = useState(false);
262
338
 
263
- // Label translations map
264
339
  const labelTranslations: Record<string, string> = {
265
340
  home: "Início",
266
341
  dashboard: "Painel",
@@ -274,21 +349,14 @@ export function Sidebar({
274
349
  children: route.children,
275
350
  }));
276
351
 
277
- const isSettingsActive = location.pathname === "/settings";
278
-
279
- // Detect vertical overflow to handle extra items
280
352
  useEffect(() => {
281
353
  if (typeof window === "undefined") return;
282
354
  const checkOverflow = () => {
283
355
  if (!navRef.current) return;
284
356
  if (variant === "assistant") return;
285
-
286
- setIsMobileViewport(window.innerWidth < 768);
287
-
288
357
  const navHeight = navRef.current.clientHeight;
289
- const itemHeight = 44; // h-10 + gap
358
+ const itemHeight = 44;
290
359
  const maxVisibleItems = Math.floor(navHeight / itemHeight);
291
-
292
360
  if (navigationItems.length > maxVisibleItems) {
293
361
  setHasOverflow(true);
294
362
  setVisibleItems(navigationItems.slice(0, maxVisibleItems - 1));
@@ -299,17 +367,14 @@ export function Sidebar({
299
367
  setOverflowItems([]);
300
368
  }
301
369
  };
302
-
303
370
  checkOverflow();
304
371
  window.addEventListener("resize", checkOverflow);
305
372
  return () => window.removeEventListener("resize", checkOverflow);
306
- }, [navigationItems.length]);
373
+ }, [navigationItems.length, variant]);
307
374
 
308
375
  const handleNavigate = (path: string) => {
309
376
  setLocalActiveItem(path);
310
377
  navigate(path);
311
- setOpenSubmenu(null);
312
- // Close menu on mobile after navigation
313
378
  if (typeof window !== "undefined" && window.innerWidth < 768) {
314
379
  onToggle();
315
380
  }
@@ -324,10 +389,70 @@ export function Sidebar({
324
389
  actions: route.actions,
325
390
  });
326
391
 
392
+ const renderActionItems = (actions: ActionMenuItem[]): React.ReactNode => {
393
+ return actions.map((action, idx) => {
394
+ const Icon = action.icon;
395
+ if (action.children && action.children.length > 0) {
396
+ return (
397
+ <DropdownMenuSub key={idx}>
398
+ <DropdownMenuSubTrigger>
399
+ {Icon && <Icon className="mr-2 h-4 w-4" />}
400
+ <span>{action.label}</span>
401
+ </DropdownMenuSubTrigger>
402
+ <DropdownMenuPortal>
403
+ <DropdownMenuSubContent className="w-48 bg-popover border-border">
404
+ {renderActionItems(action.children)}
405
+ </DropdownMenuSubContent>
406
+ </DropdownMenuPortal>
407
+ </DropdownMenuSub>
408
+ );
409
+ }
410
+ return (
411
+ <DropdownMenuItem
412
+ key={idx}
413
+ className={cn(
414
+ "flex items-center gap-2",
415
+ action.variant === "destructive" ? "text-destructive focus:text-destructive" : ""
416
+ )}
417
+ onClick={(e) => {
418
+ e.stopPropagation();
419
+ action.onClick?.(null);
420
+ }}
421
+ >
422
+ {Icon && <Icon className="h-4 w-4 flex-shrink-0" />}
423
+ <span>{action.label}</span>
424
+ </DropdownMenuItem>
425
+ );
426
+ });
427
+ };
428
+
429
+ const renderAssistantActionMenu = (actions?: ActionMenuItem[], isHeader: boolean = false) => {
430
+ if (!actions || actions.length === 0) return null;
431
+ return (
432
+ <DropdownMenu>
433
+ <DropdownMenuTrigger asChild>
434
+ <Button
435
+ variant="ghost"
436
+ size="icon"
437
+ className={cn(
438
+ "h-8 w-8 text-sidebar-foreground/80 hover:bg-sidebar-foreground/20 hover:text-sidebar-foreground rounded-full transition-all",
439
+ !isHeader && "opacity-0 group-hover/item:opacity-100"
440
+ )}
441
+ aria-label="Mais opções"
442
+ >
443
+ <MoreVertical className="h-4 w-4" />
444
+ </Button>
445
+ </DropdownMenuTrigger>
446
+ <DropdownMenuContent align="end" className="w-48 bg-popover border-border p-1">
447
+ {renderActionItems(actions)}
448
+ </DropdownMenuContent>
449
+ </DropdownMenu>
450
+ );
451
+ };
452
+
327
453
  const renderDefaultItem = (item: NavigationItem) => {
328
454
  const Icon = item.icon;
329
455
  const hasChildren = item.children && item.children.length > 0;
330
-
331
456
  const activeClass = item.active
332
457
  ? "bg-sidebar-foreground/15 text-sidebar-foreground shadow-sm"
333
458
  : "text-sidebar-foreground/80 hover:bg-sidebar-foreground/15 hover:text-sidebar-foreground";
@@ -408,7 +533,6 @@ export function Sidebar({
408
533
 
409
534
  const renderDefaultGroup = (group: RouteGroup) => {
410
535
  const GroupIcon = group.icon;
411
-
412
536
  if (!expanded) {
413
537
  return (
414
538
  <div key={group.id} className="space-y-1">
@@ -416,7 +540,6 @@ export function Sidebar({
416
540
  </div>
417
541
  );
418
542
  }
419
-
420
543
  return (
421
544
  <div key={group.id}>
422
545
  {(group.label || GroupIcon) && (
@@ -440,70 +563,6 @@ export function Sidebar({
440
563
  );
441
564
  };
442
565
 
443
- const renderActionItems = (actions: ActionMenuItem[]) => {
444
- return actions.map((action, idx) => {
445
- const Icon = action.icon;
446
-
447
- if (action.children && action.children.length > 0) {
448
- return (
449
- <DropdownMenuSub key={idx}>
450
- <DropdownMenuSubTrigger>
451
- {Icon && <Icon className="mr-2 h-4 w-4" />}
452
- <span>{action.label}</span>
453
- </DropdownMenuSubTrigger>
454
- <DropdownMenuPortal>
455
- <DropdownMenuSubContent className="w-48 bg-popover border-border">
456
- {renderActionItems(action.children)}
457
- </DropdownMenuSubContent>
458
- </DropdownMenuPortal>
459
- </DropdownMenuSub>
460
- );
461
- }
462
-
463
- return (
464
- <DropdownMenuItem
465
- key={idx}
466
- className={cn(
467
- "flex items-center gap-2",
468
- action.variant === "destructive" ? "text-destructive focus:text-destructive" : ""
469
- )}
470
- onClick={(e) => {
471
- e.stopPropagation();
472
- action.onClick?.(null);
473
- }}
474
- >
475
- {Icon && <Icon className="h-4 w-4 flex-shrink-0" />}
476
- <span>{action.label}</span>
477
- </DropdownMenuItem>
478
- );
479
- });
480
- };
481
-
482
- const renderAssistantActionMenu = (actions?: ActionMenuItem[], isHeader: boolean = false) => {
483
- if (!actions || actions.length === 0) return null;
484
-
485
- return (
486
- <DropdownMenu>
487
- <DropdownMenuTrigger asChild>
488
- <Button
489
- variant="ghost"
490
- size="icon"
491
- className={cn(
492
- "h-8 w-8 text-sidebar-foreground/80 hover:bg-sidebar-foreground/20 hover:text-sidebar-foreground rounded-full transition-all",
493
- !isHeader && "opacity-0 group-hover/item:opacity-100"
494
- )}
495
- aria-label="Mais opções"
496
- >
497
- <MoreVertical className="h-4 w-4" />
498
- </Button>
499
- </DropdownMenuTrigger>
500
- <DropdownMenuContent align="end" className="w-48 bg-popover border-border p-1">
501
- {renderActionItems(actions)}
502
- </DropdownMenuContent>
503
- </DropdownMenu>
504
- );
505
- };
506
-
507
566
  const renderAssistantGroup = (group: RouteGroup) => {
508
567
  const isAnyItemActive = group.items.some(item =>
509
568
  location.pathname === item.path || location.pathname.startsWith(item.path + "/")
@@ -512,18 +571,12 @@ export function Sidebar({
512
571
 
513
572
  if (!expanded) {
514
573
  if (!GroupIcon) return null;
515
-
516
574
  return (
517
575
  <div key={group.id} className="py-2 flex justify-center">
518
576
  <Tooltip>
519
577
  <TooltipTrigger asChild>
520
578
  <button
521
- onClick={() => {
522
- // If we click a category when closed, we could either expand
523
- // or navigate to the first item.
524
- // For now, let's just trigger the onToggle to show items.
525
- onToggle();
526
- }}
579
+ onClick={() => onToggle()}
527
580
  aria-label={group.label}
528
581
  className={cn(
529
582
  "h-10 w-10 flex items-center justify-center rounded-[var(--radius-button)] transition-all duration-200",
@@ -563,7 +616,6 @@ export function Sidebar({
563
616
  const isRouteActive = location.pathname === item.path || location.pathname.startsWith(item.path + "/");
564
617
  const isActive = isRouteActive || localActiveItem === item.path;
565
618
  const Icon = item.icon;
566
-
567
619
  return (
568
620
  <div
569
621
  key={item.path}
@@ -596,354 +648,411 @@ export function Sidebar({
596
648
  );
597
649
  };
598
650
 
599
- return (
600
- <TooltipProvider delayDuration={300}>
601
- {/* Mobile Responsiveness Override embedded in Sidebar */}
602
- <style>{`
603
- @media (max-width: 767px) {
604
- [style*="padding-left"].flex-1,
605
- [style*="paddingLeft"].flex-1 {
606
- padding-left: 0 !important;
607
- }
608
- }
609
- `}</style>
610
- {/* Sidebar */}
611
- <div
612
- className={`bg-sidebar text-sidebar-foreground transition-all duration-300 ease-in-out flex flex-col z-50 ${expanded
613
- ? "fixed inset-0 md:fixed md:inset-y-0 md:left-0"
614
- : "fixed inset-y-0 left-0 w-20 -translate-x-full md:translate-x-0"
615
- }`}
616
- style={expanded && !isMobileViewport ? { width: `${width}px` } : undefined}
617
- >
618
- {/* Menu Toggle Button */}
619
- <div className="flex-shrink-0 p-[14px] pt-[13px] pr-[14px] pb-[12px] pl-[14px]">
620
- <button
621
- onClick={onToggle}
622
- 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"
623
- aria-label={expanded ? "Recolher menu" : "Expandir menu"}
624
- >
625
- {expanded ? (
626
- <ArrowLeft className="w-5 h-5" />
627
- ) : (
628
- <Menu className="w-5 h-5" />
629
- )}
630
- </button>
631
- </div>
651
+ if (variant === "assistant") {
652
+ return (
653
+ <div className="flex-1 min-h-0 overflow-hidden">
654
+ <ScrollArea className="h-full px-4">
655
+ {navigationGroups.map(group => renderAssistantGroup(group))}
656
+ </ScrollArea>
657
+ </div>
658
+ );
659
+ }
632
660
 
633
- {/* Logo */}
634
- <div className="flex-shrink-0 px-4 py-4">
635
- <div
636
- className={`flex items-center h-10 ${expanded ? "justify-center" : "justify-center"}`}
637
- >
638
- <div className="flex items-center justify-center flex-shrink-0">
639
- {expanded ? (
640
- logo || (
641
- <XerticaLogo
642
- className="h-5 w-auto"
643
- variant="white"
644
- />
645
- )
646
- ) : (
647
- logoCollapsed || (
648
- <XerticaXLogo
649
- className="h-5 w-auto"
650
- variant="white"
651
- />
652
- )
653
- )}
654
- </div>
661
+ return (
662
+ <div className="flex-1 min-h-0 overflow-hidden">
663
+ <nav className="h-full px-4 py-4 overflow-hidden" ref={navRef}>
664
+ {navigationGroups && navigationGroups.length > 0 ? (
665
+ <div className="space-y-3">
666
+ {navigationGroups.map((group) => (
667
+ <React.Fragment key={group.id}>
668
+ {renderDefaultGroup(group)}
669
+ </React.Fragment>
670
+ ))}
655
671
  </div>
656
- </div>
657
-
658
- {/* Assistant-specific Header (Search/Fixed Area) - Always fixed when present */}
659
- {variant === "assistant" && ((fixedArea && fixedArea.show) || (search && search.show)) && (
660
- <div className="flex-shrink-0 px-4 pb-4 space-y-4 border-b border-sidebar-border/30 mb-2">
661
- {fixedArea?.show && fixedArea.content && expanded && (
662
- <div className="animate-in fade-in slide-in-from-top-1 duration-300">
663
- {fixedArea.content}
664
- </div>
672
+ ) : (
673
+ <div className="space-y-1">
674
+ {(hasOverflow ? visibleItems : navigationItems).map((item) =>
675
+ renderDefaultItem(item)
665
676
  )}
666
- {search?.show && expanded && (
667
- <>
668
- <div className="flex items-center gap-2">
669
- <div className="relative flex-1">
670
- <SearchIcon className="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-sidebar-foreground/50" />
671
- <Input
672
- type="text"
673
- placeholder={search.placeholder || "Buscar..."}
674
- aria-label={search.placeholder || "Buscar..."}
675
- value={search.value}
676
- onChange={(e) => search.onChange?.(e.target.value)}
677
- 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"
678
- />
677
+ {hasOverflow && (
678
+ <Popover>
679
+ <PopoverTrigger asChild>
680
+ <button
681
+ className={
682
+ expanded
683
+ ? "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"
684
+ : "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"
685
+ }
686
+ aria-label="Mais opções"
687
+ >
688
+ <MoreVertical className="w-5 h-5 flex-shrink-0" />
689
+ {expanded && (
690
+ <span className="truncate text-sidebar-foreground">Mais opções</span>
691
+ )}
692
+ </button>
693
+ </PopoverTrigger>
694
+ <PopoverContent
695
+ side="right"
696
+ align="start"
697
+ className="w-56 p-2 bg-popover border border-border rounded-[var(--radius-card)] shadow-lg"
698
+ sideOffset={8}
699
+ >
700
+ <div className="space-y-1">
701
+ {overflowItems.map((item) => {
702
+ const Icon = item.icon;
703
+ return (
704
+ <button
705
+ key={item.path}
706
+ onClick={() => handleNavigate(item.path)}
707
+ 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"
708
+ >
709
+ {Icon && <Icon className="w-4 h-4 flex-shrink-0" />}
710
+ <span className="truncate">{item.label}</span>
711
+ </button>
712
+ );
713
+ })}
679
714
  </div>
680
- {search.filter?.show && search.filter.content && (
681
- <Button
682
- variant="ghost"
683
- size="icon"
684
- onClick={() => setIsFilterOpen(!isFilterOpen)}
685
- className={cn(
686
- "h-9 w-9 text-sidebar-foreground transition-all duration-200",
687
- isFilterOpen ? "bg-sidebar-foreground/20" : "hover:bg-sidebar-foreground/15"
688
- )}
689
- aria-label={isFilterOpen ? "Fechar filtros" : "Abrir filtros"}
690
- >
691
- {search.filter.icon || <Filter className="h-4 w-4" />}
692
- </Button>
693
- )}
694
- </div>
695
-
696
- <AnimatePresence>
697
- {isFilterOpen && search.filter?.show && search.filter.content && (
698
- <motion.div
699
- initial={{ height: 0, opacity: 0 }}
700
- animate={{ height: "auto", opacity: 1 }}
701
- exit={{ height: 0, opacity: 0 }}
702
- transition={{ duration: 0.2 }}
703
- className="overflow-hidden"
704
- >
705
- <div className="pt-2 border-t border-sidebar-border/20">
706
- {search.filter.content}
707
- </div>
708
- </motion.div>
709
- )}
710
- </AnimatePresence>
711
- </>
712
- )}
713
- {(!expanded && (fixedArea?.show || search?.show)) && (
714
- <div className="flex flex-col items-center gap-4 py-2">
715
- {fixedArea?.show && fixedArea.icon && (
716
- <Tooltip>
717
- <TooltipTrigger asChild>
718
- <button
719
- onClick={() => fixedArea.onClick?.()}
720
- className="h-10 w-10 flex items-center justify-center rounded-[var(--radius-button)] bg-primary text-primary-foreground shadow-sm hover:bg-primary/90 transition-all"
721
- aria-label="Nova Conversa"
722
- >
723
- {React.isValidElement(fixedArea.icon) ? fixedArea.icon : <fixedArea.icon className="h-5 w-5" />}
724
- </button>
725
- </TooltipTrigger>
726
- <SidebarTooltipContent side="right">Nova Conversa</SidebarTooltipContent>
727
- </Tooltip>
728
- )}
729
- {search?.show && (
730
- <Tooltip>
731
- <TooltipTrigger asChild>
732
- <button
733
- 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"
734
- aria-label="Buscar"
735
- >
736
- <SearchIcon className="h-5 w-5" />
737
- </button>
738
- </TooltipTrigger>
739
- <SidebarTooltipContent side="right">Buscar</SidebarTooltipContent>
740
- </Tooltip>
741
- )}
742
- </div>
715
+ </PopoverContent>
716
+ </Popover>
743
717
  )}
744
718
  </div>
745
719
  )}
720
+ </nav>
721
+ </div>
722
+ );
723
+ }
746
724
 
747
- {/* Main Content Area - Scrollable */}
748
- <div className="flex-1 min-h-0 overflow-hidden">
749
- {variant === "default" ? (
750
- <nav className="h-full px-4 py-4 overflow-hidden" ref={navRef}>
751
- {navigationGroups && navigationGroups.length > 0 ? (
752
- <div className="space-y-3">
753
- {navigationGroups.map((group, idx) => (
754
- <React.Fragment key={group.id}>
755
- {renderDefaultGroup(group)}
756
- </React.Fragment>
757
- ))}
758
- </div>
759
- ) : (
760
- <div className="space-y-1">
761
- {(hasOverflow ? visibleItems : navigationItems).map((item) =>
762
- renderDefaultItem(item)
763
- )}
725
+ /**
726
+ * Assistant-specific search + fixed area header for the Sidebar.
727
+ */
728
+ function SidebarSearch({
729
+ fixedArea,
730
+ search,
731
+ }: {
732
+ fixedArea?: SidebarFixedAreaConfig;
733
+ search?: SidebarSearchConfig;
734
+ }) {
735
+ const { expanded } = useSidebarContext();
736
+ const [isFilterOpen, setIsFilterOpen] = useState(false);
764
737
 
765
- {hasOverflow && (
766
- <Popover>
767
- <PopoverTrigger asChild>
768
- <button
769
- className={
770
- expanded
771
- ? "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"
772
- : "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"
773
- }
774
- aria-label="Mais opções"
775
- >
776
- <MoreVertical className="w-5 h-5 flex-shrink-0" />
777
- {expanded && (
778
- <span className="truncate text-sidebar-foreground">
779
- Mais opções
780
- </span>
781
- )}
782
- </button>
783
- </PopoverTrigger>
784
- <PopoverContent
785
- side="right"
786
- align="start"
787
- className="w-56 p-2 bg-popover border border-border rounded-[var(--radius-card)] shadow-lg"
788
- sideOffset={8}
789
- >
790
- <div className="space-y-1">
791
- {overflowItems.map((item) => {
792
- const Icon = item.icon;
793
- return (
794
- <button
795
- key={item.path}
796
- onClick={() => handleNavigate(item.path)}
797
- 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"
798
- >
799
- {Icon && <Icon className="w-4 h-4 flex-shrink-0" />}
800
- <span className="truncate">{item.label}</span>
801
- </button>
802
- );
803
- })}
804
- </div>
805
- </PopoverContent>
806
- </Popover>
807
- )}
808
- </div>
809
- )}
810
- </nav>
811
- ) : (
812
- <ScrollArea className="h-full px-4">
813
- {navigationGroups.map(group => renderAssistantGroup(group))}
814
- </ScrollArea>
815
- )}
816
- </div>
738
+ if (!((fixedArea && fixedArea.show) || (search && search.show))) return null;
817
739
 
818
- {/* Sidebar Footer - User settings */}
819
- {displayFooter && (showUser || showSettings || showLogout) && (
820
- <div className="flex-shrink-0 p-4 space-y-2">
821
- {/* User avatar */}
822
- {showUser && (
823
- !expanded ? (
824
- <Tooltip>
825
- <TooltipTrigger asChild>
826
- <button
827
- className="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"
828
- aria-label="Perfil do usuário"
829
- >
830
- <Avatar className="w-7 h-7 flex-shrink-0">
831
- <AvatarImage src={user?.avatar} alt={user?.name || "User"} />
832
- <AvatarFallback className="bg-sidebar-foreground/15 text-sidebar-foreground text-xs">
833
- {user?.name ? user.name.charAt(0).toUpperCase() : "U"}
834
- </AvatarFallback>
835
- </Avatar>
836
- </button>
837
- </TooltipTrigger>
838
- <SidebarTooltipContent side="right" sideOffset={0}>
839
- <p>{user?.name || "Perfil"}</p>
840
- </SidebarTooltipContent>
841
- </Tooltip>
842
- ) : (
843
- <button
844
- className="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"
845
- >
846
- <Avatar className="w-7 h-7 flex-shrink-0">
847
- <AvatarImage src={user?.avatar} alt={user?.name || "User"} />
848
- <AvatarFallback className="bg-sidebar-foreground/15 text-sidebar-foreground text-xs">
849
- {user?.name ? user.name.charAt(0).toUpperCase() : "U"}
850
- </AvatarFallback>
851
- </Avatar>
852
- <span className="text-sidebar-foreground truncate">
853
- {user?.name || "Perfil"}
854
- </span>
855
- </button>
856
- )
740
+ return (
741
+ <div className="flex-shrink-0 px-4 pb-4 space-y-4 border-b border-sidebar-border/30 mb-2">
742
+ {fixedArea?.show && fixedArea.content && expanded && (
743
+ <div className="animate-in fade-in slide-in-from-top-1 duration-300">
744
+ {fixedArea.content}
745
+ </div>
746
+ )}
747
+ {search?.show && expanded && (
748
+ <>
749
+ <div className="flex items-center gap-2">
750
+ <div className="relative flex-1">
751
+ <SearchIcon className="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-sidebar-foreground/50" />
752
+ <Input
753
+ type="text"
754
+ placeholder={search.placeholder || "Buscar..."}
755
+ aria-label={search.placeholder || "Buscar..."}
756
+ value={search.value}
757
+ onChange={(e) => search.onChange?.(e.target.value)}
758
+ 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"
759
+ />
760
+ </div>
761
+ {search.filter?.show && search.filter.content && (
762
+ <Button
763
+ variant="ghost"
764
+ size="icon"
765
+ onClick={() => setIsFilterOpen(!isFilterOpen)}
766
+ className={cn(
767
+ "h-9 w-9 text-sidebar-foreground transition-all duration-200",
768
+ isFilterOpen ? "bg-sidebar-foreground/20" : "hover:bg-sidebar-foreground/15"
769
+ )}
770
+ aria-label={isFilterOpen ? "Fechar filtros" : "Abrir filtros"}
771
+ >
772
+ {search.filter.icon || <Filter className="h-4 w-4" />}
773
+ </Button>
857
774
  )}
858
-
859
- {/* Settings Button */}
860
- {showSettings && (
861
- !expanded ? (
862
- <Tooltip>
863
- <TooltipTrigger asChild>
864
- <button
865
- onClick={() => {
866
- if (onSettingsClick) {
867
- onSettingsClick();
868
- } else {
869
- navigate("/settings");
870
- }
871
- if (typeof window !== "undefined" && window.innerWidth < 768) {
872
- onToggle();
873
- }
874
- }}
875
- className={"w-full h-10 flex items-center justify-center px-0 rounded-[var(--radius-button)] transition-all duration-200 " +
876
- (isSettingsActive
877
- ? "bg-sidebar-foreground/15 text-sidebar-foreground shadow-sm"
878
- : "text-sidebar-foreground/80 hover:bg-sidebar-foreground/15 hover:text-sidebar-foreground")
879
- }
880
- aria-label="Configurações"
881
- >
882
- <Settings className="w-5 h-5 flex-shrink-0" />
883
- </button>
884
- </TooltipTrigger>
885
- <SidebarTooltipContent side="right" sideOffset={0}>
886
- <p>Configurações</p>
887
- </SidebarTooltipContent>
888
- </Tooltip>
889
- ) : (
775
+ </div>
776
+ <AnimatePresence>
777
+ {isFilterOpen && search.filter?.show && search.filter.content && (
778
+ <motion.div
779
+ initial={{ height: 0, opacity: 0 }}
780
+ animate={{ height: "auto", opacity: 1 }}
781
+ exit={{ height: 0, opacity: 0 }}
782
+ transition={{ duration: 0.2 }}
783
+ className="overflow-hidden"
784
+ >
785
+ <div className="pt-2 border-t border-sidebar-border/20">
786
+ {search.filter.content}
787
+ </div>
788
+ </motion.div>
789
+ )}
790
+ </AnimatePresence>
791
+ </>
792
+ )}
793
+ {(!expanded && (fixedArea?.show || search?.show)) && (
794
+ <div className="flex flex-col items-center gap-4 py-2">
795
+ {fixedArea?.show && fixedArea.icon && (
796
+ <Tooltip>
797
+ <TooltipTrigger asChild>
890
798
  <button
891
- onClick={() => {
892
- if (onSettingsClick) {
893
- onSettingsClick();
894
- } else {
895
- navigate("/settings");
896
- }
897
- if (typeof window !== "undefined" && window.innerWidth < 768) {
898
- onToggle();
899
- }
900
- }}
901
- className={
902
- isSettingsActive
903
- ? "w-full h-10 flex items-center gap-3 px-3 justify-start rounded-[var(--radius-button)] transition-all duration-200 bg-sidebar-foreground/15 text-sidebar-foreground shadow-sm"
904
- : "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"
905
- }
799
+ onClick={() => fixedArea.onClick?.()}
800
+ className="h-10 w-10 flex items-center justify-center rounded-[var(--radius-button)] bg-primary text-primary-foreground shadow-sm hover:bg-primary/90 transition-all"
801
+ aria-label="Nova Conversa"
906
802
  >
907
- <Settings className="w-5 h-5 flex-shrink-0" />
908
- <span className="truncate text-sidebar-foreground">
909
- Configurações
910
- </span>
803
+ {React.isValidElement(fixedArea.icon) ? fixedArea.icon : <fixedArea.icon className="h-5 w-5" />}
911
804
  </button>
912
- )
913
- )}
914
-
915
- {/* Logout Button */}
916
- {showLogout && (
917
- !expanded ? (
918
- <Tooltip>
919
- <TooltipTrigger asChild>
920
- <button
921
- onClick={onLogout}
922
- className="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"
923
- aria-label="Sair"
924
- >
925
- <LogOut className="w-5 h-5 flex-shrink-0" />
926
- </button>
927
- </TooltipTrigger>
928
- <SidebarTooltipContent side="right" sideOffset={0}>
929
- <p>Sair</p>
930
- </SidebarTooltipContent>
931
- </Tooltip>
932
- ) : (
805
+ </TooltipTrigger>
806
+ <SidebarTooltipContent side="right">Nova Conversa</SidebarTooltipContent>
807
+ </Tooltip>
808
+ )}
809
+ {search?.show && (
810
+ <Tooltip>
811
+ <TooltipTrigger asChild>
933
812
  <button
934
- onClick={onLogout}
935
- className="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"
813
+ 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"
814
+ aria-label="Buscar"
936
815
  >
937
- <LogOut className="w-5 h-5 flex-shrink-0" />
938
- <span className="truncate text-sidebar-foreground">
939
- Sair
940
- </span>
816
+ <SearchIcon className="h-5 w-5" />
941
817
  </button>
942
- )
818
+ </TooltipTrigger>
819
+ <SidebarTooltipContent side="right">Buscar</SidebarTooltipContent>
820
+ </Tooltip>
821
+ )}
822
+ </div>
823
+ )}
824
+ </div>
825
+ );
826
+ }
827
+
828
+ /**
829
+ * Footer area for the Sidebar with user info, settings, and logout.
830
+ */
831
+ function SidebarFooter({
832
+ user,
833
+ onLogout = () => { },
834
+ onSettingsClick,
835
+ showUser = true,
836
+ showSettings = true,
837
+ showLogout = true,
838
+ }: {
839
+ user?: { name?: string; email?: string; avatar?: string } | null;
840
+ onLogout?: () => void;
841
+ onSettingsClick?: () => void;
842
+ showUser?: boolean;
843
+ showSettings?: boolean;
844
+ showLogout?: boolean;
845
+ }) {
846
+ const { expanded, navigate, location, onToggle } = useSidebarContext();
847
+ const isSettingsActive = location.pathname === "/settings";
848
+
849
+ const handleSettingsClick = () => {
850
+ if (onSettingsClick) {
851
+ onSettingsClick();
852
+ } else {
853
+ navigate("/settings");
854
+ }
855
+ if (typeof window !== "undefined" && window.innerWidth < 768) {
856
+ onToggle();
857
+ }
858
+ };
859
+
860
+ return (
861
+ <div className="flex-shrink-0 p-4 space-y-2">
862
+ {showUser && (
863
+ !expanded ? (
864
+ <Tooltip>
865
+ <TooltipTrigger asChild>
866
+ <button
867
+ className="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"
868
+ aria-label="Perfil do usuário"
869
+ >
870
+ <Avatar className="w-7 h-7 flex-shrink-0">
871
+ <AvatarImage src={user?.avatar} alt={user?.name || "User"} />
872
+ <AvatarFallback className="bg-sidebar-foreground/15 text-sidebar-foreground text-xs">
873
+ {user?.name ? user.name.charAt(0).toUpperCase() : "U"}
874
+ </AvatarFallback>
875
+ </Avatar>
876
+ </button>
877
+ </TooltipTrigger>
878
+ <SidebarTooltipContent side="right" sideOffset={0}>
879
+ <p>{user?.name || "Perfil"}</p>
880
+ </SidebarTooltipContent>
881
+ </Tooltip>
882
+ ) : (
883
+ <button className="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">
884
+ <Avatar className="w-7 h-7 flex-shrink-0">
885
+ <AvatarImage src={user?.avatar} alt={user?.name || "User"} />
886
+ <AvatarFallback className="bg-sidebar-foreground/15 text-sidebar-foreground text-xs">
887
+ {user?.name ? user.name.charAt(0).toUpperCase() : "U"}
888
+ </AvatarFallback>
889
+ </Avatar>
890
+ <span className="text-sidebar-foreground truncate">{user?.name || "Perfil"}</span>
891
+ </button>
892
+ )
893
+ )}
894
+
895
+ {showSettings && (
896
+ !expanded ? (
897
+ <Tooltip>
898
+ <TooltipTrigger asChild>
899
+ <button
900
+ onClick={handleSettingsClick}
901
+ className={cn(
902
+ "w-full h-10 flex items-center justify-center px-0 rounded-[var(--radius-button)] transition-all duration-200",
903
+ isSettingsActive
904
+ ? "bg-sidebar-foreground/15 text-sidebar-foreground shadow-sm"
905
+ : "text-sidebar-foreground/80 hover:bg-sidebar-foreground/15 hover:text-sidebar-foreground"
906
+ )}
907
+ aria-label="Configurações"
908
+ >
909
+ <Settings className="w-5 h-5 flex-shrink-0" />
910
+ </button>
911
+ </TooltipTrigger>
912
+ <SidebarTooltipContent side="right" sideOffset={0}>
913
+ <p>Configurações</p>
914
+ </SidebarTooltipContent>
915
+ </Tooltip>
916
+ ) : (
917
+ <button
918
+ onClick={handleSettingsClick}
919
+ className={cn(
920
+ "w-full h-10 flex items-center gap-3 px-3 justify-start rounded-[var(--radius-button)] transition-all duration-200",
921
+ isSettingsActive
922
+ ? "bg-sidebar-foreground/15 text-sidebar-foreground shadow-sm"
923
+ : "text-sidebar-foreground/80 hover:bg-sidebar-foreground/15 hover:text-sidebar-foreground"
943
924
  )}
944
- </div>
945
- )}
946
- </div>
947
- </TooltipProvider>
925
+ >
926
+ <Settings className="w-5 h-5 flex-shrink-0" />
927
+ <span className="truncate text-sidebar-foreground">Configurações</span>
928
+ </button>
929
+ )
930
+ )}
931
+
932
+ {showLogout && (
933
+ !expanded ? (
934
+ <Tooltip>
935
+ <TooltipTrigger asChild>
936
+ <button
937
+ onClick={onLogout}
938
+ className="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"
939
+ aria-label="Sair"
940
+ >
941
+ <LogOut className="w-5 h-5 flex-shrink-0" />
942
+ </button>
943
+ </TooltipTrigger>
944
+ <SidebarTooltipContent side="right" sideOffset={0}>
945
+ <p>Sair</p>
946
+ </SidebarTooltipContent>
947
+ </Tooltip>
948
+ ) : (
949
+ <button
950
+ onClick={onLogout}
951
+ className="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"
952
+ >
953
+ <LogOut className="w-5 h-5 flex-shrink-0" />
954
+ <span className="truncate text-sidebar-foreground">Sair</span>
955
+ </button>
956
+ )
957
+ )}
958
+ </div>
959
+ );
960
+ }
961
+
962
+ // ─────────────────────────────────────────────────────────────────────────────
963
+ // Main Sidebar component (backward-compatible monolithic API)
964
+ // ─────────────────────────────────────────────────────────────────────────────
965
+
966
+ /**
967
+ * Primary navigation sidebar component.
968
+ *
969
+ * @description
970
+ * Manages desktop/mobile responsive navigation rendering with two variants:
971
+ * - `"default"` — simple flat or grouped route list.
972
+ * - `"assistant"` — advanced variant with fixed areas, search, filters, and grouped navigation.
973
+ *
974
+ * This component is autonomous: it works out-of-the-box using local state or
975
+ * integrates automatically with `LayoutContext` if wrapped in `LayoutProvider`.
976
+ *
977
+ * For advanced customization, use the Compound Component API:
978
+ * `<Sidebar.Root>`, `<Sidebar.Header>`, `<Sidebar.Search>`, `<Sidebar.Nav>`, `<Sidebar.Footer>`
979
+ *
980
+ * @ai-rules
981
+ * 1. NEVER recreate the sidebar with raw Tailwind classes — always use this component.
982
+ * 2. Use `variant="assistant"` for AI/tool sidebars; use `variant="default"` for standard navigation.
983
+ * 3. Supports `Ctrl+B` keyboard shortcut automatically via `LayoutProvider`.
984
+ */
985
+ export function Sidebar({
986
+ expanded: expandedProp,
987
+ onToggle: onToggleProp,
988
+ user,
989
+ onLogout = () => { },
990
+ onSettingsClick,
991
+ location: locationProp,
992
+ navigate: navigateProp,
993
+ routes,
994
+ logo,
995
+ logoCollapsed,
996
+ variant = "default",
997
+ fixedArea,
998
+ search,
999
+ navigationGroups = [],
1000
+ footer,
1001
+ showFooter,
1002
+ width: widthProp,
1003
+ }: SidebarProps) {
1004
+ const {
1005
+ showUser = true,
1006
+ showSettings = true,
1007
+ showLogout = true,
1008
+ } = footer || {};
1009
+
1010
+ const displayFooter = showFooter !== undefined ? showFooter : variant === "default";
1011
+
1012
+ return (
1013
+ <SidebarRoot
1014
+ expanded={expandedProp}
1015
+ onToggle={onToggleProp}
1016
+ navigate={navigateProp}
1017
+ location={locationProp}
1018
+ width={widthProp}
1019
+ >
1020
+ <SidebarHeader logo={logo} logoCollapsed={logoCollapsed} />
1021
+
1022
+ {variant === "assistant" && (
1023
+ <SidebarSearch fixedArea={fixedArea} search={search} />
1024
+ )}
1025
+
1026
+ <SidebarNav
1027
+ navigationGroups={navigationGroups}
1028
+ routes={routes}
1029
+ variant={variant}
1030
+ />
1031
+
1032
+ {displayFooter && (showUser || showSettings || showLogout) && (
1033
+ <SidebarFooter
1034
+ user={user}
1035
+ onLogout={onLogout}
1036
+ onSettingsClick={onSettingsClick}
1037
+ showUser={showUser}
1038
+ showSettings={showSettings}
1039
+ showLogout={showLogout}
1040
+ />
1041
+ )}
1042
+ </SidebarRoot>
948
1043
  );
949
1044
  }
1045
+
1046
+ // ─────────────────────────────────────────────────────────────────────────────
1047
+ // Attach Compound Components to Sidebar namespace
1048
+ // ─────────────────────────────────────────────────────────────────────────────
1049
+
1050
+ Sidebar.Root = SidebarRoot;
1051
+ Sidebar.Header = SidebarHeader;
1052
+ Sidebar.Search = SidebarSearch;
1053
+ Sidebar.Nav = SidebarNav;
1054
+ Sidebar.Footer = SidebarFooter;
1055
+
1056
+ // Re-export hook for headless usage
1057
+ export { useSidebar } from "./use-sidebar";
1058
+ export type { UseSidebarProps } from "./use-sidebar";