xertica-ui 2.5.2 → 2.6.0

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 (577) hide show
  1. package/CHANGELOG.md +687 -656
  2. package/README.md +482 -482
  3. package/assets/xertica-logo.svg +37 -37
  4. package/assets/xertica-x-logo.svg +20 -20
  5. package/bin/cli.ts +1293 -1256
  6. package/bin/generate-tokens.ts +256 -262
  7. package/bin/language-config.test.ts +32 -0
  8. package/bin/language-config.ts +36 -11
  9. package/components/Introduction.mdx +121 -121
  10. package/components/assistant/code-block/CodeBlock.tsx +7 -6
  11. package/components/assistant/code-block/code-block.mdx +38 -38
  12. package/components/assistant/code-block/code-block.test.tsx +2 -1
  13. package/components/assistant/formatted-document/FormattedDocument.tsx +3 -0
  14. package/components/assistant/formatted-document/formatted-document.mdx +31 -31
  15. package/components/assistant/formatted-document/formatted-document.test.tsx +5 -4
  16. package/components/assistant/markdown-message/MarkdownMessage.tsx +1 -1
  17. package/components/assistant/markdown-message/markdown-message.mdx +44 -44
  18. package/components/assistant/modern-chat-input/modern-chat-input.mdx +71 -71
  19. package/components/assistant/modern-chat-input/modern-chat-input.test.tsx +3 -2
  20. package/components/assistant/xertica-assistant/parts/AssistantMessageBubble.tsx +1 -1
  21. package/components/assistant/xertica-assistant/types.ts +3 -2
  22. package/components/assistant/xertica-assistant/use-assistant.ts +5 -4
  23. package/components/assistant/xertica-assistant/xertica-assistant.mdx +66 -66
  24. package/components/assistant/xertica-assistant/xertica-assistant.test.tsx +2 -1
  25. package/components/blocks/card-patterns/card-patterns.mdx +123 -123
  26. package/components/brand/branding/branding.mdx +27 -27
  27. package/components/brand/language-selector/language-selector.mdx +126 -126
  28. package/components/brand/theme-toggle/theme-toggle.mdx +47 -47
  29. package/components/brand/theme-toggle/theme-toggle.test.tsx +6 -2
  30. package/components/brand/xertica-logo/xertica-logo.mdx +56 -56
  31. package/components/brand/xertica-orbe/xertica-orbe.mdx +38 -38
  32. package/components/brand/xertica-provider/XerticaProvider.tsx +112 -112
  33. package/components/brand/xertica-provider/xertica-provider.mdx +61 -61
  34. package/components/brand/xertica-provider/xertica-provider.test.tsx +25 -13
  35. package/components/brand/xertica-xlogo/xertica-xlogo.mdx +55 -55
  36. package/components/examples/DrawingMapExample.tsx +1 -4
  37. package/components/layout/header/header.mdx +117 -117
  38. package/components/layout/header/header.test.tsx +2 -1
  39. package/components/layout/sidebar/sidebar.mdx +99 -99
  40. package/components/layout/sidebar/sidebar.stories.tsx +1033 -1033
  41. package/components/layout/sidebar/sidebar.test.tsx +2 -1
  42. package/components/layout/sidebar/sidebar.tsx +1509 -1501
  43. package/components/media/audio-player/AudioPlayer.mdx +67 -67
  44. package/components/media/audio-player/AudioPlayer.test.tsx +2 -1
  45. package/components/media/floating-media-wrapper.mdx +74 -74
  46. package/components/media/video-player/VideoPlayer.mdx +102 -102
  47. package/components/media/video-player/VideoPlayer.test.tsx +3 -2
  48. package/components/pages/forgot-password-page/ForgotPasswordPage.tsx +188 -188
  49. package/components/pages/forgot-password-page/forgot-password-page.mdx +60 -60
  50. package/components/pages/forgot-password-page/forgot-password-page.test.tsx +2 -1
  51. package/components/pages/home-content/HomeContent.tsx +2 -3
  52. package/components/pages/home-content/home-content.mdx +62 -62
  53. package/components/pages/home-page/HomePage.stories.tsx +8 -15
  54. package/components/pages/home-page/home-page.mdx +53 -53
  55. package/components/pages/home-page/home-page.test.tsx +2 -1
  56. package/components/pages/login-page/LoginPage.mdx +31 -31
  57. package/components/pages/login-page/LoginPage.tsx +218 -218
  58. package/components/pages/login-page/login-page.test.tsx +2 -1
  59. package/components/pages/reset-password-page/ResetPasswordPage.tsx +243 -243
  60. package/components/pages/reset-password-page/reset-password-page.mdx +63 -63
  61. package/components/pages/template-content/TemplateContent.tsx +2 -3
  62. package/components/pages/template-content/template-content.mdx +61 -61
  63. package/components/pages/template-page/TemplatePage.stories.tsx +32 -32
  64. package/components/pages/template-page/template-page.mdx +53 -53
  65. package/components/pages/template-page/template-page.test.tsx +2 -1
  66. package/components/pages/verify-email-page/VerifyEmailPage.tsx +206 -206
  67. package/components/pages/verify-email-page/verify-email-page.mdx +55 -55
  68. package/components/public-api-smoke.test.tsx +10 -6
  69. package/components/shared/assistant-utils.ts +3 -2
  70. package/components/shared/navigation.ts +2 -2
  71. package/components/ui/accordion/accordion.mdx +29 -29
  72. package/components/ui/alert/alert.mdx +29 -29
  73. package/components/ui/alert-dialog/alert-dialog.mdx +26 -26
  74. package/components/ui/aspect-ratio/aspect-ratio.mdx +25 -25
  75. package/components/ui/assistant-chart/AssistantChart.tsx +8 -2
  76. package/components/ui/assistant-chart/assistant-chart.mdx +25 -25
  77. package/components/ui/avatar/avatar.mdx +26 -26
  78. package/components/ui/badge/badge.mdx +28 -28
  79. package/components/ui/breadcrumb/breadcrumb.mdx +26 -26
  80. package/components/ui/button/button.mdx +29 -29
  81. package/components/ui/calendar/calendar.mdx +26 -26
  82. package/components/ui/card/card.mdx +26 -26
  83. package/components/ui/carousel/carousel.mdx +26 -26
  84. package/components/ui/chart/chart.mdx +26 -26
  85. package/components/ui/chart/chart.test.tsx +178 -178
  86. package/components/ui/chart/chart.tsx +2252 -2245
  87. package/components/ui/checkbox/checkbox.mdx +26 -26
  88. package/components/ui/collapsible/collapsible.mdx +25 -25
  89. package/components/ui/command/command.mdx +26 -26
  90. package/components/ui/context-menu/context-menu.mdx +26 -26
  91. package/components/ui/dialog/dialog.mdx +26 -26
  92. package/components/ui/drawer/drawer.mdx +26 -26
  93. package/components/ui/dropdown-menu/dropdown-menu.mdx +26 -26
  94. package/components/ui/empty/empty.mdx +26 -26
  95. package/components/ui/file-upload/file-upload.mdx +26 -26
  96. package/components/ui/form/form.mdx +20 -20
  97. package/components/ui/google-maps-loader/google-maps-loader.mdx +68 -68
  98. package/components/ui/hover-card/hover-card.mdx +26 -26
  99. package/components/ui/input/input.mdx +29 -29
  100. package/components/ui/input-otp/input-otp.mdx +26 -26
  101. package/components/ui/label/label.mdx +25 -25
  102. package/components/ui/map/map.mdx +26 -26
  103. package/components/ui/map/map.tsx +6 -1
  104. package/components/ui/menubar/menubar.mdx +26 -26
  105. package/components/ui/navigation-menu/navigation-menu.mdx +26 -26
  106. package/components/ui/notification-badge/notification-badge.mdx +26 -26
  107. package/components/ui/page-header/page-header.mdx +44 -44
  108. package/components/ui/pagination/pagination.mdx +26 -26
  109. package/components/ui/popover/popover.mdx +26 -26
  110. package/components/ui/progress/progress.mdx +25 -25
  111. package/components/ui/radio-group/radio-group.mdx +26 -26
  112. package/components/ui/rating/rating.mdx +26 -26
  113. package/components/ui/resizable/resizable.mdx +26 -26
  114. package/components/ui/resizable/resizable.tsx +13 -8
  115. package/components/ui/rich-text-editor/rich-text-editor.mdx +47 -47
  116. package/components/ui/route-map/route-map.mdx +26 -26
  117. package/components/ui/scroll-area/scroll-area.mdx +25 -25
  118. package/components/ui/search/search.mdx +27 -27
  119. package/components/ui/select/select.mdx +26 -26
  120. package/components/ui/separator/separator.mdx +25 -25
  121. package/components/ui/sheet/sheet.mdx +26 -26
  122. package/components/ui/simple-map/simple-map.mdx +25 -25
  123. package/components/ui/skeleton/skeleton.mdx +25 -25
  124. package/components/ui/slider/slider.mdx +26 -26
  125. package/components/ui/sonner/sonner.mdx +26 -26
  126. package/components/ui/stats-card/stats-card.mdx +26 -26
  127. package/components/ui/stats-card/stats-card.stories.tsx +117 -117
  128. package/components/ui/stats-card/stats-card.tsx +109 -109
  129. package/components/ui/stepper/stepper.mdx +26 -26
  130. package/components/ui/switch/switch.mdx +25 -25
  131. package/components/ui/table/table.mdx +27 -27
  132. package/components/ui/tabs/tabs.mdx +26 -26
  133. package/components/ui/textarea/textarea.mdx +25 -25
  134. package/components/ui/timeline/timeline.mdx +26 -26
  135. package/components/ui/toggle/toggle.mdx +26 -26
  136. package/components/ui/toggle-group/toggle-group.mdx +26 -26
  137. package/components/ui/tooltip/tooltip.mdx +26 -26
  138. package/components/ui/tree-view/tree-view.mdx +26 -26
  139. package/components.json +511 -511
  140. package/contexts/BrandColorsContext.tsx +282 -282
  141. package/contexts/i18n-singleton.test.tsx +57 -0
  142. package/contexts/theme-data.ts +391 -391
  143. package/dist/{AssistantChart-DMJJ_Amf.js → AssistantChart-BMNSA_Pk.js} +5 -5
  144. package/dist/{AssistantChart-D0A1FPYt.cjs → AssistantChart-BzM7plyc.cjs} +150 -114
  145. package/dist/{AssistantChart-BXU1ACnh.js → AssistantChart-C842evTi.js} +134 -240
  146. package/dist/{AssistantChart-BZTPJ5dP.cjs → AssistantChart-CVF2uh37.cjs} +5 -5
  147. package/dist/{AssistantChart-COGiOV-g.cjs → AssistantChart-JooMdVQU.cjs} +17 -7
  148. package/dist/{AssistantChart-CWX1OWNM.js → AssistantChart-KHS-tp-L.js} +17 -7
  149. package/dist/{BrandColorsContext-BwY-b6M4.cjs → BrandColorsContext-B4_81eas.cjs} +2 -2
  150. package/dist/{BrandColorsContext-BMRJ04Wf.js → BrandColorsContext-DbK5LnQ_.js} +2 -2
  151. package/dist/{CodeBlock-CjeFavqV.js → CodeBlock-DiKO8ocT.js} +11 -11
  152. package/dist/{CodeBlock-CV6dtsTM.cjs → CodeBlock-ZYThr7Kn.cjs} +12 -9
  153. package/dist/{XerticaXLogo-mqjoBiLI.js → LanguageSelector-CmTj3lWq.js} +85 -82
  154. package/dist/{XerticaXLogo-BX3ueACh.js → LanguageSelector-DxW6o7bt.js} +81 -81
  155. package/dist/{XerticaXLogo-uQgwns_E.cjs → LanguageSelector-Sw73qvWV.cjs} +84 -81
  156. package/dist/{ThemeContext-BgclCB35.js → ThemeContext-BGeh_Kdr.js} +290 -212
  157. package/dist/{ThemeContext-BblcjQup.cjs → ThemeContext-D50irx5T.cjs} +291 -205
  158. package/dist/{VerifyEmailPage-C_Zk6Gen.cjs → VerifyEmailPage-B6bOrybJ.cjs} +17 -17
  159. package/dist/{VerifyEmailPage-B31mCrMc.js → VerifyEmailPage-BRE5Fz59.js} +8 -8
  160. package/dist/{VerifyEmailPage-BJjAMUTW.js → VerifyEmailPage-BcCluouh.js} +151 -78
  161. package/dist/VerifyEmailPage-Bx1sr0uQ.cjs +3298 -0
  162. package/dist/{VerifyEmailPage-X14vhdyl.js → VerifyEmailPage-DUYyXCX9.js} +12 -12
  163. package/dist/VerifyEmailPage-DXXMHbzq.js +3289 -0
  164. package/dist/{VerifyEmailPage-u_Dn7t1U.cjs → VerifyEmailPage-DpYtFk35.cjs} +21 -21
  165. package/dist/{VerifyEmailPage-Cyl55sJb.js → VerifyEmailPage-NXKWW3Dk.js} +134 -64
  166. package/dist/{VerifyEmailPage-Bv8Ah_TK.cjs → VerifyEmailPage-QrCz4Wqr.cjs} +143 -73
  167. package/dist/{VerifyEmailPage-CkBYfsNy.cjs → VerifyEmailPage-ZmQra0Wi.cjs} +160 -87
  168. package/dist/{XerticaProvider-BErr83Bg.js → XerticaProvider-BF3q_myt.js} +5 -4
  169. package/dist/{XerticaProvider-CeS5G_n5.cjs → XerticaProvider-C_JSOz1U.cjs} +2 -2
  170. package/dist/{XerticaProvider-CwOkHxiT.cjs → XerticaProvider-D5zjrqZq.cjs} +5 -4
  171. package/dist/{XerticaProvider-ra2NciRq.js → XerticaProvider-KTGl4Cn1.js} +2 -2
  172. package/dist/{alert-dialog-DqlRW_An.js → alert-dialog-BrpwXKwS.js} +3 -3
  173. package/dist/{alert-dialog-DhwPioBa.cjs → alert-dialog-CZU1Zase.cjs} +3 -3
  174. package/dist/{alert-dialog-KpoabzJb.js → alert-dialog-ClRoLH7l.js} +40 -56
  175. package/dist/{alert-dialog-B4M3vQKS.cjs → alert-dialog-DBBqvU-V.cjs} +53 -38
  176. package/dist/assistant.cjs.js +2 -2
  177. package/dist/assistant.es.js +2 -2
  178. package/dist/brand.cjs.js +6 -6
  179. package/dist/brand.es.js +2 -2
  180. package/dist/cli.js +63 -21
  181. package/dist/components/assistant/code-block/CodeBlock.d.ts +2 -1
  182. package/dist/components/assistant/formatted-document/FormattedDocument.d.ts +2 -1
  183. package/dist/components/assistant/markdown-message/MarkdownMessage.d.ts +2 -1
  184. package/dist/components/assistant/modern-chat-input/ModernChatInput.d.ts +2 -1
  185. package/dist/components/assistant/xertica-assistant/parts/AssistantCollapsedView.d.ts +2 -1
  186. package/dist/components/assistant/xertica-assistant/parts/AssistantConversationList.d.ts +2 -1
  187. package/dist/components/assistant/xertica-assistant/parts/AssistantDocumentEditor.d.ts +2 -1
  188. package/dist/components/assistant/xertica-assistant/parts/AssistantFeedbackDialog.d.ts +2 -1
  189. package/dist/components/assistant/xertica-assistant/parts/AssistantHeader.d.ts +2 -1
  190. package/dist/components/assistant/xertica-assistant/parts/AssistantMessageBubble.d.ts +2 -1
  191. package/dist/components/assistant/xertica-assistant/parts/AssistantTabBar.d.ts +2 -1
  192. package/dist/components/assistant/xertica-assistant/parts/AssistantTypingIndicator.d.ts +2 -1
  193. package/dist/components/assistant/xertica-assistant/parts/AssistantWelcomeScreen.d.ts +2 -1
  194. package/dist/components/assistant/xertica-assistant/types.d.ts +3 -2
  195. package/dist/components/assistant/xertica-assistant/use-assistant.d.ts +1 -1
  196. package/dist/components/assistant/xertica-assistant/xertica-assistant.d.ts +2 -1
  197. package/dist/components/blocks/card-patterns/ActivityCard.d.ts +1 -1
  198. package/dist/components/blocks/card-patterns/ActivityCardSkeleton.d.ts +1 -1
  199. package/dist/components/blocks/card-patterns/FeatureCard.d.ts +1 -1
  200. package/dist/components/blocks/card-patterns/FeatureCardSkeleton.d.ts +1 -1
  201. package/dist/components/blocks/card-patterns/NotificationCard.d.ts +1 -1
  202. package/dist/components/blocks/card-patterns/NotificationCardSkeleton.d.ts +1 -1
  203. package/dist/components/blocks/card-patterns/ProfileCard.d.ts +1 -1
  204. package/dist/components/blocks/card-patterns/ProfileCardSkeleton.d.ts +1 -1
  205. package/dist/components/blocks/card-patterns/ProjectCard.d.ts +1 -1
  206. package/dist/components/blocks/card-patterns/ProjectCardSkeleton.d.ts +1 -1
  207. package/dist/components/blocks/card-patterns/QuickActionCard.d.ts +1 -1
  208. package/dist/components/blocks/card-patterns/QuickActionCardSkeleton.d.ts +1 -1
  209. package/dist/components/brand/language-selector/LanguageSelector.d.ts +2 -1
  210. package/dist/components/brand/theme-toggle/ThemeToggle.d.ts +2 -1
  211. package/dist/components/brand/xertica-logo/XerticaLogo.d.ts +2 -1
  212. package/dist/components/brand/xertica-orbe/XerticaOrbe.d.ts +2 -1
  213. package/dist/components/brand/xertica-provider/XerticaProvider.d.ts +1 -1
  214. package/dist/components/brand/xertica-xlogo/XerticaXLogo.d.ts +2 -1
  215. package/dist/components/examples/DrawingMapExample.d.ts +2 -1
  216. package/dist/components/examples/FilterableMapExample.d.ts +1 -1
  217. package/dist/components/examples/MapShowcase.d.ts +2 -1
  218. package/dist/components/examples/SimpleFilterableMap.d.ts +2 -1
  219. package/dist/components/figma/ImageWithFallback.d.ts +1 -1
  220. package/dist/components/layout/header/header.d.ts +1 -1
  221. package/dist/components/layout/sidebar/sidebar.d.ts +25 -13
  222. package/dist/components/media/FloatingMediaWrapper.d.ts +1 -1
  223. package/dist/components/media/audio-player/AudioPlayer.d.ts +2 -1
  224. package/dist/components/media/video-player/VideoPlayer.d.ts +2 -1
  225. package/dist/components/pages/forgot-password-page/ForgotPasswordPage.d.ts +2 -1
  226. package/dist/components/pages/home-content/HomeContent.d.ts +2 -1
  227. package/dist/components/pages/home-page/HomePage.d.ts +2 -1
  228. package/dist/components/pages/login-page/LoginPage.d.ts +2 -1
  229. package/dist/components/pages/reset-password-page/ResetPasswordPage.d.ts +2 -1
  230. package/dist/components/pages/template-content/TemplateContent.d.ts +2 -1
  231. package/dist/components/pages/template-page/TemplatePage.d.ts +2 -1
  232. package/dist/components/pages/verify-email-page/VerifyEmailPage.d.ts +2 -1
  233. package/dist/components/shared/CustomTooltipContent.d.ts +1 -1
  234. package/dist/components/shared/assistant-utils.d.ts +3 -2
  235. package/dist/components/shared/error-boundary.d.ts +4 -4
  236. package/dist/components/shared/error-fallbacks.d.ts +4 -3
  237. package/dist/components/shared/navigation.d.ts +6 -2
  238. package/dist/components/ui/accordion/accordion.d.ts +4 -4
  239. package/dist/components/ui/alert/alert.d.ts +3 -3
  240. package/dist/components/ui/alert-dialog/alert-dialog.d.ts +10 -10
  241. package/dist/components/ui/aspect-ratio/aspect-ratio.d.ts +1 -1
  242. package/dist/components/ui/assistant-chart/AssistantChart.d.ts +4 -3
  243. package/dist/components/ui/avatar/avatar.d.ts +3 -3
  244. package/dist/components/ui/badge/badge.d.ts +1 -1
  245. package/dist/components/ui/breadcrumb/breadcrumb.d.ts +7 -7
  246. package/dist/components/ui/calendar/calendar.d.ts +2 -2
  247. package/dist/components/ui/card/card.d.ts +7 -7
  248. package/dist/components/ui/carousel/carousel.d.ts +5 -5
  249. package/dist/components/ui/chart/chart.d.ts +16 -16
  250. package/dist/components/ui/collapsible/collapsible.d.ts +3 -3
  251. package/dist/components/ui/command/command.d.ts +9 -9
  252. package/dist/components/ui/context-menu/context-menu.d.ts +15 -15
  253. package/dist/components/ui/dialog/dialog.d.ts +9 -9
  254. package/dist/components/ui/drawer/drawer.d.ts +10 -10
  255. package/dist/components/ui/dropdown-menu/dropdown-menu.d.ts +15 -15
  256. package/dist/components/ui/form/form.d.ts +7 -7
  257. package/dist/components/ui/google-maps-loader/google-maps-loader.d.ts +3 -3
  258. package/dist/components/ui/hover-card/hover-card.d.ts +3 -3
  259. package/dist/components/ui/input-otp/input-otp.d.ts +4 -4
  260. package/dist/components/ui/label/label.d.ts +1 -1
  261. package/dist/components/ui/menubar/menubar.d.ts +16 -16
  262. package/dist/components/ui/navigation-menu/navigation-menu.d.ts +8 -8
  263. package/dist/components/ui/page-header/page-header.d.ts +3 -3
  264. package/dist/components/ui/pagination/pagination.d.ts +7 -7
  265. package/dist/components/ui/popover/popover.d.ts +4 -4
  266. package/dist/components/ui/progress/progress.d.ts +1 -1
  267. package/dist/components/ui/resizable/resizable.d.ts +11 -7
  268. package/dist/components/ui/rich-text-editor/rich-text-editor.d.ts +1 -1
  269. package/dist/components/ui/scroll-area/scroll-area.d.ts +2 -2
  270. package/dist/components/ui/separator/separator.d.ts +1 -1
  271. package/dist/components/ui/sheet/sheet.d.ts +10 -10
  272. package/dist/components/ui/skeleton/skeleton.d.ts +1 -1
  273. package/dist/components/ui/sonner/sonner.d.ts +2 -1
  274. package/dist/components/ui/stats-card/stats-card-skeleton.d.ts +1 -1
  275. package/dist/components/ui/table/table.d.ts +8 -8
  276. package/dist/components/ui/tabs/tabs.d.ts +4 -4
  277. package/dist/components/ui/tooltip/tooltip.d.ts +4 -4
  278. package/dist/contexts/ApiKeyContext.d.ts +2 -2
  279. package/dist/contexts/AssistenteContext.d.ts +1 -1
  280. package/dist/contexts/AuthContext.d.ts +1 -1
  281. package/dist/contexts/LanguageContext.d.ts +2 -2
  282. package/dist/contexts/LayoutContext.d.ts +2 -2
  283. package/dist/contexts/ThemeContext.d.ts +1 -1
  284. package/dist/features/assistant/hooks/useAssistantConfig.d.ts +1 -1
  285. package/dist/features/home/hooks/useDashboardStats.d.ts +1 -1
  286. package/dist/features/home/hooks/useFeatureCards.d.ts +1 -1
  287. package/dist/features/home/hooks/useTeamMembers.d.ts +1 -1
  288. package/dist/{google-maps-loader-BCe58h9D.js → google-maps-loader-BOug999f.js} +0 -4
  289. package/dist/{google-maps-loader-casMyxlo.cjs → google-maps-loader-DfeBRI8Q.cjs} +0 -5
  290. package/dist/hooks.cjs.js +2 -2
  291. package/dist/hooks.es.js +2 -2
  292. package/dist/{index-9GWd0qxq.cjs → index-BQjx1Mdf.cjs} +1 -1
  293. package/dist/index-Bei5DzAL.js +6 -0
  294. package/dist/index.cjs.js +18 -18
  295. package/dist/index.es.js +12 -12
  296. package/dist/{input-cc-PTD4R.js → input-B0_vbA3g.js} +3 -61
  297. package/dist/{input-C_UiS2Py.cjs → input-ByYuOv8A.cjs} +1 -62
  298. package/dist/layout.cjs.js +1 -1
  299. package/dist/layout.es.js +1 -1
  300. package/dist/pages.cjs.js +1 -1
  301. package/dist/pages.es.js +1 -1
  302. package/dist/{rich-text-editor-DxO1Hz3a.cjs → rich-text-editor-BuqSu718.cjs} +70 -5
  303. package/dist/{rich-text-editor-DqLICivI.js → rich-text-editor-CV8FtA_l.js} +68 -6
  304. package/dist/{rich-text-editor-DloeW0wc.js → rich-text-editor-DV1SixzE.js} +61 -3
  305. package/dist/{rich-text-editor-B2CKz7nx.cjs → rich-text-editor-DnSDglzW.cjs} +63 -2
  306. package/dist/{sidebar-BS1p2V7t.cjs → sidebar-BGJ0j567.cjs} +367 -9
  307. package/dist/{sidebar-3XyzjVBw.js → sidebar-BI6mZlC8.js} +364 -6
  308. package/dist/{sidebar-DyYvgyBj.cjs → sidebar-BtmotOg-.cjs} +367 -9
  309. package/dist/{sidebar-Duermn32.js → sidebar-D3EgGVxF.js} +77 -63
  310. package/dist/{sidebar-CLmIjgNd.cjs → sidebar-DDTqMGnR.cjs} +80 -66
  311. package/dist/{sidebar-B4ZWaMrE.js → sidebar-I7EXyf5D.js} +364 -6
  312. package/dist/ui.cjs.js +7 -7
  313. package/dist/ui.es.js +5 -5
  314. package/dist/{xertica-assistant-QFUnv5I2.cjs → xertica-assistant-BrF9jScR.cjs} +4 -4
  315. package/dist/{xertica-assistant-BbgzsgsG.js → xertica-assistant-CJVnvMqU.js} +55 -45
  316. package/dist/{xertica-assistant-d2qs2Vd9.cjs → xertica-assistant-CKPtc1z5.cjs} +58 -40
  317. package/dist/{xertica-assistant-CyikE3N_.js → xertica-assistant-CsBsxTsI.js} +2 -2
  318. package/dist/{xertica-assistant-CIaUlbIt.cjs → xertica-assistant-DER0brPQ.cjs} +6 -5
  319. package/dist/{xertica-assistant-B1NaSFFj.js → xertica-assistant-DQCjpk5M.js} +4 -3
  320. package/dist/{xertica-assistant-B687qEPU.js → xertica-assistant-Dk-9AJzw.js} +16 -7
  321. package/dist/{xertica-assistant-sOHwTgIP.cjs → xertica-assistant-DxhIKvuY.cjs} +18 -9
  322. package/dist/xertica-ui.css +2 -2
  323. package/docs/components/audio-player.md +1 -1
  324. package/docs/components/sidebar.md +331 -331
  325. package/docs/components/stats-card.md +138 -138
  326. package/docs/components/toggle.md +12 -0
  327. package/docs/doc-audit.md +35 -229
  328. package/docs/i18n.md +31 -10
  329. package/docs/llms.md +6 -4
  330. package/llms-compact.txt +444 -444
  331. package/llms-full.txt +12144 -12144
  332. package/llms.txt +187 -187
  333. package/package.json +223 -219
  334. package/scripts/a11y-audit.mjs +162 -162
  335. package/scripts/fix-stories.cjs +19 -19
  336. package/styles/globals.css +0 -1
  337. package/styles/xertica/base.css +8 -0
  338. package/styles/xertica/tokens.css +240 -240
  339. package/templates/.env.example +2 -2
  340. package/templates/.prettierignore +4 -4
  341. package/templates/.prettierrc +10 -10
  342. package/templates/guidelines/Guidelines.md +865 -865
  343. package/templates/index.html +13 -13
  344. package/templates/package.json +69 -69
  345. package/templates/src/app/components/AuthGuard.tsx +11 -0
  346. package/templates/src/features/auth/ui/AuthPageShell.tsx +32 -32
  347. package/templates/src/features/home/ui/HomeContent.tsx +2 -3
  348. package/templates/src/features/settings/index.ts +2 -0
  349. package/templates/src/features/settings/ui/SettingsContent.tsx +134 -0
  350. package/templates/src/features/template/ui/TemplateContent.tsx +2 -3
  351. package/templates/src/i18n.ts +36 -13
  352. package/templates/src/locales/en/pages/settings.json +15 -0
  353. package/templates/src/locales/es/pages/settings.json +15 -0
  354. package/templates/src/locales/pt-BR/pages/settings.json +15 -0
  355. package/templates/src/pages/SettingsPage.tsx +28 -0
  356. package/templates/src/shared/config/navigation.ts +2 -2
  357. package/templates/src/styles/xertica/tokens.css +240 -240
  358. package/templates/vite.config.d.ts +2 -2
  359. package/templates/vite.config.js +20 -20
  360. package/utils/gemini.ts +2 -2
  361. package/dist/AssistantChart-9w31gdAb.cjs +0 -3363
  362. package/dist/AssistantChart-BAudAfne.cjs +0 -3591
  363. package/dist/AssistantChart-BAx9VQvb.cjs +0 -3374
  364. package/dist/AssistantChart-BP8upjMk.js +0 -3565
  365. package/dist/AssistantChart-CVko2A1W.js +0 -3348
  366. package/dist/AssistantChart-CVzmmhx4.js +0 -3337
  367. package/dist/AssistantChart-CeU2dIb6.js +0 -3469
  368. package/dist/AssistantChart-DO5UuX4J.cjs +0 -3495
  369. package/dist/AudioPlayer-1ypwE2Wh.cjs +0 -936
  370. package/dist/AudioPlayer-BJ2IuRQW.cjs +0 -1275
  371. package/dist/AudioPlayer-BWDW6TN9.cjs +0 -1279
  372. package/dist/AudioPlayer-CM962GEh.js +0 -1280
  373. package/dist/AudioPlayer-CbZId7cj.cjs +0 -998
  374. package/dist/AudioPlayer-DU8zt8-z.js +0 -999
  375. package/dist/AudioPlayer-DuKXrCfy.js +0 -937
  376. package/dist/AudioPlayer-Xxma6_-H.js +0 -1276
  377. package/dist/BrandColorsContext-DLVJgtmH.js +0 -666
  378. package/dist/BrandColorsContext-DZT7JjeD.js +0 -659
  379. package/dist/BrandColorsContext-GUyFmkX0.cjs +0 -665
  380. package/dist/BrandColorsContext-awnBCmC4.cjs +0 -666
  381. package/dist/CodeBlock-C2ZUXt8V.js +0 -219
  382. package/dist/CodeBlock-C5oYnLQp.cjs +0 -218
  383. package/dist/CodeBlock-DuxdtN-l.js +0 -219
  384. package/dist/CodeBlock-POVYONvC.cjs +0 -218
  385. package/dist/CustomTooltipContent-BT6brkVJ.cjs +0 -49
  386. package/dist/CustomTooltipContent-CfOfikhq.js +0 -33
  387. package/dist/CustomTooltipContent-DHjkY0ww.js +0 -40
  388. package/dist/CustomTooltipContent-c_K-DWRr.cjs +0 -56
  389. package/dist/FeatureCard-9GhnlgL3.js +0 -336
  390. package/dist/FeatureCard-Bcb3Zi54.js +0 -302
  391. package/dist/FeatureCard-Cfs6Ishq.js +0 -485
  392. package/dist/FeatureCard-CkPd2psQ.cjs +0 -301
  393. package/dist/FeatureCard-Dkp-xmzM.cjs +0 -335
  394. package/dist/FeatureCard-ie3L8Fpr.cjs +0 -484
  395. package/dist/FeatureCardSkeleton-CAgu8stE.js +0 -27
  396. package/dist/FeatureCardSkeleton-mO3Bdm_V.cjs +0 -26
  397. package/dist/LanguageContext-BwhwC3G2.js +0 -657
  398. package/dist/LanguageContext-DvUt5jBg.cjs +0 -656
  399. package/dist/LayoutContext-BDmcZfMH.cjs +0 -84
  400. package/dist/LayoutContext-C4kKN9RV.cjs +0 -93
  401. package/dist/LayoutContext-CGEe1oPq.js +0 -94
  402. package/dist/LayoutContext-CwT5KLiW.cjs +0 -104
  403. package/dist/LayoutContext-DVLCsoQn.js +0 -105
  404. package/dist/LayoutContext-dbQvdC4O.js +0 -85
  405. package/dist/ThemeContext-BbBNoFTG.js +0 -735
  406. package/dist/ThemeContext-Bo-W2WZH.js +0 -1856
  407. package/dist/ThemeContext-CP3a0jxy.cjs +0 -1855
  408. package/dist/ThemeContext-Cmr8Ex8H.cjs +0 -734
  409. package/dist/ThemeContext-CpqYShLq.cjs +0 -324
  410. package/dist/ThemeContext-D3LzacmG.js +0 -188
  411. package/dist/ThemeContext-DQUOeziy.cjs +0 -1855
  412. package/dist/ThemeContext-Du2nE1PL.js +0 -325
  413. package/dist/ThemeContext-GeEBTJ3q.cjs +0 -1621
  414. package/dist/ThemeContext-JyLK9B1o.js +0 -1622
  415. package/dist/ThemeContext-RTy1m2Uq.js +0 -82
  416. package/dist/ThemeContext-U4dEYc6C.cjs +0 -187
  417. package/dist/ThemeContext-bSzuOit2.cjs +0 -81
  418. package/dist/ThemeContext-ept8jhXI.js +0 -1856
  419. package/dist/VerifyEmailPage-BE-L9mB7.js +0 -2828
  420. package/dist/VerifyEmailPage-BIBOKV7Z.js +0 -3214
  421. package/dist/VerifyEmailPage-BRSP-Pwt.cjs +0 -3196
  422. package/dist/VerifyEmailPage-Bae2cBXT.cjs +0 -2827
  423. package/dist/VerifyEmailPage-BiRm7Nh4.cjs +0 -3213
  424. package/dist/VerifyEmailPage-Bvfv8HVQ.js +0 -3214
  425. package/dist/VerifyEmailPage-CR7kb5df.cjs +0 -2827
  426. package/dist/VerifyEmailPage-C_ihbcth.js +0 -2828
  427. package/dist/VerifyEmailPage-CbgjOF0v.js +0 -2828
  428. package/dist/VerifyEmailPage-CdYPSJoO.js +0 -2828
  429. package/dist/VerifyEmailPage-D-FRj5TU.cjs +0 -3213
  430. package/dist/VerifyEmailPage-DF2ilhum.cjs +0 -3210
  431. package/dist/VerifyEmailPage-DMBh4NM9.cjs +0 -2827
  432. package/dist/VerifyEmailPage-DTtFfC-J.js +0 -3197
  433. package/dist/VerifyEmailPage-Dt7zgA4w.cjs +0 -2827
  434. package/dist/VerifyEmailPage-EhudUdqF.js +0 -3211
  435. package/dist/VerifyEmailPage-RrUApqBN.js +0 -3214
  436. package/dist/VerifyEmailPage-VoMI7MYH.cjs +0 -3213
  437. package/dist/VerifyEmailPage-hdB8JQGv.cjs +0 -3213
  438. package/dist/VerifyEmailPage-vYHbYK3q.js +0 -3214
  439. package/dist/XerticaOrbe-BCUVSPPB.cjs +0 -1924
  440. package/dist/XerticaOrbe-BGIB1CTK.js +0 -1355
  441. package/dist/XerticaOrbe-By8eEvR-.js +0 -1925
  442. package/dist/XerticaOrbe-FWGjBj0U.cjs +0 -1354
  443. package/dist/XerticaProvider-AChwphCO.cjs +0 -48
  444. package/dist/XerticaProvider-AbWlr7Af.cjs +0 -48
  445. package/dist/XerticaProvider-B8CaV7xu.cjs +0 -39
  446. package/dist/XerticaProvider-BITjgC5p.js +0 -39
  447. package/dist/XerticaProvider-BSyFrmC0.js +0 -49
  448. package/dist/XerticaProvider-By8q3Roe.cjs +0 -38
  449. package/dist/XerticaProvider-CUYJZc32.js +0 -49
  450. package/dist/XerticaProvider-CW9hpCdF.cjs +0 -39
  451. package/dist/XerticaProvider-CWgby5mY.js +0 -49
  452. package/dist/XerticaProvider-CWs6EwNa.js +0 -49
  453. package/dist/XerticaProvider-CiNKjMx1.cjs +0 -48
  454. package/dist/XerticaProvider-CjQAQPcn.cjs +0 -48
  455. package/dist/XerticaProvider-D5lLumH-.js +0 -49
  456. package/dist/XerticaProvider-DQtvJU7m.js +0 -40
  457. package/dist/XerticaProvider-DUOJg9iX.js +0 -49
  458. package/dist/XerticaProvider-Dl_b72_l.cjs +0 -51
  459. package/dist/XerticaProvider-qQUDop71.cjs +0 -48
  460. package/dist/XerticaProvider-siSt9uG2.js +0 -40
  461. package/dist/XerticaXLogo-8TTzBjHw.cjs +0 -251
  462. package/dist/XerticaXLogo-B2svDGZh.cjs +0 -251
  463. package/dist/XerticaXLogo-BWaag64t.js +0 -252
  464. package/dist/XerticaXLogo-CFuIlYFH.js +0 -252
  465. package/dist/XerticaXLogo-CU-U-GP4.cjs +0 -251
  466. package/dist/XerticaXLogo-ChryA6xj.js +0 -252
  467. package/dist/XerticaXLogo-CowGv7BC.js +0 -252
  468. package/dist/XerticaXLogo-CziKMQil.cjs +0 -242
  469. package/dist/XerticaXLogo-D8jf0SNv.cjs +0 -214
  470. package/dist/XerticaXLogo-DHz5SugF.js +0 -252
  471. package/dist/XerticaXLogo-DTee_y8X.cjs +0 -251
  472. package/dist/XerticaXLogo-DfUvz-lD.js +0 -243
  473. package/dist/XerticaXLogo-DzXKZBtG.js +0 -231
  474. package/dist/XerticaXLogo-fAJMy3H4.js +0 -215
  475. package/dist/XerticaXLogo-kslQ8Tk_.cjs +0 -251
  476. package/dist/XerticaXLogo-xWbElCZj.cjs +0 -230
  477. package/dist/alert-dialog-iDe5VE5o.js +0 -842
  478. package/dist/alert-dialog-yckpaOpy.cjs +0 -865
  479. package/dist/avatar-Cq5_E8r9.cjs +0 -78
  480. package/dist/avatar-CuYu2MKt.cjs +0 -72
  481. package/dist/avatar-Cxyofu1H.js +0 -56
  482. package/dist/avatar-DSBfJMRJ.js +0 -62
  483. package/dist/breadcrumb-CVzzzqfo.js +0 -103
  484. package/dist/breadcrumb-DIJ0X3Ot.js +0 -96
  485. package/dist/breadcrumb-DjSxkjlQ.cjs +0 -95
  486. package/dist/breadcrumb-DnDlQFg2.cjs +0 -102
  487. package/dist/button-0CXwCRTm.cjs +0 -85
  488. package/dist/button-Bnv9SvYK.cjs +0 -79
  489. package/dist/button-C6uvh0rV.js +0 -63
  490. package/dist/button-qkz7Icqf.js +0 -69
  491. package/dist/card-B8-Gl5DL.js +0 -73
  492. package/dist/card-Bv_c57KU.cjs +0 -72
  493. package/dist/card-DJTsYhLL.cjs +0 -89
  494. package/dist/card-fi8zAN-I.js +0 -90
  495. package/dist/dropdown-menu-B_uEXNc4.cjs +0 -220
  496. package/dist/dropdown-menu-CFuCssWA.js +0 -204
  497. package/dist/dropdown-menu-Ze1YZFyc.cjs +0 -247
  498. package/dist/dropdown-menu-nc_Lvg-u.js +0 -231
  499. package/dist/google-maps-loader-BVoVhUwA.js +0 -316
  500. package/dist/google-maps-loader-BqsYL48U.cjs +0 -285
  501. package/dist/google-maps-loader-DjJaYhV6.cjs +0 -316
  502. package/dist/google-maps-loader-t2IlYBzw.js +0 -286
  503. package/dist/index-B7A523O_.js +0 -8
  504. package/dist/index-BabBx2pa.js +0 -6
  505. package/dist/index-CkTUgOwX.js +0 -8
  506. package/dist/index-D3RLKRAs.cjs +0 -7
  507. package/dist/index-DmHhnb7a.cjs +0 -7
  508. package/dist/index.umd.js +0 -13712
  509. package/dist/input-CNFHVKvo.cjs +0 -145
  510. package/dist/input-CPl84zAx.cjs +0 -151
  511. package/dist/input-D-6IsAca.js +0 -133
  512. package/dist/input-wPYg0d0P.js +0 -127
  513. package/dist/progress-BLwqortq.cjs +0 -105
  514. package/dist/progress-Cs9i4DPx.js +0 -81
  515. package/dist/progress-D9mtsf6j.js +0 -89
  516. package/dist/progress-DDeuWPZw.cjs +0 -97
  517. package/dist/rich-text-editor-1UW3Y2JK.js +0 -2891
  518. package/dist/rich-text-editor-BM-LPpTh.js +0 -2902
  519. package/dist/rich-text-editor-BmsjY03B.js +0 -2949
  520. package/dist/rich-text-editor-BrdC5evx.js +0 -2244
  521. package/dist/rich-text-editor-CyggTOLQ.cjs +0 -2908
  522. package/dist/rich-text-editor-DbV-S7Fi.cjs +0 -2261
  523. package/dist/rich-text-editor-DoCkKhzk.cjs +0 -2919
  524. package/dist/rich-text-editor-GS2kpTAK.cjs +0 -2966
  525. package/dist/routes.d.ts +0 -10
  526. package/dist/select-Cmb8zF7w.cjs +0 -156
  527. package/dist/select-CprSNX0z.js +0 -139
  528. package/dist/select-DT7Zjn42.cjs +0 -162
  529. package/dist/select-En8DkoaQ.js +0 -145
  530. package/dist/sidebar-B3EYhli0.cjs +0 -800
  531. package/dist/sidebar-B9NR0lCe.cjs +0 -800
  532. package/dist/sidebar-BcJ6LxvX.cjs +0 -733
  533. package/dist/sidebar-BvF5I2Ue.cjs +0 -800
  534. package/dist/sidebar-C5B_LHek.cjs +0 -795
  535. package/dist/sidebar-CA6_ek3f.js +0 -801
  536. package/dist/sidebar-CRMiBtAi.js +0 -801
  537. package/dist/sidebar-CVUGHOS_.cjs +0 -756
  538. package/dist/sidebar-CZ2mWaMM.cjs +0 -800
  539. package/dist/sidebar-CmvwjnVb.js +0 -757
  540. package/dist/sidebar-CplprZpM.js +0 -801
  541. package/dist/sidebar-Dz7bd3zP.js +0 -796
  542. package/dist/sidebar-KIS0C2JH.js +0 -801
  543. package/dist/sidebar-OTO_up7Z.js +0 -801
  544. package/dist/sidebar-WoKvFWCN.js +0 -717
  545. package/dist/sidebar-zowjejT2.cjs +0 -800
  546. package/dist/skeleton-4zoHiFJ_.cjs +0 -83
  547. package/dist/skeleton-C4bHqUBf.js +0 -84
  548. package/dist/slider-C0JKA9k3.js +0 -56
  549. package/dist/slider-CPmwzm_3.cjs +0 -73
  550. package/dist/slider-DcJharR9.cjs +0 -73
  551. package/dist/slider-Pay3fka2.js +0 -56
  552. package/dist/sonner-DeUIj0Zg.cjs +0 -63
  553. package/dist/sonner-g9RIfi35.js +0 -47
  554. package/dist/tooltip-CNtsL5cB.cjs +0 -71
  555. package/dist/tooltip-CTyZCD8h.js +0 -55
  556. package/dist/tooltip-CwgSdy3h.cjs +0 -67
  557. package/dist/tooltip-D-OrYnKO.js +0 -51
  558. package/dist/use-audio-player-BJFV24XW.js +0 -188
  559. package/dist/use-audio-player-Bkh23vQ3.js +0 -177
  560. package/dist/use-audio-player-Dn1NR9xN.cjs +0 -176
  561. package/dist/use-audio-player-DyVYlOnR.cjs +0 -187
  562. package/dist/use-mobile-BB4M_4Ru.cjs +0 -47
  563. package/dist/use-mobile-CLu9nqGL.js +0 -31
  564. package/dist/use-mobile-CNwiBNxu.js +0 -33
  565. package/dist/use-mobile-Dlf74ufz.cjs +0 -49
  566. package/dist/xertica-assistant-B1IaHXnB.cjs +0 -1894
  567. package/dist/xertica-assistant-BMqdyRVi.js +0 -2082
  568. package/dist/xertica-assistant-Bj3vBCq_.cjs +0 -2081
  569. package/dist/xertica-assistant-CfBCo21g.js +0 -2148
  570. package/dist/xertica-assistant-DIM_Do1k.js +0 -1866
  571. package/dist/xertica-assistant-DPsESB6t.js +0 -1878
  572. package/dist/xertica-assistant-DrsRwla1.cjs +0 -2147
  573. package/dist/xertica-assistant-Qp3ydksa.cjs +0 -1882
  574. package/dist/xertica-assistant-gnCJdcZY.js +0 -1866
  575. package/dist/xertica-assistant-yyrR0JKl.cjs +0 -1882
  576. package/styles/xertica/app-overrides/scrollbar.css +0 -33
  577. package/dist/{XerticaXLogo-qBPhwK3g.cjs → LanguageSelector-1cINLJ23.cjs} +80 -80
@@ -1,1501 +1,1509 @@
1
- import React, { useState, useRef, useEffect, createContext, useContext, useMemo } from 'react';
2
- import { motion, AnimatePresence } from 'framer-motion';
3
- import { useTranslation } from 'react-i18next';
4
- import {
5
- Menu,
6
- ArrowLeft,
7
- LogOut,
8
- Settings,
9
- MoreVertical,
10
- ChevronRight,
11
- Filter,
12
- } from 'lucide-react';
13
- import { Avatar, AvatarFallback, AvatarImage } from '../../ui/avatar';
14
- import { Popover, PopoverContent, PopoverTrigger } from '../../ui/popover';
15
- import { Tooltip, TooltipProvider, TooltipTrigger } from '../../ui/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';
29
- import { cn } from '../../shared/utils';
30
- import { XerticaLogo } from '../../brand/xertica-logo';
31
- import { XerticaXLogo } from '../../brand/xertica-xlogo';
32
- import { Button } from '../../ui/button';
33
- import { useOptionalLayout } from '../../../contexts/LayoutContext';
34
- import { useSidebar } from './use-sidebar';
35
- import { CustomTooltipContent as SidebarTooltipContent } from '../../shared/CustomTooltipContent';
36
-
37
- // ─────────────────────────────────────────────────────────────────────────────
38
- // Public interfaces
39
- // ─────────────────────────────────────────────────────────────────────────────
40
-
41
- /**
42
- * Contextual action menu item for routes and groups.
43
- */
44
- export interface ActionMenuItem {
45
- /** Text label */
46
- label: string;
47
- /** Optional icon component */
48
- icon?: React.ComponentType<any>;
49
- /** Click handler */
50
- onClick?: (item: any) => void;
51
- /** Visual variant */
52
- variant?: 'default' | 'destructive';
53
- /** Nested sub-actions */
54
- children?: ActionMenuItem[];
55
- }
56
-
57
- /**
58
- * Navigation route configuration.
59
- */
60
- export interface RouteConfig {
61
- /** Relative or absolute path */
62
- path: string;
63
- /** Navigation label */
64
- label: string;
65
- /** Icon component */
66
- icon?: React.ComponentType<any>;
67
- /** React component for the route (optional) */
68
- component?: React.ComponentType<any>;
69
- /** Hover action menu items (ellipsis) */
70
- actions?: ActionMenuItem[];
71
- /** Auxiliary content shown when the route is selected */
72
- description?: React.ReactNode;
73
- /** Child routes exposed via contextual menu button at the end of the item */
74
- children?: RouteConfig[];
75
- }
76
-
77
- export interface NavigationItem {
78
- path: string;
79
- label: string;
80
- icon?: any;
81
- active: boolean;
82
- children?: RouteConfig[];
83
- actions?: ActionMenuItem[];
84
- }
85
-
86
- export interface SidebarFilterConfig {
87
- show: boolean;
88
- content?: React.ReactNode;
89
- icon?: React.ReactNode;
90
- }
91
-
92
- export interface SidebarSearchConfig {
93
- show: boolean;
94
- placeholder?: string;
95
- value?: string;
96
- onChange?: (value: string) => void;
97
- filter?: SidebarFilterConfig;
98
- }
99
-
100
- export interface SidebarFixedAreaConfig {
101
- show: boolean;
102
- content?: React.ReactNode;
103
- icon?: any;
104
- onClick?: () => void;
105
- }
106
-
107
- export interface SidebarFooterConfig {
108
- showUser?: boolean;
109
- showSettings?: boolean;
110
- showLogout?: boolean;
111
- }
112
-
113
- /**
114
- * Logical grouping of navigation routes (e.g., in Assistant variant).
115
- */
116
- export interface RouteGroup {
117
- /** Unique ID */
118
- id: string;
119
- /** Group title */
120
- label?: string;
121
- /** Group icon */
122
- icon?: React.ComponentType<any>;
123
- /** Navigational items in the group */
124
- items: RouteConfig[];
125
- /** Context menu for the entire group */
126
- actions?: ActionMenuItem[];
127
- }
128
-
129
- /**
130
- * Navigation Sidebar component.
131
- */
132
- export interface SidebarProps {
133
- /** Whether the sidebar is expanded (defaults to LayoutContext state if available) */
134
- expanded?: boolean;
135
- /** Callback to toggle expansion state (defaults to LayoutContext toggle if available) */
136
- onToggle?: () => void;
137
- /** Authenticated user info */
138
- user?: {
139
- name?: string;
140
- email?: string;
141
- avatar?: string;
142
- } | null;
143
- /** Logout callback */
144
- onLogout?: () => void;
145
- /** Settings callback */
146
- onSettingsClick?: () => void;
147
- /** Current location for active state detection (defaults to window.location if missing) */
148
- location?: { pathname: string };
149
- /** Navigation callback (defaults to window.location.href if missing) */
150
- navigate?: (path: string) => void;
151
- /** Flat list of navigation routes */
152
- routes?: RouteConfig[];
153
- /** Logo shown in expanded state */
154
- logo?: React.ReactNode;
155
- /** Logo shown in collapsed state */
156
- logoCollapsed?: React.ReactNode;
157
- /** Visual variant */
158
- variant?: 'default' | 'assistant';
159
- /** Assistant-only fixed area configuration */
160
- fixedArea?: SidebarFixedAreaConfig;
161
- /** Assistant-only search bar configuration */
162
- search?: SidebarSearchConfig;
163
- /** Grouped navigation items */
164
- navigationGroups?: RouteGroup[];
165
- /** Footer content configuration */
166
- footer?: SidebarFooterConfig;
167
- /** Whether to show the footer (defaults: true for 'default', false for 'assistant') */
168
- showFooter?: boolean;
169
- /** Pixel width when expanded (desktop) */
170
- width?: number;
171
- }
172
-
173
- // ─────────────────────────────────────────────────────────────────────────────
174
- // Compound Component Context
175
- // ─────────────────────────────────────────────────────────────────────────────
176
-
177
- interface SidebarContextValue {
178
- expanded: boolean;
179
- isMobileViewport: boolean;
180
- onToggle: () => void;
181
- navigate: (path: string) => void;
182
- location: { pathname: string };
183
- width: number;
184
- }
185
-
186
- const SidebarContext = createContext<SidebarContextValue | null>(null);
187
-
188
- function useSidebarContext() {
189
- const ctx = useContext(SidebarContext);
190
- if (!ctx) {
191
- throw new Error('Sidebar compound components must be used within <Sidebar.Root>');
192
- }
193
- return ctx;
194
- }
195
-
196
- // ─────────────────────────────────────────────────────────────────────────────
197
- // Compound Sub-components
198
- // ─────────────────────────────────────────────────────────────────────────────
199
-
200
- /**
201
- * Root container for the Sidebar. Provides context to all sub-components.
202
- * Use this when building a fully custom sidebar layout.
203
- *
204
- * @example
205
- * <Sidebar.Root expanded={expanded} onToggle={toggle} width={280}>
206
- * <Sidebar.Header logo={<MyLogo />} />
207
- * <Sidebar.Nav navigationGroups={groups} />
208
- * <Sidebar.Footer user={user} onLogout={logout} />
209
- * </Sidebar.Root>
210
- */
211
- function SidebarRoot({
212
- expanded: expandedProp,
213
- onToggle: onToggleProp,
214
- navigate: navigateProp,
215
- location: locationProp,
216
- width: widthProp,
217
- children,
218
- className,
219
- }: {
220
- expanded?: boolean;
221
- onToggle?: () => void;
222
- navigate?: (path: string) => void;
223
- location?: { pathname: string };
224
- width?: number;
225
- children: React.ReactNode;
226
- className?: string;
227
- }) {
228
- const layoutContext = useOptionalLayout();
229
- const [localExpanded, setLocalExpanded] = useState(false);
230
- const [isMobileViewport, setIsMobileViewport] = useState(false);
231
-
232
- const expanded =
233
- expandedProp !== undefined ? expandedProp : (layoutContext?.sidebarExpanded ?? localExpanded);
234
- const onToggle =
235
- onToggleProp || layoutContext?.toggleSidebar || (() => setLocalExpanded(prev => !prev));
236
- const width = widthProp !== undefined ? widthProp : (layoutContext?.sidebarWidth ?? 280);
237
- const navigate =
238
- navigateProp ||
239
- ((path: string) => {
240
- if (typeof window !== 'undefined') window.location.href = path;
241
- });
242
- const location =
243
- locationProp || (typeof window !== 'undefined' ? window.location : { pathname: '/' });
244
-
245
- useEffect(() => {
246
- const checkViewport = () => setIsMobileViewport(window.innerWidth < 768);
247
- checkViewport();
248
- window.addEventListener('resize', checkViewport);
249
- return () => window.removeEventListener('resize', checkViewport);
250
- }, []);
251
-
252
- return (
253
- <SidebarContext.Provider
254
- value={{ expanded, isMobileViewport, onToggle, navigate, location, width }}
255
- >
256
- <TooltipProvider delayDuration={300}>
257
- <style>{`
258
- @media (max-width: 767px) {
259
- [style*="padding-left"].flex-1,
260
- [style*="paddingLeft"].flex-1 {
261
- padding-left: 0 !important;
262
- }
263
- }
264
- `}</style>
265
- <div
266
- className={cn(
267
- 'bg-sidebar text-sidebar-foreground transition-all duration-300 ease-in-out flex flex-col z-50',
268
- expanded
269
- ? 'fixed inset-0 md:fixed md:inset-y-0 md:left-0'
270
- : 'fixed inset-y-0 left-0 w-20 -translate-x-full md:translate-x-0',
271
- className
272
- )}
273
- style={expanded && !isMobileViewport ? { width: `${width}px` } : undefined}
274
- >
275
- {children}
276
- </div>
277
- </TooltipProvider>
278
- </SidebarContext.Provider>
279
- );
280
- }
281
-
282
- /**
283
- * Toggle button + logo header area for the Sidebar.
284
- */
285
- function SidebarHeader({
286
- logo,
287
- logoCollapsed,
288
- }: {
289
- logo?: React.ReactNode;
290
- logoCollapsed?: React.ReactNode;
291
- }) {
292
- const { expanded, onToggle } = useSidebarContext();
293
- const { t } = useTranslation();
294
-
295
- return (
296
- <>
297
- {/* Menu Toggle Button */}
298
- <div className="flex-shrink-0 p-[14px] pt-[13px] pr-[14px] pb-[12px] pl-[14px]">
299
- <button
300
- onClick={onToggle}
301
- 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"
302
- aria-label={expanded ? t('sidebar.collapse') : t('sidebar.expand')}
303
- aria-expanded={expanded}
304
- aria-controls="sidebar-nav"
305
- >
306
- {expanded ? <ArrowLeft className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
307
- </button>
308
- </div>
309
-
310
- {/* Logo */}
311
- <div className="flex-shrink-0 px-4 py-4">
312
- <div className="flex items-center h-10 justify-center">
313
- <div className="flex items-center justify-center flex-shrink-0">
314
- {expanded
315
- ? logo || <XerticaLogo className="h-5 w-auto" variant="white" />
316
- : logoCollapsed || <XerticaXLogo className="h-5 w-auto" variant="white" />}
317
- </div>
318
- </div>
319
- </div>
320
- </>
321
- );
322
- }
323
-
324
- /**
325
- * Accordion inline para "mais opções" no mobile.
326
- * Abre abaixo do botão, igual ao padrão dos subitens mobile.
327
- */
328
- function OverflowGroupsAccordion({
329
- expanded,
330
- overflowGroups,
331
- location,
332
- handleNavigate,
333
- moreOptionsLabel,
334
- onOpenChange,
335
- }: {
336
- expanded: boolean;
337
- overflowGroups: RouteGroup[];
338
- location: { pathname: string };
339
- handleNavigate: (path: string) => void;
340
- moreOptionsLabel: string;
341
- onOpenChange?: (open: boolean) => void;
342
- }) {
343
- const [isOpen, setIsOpen] = useState(false);
344
-
345
- const toggle = () => {
346
- const next = !isOpen;
347
- setIsOpen(next);
348
- onOpenChange?.(next);
349
- };
350
-
351
- return (
352
- <div>
353
- <button
354
- onClick={toggle}
355
- className={cn(
356
- 'w-full h-10 flex items-center rounded-[var(--radius-button)] transition-all duration-200 text-sidebar-foreground/80 hover:bg-sidebar-foreground/15 hover:text-sidebar-foreground',
357
- expanded ? 'gap-3 px-3 justify-start' : 'justify-center px-0'
358
- )}
359
- aria-expanded={isOpen}
360
- aria-label={moreOptionsLabel}
361
- >
362
- <MoreVertical className="w-5 h-5 flex-shrink-0" />
363
- {expanded && (
364
- <>
365
- <span className="truncate flex-1 text-sidebar-foreground text-left">
366
- {moreOptionsLabel}
367
- </span>
368
- <ChevronRight
369
- className={cn('w-4 h-4 flex-shrink-0 transition-transform duration-200', isOpen && 'rotate-90')}
370
- />
371
- </>
372
- )}
373
- </button>
374
- <AnimatePresence initial={false}>
375
- {isOpen && (
376
- <motion.div
377
- initial={{ height: 0, opacity: 0 }}
378
- animate={{ height: 'auto', opacity: 1 }}
379
- exit={{ height: 0, opacity: 0 }}
380
- transition={{ duration: 0.2 }}
381
- className="overflow-hidden"
382
- >
383
- <div className="mt-1 space-y-3">
384
- {overflowGroups.map(group => {
385
- const GroupIcon = group.icon;
386
- return (
387
- <div key={group.id}>
388
- {(group.label || GroupIcon) && expanded && (
389
- <div className="px-3 mb-1 flex items-center gap-2">
390
- {GroupIcon &&
391
- (React.isValidElement(GroupIcon) ? (
392
- GroupIcon
393
- ) : (
394
- <GroupIcon className="h-3 w-3 text-sidebar-foreground/60" />
395
- ))}
396
- {group.label && (
397
- <span className="text-xs font-semibold uppercase tracking-wider text-sidebar-foreground/60">
398
- {group.label}
399
- </span>
400
- )}
401
- </div>
402
- )}
403
- <div className="space-y-0.5">
404
- {group.items.map(item => {
405
- const Icon = item.icon;
406
- const isActive =
407
- location.pathname === item.path ||
408
- location.pathname.startsWith(item.path + '/');
409
- return (
410
- <button
411
- key={item.path}
412
- onClick={() => handleNavigate(item.path)}
413
- className={cn(
414
- 'w-full h-9 flex items-center gap-2.5 rounded-[var(--radius-button)] transition-all duration-200 text-left text-sm',
415
- expanded ? 'px-3' : 'justify-center px-0',
416
- isActive
417
- ? 'bg-sidebar-foreground/15 text-sidebar-foreground'
418
- : 'text-sidebar-foreground/70 hover:bg-sidebar-foreground/10 hover:text-sidebar-foreground'
419
- )}
420
- >
421
- {Icon && <Icon className="w-4 h-4 flex-shrink-0" />}
422
- {expanded && <span className="truncate">{item.label}</span>}
423
- </button>
424
- );
425
- })}
426
- </div>
427
- </div>
428
- );
429
- })}
430
- </div>
431
- </motion.div>
432
- )}
433
- </AnimatePresence>
434
- </div>
435
- );
436
- }
437
-
438
- /**
439
- * Navigation area for the Sidebar (default variant).
440
- * Renders grouped or flat navigation items with overflow handling.
441
- */
442
- function SidebarNav({
443
- navigationGroups = [],
444
- routes = [],
445
- variant = 'default',
446
- }: {
447
- navigationGroups?: RouteGroup[];
448
- routes?: RouteConfig[];
449
- variant?: 'default' | 'assistant';
450
- }) {
451
- const { expanded, isMobileViewport, navigate, location, onToggle } = useSidebarContext();
452
- const { t } = useTranslation();
453
- const navRef = useRef<HTMLDivElement>(null);
454
- const [localActiveItem, setLocalActiveItem] = useState<string | null>(null);
455
- const [hasOverflow, setHasOverflow] = useState(false);
456
- const [visibleItems, setVisibleItems] = useState<NavigationItem[]>([]);
457
- const [overflowItems, setOverflowItems] = useState<NavigationItem[]>([]);
458
- const [openSubmenus, setOpenSubmenus] = useState<Set<string>>(new Set());
459
- const [hasGroupOverflow, setHasGroupOverflow] = useState(false);
460
- const [visibleGroups, setVisibleGroups] = useState<RouteGroup[]>([]);
461
- const [overflowGroups, setOverflowGroups] = useState<RouteGroup[]>([]);
462
- const [isOverflowAccordionOpen, setIsOverflowAccordionOpen] = useState(false);
463
-
464
- const toggleSubmenu = (path: string) => {
465
- setOpenSubmenus(prev => {
466
- const next = new Set(prev);
467
- if (next.has(path)) {
468
- next.delete(path);
469
- } else {
470
- next.add(path);
471
- }
472
- return next;
473
- });
474
- };
475
-
476
- const labelTranslations = useMemo<Record<string, string>>(
477
- () => ({
478
- home: 'Início',
479
- dashboard: 'Painel',
480
- components: 'Componentes',
481
- }),
482
- []
483
- );
484
-
485
- const navigationItems = useMemo<NavigationItem[]>(
486
- () =>
487
- (routes || []).map(route => ({
488
- ...route,
489
- label: labelTranslations[route.label.toLowerCase()] || route.label,
490
- active: location.pathname === route.path || location.pathname.startsWith(route.path + '/'),
491
- children: route.children,
492
- })),
493
- [routes, location.pathname, labelTranslations]
494
- );
495
-
496
- useEffect(() => {
497
- if (typeof window === 'undefined') return;
498
- const checkOverflow = () => {
499
- if (!navRef.current) return;
500
- if (variant === 'assistant') return;
501
- const navHeight = navRef.current.clientHeight;
502
- const itemHeight = 44;
503
- const maxVisibleItems = Math.floor(navHeight / itemHeight);
504
- if (navigationItems.length > maxVisibleItems) {
505
- setHasOverflow(true);
506
- setVisibleItems(navigationItems.slice(0, maxVisibleItems - 1));
507
- setOverflowItems(navigationItems.slice(maxVisibleItems - 1));
508
- } else {
509
- setHasOverflow(false);
510
- setVisibleItems(navigationItems);
511
- setOverflowItems([]);
512
- }
513
- };
514
- checkOverflow();
515
- window.addEventListener('resize', checkOverflow);
516
- return () => window.removeEventListener('resize', checkOverflow);
517
- }, [navigationItems.length, variant]);
518
-
519
- useEffect(() => {
520
- if (typeof window === 'undefined') return;
521
- if (variant === 'assistant') return;
522
- if (!navigationGroups || navigationGroups.length === 0) return;
523
-
524
- const checkGroupOverflow = () => {
525
- if (!navRef.current) return;
526
-
527
- const containerHeight = navRef.current.clientHeight;
528
- const itemHeight = 40;
529
- const groupHeaderHeight = 32;
530
- const groupSpacing = 12;
531
- const moreButtonHeight = 44;
532
- const padding = 32;
533
-
534
- let currentHeight = padding;
535
- let visibleCount = 0;
536
-
537
- for (let i = 0; i < navigationGroups.length; i++) {
538
- const group = navigationGroups[i];
539
- let groupHeight = 0;
540
- if (group.label) groupHeight += groupHeaderHeight;
541
- groupHeight += group.items.length * itemHeight;
542
- if (i > 0) groupHeight += groupSpacing;
543
-
544
- const wouldExceed =
545
- currentHeight + groupHeight + (visibleCount < navigationGroups.length - 1 ? moreButtonHeight : 0) >
546
- containerHeight;
547
-
548
- if (wouldExceed && visibleCount > 0) break;
549
-
550
- currentHeight += groupHeight;
551
- visibleCount++;
552
- }
553
-
554
- if (visibleCount < navigationGroups.length) {
555
- setHasGroupOverflow(true);
556
- setVisibleGroups(navigationGroups.slice(0, visibleCount));
557
- setOverflowGroups(navigationGroups.slice(visibleCount));
558
- } else {
559
- setHasGroupOverflow(false);
560
- setVisibleGroups(navigationGroups);
561
- setOverflowGroups([]);
562
- }
563
- };
564
-
565
- checkGroupOverflow();
566
- window.addEventListener('resize', checkGroupOverflow);
567
- return () => window.removeEventListener('resize', checkGroupOverflow);
568
- }, [navigationGroups, expanded, variant]);
569
-
570
- const handleNavigate = (path: string) => {
571
- setLocalActiveItem(path);
572
- navigate(path);
573
- if (typeof window !== 'undefined' && window.innerWidth < 768) {
574
- onToggle();
575
- }
576
- };
577
-
578
- const toNavItem = (route: RouteConfig): NavigationItem => ({
579
- path: route.path,
580
- label: labelTranslations[route.label.toLowerCase()] || route.label,
581
- icon: route.icon,
582
- active: location.pathname === route.path || location.pathname.startsWith(route.path + '/'),
583
- children: route.children,
584
- actions: route.actions,
585
- });
586
-
587
- const renderActionItems = (actions: ActionMenuItem[]): React.ReactNode => {
588
- return actions.map((action, idx) => {
589
- const Icon = action.icon;
590
- if (action.children && action.children.length > 0) {
591
- return (
592
- <DropdownMenuSub key={idx}>
593
- <DropdownMenuSubTrigger>
594
- {Icon && <Icon className="mr-2 h-4 w-4" />}
595
- <span>{action.label}</span>
596
- </DropdownMenuSubTrigger>
597
- <DropdownMenuPortal>
598
- <DropdownMenuSubContent className="w-48 bg-popover border-border">
599
- {renderActionItems(action.children)}
600
- </DropdownMenuSubContent>
601
- </DropdownMenuPortal>
602
- </DropdownMenuSub>
603
- );
604
- }
605
- return (
606
- <DropdownMenuItem
607
- key={idx}
608
- className={cn(
609
- 'flex items-center gap-2',
610
- action.variant === 'destructive' ? 'text-destructive focus:text-destructive' : ''
611
- )}
612
- onClick={e => {
613
- e.stopPropagation();
614
- action.onClick?.(null);
615
- }}
616
- >
617
- {Icon && <Icon className="h-4 w-4 flex-shrink-0" />}
618
- <span>{action.label}</span>
619
- </DropdownMenuItem>
620
- );
621
- });
622
- };
623
-
624
- const renderAssistantActionMenu = (actions?: ActionMenuItem[], isHeader: boolean = false) => {
625
- if (!actions || actions.length === 0) return null;
626
- return (
627
- <DropdownMenu>
628
- <DropdownMenuTrigger asChild>
629
- <Button
630
- variant="ghost"
631
- size="icon"
632
- className={cn(
633
- 'h-8 w-8 text-sidebar-foreground/80 hover:bg-sidebar-foreground/20 hover:text-sidebar-foreground rounded-full transition-all',
634
- !isHeader && 'opacity-0 group-hover/item:opacity-100'
635
- )}
636
- aria-label={t('sidebar.moreOptions')}
637
- >
638
- <MoreVertical className="h-4 w-4" />
639
- </Button>
640
- </DropdownMenuTrigger>
641
- <DropdownMenuContent align="end" className="w-48 bg-popover border-border p-1">
642
- {renderActionItems(actions)}
643
- </DropdownMenuContent>
644
- </DropdownMenu>
645
- );
646
- };
647
-
648
- const renderDefaultItem = (item: NavigationItem) => {
649
- const Icon = item.icon;
650
- const hasChildren = item.children && item.children.length > 0;
651
- const activeClass = item.active
652
- ? 'bg-sidebar-foreground/15 text-sidebar-foreground shadow-sm'
653
- : 'text-sidebar-foreground/80 hover:bg-sidebar-foreground/15 hover:text-sidebar-foreground';
654
-
655
- if (!expanded) {
656
- return (
657
- <Tooltip key={item.path}>
658
- <TooltipTrigger asChild>
659
- <button
660
- onClick={() => handleNavigate(item.path)}
661
- className={cn(
662
- 'w-full h-10 flex items-center justify-center px-0 rounded-[var(--radius-button)] transition-all duration-200',
663
- activeClass
664
- )}
665
- aria-label={item.label}
666
- >
667
- {Icon && <Icon className="w-5 h-5 flex-shrink-0" />}
668
- </button>
669
- </TooltipTrigger>
670
- <SidebarTooltipContent side="right" sideOffset={0}>
671
- <p>{item.label}</p>
672
- </SidebarTooltipContent>
673
- </Tooltip>
674
- );
675
- }
676
-
677
- // Mobile: accordion inline — subitens abrem abaixo do item pai
678
- if (isMobileViewport && hasChildren) {
679
- const isOpen = openSubmenus.has(item.path);
680
- return (
681
- <div key={item.path}>
682
- <div
683
- className={cn(
684
- 'group/item flex items-center w-full h-10 rounded-[var(--radius-button)] transition-all duration-200',
685
- activeClass
686
- )}
687
- >
688
- <button
689
- onClick={() => handleNavigate(item.path)}
690
- className="flex items-center gap-3 px-3 flex-1 h-full min-w-0 text-left"
691
- >
692
- {Icon && <Icon className="w-5 h-5 flex-shrink-0" />}
693
- <span className="truncate flex-1">{item.label}</span>
694
- </button>
695
- <button
696
- onClick={e => {
697
- e.stopPropagation();
698
- toggleSubmenu(item.path);
699
- }}
700
- className="h-full px-2 pr-2.5 flex items-center justify-center text-sidebar-foreground/40 hover:text-sidebar-foreground transition-colors"
701
- aria-label={t('sidebar.submenu', { label: item.label })}
702
- aria-expanded={isOpen}
703
- >
704
- <ChevronRight
705
- className={cn('w-4 h-4 transition-transform duration-200', isOpen && 'rotate-90')}
706
- />
707
- </button>
708
- </div>
709
- <AnimatePresence initial={false}>
710
- {isOpen && (
711
- <motion.div
712
- initial={{ height: 0, opacity: 0 }}
713
- animate={{ height: 'auto', opacity: 1 }}
714
- exit={{ height: 0, opacity: 0 }}
715
- transition={{ duration: 0.2 }}
716
- className="overflow-hidden"
717
- >
718
- <div className="ml-4 mt-0.5 mb-0.5 space-y-0.5">
719
- {item.children!.map(child => {
720
- const ChildIcon = child.icon;
721
- const isChildActive =
722
- location.pathname === child.path ||
723
- location.pathname.startsWith(child.path + '/');
724
- return (
725
- <button
726
- key={child.path}
727
- onClick={() => handleNavigate(child.path)}
728
- className={cn(
729
- 'w-full h-9 flex items-center gap-2.5 px-3 rounded-[var(--radius-button)] transition-all duration-200 text-left',
730
- isChildActive
731
- ? 'bg-sidebar-foreground/15 text-sidebar-foreground'
732
- : 'text-sidebar-foreground/70 hover:bg-sidebar-foreground/10 hover:text-sidebar-foreground'
733
- )}
734
- >
735
- {ChildIcon && <ChildIcon className="h-4 w-4 flex-shrink-0" />}
736
- <span className="truncate text-sm">{child.label}</span>
737
- </button>
738
- );
739
- })}
740
- </div>
741
- </motion.div>
742
- )}
743
- </AnimatePresence>
744
- </div>
745
- );
746
- }
747
-
748
- // Desktop: dropdown lateral (comportamento original)
749
- return (
750
- <div
751
- key={item.path}
752
- className={cn(
753
- 'group/item flex items-center w-full h-10 rounded-[var(--radius-button)] transition-all duration-200',
754
- activeClass
755
- )}
756
- >
757
- <button
758
- onClick={() => handleNavigate(item.path)}
759
- className="flex items-center gap-3 px-3 flex-1 h-full min-w-0 text-left"
760
- >
761
- {Icon && <Icon className="w-5 h-5 flex-shrink-0" />}
762
- <span className="truncate flex-1">{item.label}</span>
763
- </button>
764
-
765
- {hasChildren && (
766
- <DropdownMenu>
767
- <DropdownMenuTrigger asChild>
768
- <button
769
- onClick={e => e.stopPropagation()}
770
- className="h-full px-2 pr-2.5 flex items-center justify-center text-sidebar-foreground/40 hover:text-sidebar-foreground transition-colors"
771
- aria-label={t('sidebar.submenu', { label: item.label })}
772
- >
773
- <ChevronRight className="w-4 h-4" />
774
- </button>
775
- </DropdownMenuTrigger>
776
- <DropdownMenuContent
777
- side="right"
778
- align="start"
779
- className="w-48 bg-popover border-border p-1"
780
- >
781
- {item.children!.map(child => {
782
- const ChildIcon = child.icon;
783
- const isChildActive =
784
- location.pathname === child.path ||
785
- location.pathname.startsWith(child.path + '/');
786
- return (
787
- <DropdownMenuItem
788
- key={child.path}
789
- onClick={() => handleNavigate(child.path)}
790
- className={cn(
791
- 'flex items-center gap-2 cursor-pointer',
792
- isChildActive && 'bg-accent text-accent-foreground'
793
- )}
794
- >
795
- {ChildIcon && <ChildIcon className="h-4 w-4 flex-shrink-0" />}
796
- <span className="truncate">{child.label}</span>
797
- </DropdownMenuItem>
798
- );
799
- })}
800
- </DropdownMenuContent>
801
- </DropdownMenu>
802
- )}
803
- </div>
804
- );
805
- };
806
-
807
- const renderDefaultGroup = (group: RouteGroup) => {
808
- const GroupIcon = group.icon;
809
- if (!expanded) {
810
- return (
811
- <div key={group.id} className="space-y-1">
812
- {group.items.map(item => renderDefaultItem(toNavItem(item)))}
813
- </div>
814
- );
815
- }
816
- return (
817
- <div key={group.id}>
818
- {(group.label || GroupIcon) && (
819
- <div className="px-3 mb-1 flex items-center gap-2">
820
- {GroupIcon &&
821
- (React.isValidElement(GroupIcon) ? (
822
- GroupIcon
823
- ) : (
824
- <GroupIcon className="h-3 w-3 text-sidebar-foreground/80" />
825
- ))}
826
- {group.label && (
827
- <span className="text-xs font-semibold uppercase tracking-wider text-sidebar-foreground/80">
828
- {group.label}
829
- </span>
830
- )}
831
- </div>
832
- )}
833
- <div className="space-y-1">
834
- {group.items.map(item => renderDefaultItem(toNavItem(item)))}
835
- </div>
836
- </div>
837
- );
838
- };
839
-
840
- const renderAssistantGroup = (group: RouteGroup) => {
841
- const isAnyItemActive = group.items.some(
842
- item => location.pathname === item.path || location.pathname.startsWith(item.path + '/')
843
- );
844
- const GroupIcon = group.icon;
845
-
846
- if (!expanded) {
847
- if (!GroupIcon) return null;
848
- return (
849
- <div key={group.id} className="py-2 flex justify-center">
850
- <Tooltip>
851
- <TooltipTrigger asChild>
852
- <button
853
- onClick={() => onToggle()}
854
- aria-label={group.label}
855
- className={cn(
856
- 'h-10 w-10 flex items-center justify-center rounded-[var(--radius-button)] transition-all duration-200',
857
- isAnyItemActive
858
- ? 'bg-sidebar-foreground/15 text-sidebar-foreground shadow-sm'
859
- : 'text-sidebar-foreground/80 hover:bg-sidebar-foreground/15 hover:text-sidebar-foreground'
860
- )}
861
- >
862
- {React.isValidElement(GroupIcon) ? GroupIcon : <GroupIcon className="h-5 w-5" />}
863
- </button>
864
- </TooltipTrigger>
865
- <SidebarTooltipContent side="right">
866
- <p>{group.label}</p>
867
- </SidebarTooltipContent>
868
- </Tooltip>
869
- </div>
870
- );
871
- }
872
-
873
- return (
874
- <div key={group.id} className="py-2 group">
875
- {(group.label || group.icon) && (
876
- <div className="flex items-center justify-between px-3 mb-1">
877
- <div className="flex items-center gap-2 text-sidebar-foreground/80 text-xs font-semibold uppercase tracking-wider">
878
- {GroupIcon && (
879
- <div className="flex items-center justify-center">
880
- {React.isValidElement(GroupIcon) ? GroupIcon : <GroupIcon className="h-4 w-4" />}
881
- </div>
882
- )}
883
- {group.label && <span>{group.label}</span>}
884
- </div>
885
- {renderAssistantActionMenu(group.actions, true)}
886
- </div>
887
- )}
888
- <div className="space-y-1">
889
- {group.items.map(item => {
890
- const isRouteActive =
891
- location.pathname === item.path || location.pathname.startsWith(item.path + '/');
892
- const isActive = isRouteActive || localActiveItem === item.path;
893
- const Icon = item.icon;
894
- return (
895
- <div
896
- key={item.path}
897
- className={`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'}`}
898
- onClick={() => handleNavigate(item.path)}
899
- >
900
- <div className="flex flex-col min-w-0 flex-1">
901
- <div className="flex items-center gap-3 overflow-hidden h-5">
902
- {Icon && (
903
- <div className="w-4 h-4 flex-shrink-0 flex items-center justify-center">
904
- {React.isValidElement(Icon) ? Icon : <Icon className="w-4 h-4" />}
905
- </div>
906
- )}
907
- <span className="truncate text-sm font-medium leading-none">{item.label}</span>
908
- </div>
909
- {isActive && item.description && (
910
- <div className="text-[11px] text-sidebar-foreground/60 mt-1.5 animate-in fade-in slide-in-from-top-1 duration-200">
911
- {item.description}
912
- </div>
913
- )}
914
- </div>
915
- <div className="h-5 flex items-center ml-1">
916
- {renderAssistantActionMenu(item.actions)}
917
- </div>
918
- </div>
919
- );
920
- })}
921
- </div>
922
- </div>
923
- );
924
- };
925
-
926
- if (variant === 'assistant') {
927
- return (
928
- <div className="flex-1 min-h-0 overflow-hidden">
929
- <ScrollArea className="h-full px-4">
930
- {navigationGroups.map(group => renderAssistantGroup(group))}
931
- </ScrollArea>
932
- </div>
933
- );
934
- }
935
-
936
- if (isMobileViewport && isOverflowAccordionOpen) {
937
- return (
938
- <div className="flex-1 min-h-0 overflow-hidden">
939
- <ScrollArea className="h-full">
940
- <nav
941
- id="sidebar-nav"
942
- aria-label={t('sidebar.mainNavigation')}
943
- className="px-4 py-4"
944
- ref={navRef}
945
- >
946
- {navigationGroups && navigationGroups.length > 0 ? (
947
- <div className="space-y-3">
948
- {(hasGroupOverflow ? visibleGroups : navigationGroups).map(group => (
949
- <React.Fragment key={group.id}>{renderDefaultGroup(group)}</React.Fragment>
950
- ))}
951
- {hasGroupOverflow && (
952
- <OverflowGroupsAccordion
953
- expanded={expanded}
954
- overflowGroups={overflowGroups}
955
- location={location}
956
- handleNavigate={handleNavigate}
957
- moreOptionsLabel={t('sidebar.moreOptions')}
958
- onOpenChange={setIsOverflowAccordionOpen}
959
- />
960
- )}
961
- </div>
962
- ) : (
963
- <div className="space-y-1">
964
- {(hasOverflow ? visibleItems : navigationItems).map(item => renderDefaultItem(item))}
965
- {hasOverflow && (
966
- <Popover>
967
- <PopoverTrigger asChild>
968
- <button
969
- className={
970
- expanded
971
- ? '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'
972
- : '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'
973
- }
974
- aria-label={t('sidebar.moreOptions')}
975
- >
976
- <MoreVertical className="w-5 h-5 flex-shrink-0" />
977
- {expanded && (
978
- <span className="truncate text-sidebar-foreground">
979
- {t('sidebar.moreOptions')}
980
- </span>
981
- )}
982
- </button>
983
- </PopoverTrigger>
984
- <PopoverContent
985
- side="right"
986
- align="start"
987
- className="w-56 p-2 bg-popover border border-border rounded-[var(--radius-card)] shadow-lg"
988
- sideOffset={8}
989
- >
990
- <div className="space-y-1">
991
- {overflowItems.map(item => {
992
- const Icon = item.icon;
993
- return (
994
- <button
995
- key={item.path}
996
- onClick={() => handleNavigate(item.path)}
997
- 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"
998
- >
999
- {Icon && <Icon className="w-4 h-4 flex-shrink-0" />}
1000
- <span className="truncate">{item.label}</span>
1001
- </button>
1002
- );
1003
- })}
1004
- </div>
1005
- </PopoverContent>
1006
- </Popover>
1007
- )}
1008
- </div>
1009
- )}
1010
- </nav>
1011
- </ScrollArea>
1012
- </div>
1013
- );
1014
- }
1015
-
1016
- return (
1017
- <div className="flex-1 min-h-0 overflow-hidden">
1018
- <nav
1019
- id="sidebar-nav"
1020
- aria-label={t('sidebar.mainNavigation')}
1021
- className="h-full px-4 py-4 overflow-hidden"
1022
- ref={navRef}
1023
- >
1024
- {navigationGroups && navigationGroups.length > 0 ? (
1025
- <div className="space-y-3">
1026
- {(hasGroupOverflow ? visibleGroups : navigationGroups).map(group => (
1027
- <React.Fragment key={group.id}>{renderDefaultGroup(group)}</React.Fragment>
1028
- ))}
1029
- {hasGroupOverflow && (
1030
- isMobileViewport ? (
1031
- // Mobile: accordion inline abre abaixo, igual aos subitens
1032
- <OverflowGroupsAccordion
1033
- expanded={expanded}
1034
- overflowGroups={overflowGroups}
1035
- location={location}
1036
- handleNavigate={handleNavigate}
1037
- moreOptionsLabel={t('sidebar.moreOptions')}
1038
- onOpenChange={setIsOverflowAccordionOpen}
1039
- />
1040
- ) : (
1041
- // Desktop: popover lateral direito
1042
- <Popover>
1043
- <PopoverTrigger asChild>
1044
- <button
1045
- className={
1046
- expanded
1047
- ? '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'
1048
- : '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'
1049
- }
1050
- aria-label={t('sidebar.moreOptions')}
1051
- >
1052
- <MoreVertical className="w-5 h-5 flex-shrink-0" />
1053
- {expanded && (
1054
- <span className="truncate text-sidebar-foreground">
1055
- {t('sidebar.moreOptions')}
1056
- </span>
1057
- )}
1058
- </button>
1059
- </PopoverTrigger>
1060
- <PopoverContent
1061
- side="right"
1062
- align="start"
1063
- className="w-56 p-2 bg-popover border border-border rounded-[var(--radius-card)] shadow-lg"
1064
- sideOffset={8}
1065
- >
1066
- <div className="space-y-3">
1067
- {overflowGroups.map(group => {
1068
- const GroupIcon = group.icon;
1069
- return (
1070
- <div key={group.id}>
1071
- {(group.label || GroupIcon) && (
1072
- <div className="px-2 mb-1 flex items-center gap-2">
1073
- {GroupIcon &&
1074
- (React.isValidElement(GroupIcon) ? (
1075
- GroupIcon
1076
- ) : (
1077
- <GroupIcon className="h-3 w-3 text-popover-foreground/60" />
1078
- ))}
1079
- {group.label && (
1080
- <span className="text-xs font-semibold uppercase tracking-wider text-popover-foreground/60">
1081
- {group.label}
1082
- </span>
1083
- )}
1084
- </div>
1085
- )}
1086
- <div className="space-y-1">
1087
- {group.items.map(item => {
1088
- const Icon = item.icon;
1089
- const isActive =
1090
- location.pathname === item.path ||
1091
- location.pathname.startsWith(item.path + '/');
1092
- return (
1093
- <button
1094
- key={item.path}
1095
- onClick={() => handleNavigate(item.path)}
1096
- className={cn(
1097
- 'w-full h-9 flex items-center gap-2 px-2 rounded-[var(--radius-button)] transition-all duration-200 text-left text-sm',
1098
- isActive
1099
- ? 'bg-accent text-accent-foreground font-medium'
1100
- : 'text-popover-foreground/80 hover:bg-accent hover:text-accent-foreground'
1101
- )}
1102
- >
1103
- {Icon && <Icon className="w-4 h-4 flex-shrink-0" />}
1104
- <span className="truncate">{item.label}</span>
1105
- </button>
1106
- );
1107
- })}
1108
- </div>
1109
- </div>
1110
- );
1111
- })}
1112
- </div>
1113
- </PopoverContent>
1114
- </Popover>
1115
- )
1116
- )}
1117
- </div>
1118
- ) : (
1119
- <div className="space-y-1">
1120
- {(hasOverflow ? visibleItems : navigationItems).map(item => renderDefaultItem(item))}
1121
- {hasOverflow && (
1122
- <Popover>
1123
- <PopoverTrigger asChild>
1124
- <button
1125
- className={
1126
- expanded
1127
- ? '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'
1128
- : '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'
1129
- }
1130
- aria-label={t('sidebar.moreOptions')}
1131
- >
1132
- <MoreVertical className="w-5 h-5 flex-shrink-0" />
1133
- {expanded && (
1134
- <span className="truncate text-sidebar-foreground">
1135
- {t('sidebar.moreOptions')}
1136
- </span>
1137
- )}
1138
- </button>
1139
- </PopoverTrigger>
1140
- <PopoverContent
1141
- side="right"
1142
- align="start"
1143
- className="w-56 p-2 bg-popover border border-border rounded-[var(--radius-card)] shadow-lg"
1144
- sideOffset={8}
1145
- >
1146
- <div className="space-y-1">
1147
- {overflowItems.map(item => {
1148
- const Icon = item.icon;
1149
- return (
1150
- <button
1151
- key={item.path}
1152
- onClick={() => handleNavigate(item.path)}
1153
- 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"
1154
- >
1155
- {Icon && <Icon className="w-4 h-4 flex-shrink-0" />}
1156
- <span className="truncate">{item.label}</span>
1157
- </button>
1158
- );
1159
- })}
1160
- </div>
1161
- </PopoverContent>
1162
- </Popover>
1163
- )}
1164
- </div>
1165
- )}
1166
- </nav>
1167
- </div>
1168
- );
1169
- }
1170
-
1171
- /**
1172
- * Assistant-specific search + fixed area header for the Sidebar.
1173
- */
1174
- function SidebarSearch({
1175
- fixedArea,
1176
- search,
1177
- }: {
1178
- fixedArea?: SidebarFixedAreaConfig;
1179
- search?: SidebarSearchConfig;
1180
- }) {
1181
- const { expanded } = useSidebarContext();
1182
- const { t } = useTranslation();
1183
- const [isFilterOpen, setIsFilterOpen] = useState(false);
1184
-
1185
- if (!((fixedArea && fixedArea.show) || (search && search.show))) return null;
1186
-
1187
- return (
1188
- <div className="flex-shrink-0 px-4 pb-4 space-y-4 border-b border-sidebar-border/30 mb-2">
1189
- {fixedArea?.show && fixedArea.content && expanded && (
1190
- <div className="animate-in fade-in slide-in-from-top-1 duration-300">
1191
- {fixedArea.content}
1192
- </div>
1193
- )}
1194
- {search?.show && expanded && (
1195
- <>
1196
- <div className="flex items-center gap-2">
1197
- <div className="relative flex-1">
1198
- <SearchIcon className="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-sidebar-foreground/50" />
1199
- <Input
1200
- type="text"
1201
- placeholder={search.placeholder || t('sidebar.searchPlaceholder')}
1202
- aria-label={search.placeholder || t('sidebar.searchPlaceholder')}
1203
- value={search.value}
1204
- onChange={e => search.onChange?.(e.target.value)}
1205
- 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"
1206
- />
1207
- </div>
1208
- {search.filter?.show && search.filter.content && (
1209
- <Button
1210
- variant="ghost"
1211
- size="icon"
1212
- onClick={() => setIsFilterOpen(!isFilterOpen)}
1213
- className={cn(
1214
- 'h-9 w-9 text-sidebar-foreground transition-all duration-200',
1215
- isFilterOpen ? 'bg-sidebar-foreground/20' : 'hover:bg-sidebar-foreground/15'
1216
- )}
1217
- aria-label={isFilterOpen ? t('sidebar.closeFilters') : t('sidebar.openFilters')}
1218
- >
1219
- {search.filter.icon || <Filter className="h-4 w-4" />}
1220
- </Button>
1221
- )}
1222
- </div>
1223
- <AnimatePresence>
1224
- {isFilterOpen && search.filter?.show && search.filter.content && (
1225
- <motion.div
1226
- initial={{ height: 0, opacity: 0 }}
1227
- animate={{ height: 'auto', opacity: 1 }}
1228
- exit={{ height: 0, opacity: 0 }}
1229
- transition={{ duration: 0.2 }}
1230
- className="overflow-hidden"
1231
- >
1232
- <div className="pt-2 border-t border-sidebar-border/20">
1233
- {search.filter.content}
1234
- </div>
1235
- </motion.div>
1236
- )}
1237
- </AnimatePresence>
1238
- </>
1239
- )}
1240
- {!expanded && (fixedArea?.show || search?.show) && (
1241
- <div className="flex flex-col items-center gap-4 py-2">
1242
- {fixedArea?.show && fixedArea.icon && (
1243
- <Tooltip>
1244
- <TooltipTrigger asChild>
1245
- <button
1246
- onClick={() => fixedArea.onClick?.()}
1247
- 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"
1248
- aria-label={t('assistant.newConversation')}
1249
- >
1250
- {React.isValidElement(fixedArea.icon) ? (
1251
- fixedArea.icon
1252
- ) : (
1253
- <fixedArea.icon className="h-5 w-5" />
1254
- )}
1255
- </button>
1256
- </TooltipTrigger>
1257
- <SidebarTooltipContent side="right">
1258
- {t('assistant.newConversation')}
1259
- </SidebarTooltipContent>
1260
- </Tooltip>
1261
- )}
1262
- {search?.show && (
1263
- <Tooltip>
1264
- <TooltipTrigger asChild>
1265
- <button
1266
- 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"
1267
- aria-label={t('sidebar.search')}
1268
- >
1269
- <SearchIcon className="h-5 w-5" />
1270
- </button>
1271
- </TooltipTrigger>
1272
- <SidebarTooltipContent side="right">{t('sidebar.search')}</SidebarTooltipContent>
1273
- </Tooltip>
1274
- )}
1275
- </div>
1276
- )}
1277
- </div>
1278
- );
1279
- }
1280
-
1281
- /**
1282
- * Footer area for the Sidebar with user info, settings, and logout.
1283
- */
1284
- function SidebarFooter({
1285
- user,
1286
- onLogout = () => {},
1287
- onSettingsClick,
1288
- showUser = true,
1289
- showSettings = true,
1290
- showLogout = true,
1291
- }: {
1292
- user?: { name?: string; email?: string; avatar?: string } | null;
1293
- onLogout?: () => void;
1294
- onSettingsClick?: () => void;
1295
- showUser?: boolean;
1296
- showSettings?: boolean;
1297
- showLogout?: boolean;
1298
- }) {
1299
- const { expanded, navigate, location, onToggle } = useSidebarContext();
1300
- const { t } = useTranslation();
1301
- const isSettingsActive = location.pathname === '/settings';
1302
-
1303
- const handleSettingsClick = () => {
1304
- if (onSettingsClick) {
1305
- onSettingsClick();
1306
- } else {
1307
- navigate('/settings');
1308
- }
1309
- if (typeof window !== 'undefined' && window.innerWidth < 768) {
1310
- onToggle();
1311
- }
1312
- };
1313
-
1314
- return (
1315
- <div className="flex-shrink-0 p-4 space-y-2">
1316
- {showUser &&
1317
- (!expanded ? (
1318
- <Tooltip>
1319
- <TooltipTrigger asChild>
1320
- <button
1321
- 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"
1322
- aria-label={t('sidebar.userProfile')}
1323
- >
1324
- <Avatar className="w-7 h-7 flex-shrink-0">
1325
- <AvatarImage src={user?.avatar} alt={user?.name || 'User'} />
1326
- <AvatarFallback className="bg-sidebar-foreground/15 text-sidebar-foreground text-xs">
1327
- {user?.name ? user.name.charAt(0).toUpperCase() : 'U'}
1328
- </AvatarFallback>
1329
- </Avatar>
1330
- </button>
1331
- </TooltipTrigger>
1332
- <SidebarTooltipContent side="right" sideOffset={0}>
1333
- <p>{user?.name || t('sidebar.profile')}</p>
1334
- </SidebarTooltipContent>
1335
- </Tooltip>
1336
- ) : (
1337
- <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">
1338
- <Avatar className="w-7 h-7 flex-shrink-0">
1339
- <AvatarImage src={user?.avatar} alt={user?.name || 'User'} />
1340
- <AvatarFallback className="bg-sidebar-foreground/15 text-sidebar-foreground text-xs">
1341
- {user?.name ? user.name.charAt(0).toUpperCase() : 'U'}
1342
- </AvatarFallback>
1343
- </Avatar>
1344
- <span className="text-sidebar-foreground truncate">
1345
- {user?.name || t('sidebar.profile')}
1346
- </span>
1347
- </button>
1348
- ))}
1349
-
1350
- {showSettings &&
1351
- (!expanded ? (
1352
- <Tooltip>
1353
- <TooltipTrigger asChild>
1354
- <button
1355
- onClick={handleSettingsClick}
1356
- className={cn(
1357
- 'w-full h-10 flex items-center justify-center px-0 rounded-[var(--radius-button)] transition-all duration-200',
1358
- isSettingsActive
1359
- ? 'bg-sidebar-foreground/15 text-sidebar-foreground shadow-sm'
1360
- : 'text-sidebar-foreground/80 hover:bg-sidebar-foreground/15 hover:text-sidebar-foreground'
1361
- )}
1362
- aria-label={t('nav.settings')}
1363
- >
1364
- <Settings className="w-5 h-5 flex-shrink-0" />
1365
- </button>
1366
- </TooltipTrigger>
1367
- <SidebarTooltipContent side="right" sideOffset={0}>
1368
- <p>{t('nav.settings')}</p>
1369
- </SidebarTooltipContent>
1370
- </Tooltip>
1371
- ) : (
1372
- <button
1373
- onClick={handleSettingsClick}
1374
- className={cn(
1375
- 'w-full h-10 flex items-center gap-3 px-3 justify-start rounded-[var(--radius-button)] transition-all duration-200',
1376
- isSettingsActive
1377
- ? 'bg-sidebar-foreground/15 text-sidebar-foreground shadow-sm'
1378
- : 'text-sidebar-foreground/80 hover:bg-sidebar-foreground/15 hover:text-sidebar-foreground'
1379
- )}
1380
- >
1381
- <Settings className="w-5 h-5 flex-shrink-0" />
1382
- <span className="truncate text-sidebar-foreground">{t('nav.settings')}</span>
1383
- </button>
1384
- ))}
1385
-
1386
- {showLogout &&
1387
- (!expanded ? (
1388
- <Tooltip>
1389
- <TooltipTrigger asChild>
1390
- <button
1391
- onClick={onLogout}
1392
- 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"
1393
- aria-label={t('sidebar.logout')}
1394
- >
1395
- <LogOut className="w-5 h-5 flex-shrink-0" />
1396
- </button>
1397
- </TooltipTrigger>
1398
- <SidebarTooltipContent side="right" sideOffset={0}>
1399
- <p>{t('sidebar.logout')}</p>
1400
- </SidebarTooltipContent>
1401
- </Tooltip>
1402
- ) : (
1403
- <button
1404
- onClick={onLogout}
1405
- 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"
1406
- >
1407
- <LogOut className="w-5 h-5 flex-shrink-0" />
1408
- <span className="truncate text-sidebar-foreground">{t('sidebar.logout')}</span>
1409
- </button>
1410
- ))}
1411
- </div>
1412
- );
1413
- }
1414
-
1415
- // ─────────────────────────────────────────────────────────────────────────────
1416
- // Main Sidebar component (backward-compatible monolithic API)
1417
- // ─────────────────────────────────────────────────────────────────────────────
1418
-
1419
- /**
1420
- * Primary navigation sidebar component.
1421
- *
1422
- * @description
1423
- * Manages desktop/mobile responsive navigation rendering with two variants:
1424
- * - `"default"` simple flat or grouped route list.
1425
- * - `"assistant"` — advanced variant with fixed areas, search, filters, and grouped navigation.
1426
- *
1427
- * This component is autonomous: it works out-of-the-box using local state or
1428
- * integrates automatically with `LayoutContext` if wrapped in `LayoutProvider`.
1429
- *
1430
- * For advanced customization, use the Compound Component API:
1431
- * `<Sidebar.Root>`, `<Sidebar.Header>`, `<Sidebar.Search>`, `<Sidebar.Nav>`, `<Sidebar.Footer>`
1432
- *
1433
- * @ai-rules
1434
- * 1. NEVER recreate the sidebar with raw Tailwind classes — always use this component.
1435
- * 2. Use `variant="assistant"` for AI/tool sidebars; use `variant="default"` for standard navigation.
1436
- * 3. Supports `Ctrl+B` keyboard shortcut automatically via `LayoutProvider`.
1437
- */
1438
- export function Sidebar({
1439
- expanded: expandedProp,
1440
- onToggle: onToggleProp,
1441
- user,
1442
- onLogout = () => {},
1443
- onSettingsClick,
1444
- location: locationProp,
1445
- navigate: navigateProp,
1446
- routes,
1447
- logo,
1448
- logoCollapsed,
1449
- variant = 'default',
1450
- fixedArea,
1451
- search,
1452
- navigationGroups = [],
1453
- footer,
1454
- showFooter,
1455
- width: widthProp,
1456
- }: SidebarProps) {
1457
- const { showUser = true, showSettings = true, showLogout = true } = footer || {};
1458
-
1459
- const displayFooter = showFooter !== undefined ? showFooter : variant === 'default';
1460
-
1461
- return (
1462
- <SidebarRoot
1463
- expanded={expandedProp}
1464
- onToggle={onToggleProp}
1465
- navigate={navigateProp}
1466
- location={locationProp}
1467
- width={widthProp}
1468
- >
1469
- <SidebarHeader logo={logo} logoCollapsed={logoCollapsed} />
1470
-
1471
- {variant === 'assistant' && <SidebarSearch fixedArea={fixedArea} search={search} />}
1472
-
1473
- <SidebarNav navigationGroups={navigationGroups} routes={routes} variant={variant} />
1474
-
1475
- {displayFooter && (showUser || showSettings || showLogout) && (
1476
- <SidebarFooter
1477
- user={user}
1478
- onLogout={onLogout}
1479
- onSettingsClick={onSettingsClick}
1480
- showUser={showUser}
1481
- showSettings={showSettings}
1482
- showLogout={showLogout}
1483
- />
1484
- )}
1485
- </SidebarRoot>
1486
- );
1487
- }
1488
-
1489
- // ─────────────────────────────────────────────────────────────────────────────
1490
- // Attach Compound Components to Sidebar namespace
1491
- // ─────────────────────────────────────────────────────────────────────────────
1492
-
1493
- Sidebar.Root = SidebarRoot;
1494
- Sidebar.Header = SidebarHeader;
1495
- Sidebar.Search = SidebarSearch;
1496
- Sidebar.Nav = SidebarNav;
1497
- Sidebar.Footer = SidebarFooter;
1498
-
1499
- // Re-export hook for headless usage
1500
- export { useSidebar } from './use-sidebar';
1501
- export type { UseSidebarProps } from './use-sidebar';
1
+ import React, { useState, useRef, useEffect, createContext, useContext, useMemo } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import { useTranslation } from 'react-i18next';
4
+ import {
5
+ Menu,
6
+ ArrowLeft,
7
+ LogOut,
8
+ Settings,
9
+ MoreVertical,
10
+ ChevronRight,
11
+ Filter,
12
+ } from 'lucide-react';
13
+ import { Avatar, AvatarFallback, AvatarImage } from '../../ui/avatar';
14
+ import { Popover, PopoverContent, PopoverTrigger } from '../../ui/popover';
15
+ import { Tooltip, TooltipProvider, TooltipTrigger } from '../../ui/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 { Input } from '../../ui/input';
27
+ import { Search as SearchIcon } from 'lucide-react';
28
+ import { cn } from '../../shared/utils';
29
+ import { XerticaLogo } from '../../brand/xertica-logo';
30
+ import { XerticaXLogo } from '../../brand/xertica-xlogo';
31
+ import { Button } from '../../ui/button';
32
+ import { useOptionalLayout } from '../../../contexts/LayoutContext';
33
+ import { CustomTooltipContent as SidebarTooltipContent } from '../../shared/CustomTooltipContent';
34
+
35
+ // ─────────────────────────────────────────────────────────────────────────────
36
+ // Public interfaces
37
+ // ─────────────────────────────────────────────────────────────────────────────
38
+
39
+ /**
40
+ * Contextual action menu item for routes and groups.
41
+ */
42
+ export interface ActionMenuItem {
43
+ /** Text label */
44
+ label: string;
45
+ /** Optional icon component */
46
+ icon?: React.ComponentType<{ className?: string }>;
47
+ /** Click handler */
48
+ onClick?: (item: unknown) => void;
49
+ /** Visual variant */
50
+ variant?: 'default' | 'destructive';
51
+ /** Nested sub-actions */
52
+ children?: ActionMenuItem[];
53
+ }
54
+
55
+ /**
56
+ * Navigation route configuration.
57
+ */
58
+ export interface RouteConfig {
59
+ /** Relative or absolute path */
60
+ path: string;
61
+ /** Navigation label */
62
+ label: string;
63
+ /** Icon component */
64
+ icon?: React.ComponentType<{ className?: string }>;
65
+ /** React component for the route (optional) */
66
+ component?: React.ComponentType<{ className?: string }>;
67
+ /** Hover action menu items (ellipsis) */
68
+ actions?: ActionMenuItem[];
69
+ /** Auxiliary content shown when the route is selected */
70
+ description?: React.ReactNode;
71
+ /** Child routes exposed via contextual menu button at the end of the item */
72
+ children?: RouteConfig[];
73
+ }
74
+
75
+ export interface NavigationItem {
76
+ path: string;
77
+ label: string;
78
+ icon?: React.ComponentType<{ className?: string }>;
79
+ active: boolean;
80
+ children?: RouteConfig[];
81
+ actions?: ActionMenuItem[];
82
+ }
83
+
84
+ export interface SidebarFilterConfig {
85
+ show: boolean;
86
+ content?: React.ReactNode;
87
+ icon?: React.ReactNode;
88
+ }
89
+
90
+ export interface SidebarSearchConfig {
91
+ show: boolean;
92
+ placeholder?: string;
93
+ value?: string;
94
+ onChange?: (value: string) => void;
95
+ filter?: SidebarFilterConfig;
96
+ }
97
+
98
+ export interface SidebarFixedAreaConfig {
99
+ show: boolean;
100
+ content?: React.ReactNode;
101
+ icon?: React.ComponentType<{ className?: string }> | React.ReactElement;
102
+ onClick?: () => void;
103
+ }
104
+
105
+ export interface SidebarFooterConfig {
106
+ showUser?: boolean;
107
+ showSettings?: boolean;
108
+ showLogout?: boolean;
109
+ }
110
+
111
+ /**
112
+ * Logical grouping of navigation routes (e.g., in Assistant variant).
113
+ */
114
+ export interface RouteGroup {
115
+ /** Unique ID */
116
+ id: string;
117
+ /** Group title */
118
+ label?: string;
119
+ /** Group icon */
120
+ icon?: React.ComponentType<{ className?: string }>;
121
+ /** Navigational items in the group */
122
+ items: RouteConfig[];
123
+ /** Context menu for the entire group */
124
+ actions?: ActionMenuItem[];
125
+ }
126
+
127
+ /**
128
+ * Navigation Sidebar component.
129
+ */
130
+ export interface SidebarProps {
131
+ /** Whether the sidebar is expanded (defaults to LayoutContext state if available) */
132
+ expanded?: boolean;
133
+ /** Callback to toggle expansion state (defaults to LayoutContext toggle if available) */
134
+ onToggle?: () => void;
135
+ /** Authenticated user info */
136
+ user?: {
137
+ name?: string;
138
+ email?: string;
139
+ avatar?: string;
140
+ } | null;
141
+ /** Logout callback */
142
+ onLogout?: () => void;
143
+ /** Settings callback */
144
+ onSettingsClick?: () => void;
145
+ /** Current location for active state detection (defaults to window.location if missing) */
146
+ location?: { pathname: string };
147
+ /** Navigation callback (defaults to window.location.href if missing) */
148
+ navigate?: (path: string) => void;
149
+ /** Flat list of navigation routes */
150
+ routes?: RouteConfig[];
151
+ /** Logo shown in expanded state */
152
+ logo?: React.ReactNode;
153
+ /** Logo shown in collapsed state */
154
+ logoCollapsed?: React.ReactNode;
155
+ /** Visual variant */
156
+ variant?: 'default' | 'assistant';
157
+ /** Assistant-only fixed area configuration */
158
+ fixedArea?: SidebarFixedAreaConfig;
159
+ /** Assistant-only search bar configuration */
160
+ search?: SidebarSearchConfig;
161
+ /** Grouped navigation items */
162
+ navigationGroups?: RouteGroup[];
163
+ /** Footer content configuration */
164
+ footer?: SidebarFooterConfig;
165
+ /** Whether to show the footer (defaults: true for 'default', false for 'assistant') */
166
+ showFooter?: boolean;
167
+ /** Pixel width when expanded (desktop) */
168
+ width?: number;
169
+ }
170
+
171
+ // ─────────────────────────────────────────────────────────────────────────────
172
+ // Compound Component Context
173
+ // ─────────────────────────────────────────────────────────────────────────────
174
+
175
+ interface SidebarContextValue {
176
+ expanded: boolean;
177
+ isMobileViewport: boolean;
178
+ onToggle: () => void;
179
+ navigate: (path: string) => void;
180
+ location: { pathname: string };
181
+ width: number;
182
+ }
183
+
184
+ const SidebarContext = createContext<SidebarContextValue | null>(null);
185
+
186
+ function useSidebarContext() {
187
+ const ctx = useContext(SidebarContext);
188
+ if (!ctx) {
189
+ throw new Error('Sidebar compound components must be used within <Sidebar.Root>');
190
+ }
191
+ return ctx;
192
+ }
193
+
194
+ // ─────────────────────────────────────────────────────────────────────────────
195
+ // Compound Sub-components
196
+ // ─────────────────────────────────────────────────────────────────────────────
197
+
198
+ /**
199
+ * Root container for the Sidebar. Provides context to all sub-components.
200
+ * Use this when building a fully custom sidebar layout.
201
+ *
202
+ * @example
203
+ * <Sidebar.Root expanded={expanded} onToggle={toggle} width={280}>
204
+ * <Sidebar.Header logo={<MyLogo />} />
205
+ * <Sidebar.Nav navigationGroups={groups} />
206
+ * <Sidebar.Footer user={user} onLogout={logout} />
207
+ * </Sidebar.Root>
208
+ */
209
+ function SidebarRoot({
210
+ expanded: expandedProp,
211
+ onToggle: onToggleProp,
212
+ navigate: navigateProp,
213
+ location: locationProp,
214
+ width: widthProp,
215
+ children,
216
+ className,
217
+ }: {
218
+ expanded?: boolean;
219
+ onToggle?: () => void;
220
+ navigate?: (path: string) => void;
221
+ location?: { pathname: string };
222
+ width?: number;
223
+ children: React.ReactNode;
224
+ className?: string;
225
+ }) {
226
+ const layoutContext = useOptionalLayout();
227
+ const [localExpanded, setLocalExpanded] = useState(false);
228
+ const [isMobileViewport, setIsMobileViewport] = useState(false);
229
+
230
+ const expanded =
231
+ expandedProp !== undefined ? expandedProp : (layoutContext?.sidebarExpanded ?? localExpanded);
232
+ const onToggle =
233
+ onToggleProp || layoutContext?.toggleSidebar || (() => setLocalExpanded(prev => !prev));
234
+ const width = widthProp !== undefined ? widthProp : (layoutContext?.sidebarWidth ?? 280);
235
+ const navigate =
236
+ navigateProp ||
237
+ ((path: string) => {
238
+ if (typeof window !== 'undefined') window.location.href = path;
239
+ });
240
+ const location =
241
+ locationProp || (typeof window !== 'undefined' ? window.location : { pathname: '/' });
242
+
243
+ useEffect(() => {
244
+ const checkViewport = () => setIsMobileViewport(window.innerWidth < 768);
245
+ checkViewport();
246
+ window.addEventListener('resize', checkViewport);
247
+ return () => window.removeEventListener('resize', checkViewport);
248
+ }, []);
249
+
250
+ return (
251
+ <SidebarContext.Provider
252
+ value={{ expanded, isMobileViewport, onToggle, navigate, location, width }}
253
+ >
254
+ <TooltipProvider delayDuration={300}>
255
+ <style>{`
256
+ @media (max-width: 767px) {
257
+ [style*="padding-left"].flex-1,
258
+ [style*="paddingLeft"].flex-1 {
259
+ padding-left: 0 !important;
260
+ }
261
+ }
262
+ `}</style>
263
+ <div
264
+ className={cn(
265
+ 'bg-sidebar text-sidebar-foreground transition-all duration-300 ease-in-out flex flex-col z-50',
266
+ expanded
267
+ ? 'fixed inset-0 md:fixed md:inset-y-0 md:left-0'
268
+ : 'fixed inset-y-0 left-0 w-20 -translate-x-full md:translate-x-0',
269
+ className
270
+ )}
271
+ style={expanded && !isMobileViewport ? { width: `${width}px` } : undefined}
272
+ >
273
+ {children}
274
+ </div>
275
+ </TooltipProvider>
276
+ </SidebarContext.Provider>
277
+ );
278
+ }
279
+
280
+ /**
281
+ * Toggle button + logo header area for the Sidebar.
282
+ */
283
+ function SidebarHeader({
284
+ logo,
285
+ logoCollapsed,
286
+ }: {
287
+ logo?: React.ReactNode;
288
+ logoCollapsed?: React.ReactNode;
289
+ }) {
290
+ const { expanded, onToggle } = useSidebarContext();
291
+ const { t } = useTranslation();
292
+
293
+ return (
294
+ <>
295
+ {/* Menu Toggle Button */}
296
+ <div className="flex-shrink-0 p-[14px] pt-[13px] pr-[14px] pb-[12px] pl-[14px]">
297
+ <button
298
+ onClick={onToggle}
299
+ 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"
300
+ aria-label={expanded ? t('sidebar.collapse') : t('sidebar.expand')}
301
+ aria-expanded={expanded}
302
+ aria-controls="sidebar-nav"
303
+ >
304
+ {expanded ? <ArrowLeft className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
305
+ </button>
306
+ </div>
307
+
308
+ {/* Logo */}
309
+ <div className="flex-shrink-0 px-4 py-4">
310
+ <div className="flex items-center h-10 justify-center">
311
+ <div className="flex items-center justify-center flex-shrink-0">
312
+ {expanded
313
+ ? logo || <XerticaLogo className="h-5 w-auto" variant="white" />
314
+ : logoCollapsed || <XerticaXLogo className="h-5 w-auto" variant="white" />}
315
+ </div>
316
+ </div>
317
+ </div>
318
+ </>
319
+ );
320
+ }
321
+
322
+ /**
323
+ * Accordion inline para "mais opções" no mobile.
324
+ * Abre abaixo do botão, igual ao padrão dos subitens mobile.
325
+ */
326
+ function OverflowGroupsAccordion({
327
+ expanded,
328
+ overflowGroups,
329
+ location,
330
+ handleNavigate,
331
+ moreOptionsLabel,
332
+ onOpenChange,
333
+ }: {
334
+ expanded: boolean;
335
+ overflowGroups: RouteGroup[];
336
+ location: { pathname: string };
337
+ handleNavigate: (path: string) => void;
338
+ moreOptionsLabel: string;
339
+ onOpenChange?: (open: boolean) => void;
340
+ }) {
341
+ const [isOpen, setIsOpen] = useState(false);
342
+
343
+ const toggle = () => {
344
+ const next = !isOpen;
345
+ setIsOpen(next);
346
+ onOpenChange?.(next);
347
+ };
348
+
349
+ return (
350
+ <div>
351
+ <button
352
+ onClick={toggle}
353
+ className={cn(
354
+ 'w-full h-10 flex items-center rounded-[var(--radius-button)] transition-all duration-200 text-sidebar-foreground/80 hover:bg-sidebar-foreground/15 hover:text-sidebar-foreground',
355
+ expanded ? 'gap-3 px-3 justify-start' : 'justify-center px-0'
356
+ )}
357
+ aria-expanded={isOpen}
358
+ aria-label={moreOptionsLabel}
359
+ >
360
+ <MoreVertical className="w-5 h-5 flex-shrink-0" />
361
+ {expanded && (
362
+ <>
363
+ <span className="truncate flex-1 text-sidebar-foreground text-left">
364
+ {moreOptionsLabel}
365
+ </span>
366
+ <ChevronRight
367
+ className={cn('w-4 h-4 flex-shrink-0 transition-transform duration-200', isOpen && 'rotate-90')}
368
+ />
369
+ </>
370
+ )}
371
+ </button>
372
+ <AnimatePresence initial={false}>
373
+ {isOpen && (
374
+ <motion.div
375
+ initial={{ height: 0, opacity: 0 }}
376
+ animate={{ height: 'auto', opacity: 1 }}
377
+ exit={{ height: 0, opacity: 0 }}
378
+ transition={{ duration: 0.2 }}
379
+ className="overflow-hidden"
380
+ >
381
+ <div className="mt-1 space-y-3">
382
+ {overflowGroups.map(group => {
383
+ const GroupIcon = group.icon;
384
+ return (
385
+ <div key={group.id}>
386
+ {(group.label || GroupIcon) && expanded && (
387
+ <div className="px-3 mb-1 flex items-center gap-2">
388
+ {GroupIcon &&
389
+ (React.isValidElement(GroupIcon) ? (
390
+ GroupIcon
391
+ ) : (
392
+ <GroupIcon className="h-3 w-3 text-sidebar-foreground/60" />
393
+ ))}
394
+ {group.label && (
395
+ <span className="text-xs font-semibold uppercase tracking-wider text-sidebar-foreground/60">
396
+ {group.label}
397
+ </span>
398
+ )}
399
+ </div>
400
+ )}
401
+ <div className="space-y-0.5">
402
+ {group.items.map(item => {
403
+ const Icon = item.icon;
404
+ const isActive =
405
+ location.pathname === item.path ||
406
+ location.pathname.startsWith(item.path + '/');
407
+ return (
408
+ <button
409
+ key={item.path}
410
+ onClick={() => handleNavigate(item.path)}
411
+ className={cn(
412
+ 'w-full h-9 flex items-center gap-2.5 rounded-[var(--radius-button)] transition-all duration-200 text-left text-sm',
413
+ expanded ? 'px-3' : 'justify-center px-0',
414
+ isActive
415
+ ? 'bg-sidebar-foreground/15 text-sidebar-foreground'
416
+ : 'text-sidebar-foreground/70 hover:bg-sidebar-foreground/10 hover:text-sidebar-foreground'
417
+ )}
418
+ >
419
+ {Icon && <Icon className="w-4 h-4 flex-shrink-0" />}
420
+ {expanded && <span className="truncate">{item.label}</span>}
421
+ </button>
422
+ );
423
+ })}
424
+ </div>
425
+ </div>
426
+ );
427
+ })}
428
+ </div>
429
+ </motion.div>
430
+ )}
431
+ </AnimatePresence>
432
+ </div>
433
+ );
434
+ }
435
+
436
+ /**
437
+ * Navigation area for the Sidebar (default variant).
438
+ * Renders grouped or flat navigation items with overflow handling.
439
+ */
440
+ function SidebarNav({
441
+ navigationGroups = [],
442
+ routes = [],
443
+ variant = 'default',
444
+ }: {
445
+ navigationGroups?: RouteGroup[];
446
+ routes?: RouteConfig[];
447
+ variant?: 'default' | 'assistant';
448
+ }) {
449
+ const { expanded, isMobileViewport, navigate, location, onToggle } = useSidebarContext();
450
+ const { t } = useTranslation();
451
+ const navRef = useRef<HTMLDivElement>(null);
452
+ const [localActiveItem, setLocalActiveItem] = useState<string | null>(null);
453
+ const [hasOverflow, setHasOverflow] = useState(false);
454
+ const [visibleItems, setVisibleItems] = useState<NavigationItem[]>([]);
455
+ const [overflowItems, setOverflowItems] = useState<NavigationItem[]>([]);
456
+ const [openSubmenus, setOpenSubmenus] = useState<Set<string>>(new Set());
457
+ const [hasGroupOverflow, setHasGroupOverflow] = useState(false);
458
+ const [visibleGroups, setVisibleGroups] = useState<RouteGroup[]>([]);
459
+ const [overflowGroups, setOverflowGroups] = useState<RouteGroup[]>([]);
460
+ const [isOverflowAccordionOpen, setIsOverflowAccordionOpen] = useState(false);
461
+
462
+ const toggleSubmenu = (path: string) => {
463
+ setOpenSubmenus(prev => {
464
+ const next = new Set(prev);
465
+ if (next.has(path)) {
466
+ next.delete(path);
467
+ } else {
468
+ next.add(path);
469
+ }
470
+ return next;
471
+ });
472
+ };
473
+
474
+ const labelTranslations = useMemo<Record<string, string>>(
475
+ () => ({
476
+ home: 'Início',
477
+ dashboard: 'Painel',
478
+ components: 'Componentes',
479
+ }),
480
+ []
481
+ );
482
+
483
+ const navigationItems = useMemo<NavigationItem[]>(
484
+ () =>
485
+ (routes || []).map(route => ({
486
+ ...route,
487
+ label: labelTranslations[route.label.toLowerCase()] || route.label,
488
+ active: location.pathname === route.path || location.pathname.startsWith(route.path + '/'),
489
+ children: route.children,
490
+ })),
491
+ [routes, location.pathname, labelTranslations]
492
+ );
493
+
494
+ useEffect(() => {
495
+ if (typeof window === 'undefined') return;
496
+ const checkOverflow = () => {
497
+ if (!navRef.current) return;
498
+ if (variant === 'assistant') return;
499
+ const navHeight = navRef.current.clientHeight;
500
+ const itemHeight = 44;
501
+ const maxVisibleItems = Math.floor(navHeight / itemHeight);
502
+ if (navigationItems.length > maxVisibleItems) {
503
+ setHasOverflow(true);
504
+ setVisibleItems(navigationItems.slice(0, maxVisibleItems - 1));
505
+ setOverflowItems(navigationItems.slice(maxVisibleItems - 1));
506
+ } else {
507
+ setHasOverflow(false);
508
+ setVisibleItems(navigationItems);
509
+ setOverflowItems([]);
510
+ }
511
+ };
512
+ checkOverflow();
513
+ window.addEventListener('resize', checkOverflow);
514
+ return () => window.removeEventListener('resize', checkOverflow);
515
+ }, [navigationItems.length, variant]);
516
+
517
+ useEffect(() => {
518
+ if (typeof window === 'undefined') return;
519
+ if (variant === 'assistant') return;
520
+ if (!navigationGroups || navigationGroups.length === 0) return;
521
+
522
+ const checkGroupOverflow = () => {
523
+ if (!navRef.current) return;
524
+
525
+ const containerHeight = navRef.current.clientHeight;
526
+ const itemHeight = 40;
527
+ const groupHeaderHeight = 32;
528
+ const groupSpacing = 12;
529
+ const moreButtonHeight = 44;
530
+ const padding = 32;
531
+
532
+ let currentHeight = padding;
533
+ let visibleCount = 0;
534
+
535
+ for (let i = 0; i < navigationGroups.length; i++) {
536
+ const group = navigationGroups[i];
537
+ let groupHeight = 0;
538
+ if (group.label) groupHeight += groupHeaderHeight;
539
+ groupHeight += group.items.length * itemHeight;
540
+ if (i > 0) groupHeight += groupSpacing;
541
+
542
+ const wouldExceed =
543
+ currentHeight + groupHeight + (visibleCount < navigationGroups.length - 1 ? moreButtonHeight : 0) >
544
+ containerHeight;
545
+
546
+ if (wouldExceed && visibleCount > 0) break;
547
+
548
+ currentHeight += groupHeight;
549
+ visibleCount++;
550
+ }
551
+
552
+ if (visibleCount < navigationGroups.length) {
553
+ setHasGroupOverflow(true);
554
+ setVisibleGroups(navigationGroups.slice(0, visibleCount));
555
+ setOverflowGroups(navigationGroups.slice(visibleCount));
556
+ } else {
557
+ setHasGroupOverflow(false);
558
+ setVisibleGroups(navigationGroups);
559
+ setOverflowGroups([]);
560
+ }
561
+ };
562
+
563
+ checkGroupOverflow();
564
+ window.addEventListener('resize', checkGroupOverflow);
565
+ return () => window.removeEventListener('resize', checkGroupOverflow);
566
+ }, [navigationGroups, expanded, variant]);
567
+
568
+ const handleNavigate = (path: string) => {
569
+ setLocalActiveItem(path);
570
+ navigate(path);
571
+ if (typeof window !== 'undefined' && window.innerWidth < 768) {
572
+ onToggle();
573
+ }
574
+ };
575
+
576
+ const toNavItem = (route: RouteConfig): NavigationItem => ({
577
+ path: route.path,
578
+ label: labelTranslations[route.label.toLowerCase()] || route.label,
579
+ icon: route.icon,
580
+ active: location.pathname === route.path || location.pathname.startsWith(route.path + '/'),
581
+ children: route.children,
582
+ actions: route.actions,
583
+ });
584
+
585
+ const renderActionItems = (actions: ActionMenuItem[]): React.ReactNode => {
586
+ return actions.map((action, idx) => {
587
+ const Icon = action.icon;
588
+ if (action.children && action.children.length > 0) {
589
+ return (
590
+ <DropdownMenuSub key={idx}>
591
+ <DropdownMenuSubTrigger>
592
+ {Icon && <Icon className="mr-2 h-4 w-4" />}
593
+ <span>{action.label}</span>
594
+ </DropdownMenuSubTrigger>
595
+ <DropdownMenuPortal>
596
+ <DropdownMenuSubContent className="w-48 bg-popover border-border">
597
+ {renderActionItems(action.children)}
598
+ </DropdownMenuSubContent>
599
+ </DropdownMenuPortal>
600
+ </DropdownMenuSub>
601
+ );
602
+ }
603
+ return (
604
+ <DropdownMenuItem
605
+ key={idx}
606
+ className={cn(
607
+ 'flex items-center gap-2',
608
+ action.variant === 'destructive' ? 'text-destructive focus:text-destructive' : ''
609
+ )}
610
+ onClick={e => {
611
+ e.stopPropagation();
612
+ action.onClick?.(null);
613
+ }}
614
+ >
615
+ {Icon && <Icon className="h-4 w-4 flex-shrink-0" />}
616
+ <span>{action.label}</span>
617
+ </DropdownMenuItem>
618
+ );
619
+ });
620
+ };
621
+
622
+ const renderAssistantActionMenu = (actions?: ActionMenuItem[], isHeader: boolean = false) => {
623
+ if (!actions || actions.length === 0) return null;
624
+ return (
625
+ <DropdownMenu>
626
+ <DropdownMenuTrigger asChild>
627
+ <Button
628
+ variant="ghost"
629
+ size="icon"
630
+ className={cn(
631
+ 'h-8 w-8 text-sidebar-foreground/80 hover:bg-sidebar-foreground/20 hover:text-sidebar-foreground rounded-full transition-all',
632
+ !isHeader && 'opacity-0 group-hover/item:opacity-100'
633
+ )}
634
+ aria-label={t('sidebar.moreOptions')}
635
+ >
636
+ <MoreVertical className="h-4 w-4" />
637
+ </Button>
638
+ </DropdownMenuTrigger>
639
+ <DropdownMenuContent align="end" className="w-48 bg-popover border-border p-1">
640
+ {renderActionItems(actions)}
641
+ </DropdownMenuContent>
642
+ </DropdownMenu>
643
+ );
644
+ };
645
+
646
+ const renderDefaultItem = (item: NavigationItem) => {
647
+ const Icon = item.icon;
648
+ const hasChildren = item.children && item.children.length > 0;
649
+ const activeClass = item.active
650
+ ? 'bg-sidebar-foreground/15 text-sidebar-foreground shadow-sm'
651
+ : 'text-sidebar-foreground/80 hover:bg-sidebar-foreground/15 hover:text-sidebar-foreground';
652
+
653
+ if (!expanded) {
654
+ return (
655
+ <Tooltip key={item.path}>
656
+ <TooltipTrigger asChild>
657
+ <button
658
+ onClick={() => handleNavigate(item.path)}
659
+ className={cn(
660
+ 'w-full h-10 flex items-center justify-center px-0 rounded-[var(--radius-button)] transition-all duration-200',
661
+ activeClass
662
+ )}
663
+ aria-label={item.label}
664
+ >
665
+ {Icon && <Icon className="w-5 h-5 flex-shrink-0" />}
666
+ </button>
667
+ </TooltipTrigger>
668
+ <SidebarTooltipContent side="right" sideOffset={0}>
669
+ <p>{item.label}</p>
670
+ </SidebarTooltipContent>
671
+ </Tooltip>
672
+ );
673
+ }
674
+
675
+ // Mobile: accordion inline — subitens abrem abaixo do item pai
676
+ if (isMobileViewport && hasChildren) {
677
+ const isOpen = openSubmenus.has(item.path);
678
+ return (
679
+ <div key={item.path}>
680
+ <div
681
+ className={cn(
682
+ 'group/item flex items-center w-full h-10 rounded-[var(--radius-button)] transition-all duration-200',
683
+ activeClass
684
+ )}
685
+ >
686
+ <button
687
+ onClick={() => handleNavigate(item.path)}
688
+ className="flex items-center gap-3 px-3 flex-1 h-full min-w-0 text-left"
689
+ >
690
+ {Icon && <Icon className="w-5 h-5 flex-shrink-0" />}
691
+ <span className="truncate flex-1">{item.label}</span>
692
+ </button>
693
+ <button
694
+ onClick={e => {
695
+ e.stopPropagation();
696
+ toggleSubmenu(item.path);
697
+ }}
698
+ className="h-full px-2 pr-2.5 flex items-center justify-center text-sidebar-foreground/40 hover:text-sidebar-foreground transition-colors"
699
+ aria-label={t('sidebar.submenu', { label: item.label })}
700
+ aria-expanded={isOpen}
701
+ >
702
+ <ChevronRight
703
+ className={cn('w-4 h-4 transition-transform duration-200', isOpen && 'rotate-90')}
704
+ />
705
+ </button>
706
+ </div>
707
+ <AnimatePresence initial={false}>
708
+ {isOpen && (
709
+ <motion.div
710
+ initial={{ height: 0, opacity: 0 }}
711
+ animate={{ height: 'auto', opacity: 1 }}
712
+ exit={{ height: 0, opacity: 0 }}
713
+ transition={{ duration: 0.2 }}
714
+ className="overflow-hidden"
715
+ >
716
+ <div className="ml-4 mt-0.5 mb-0.5 space-y-0.5">
717
+ {item.children!.map(child => {
718
+ const ChildIcon = child.icon;
719
+ const isChildActive =
720
+ location.pathname === child.path ||
721
+ location.pathname.startsWith(child.path + '/');
722
+ return (
723
+ <button
724
+ key={child.path}
725
+ onClick={() => handleNavigate(child.path)}
726
+ className={cn(
727
+ 'w-full h-9 flex items-center gap-2.5 px-3 rounded-[var(--radius-button)] transition-all duration-200 text-left',
728
+ isChildActive
729
+ ? 'bg-sidebar-foreground/15 text-sidebar-foreground'
730
+ : 'text-sidebar-foreground/70 hover:bg-sidebar-foreground/10 hover:text-sidebar-foreground'
731
+ )}
732
+ >
733
+ {ChildIcon && <ChildIcon className="h-4 w-4 flex-shrink-0" />}
734
+ <span className="truncate text-sm">{child.label}</span>
735
+ </button>
736
+ );
737
+ })}
738
+ </div>
739
+ </motion.div>
740
+ )}
741
+ </AnimatePresence>
742
+ </div>
743
+ );
744
+ }
745
+
746
+ // Desktop: dropdown lateral (comportamento original)
747
+ return (
748
+ <div
749
+ key={item.path}
750
+ className={cn(
751
+ 'group/item flex items-center w-full h-10 rounded-[var(--radius-button)] transition-all duration-200',
752
+ activeClass
753
+ )}
754
+ >
755
+ <button
756
+ onClick={() => handleNavigate(item.path)}
757
+ className="flex items-center gap-3 px-3 flex-1 h-full min-w-0 text-left"
758
+ >
759
+ {Icon && <Icon className="w-5 h-5 flex-shrink-0" />}
760
+ <span className="truncate flex-1">{item.label}</span>
761
+ </button>
762
+
763
+ {hasChildren && (
764
+ <DropdownMenu>
765
+ <DropdownMenuTrigger asChild>
766
+ <button
767
+ onClick={e => e.stopPropagation()}
768
+ className="h-full px-2 pr-2.5 flex items-center justify-center text-sidebar-foreground/40 hover:text-sidebar-foreground transition-colors"
769
+ aria-label={t('sidebar.submenu', { label: item.label })}
770
+ >
771
+ <ChevronRight className="w-4 h-4" />
772
+ </button>
773
+ </DropdownMenuTrigger>
774
+ <DropdownMenuContent
775
+ side="right"
776
+ align="start"
777
+ className="w-48 bg-popover border-border p-1"
778
+ >
779
+ {item.children!.map(child => {
780
+ const ChildIcon = child.icon;
781
+ const isChildActive =
782
+ location.pathname === child.path ||
783
+ location.pathname.startsWith(child.path + '/');
784
+ return (
785
+ <DropdownMenuItem
786
+ key={child.path}
787
+ onClick={() => handleNavigate(child.path)}
788
+ className={cn(
789
+ 'flex items-center gap-2 cursor-pointer',
790
+ isChildActive && 'bg-accent text-accent-foreground'
791
+ )}
792
+ >
793
+ {ChildIcon && <ChildIcon className="h-4 w-4 flex-shrink-0" />}
794
+ <span className="truncate">{child.label}</span>
795
+ </DropdownMenuItem>
796
+ );
797
+ })}
798
+ </DropdownMenuContent>
799
+ </DropdownMenu>
800
+ )}
801
+ </div>
802
+ );
803
+ };
804
+
805
+ const renderDefaultGroup = (group: RouteGroup) => {
806
+ const GroupIcon = group.icon;
807
+ if (!expanded) {
808
+ return (
809
+ <div key={group.id} className="space-y-1">
810
+ {group.items.map(item => renderDefaultItem(toNavItem(item)))}
811
+ </div>
812
+ );
813
+ }
814
+ return (
815
+ <div key={group.id}>
816
+ {(group.label || GroupIcon) && (
817
+ <div className="px-3 mb-1 flex items-center gap-2">
818
+ {GroupIcon &&
819
+ (React.isValidElement(GroupIcon) ? (
820
+ GroupIcon
821
+ ) : (
822
+ <GroupIcon className="h-3 w-3 text-sidebar-foreground/80" />
823
+ ))}
824
+ {group.label && (
825
+ <span className="text-xs font-semibold uppercase tracking-wider text-sidebar-foreground/80">
826
+ {group.label}
827
+ </span>
828
+ )}
829
+ </div>
830
+ )}
831
+ <div className="space-y-1">
832
+ {group.items.map(item => renderDefaultItem(toNavItem(item)))}
833
+ </div>
834
+ </div>
835
+ );
836
+ };
837
+
838
+ const renderAssistantGroup = (group: RouteGroup) => {
839
+ const isAnyItemActive = group.items.some(
840
+ item => location.pathname === item.path || location.pathname.startsWith(item.path + '/')
841
+ );
842
+ const GroupIcon = group.icon;
843
+
844
+ if (!expanded) {
845
+ if (!GroupIcon) return null;
846
+ return (
847
+ <div key={group.id} className="py-2 flex justify-center">
848
+ <Tooltip>
849
+ <TooltipTrigger asChild>
850
+ <button
851
+ onClick={() => onToggle()}
852
+ aria-label={group.label}
853
+ className={cn(
854
+ 'h-10 w-10 flex items-center justify-center rounded-[var(--radius-button)] transition-all duration-200',
855
+ isAnyItemActive
856
+ ? 'bg-sidebar-foreground/15 text-sidebar-foreground shadow-sm'
857
+ : 'text-sidebar-foreground/80 hover:bg-sidebar-foreground/15 hover:text-sidebar-foreground'
858
+ )}
859
+ >
860
+ {React.isValidElement(GroupIcon) ? GroupIcon : <GroupIcon className="h-5 w-5" />}
861
+ </button>
862
+ </TooltipTrigger>
863
+ <SidebarTooltipContent side="right">
864
+ <p>{group.label}</p>
865
+ </SidebarTooltipContent>
866
+ </Tooltip>
867
+ </div>
868
+ );
869
+ }
870
+
871
+ return (
872
+ <div key={group.id} className="py-2 group">
873
+ {(group.label || group.icon) && (
874
+ <div className="flex items-center justify-between px-3 mb-1">
875
+ <div className="flex items-center gap-2 text-sidebar-foreground/80 text-xs font-semibold uppercase tracking-wider">
876
+ {GroupIcon && (
877
+ <div className="flex items-center justify-center">
878
+ {React.isValidElement(GroupIcon) ? GroupIcon : <GroupIcon className="h-4 w-4" />}
879
+ </div>
880
+ )}
881
+ {group.label && <span>{group.label}</span>}
882
+ </div>
883
+ {renderAssistantActionMenu(group.actions, true)}
884
+ </div>
885
+ )}
886
+ <div className="space-y-1">
887
+ {group.items.map(item => {
888
+ const isRouteActive =
889
+ location.pathname === item.path || location.pathname.startsWith(item.path + '/');
890
+ const isActive = isRouteActive || localActiveItem === item.path;
891
+ const Icon = item.icon;
892
+ return (
893
+ <div
894
+ key={item.path}
895
+ className={`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'}`}
896
+ onClick={() => handleNavigate(item.path)}
897
+ >
898
+ <div className="flex flex-col min-w-0 flex-1">
899
+ <div className="flex items-center gap-3 overflow-hidden h-5">
900
+ {Icon && (
901
+ <div className="w-4 h-4 flex-shrink-0 flex items-center justify-center">
902
+ {React.isValidElement(Icon) ? Icon : <Icon className="w-4 h-4" />}
903
+ </div>
904
+ )}
905
+ <span className="truncate text-sm font-medium leading-none">{item.label}</span>
906
+ </div>
907
+ {isActive && item.description && (
908
+ <div className="text-[11px] text-sidebar-foreground/60 mt-1.5 animate-in fade-in slide-in-from-top-1 duration-200">
909
+ {item.description}
910
+ </div>
911
+ )}
912
+ </div>
913
+ <div className="h-5 flex items-center ml-1">
914
+ {renderAssistantActionMenu(item.actions)}
915
+ </div>
916
+ </div>
917
+ );
918
+ })}
919
+ </div>
920
+ </div>
921
+ );
922
+ };
923
+
924
+ if (variant === 'assistant') {
925
+ return (
926
+ <div className="flex-1 min-h-0 overflow-hidden">
927
+ <div
928
+ className="h-full px-4"
929
+ style={{ overflowY: 'auto', overflowX: 'hidden' }}
930
+ >
931
+ {navigationGroups.map(group => renderAssistantGroup(group))}
932
+ </div>
933
+ </div>
934
+ );
935
+ }
936
+
937
+ if (isMobileViewport && isOverflowAccordionOpen) {
938
+ return (
939
+ <div className="flex-1 min-h-0 overflow-hidden">
940
+ <div
941
+ className="h-full"
942
+ style={{ overflowY: 'auto', overflowX: 'hidden' }}
943
+ >
944
+ <nav
945
+ id="sidebar-nav"
946
+ aria-label={t('sidebar.mainNavigation')}
947
+ className="px-4 py-4"
948
+ ref={navRef}
949
+ >
950
+ {navigationGroups && navigationGroups.length > 0 ? (
951
+ <div className="space-y-3">
952
+ {(hasGroupOverflow ? visibleGroups : navigationGroups).map(group => (
953
+ <React.Fragment key={group.id}>{renderDefaultGroup(group)}</React.Fragment>
954
+ ))}
955
+ {hasGroupOverflow && (
956
+ <OverflowGroupsAccordion
957
+ expanded={expanded}
958
+ overflowGroups={overflowGroups}
959
+ location={location}
960
+ handleNavigate={handleNavigate}
961
+ moreOptionsLabel={t('sidebar.moreOptions')}
962
+ onOpenChange={setIsOverflowAccordionOpen}
963
+ />
964
+ )}
965
+ </div>
966
+ ) : (
967
+ <div className="space-y-1">
968
+ {(hasOverflow ? visibleItems : navigationItems).map(item => renderDefaultItem(item))}
969
+ {hasOverflow && (
970
+ <Popover>
971
+ <PopoverTrigger asChild>
972
+ <button
973
+ className={
974
+ expanded
975
+ ? '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'
976
+ : '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'
977
+ }
978
+ aria-label={t('sidebar.moreOptions')}
979
+ >
980
+ <MoreVertical className="w-5 h-5 flex-shrink-0" />
981
+ {expanded && (
982
+ <span className="truncate text-sidebar-foreground">
983
+ {t('sidebar.moreOptions')}
984
+ </span>
985
+ )}
986
+ </button>
987
+ </PopoverTrigger>
988
+ <PopoverContent
989
+ side="right"
990
+ align="start"
991
+ className="w-56 p-2 bg-popover border border-border rounded-[var(--radius-card)] shadow-lg"
992
+ sideOffset={8}
993
+ >
994
+ <div className="space-y-1">
995
+ {overflowItems.map(item => {
996
+ const Icon = item.icon;
997
+ return (
998
+ <button
999
+ key={item.path}
1000
+ onClick={() => handleNavigate(item.path)}
1001
+ 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"
1002
+ >
1003
+ {Icon && <Icon className="w-4 h-4 flex-shrink-0" />}
1004
+ <span className="truncate">{item.label}</span>
1005
+ </button>
1006
+ );
1007
+ })}
1008
+ </div>
1009
+ </PopoverContent>
1010
+ </Popover>
1011
+ )}
1012
+ </div>
1013
+ )}
1014
+ </nav>
1015
+ </div>
1016
+ </div>
1017
+ );
1018
+ }
1019
+
1020
+ return (
1021
+ <div className="flex-1 min-h-0 overflow-hidden">
1022
+ <nav
1023
+ id="sidebar-nav"
1024
+ aria-label={t('sidebar.mainNavigation')}
1025
+ className="h-full px-4 py-4 overflow-hidden"
1026
+ ref={navRef}
1027
+ >
1028
+ {navigationGroups && navigationGroups.length > 0 ? (
1029
+ <div className="space-y-3">
1030
+ {(hasGroupOverflow ? visibleGroups : navigationGroups).map(group => (
1031
+ <React.Fragment key={group.id}>{renderDefaultGroup(group)}</React.Fragment>
1032
+ ))}
1033
+ {hasGroupOverflow && (
1034
+ isMobileViewport ? (
1035
+ // Mobile: accordion inline abre abaixo, igual aos subitens
1036
+ <OverflowGroupsAccordion
1037
+ expanded={expanded}
1038
+ overflowGroups={overflowGroups}
1039
+ location={location}
1040
+ handleNavigate={handleNavigate}
1041
+ moreOptionsLabel={t('sidebar.moreOptions')}
1042
+ onOpenChange={setIsOverflowAccordionOpen}
1043
+ />
1044
+ ) : (
1045
+ // Desktop: popover lateral direito
1046
+ <Popover>
1047
+ <PopoverTrigger asChild>
1048
+ <button
1049
+ className={
1050
+ expanded
1051
+ ? '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'
1052
+ : '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'
1053
+ }
1054
+ aria-label={t('sidebar.moreOptions')}
1055
+ >
1056
+ <MoreVertical className="w-5 h-5 flex-shrink-0" />
1057
+ {expanded && (
1058
+ <span className="truncate text-sidebar-foreground">
1059
+ {t('sidebar.moreOptions')}
1060
+ </span>
1061
+ )}
1062
+ </button>
1063
+ </PopoverTrigger>
1064
+ <PopoverContent
1065
+ side="right"
1066
+ align="start"
1067
+ className="w-56 p-2 bg-popover border border-border rounded-[var(--radius-card)] shadow-lg"
1068
+ sideOffset={8}
1069
+ >
1070
+ <div className="space-y-3">
1071
+ {overflowGroups.map(group => {
1072
+ const GroupIcon = group.icon;
1073
+ return (
1074
+ <div key={group.id}>
1075
+ {(group.label || GroupIcon) && (
1076
+ <div className="px-2 mb-1 flex items-center gap-2">
1077
+ {GroupIcon &&
1078
+ (React.isValidElement(GroupIcon) ? (
1079
+ GroupIcon
1080
+ ) : (
1081
+ <GroupIcon className="h-3 w-3 text-popover-foreground/60" />
1082
+ ))}
1083
+ {group.label && (
1084
+ <span className="text-xs font-semibold uppercase tracking-wider text-popover-foreground/60">
1085
+ {group.label}
1086
+ </span>
1087
+ )}
1088
+ </div>
1089
+ )}
1090
+ <div className="space-y-1">
1091
+ {group.items.map(item => {
1092
+ const Icon = item.icon;
1093
+ const isActive =
1094
+ location.pathname === item.path ||
1095
+ location.pathname.startsWith(item.path + '/');
1096
+ return (
1097
+ <button
1098
+ key={item.path}
1099
+ onClick={() => handleNavigate(item.path)}
1100
+ className={cn(
1101
+ 'w-full h-9 flex items-center gap-2 px-2 rounded-[var(--radius-button)] transition-all duration-200 text-left text-sm',
1102
+ isActive
1103
+ ? 'bg-accent text-accent-foreground font-medium'
1104
+ : 'text-popover-foreground/80 hover:bg-accent hover:text-accent-foreground'
1105
+ )}
1106
+ >
1107
+ {Icon && <Icon className="w-4 h-4 flex-shrink-0" />}
1108
+ <span className="truncate">{item.label}</span>
1109
+ </button>
1110
+ );
1111
+ })}
1112
+ </div>
1113
+ </div>
1114
+ );
1115
+ })}
1116
+ </div>
1117
+ </PopoverContent>
1118
+ </Popover>
1119
+ )
1120
+ )}
1121
+ </div>
1122
+ ) : (
1123
+ <div className="space-y-1">
1124
+ {(hasOverflow ? visibleItems : navigationItems).map(item => renderDefaultItem(item))}
1125
+ {hasOverflow && (
1126
+ <Popover>
1127
+ <PopoverTrigger asChild>
1128
+ <button
1129
+ className={
1130
+ expanded
1131
+ ? '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'
1132
+ : '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'
1133
+ }
1134
+ aria-label={t('sidebar.moreOptions')}
1135
+ >
1136
+ <MoreVertical className="w-5 h-5 flex-shrink-0" />
1137
+ {expanded && (
1138
+ <span className="truncate text-sidebar-foreground">
1139
+ {t('sidebar.moreOptions')}
1140
+ </span>
1141
+ )}
1142
+ </button>
1143
+ </PopoverTrigger>
1144
+ <PopoverContent
1145
+ side="right"
1146
+ align="start"
1147
+ className="w-56 p-2 bg-popover border border-border rounded-[var(--radius-card)] shadow-lg"
1148
+ sideOffset={8}
1149
+ >
1150
+ <div className="space-y-1">
1151
+ {overflowItems.map(item => {
1152
+ const Icon = item.icon;
1153
+ return (
1154
+ <button
1155
+ key={item.path}
1156
+ onClick={() => handleNavigate(item.path)}
1157
+ 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"
1158
+ >
1159
+ {Icon && <Icon className="w-4 h-4 flex-shrink-0" />}
1160
+ <span className="truncate">{item.label}</span>
1161
+ </button>
1162
+ );
1163
+ })}
1164
+ </div>
1165
+ </PopoverContent>
1166
+ </Popover>
1167
+ )}
1168
+ </div>
1169
+ )}
1170
+ </nav>
1171
+ </div>
1172
+ );
1173
+ }
1174
+
1175
+ /**
1176
+ * Assistant-specific search + fixed area header for the Sidebar.
1177
+ */
1178
+ function SidebarSearch({
1179
+ fixedArea,
1180
+ search,
1181
+ }: {
1182
+ fixedArea?: SidebarFixedAreaConfig;
1183
+ search?: SidebarSearchConfig;
1184
+ }) {
1185
+ const { expanded } = useSidebarContext();
1186
+ const { t } = useTranslation();
1187
+ const [isFilterOpen, setIsFilterOpen] = useState(false);
1188
+
1189
+ if (!((fixedArea && fixedArea.show) || (search && search.show))) return null;
1190
+
1191
+ return (
1192
+ <div className="flex-shrink-0 px-4 pb-4 space-y-4 border-b border-sidebar-border/30 mb-2">
1193
+ {fixedArea?.show && fixedArea.content && expanded && (
1194
+ <div className="animate-in fade-in slide-in-from-top-1 duration-300">
1195
+ {fixedArea.content}
1196
+ </div>
1197
+ )}
1198
+ {search?.show && expanded && (
1199
+ <>
1200
+ <div className="flex items-center gap-2">
1201
+ <div className="relative flex-1">
1202
+ <SearchIcon className="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-sidebar-foreground/50" />
1203
+ <Input
1204
+ type="text"
1205
+ placeholder={search.placeholder || t('sidebar.searchPlaceholder')}
1206
+ aria-label={search.placeholder || t('sidebar.searchPlaceholder')}
1207
+ value={search.value}
1208
+ onChange={e => search.onChange?.(e.target.value)}
1209
+ 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"
1210
+ />
1211
+ </div>
1212
+ {search.filter?.show && search.filter.content && (
1213
+ <Button
1214
+ variant="ghost"
1215
+ size="icon"
1216
+ onClick={() => setIsFilterOpen(!isFilterOpen)}
1217
+ className={cn(
1218
+ 'h-9 w-9 text-sidebar-foreground transition-all duration-200',
1219
+ isFilterOpen ? 'bg-sidebar-foreground/20' : 'hover:bg-sidebar-foreground/15'
1220
+ )}
1221
+ aria-label={isFilterOpen ? t('sidebar.closeFilters') : t('sidebar.openFilters')}
1222
+ >
1223
+ {search.filter.icon || <Filter className="h-4 w-4" />}
1224
+ </Button>
1225
+ )}
1226
+ </div>
1227
+ <AnimatePresence>
1228
+ {isFilterOpen && search.filter?.show && search.filter.content && (
1229
+ <motion.div
1230
+ initial={{ height: 0, opacity: 0 }}
1231
+ animate={{ height: 'auto', opacity: 1 }}
1232
+ exit={{ height: 0, opacity: 0 }}
1233
+ transition={{ duration: 0.2 }}
1234
+ className="overflow-hidden"
1235
+ >
1236
+ <div className="pt-2 border-t border-sidebar-border/20">
1237
+ {search.filter.content}
1238
+ </div>
1239
+ </motion.div>
1240
+ )}
1241
+ </AnimatePresence>
1242
+ </>
1243
+ )}
1244
+ {!expanded && (fixedArea?.show || search?.show) && (
1245
+ <div className="flex flex-col items-center gap-4 py-2">
1246
+ {fixedArea?.show && fixedArea.icon && (
1247
+ <Tooltip>
1248
+ <TooltipTrigger asChild>
1249
+ <button
1250
+ onClick={() => fixedArea.onClick?.()}
1251
+ 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"
1252
+ aria-label={t('assistant.newConversation')}
1253
+ >
1254
+ {React.isValidElement(fixedArea.icon) ? (
1255
+ fixedArea.icon
1256
+ ) : (
1257
+ (() => {
1258
+ const FixedAreaIcon =
1259
+ fixedArea.icon as React.ComponentType<{ className?: string }>;
1260
+ return <FixedAreaIcon className="h-5 w-5" />;
1261
+ })()
1262
+ )}
1263
+ </button>
1264
+ </TooltipTrigger>
1265
+ <SidebarTooltipContent side="right">
1266
+ {t('assistant.newConversation')}
1267
+ </SidebarTooltipContent>
1268
+ </Tooltip>
1269
+ )}
1270
+ {search?.show && (
1271
+ <Tooltip>
1272
+ <TooltipTrigger asChild>
1273
+ <button
1274
+ 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"
1275
+ aria-label={t('sidebar.search')}
1276
+ >
1277
+ <SearchIcon className="h-5 w-5" />
1278
+ </button>
1279
+ </TooltipTrigger>
1280
+ <SidebarTooltipContent side="right">{t('sidebar.search')}</SidebarTooltipContent>
1281
+ </Tooltip>
1282
+ )}
1283
+ </div>
1284
+ )}
1285
+ </div>
1286
+ );
1287
+ }
1288
+
1289
+ /**
1290
+ * Footer area for the Sidebar with user info, settings, and logout.
1291
+ */
1292
+ function SidebarFooter({
1293
+ user,
1294
+ onLogout = () => {},
1295
+ onSettingsClick,
1296
+ showUser = true,
1297
+ showSettings = true,
1298
+ showLogout = true,
1299
+ }: {
1300
+ user?: { name?: string; email?: string; avatar?: string } | null;
1301
+ onLogout?: () => void;
1302
+ onSettingsClick?: () => void;
1303
+ showUser?: boolean;
1304
+ showSettings?: boolean;
1305
+ showLogout?: boolean;
1306
+ }) {
1307
+ const { expanded, navigate, location, onToggle } = useSidebarContext();
1308
+ const { t } = useTranslation();
1309
+ const isSettingsActive = location.pathname === '/settings';
1310
+
1311
+ const handleSettingsClick = () => {
1312
+ if (onSettingsClick) {
1313
+ onSettingsClick();
1314
+ } else {
1315
+ navigate('/settings');
1316
+ }
1317
+ if (typeof window !== 'undefined' && window.innerWidth < 768) {
1318
+ onToggle();
1319
+ }
1320
+ };
1321
+
1322
+ return (
1323
+ <div className="flex-shrink-0 p-4 space-y-2">
1324
+ {showUser &&
1325
+ (!expanded ? (
1326
+ <Tooltip>
1327
+ <TooltipTrigger asChild>
1328
+ <button
1329
+ 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"
1330
+ aria-label={t('sidebar.userProfile')}
1331
+ >
1332
+ <Avatar className="w-7 h-7 flex-shrink-0">
1333
+ <AvatarImage src={user?.avatar} alt={user?.name || 'User'} />
1334
+ <AvatarFallback className="bg-sidebar-foreground/15 text-sidebar-foreground text-xs">
1335
+ {user?.name ? user.name.charAt(0).toUpperCase() : 'U'}
1336
+ </AvatarFallback>
1337
+ </Avatar>
1338
+ </button>
1339
+ </TooltipTrigger>
1340
+ <SidebarTooltipContent side="right" sideOffset={0}>
1341
+ <p>{user?.name || t('sidebar.profile')}</p>
1342
+ </SidebarTooltipContent>
1343
+ </Tooltip>
1344
+ ) : (
1345
+ <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">
1346
+ <Avatar className="w-7 h-7 flex-shrink-0">
1347
+ <AvatarImage src={user?.avatar} alt={user?.name || 'User'} />
1348
+ <AvatarFallback className="bg-sidebar-foreground/15 text-sidebar-foreground text-xs">
1349
+ {user?.name ? user.name.charAt(0).toUpperCase() : 'U'}
1350
+ </AvatarFallback>
1351
+ </Avatar>
1352
+ <span className="text-sidebar-foreground truncate">
1353
+ {user?.name || t('sidebar.profile')}
1354
+ </span>
1355
+ </button>
1356
+ ))}
1357
+
1358
+ {showSettings &&
1359
+ (!expanded ? (
1360
+ <Tooltip>
1361
+ <TooltipTrigger asChild>
1362
+ <button
1363
+ onClick={handleSettingsClick}
1364
+ className={cn(
1365
+ 'w-full h-10 flex items-center justify-center px-0 rounded-[var(--radius-button)] transition-all duration-200',
1366
+ isSettingsActive
1367
+ ? 'bg-sidebar-foreground/15 text-sidebar-foreground shadow-sm'
1368
+ : 'text-sidebar-foreground/80 hover:bg-sidebar-foreground/15 hover:text-sidebar-foreground'
1369
+ )}
1370
+ aria-label={t('nav.settings')}
1371
+ >
1372
+ <Settings className="w-5 h-5 flex-shrink-0" />
1373
+ </button>
1374
+ </TooltipTrigger>
1375
+ <SidebarTooltipContent side="right" sideOffset={0}>
1376
+ <p>{t('nav.settings')}</p>
1377
+ </SidebarTooltipContent>
1378
+ </Tooltip>
1379
+ ) : (
1380
+ <button
1381
+ onClick={handleSettingsClick}
1382
+ className={cn(
1383
+ 'w-full h-10 flex items-center gap-3 px-3 justify-start rounded-[var(--radius-button)] transition-all duration-200',
1384
+ isSettingsActive
1385
+ ? 'bg-sidebar-foreground/15 text-sidebar-foreground shadow-sm'
1386
+ : 'text-sidebar-foreground/80 hover:bg-sidebar-foreground/15 hover:text-sidebar-foreground'
1387
+ )}
1388
+ >
1389
+ <Settings className="w-5 h-5 flex-shrink-0" />
1390
+ <span className="truncate text-sidebar-foreground">{t('nav.settings')}</span>
1391
+ </button>
1392
+ ))}
1393
+
1394
+ {showLogout &&
1395
+ (!expanded ? (
1396
+ <Tooltip>
1397
+ <TooltipTrigger asChild>
1398
+ <button
1399
+ onClick={onLogout}
1400
+ 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"
1401
+ aria-label={t('sidebar.logout')}
1402
+ >
1403
+ <LogOut className="w-5 h-5 flex-shrink-0" />
1404
+ </button>
1405
+ </TooltipTrigger>
1406
+ <SidebarTooltipContent side="right" sideOffset={0}>
1407
+ <p>{t('sidebar.logout')}</p>
1408
+ </SidebarTooltipContent>
1409
+ </Tooltip>
1410
+ ) : (
1411
+ <button
1412
+ onClick={onLogout}
1413
+ 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"
1414
+ >
1415
+ <LogOut className="w-5 h-5 flex-shrink-0" />
1416
+ <span className="truncate text-sidebar-foreground">{t('sidebar.logout')}</span>
1417
+ </button>
1418
+ ))}
1419
+ </div>
1420
+ );
1421
+ }
1422
+
1423
+ // ─────────────────────────────────────────────────────────────────────────────
1424
+ // Main Sidebar component (backward-compatible monolithic API)
1425
+ // ─────────────────────────────────────────────────────────────────────────────
1426
+
1427
+ /**
1428
+ * Primary navigation sidebar component.
1429
+ *
1430
+ * @description
1431
+ * Manages desktop/mobile responsive navigation rendering with two variants:
1432
+ * - `"default"` — simple flat or grouped route list.
1433
+ * - `"assistant"` — advanced variant with fixed areas, search, filters, and grouped navigation.
1434
+ *
1435
+ * This component is autonomous: it works out-of-the-box using local state or
1436
+ * integrates automatically with `LayoutContext` if wrapped in `LayoutProvider`.
1437
+ *
1438
+ * For advanced customization, use the Compound Component API:
1439
+ * `<Sidebar.Root>`, `<Sidebar.Header>`, `<Sidebar.Search>`, `<Sidebar.Nav>`, `<Sidebar.Footer>`
1440
+ *
1441
+ * @ai-rules
1442
+ * 1. NEVER recreate the sidebar with raw Tailwind classes — always use this component.
1443
+ * 2. Use `variant="assistant"` for AI/tool sidebars; use `variant="default"` for standard navigation.
1444
+ * 3. Supports `Ctrl+B` keyboard shortcut automatically via `LayoutProvider`.
1445
+ */
1446
+ export function Sidebar({
1447
+ expanded: expandedProp,
1448
+ onToggle: onToggleProp,
1449
+ user,
1450
+ onLogout = () => {},
1451
+ onSettingsClick,
1452
+ location: locationProp,
1453
+ navigate: navigateProp,
1454
+ routes,
1455
+ logo,
1456
+ logoCollapsed,
1457
+ variant = 'default',
1458
+ fixedArea,
1459
+ search,
1460
+ navigationGroups = [],
1461
+ footer,
1462
+ showFooter,
1463
+ width: widthProp,
1464
+ }: SidebarProps) {
1465
+ const { showUser = true, showSettings = true, showLogout = true } = footer || {};
1466
+
1467
+ const displayFooter = showFooter !== undefined ? showFooter : variant === 'default';
1468
+
1469
+ return (
1470
+ <SidebarRoot
1471
+ expanded={expandedProp}
1472
+ onToggle={onToggleProp}
1473
+ navigate={navigateProp}
1474
+ location={locationProp}
1475
+ width={widthProp}
1476
+ >
1477
+ <SidebarHeader logo={logo} logoCollapsed={logoCollapsed} />
1478
+
1479
+ {variant === 'assistant' && <SidebarSearch fixedArea={fixedArea} search={search} />}
1480
+
1481
+ <SidebarNav navigationGroups={navigationGroups} routes={routes} variant={variant} />
1482
+
1483
+ {displayFooter && (showUser || showSettings || showLogout) && (
1484
+ <SidebarFooter
1485
+ user={user}
1486
+ onLogout={onLogout}
1487
+ onSettingsClick={onSettingsClick}
1488
+ showUser={showUser}
1489
+ showSettings={showSettings}
1490
+ showLogout={showLogout}
1491
+ />
1492
+ )}
1493
+ </SidebarRoot>
1494
+ );
1495
+ }
1496
+
1497
+ // ─────────────────────────────────────────────────────────────────────────────
1498
+ // Attach Compound Components to Sidebar namespace
1499
+ // ─────────────────────────────────────────────────────────────────────────────
1500
+
1501
+ Sidebar.Root = SidebarRoot;
1502
+ Sidebar.Header = SidebarHeader;
1503
+ Sidebar.Search = SidebarSearch;
1504
+ Sidebar.Nav = SidebarNav;
1505
+ Sidebar.Footer = SidebarFooter;
1506
+
1507
+ // Re-export hook for headless usage
1508
+ export { useSidebar } from './use-sidebar';
1509
+ export type { UseSidebarProps } from './use-sidebar';