xertica-ui 2.5.3 → 2.7.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 (635) hide show
  1. package/CHANGELOG.md +705 -664
  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 +384 -359
  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 +10 -10
  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/{templates/src/features/auth/ui → components/blocks/auth}/AuthPageShell.tsx +37 -32
  26. package/components/blocks/auth/PasswordStrengthMeter.tsx +41 -0
  27. package/components/blocks/auth/SocialLoginButtons.tsx +95 -0
  28. package/components/blocks/auth/index.ts +11 -0
  29. package/components/blocks/auth/usePasswordStrength.ts +13 -0
  30. package/components/blocks/card-patterns/card-patterns.mdx +123 -123
  31. package/components/blocks/index.ts +1 -0
  32. package/components/brand/branding/branding.mdx +27 -27
  33. package/components/brand/language-selector/language-selector.mdx +126 -126
  34. package/components/brand/theme-toggle/theme-toggle.mdx +47 -47
  35. package/components/brand/theme-toggle/theme-toggle.test.tsx +6 -2
  36. package/components/brand/xertica-logo/xertica-logo.mdx +56 -56
  37. package/components/brand/xertica-orbe/xertica-orbe.mdx +38 -38
  38. package/components/brand/xertica-provider/XerticaProvider.tsx +112 -112
  39. package/components/brand/xertica-provider/xertica-provider.mdx +61 -61
  40. package/components/brand/xertica-provider/xertica-provider.test.tsx +25 -13
  41. package/components/brand/xertica-xlogo/xertica-xlogo.mdx +55 -55
  42. package/components/examples/DrawingMapExample.tsx +1 -4
  43. package/components/layout/header/header.mdx +117 -117
  44. package/components/layout/header/header.test.tsx +2 -1
  45. package/components/layout/sidebar/sidebar.mdx +99 -99
  46. package/components/layout/sidebar/sidebar.stories.tsx +1033 -1033
  47. package/components/layout/sidebar/sidebar.test.tsx +2 -1
  48. package/components/layout/sidebar/sidebar.tsx +1509 -1506
  49. package/components/media/audio-player/AudioPlayer.mdx +67 -67
  50. package/components/media/audio-player/AudioPlayer.test.tsx +2 -1
  51. package/components/media/floating-media-wrapper.mdx +74 -74
  52. package/components/media/video-player/VideoPlayer.mdx +102 -102
  53. package/components/media/video-player/VideoPlayer.test.tsx +3 -2
  54. package/components/pages/forgot-password-page/ForgotPasswordPage.tsx +94 -188
  55. package/components/pages/forgot-password-page/forgot-password-page.mdx +60 -60
  56. package/components/pages/forgot-password-page/forgot-password-page.test.tsx +2 -1
  57. package/components/pages/home-content/HomeContent.tsx +119 -119
  58. package/components/pages/home-content/home-content.mdx +62 -62
  59. package/components/pages/home-page/HomePage.stories.tsx +8 -15
  60. package/components/pages/home-page/home-page.mdx +53 -53
  61. package/components/pages/home-page/home-page.test.tsx +2 -1
  62. package/components/pages/login-page/LoginPage.mdx +31 -31
  63. package/components/pages/login-page/LoginPage.tsx +125 -218
  64. package/components/pages/login-page/login-page.test.tsx +2 -1
  65. package/components/pages/reset-password-page/ResetPasswordPage.tsx +153 -243
  66. package/components/pages/reset-password-page/reset-password-page.mdx +63 -63
  67. package/components/pages/template-content/TemplateContent.tsx +1353 -1353
  68. package/components/pages/template-content/template-content.mdx +61 -61
  69. package/components/pages/template-page/TemplatePage.stories.tsx +32 -32
  70. package/components/pages/template-page/template-page.mdx +53 -53
  71. package/components/pages/template-page/template-page.test.tsx +2 -1
  72. package/components/pages/verify-email-page/VerifyEmailPage.tsx +104 -206
  73. package/components/pages/verify-email-page/verify-email-page.mdx +55 -55
  74. package/components/public-api-smoke.test.tsx +10 -6
  75. package/components/shared/assistant-utils.ts +3 -2
  76. package/components/shared/navigation.ts +2 -2
  77. package/components/ui/accordion/accordion.mdx +29 -29
  78. package/components/ui/alert/alert.mdx +29 -29
  79. package/components/ui/alert-dialog/alert-dialog.mdx +26 -26
  80. package/components/ui/aspect-ratio/aspect-ratio.mdx +25 -25
  81. package/components/ui/assistant-chart/AssistantChart.tsx +8 -2
  82. package/components/ui/assistant-chart/assistant-chart.mdx +25 -25
  83. package/components/ui/avatar/avatar.mdx +26 -26
  84. package/components/ui/badge/badge.mdx +28 -28
  85. package/components/ui/breadcrumb/breadcrumb.mdx +26 -26
  86. package/components/ui/button/button.mdx +29 -29
  87. package/components/ui/calendar/calendar.mdx +26 -26
  88. package/components/ui/card/card.mdx +26 -26
  89. package/components/ui/carousel/carousel.mdx +26 -26
  90. package/components/ui/chart/chart.mdx +26 -26
  91. package/components/ui/chart/chart.test.tsx +178 -178
  92. package/components/ui/chart/chart.tsx +2252 -2245
  93. package/components/ui/checkbox/checkbox.mdx +26 -26
  94. package/components/ui/collapsible/collapsible.mdx +25 -25
  95. package/components/ui/command/command.mdx +26 -26
  96. package/components/ui/context-menu/context-menu.mdx +26 -26
  97. package/components/ui/dialog/dialog.mdx +26 -26
  98. package/components/ui/drawer/drawer.mdx +26 -26
  99. package/components/ui/dropdown-menu/dropdown-menu.mdx +26 -26
  100. package/components/ui/empty/empty.mdx +26 -26
  101. package/components/ui/file-upload/file-upload.mdx +26 -26
  102. package/components/ui/form/form.mdx +20 -20
  103. package/components/ui/google-maps-loader/google-maps-loader.mdx +68 -68
  104. package/components/ui/hover-card/hover-card.mdx +26 -26
  105. package/components/ui/input/input.mdx +29 -29
  106. package/components/ui/input-otp/input-otp.mdx +26 -26
  107. package/components/ui/label/label.mdx +25 -25
  108. package/components/ui/map/map.mdx +26 -26
  109. package/components/ui/map/map.tsx +6 -1
  110. package/components/ui/menubar/menubar.mdx +26 -26
  111. package/components/ui/navigation-menu/navigation-menu.mdx +26 -26
  112. package/components/ui/notification-badge/notification-badge.mdx +26 -26
  113. package/components/ui/page-header/page-header.mdx +44 -44
  114. package/components/ui/pagination/pagination.mdx +26 -26
  115. package/components/ui/popover/popover.mdx +26 -26
  116. package/components/ui/progress/progress.mdx +25 -25
  117. package/components/ui/radio-group/radio-group.mdx +26 -26
  118. package/components/ui/rating/rating.mdx +26 -26
  119. package/components/ui/resizable/resizable.mdx +26 -26
  120. package/components/ui/resizable/resizable.tsx +13 -8
  121. package/components/ui/rich-text-editor/rich-text-editor.mdx +47 -47
  122. package/components/ui/route-map/route-map.mdx +26 -26
  123. package/components/ui/scroll-area/scroll-area.mdx +25 -25
  124. package/components/ui/search/search.mdx +27 -27
  125. package/components/ui/select/select.mdx +26 -26
  126. package/components/ui/separator/separator.mdx +25 -25
  127. package/components/ui/sheet/sheet.mdx +26 -26
  128. package/components/ui/simple-map/simple-map.mdx +25 -25
  129. package/components/ui/skeleton/skeleton.mdx +25 -25
  130. package/components/ui/slider/slider.mdx +26 -26
  131. package/components/ui/sonner/sonner.mdx +26 -26
  132. package/components/ui/stats-card/stats-card.mdx +26 -26
  133. package/components/ui/stats-card/stats-card.stories.tsx +117 -117
  134. package/components/ui/stats-card/stats-card.tsx +109 -109
  135. package/components/ui/stepper/stepper.mdx +26 -26
  136. package/components/ui/switch/switch.mdx +25 -25
  137. package/components/ui/table/table.mdx +27 -27
  138. package/components/ui/tabs/tabs.mdx +26 -26
  139. package/components/ui/textarea/textarea.mdx +25 -25
  140. package/components/ui/timeline/timeline.mdx +26 -26
  141. package/components/ui/toggle/toggle.mdx +26 -26
  142. package/components/ui/toggle-group/toggle-group.mdx +26 -26
  143. package/components/ui/tooltip/tooltip.mdx +26 -26
  144. package/components/ui/tree-view/tree-view.mdx +26 -26
  145. package/components.json +511 -511
  146. package/contexts/BrandColorsContext.tsx +282 -282
  147. package/contexts/i18n-singleton.test.tsx +57 -0
  148. package/contexts/theme-data.ts +391 -391
  149. package/dist/{AssistantChart-DNdwuS-t.js → AssistantChart-BMNSA_Pk.js} +4 -4
  150. package/dist/{AssistantChart-COGiOV-g.cjs → AssistantChart-BzM7plyc.cjs} +31 -21
  151. package/dist/{AssistantChart-CWX1OWNM.js → AssistantChart-C842evTi.js} +31 -21
  152. package/dist/{AssistantChart-D0A1FPYt.cjs → AssistantChart-CImCNLfD.cjs} +150 -114
  153. package/dist/{AssistantChart-BW9JK6jw.cjs → AssistantChart-CVF2uh37.cjs} +4 -4
  154. package/dist/{AssistantChart-BXU1ACnh.js → AssistantChart-DfdIfmQr.js} +134 -240
  155. package/dist/{AssistantChart-BZTPJ5dP.cjs → AssistantChart-JooMdVQU.cjs} +5 -5
  156. package/dist/{AssistantChart-DMJJ_Amf.js → AssistantChart-KHS-tp-L.js} +5 -5
  157. package/dist/{BrandColorsContext-BwY-b6M4.cjs → BrandColorsContext-B4_81eas.cjs} +2 -2
  158. package/dist/{BrandColorsContext-BMRJ04Wf.js → BrandColorsContext-DbK5LnQ_.js} +2 -2
  159. package/dist/{CodeBlock-CjeFavqV.js → CodeBlock-DiKO8ocT.js} +11 -11
  160. package/dist/{CodeBlock-CV6dtsTM.cjs → CodeBlock-ZYThr7Kn.cjs} +12 -9
  161. package/dist/{FeatureCard-Cfs6Ishq.js → FeatureCard-BhpfmPPx.js} +20 -20
  162. package/dist/{FeatureCard-ie3L8Fpr.cjs → FeatureCard-D-5yS_OD.cjs} +31 -18
  163. package/dist/{progress-DDeuWPZw.cjs → ImageWithFallback-CfkAkctC.cjs} +34 -9
  164. package/dist/{progress-Cs9i4DPx.js → ImageWithFallback-bw-CxxDc.js} +27 -10
  165. package/dist/LanguageContext-CX_zdxre.js +1912 -0
  166. package/dist/LanguageContext-CjhSSqJT.cjs +1917 -0
  167. package/dist/LanguageSelector-BR3Ss0k_.js +46 -0
  168. package/dist/LanguageSelector-CHOLPmHr.cjs +48 -0
  169. package/dist/{XerticaXLogo-mqjoBiLI.js → LanguageSelector-CmTj3lWq.js} +85 -82
  170. package/dist/{XerticaXLogo-BX3ueACh.js → LanguageSelector-DxW6o7bt.js} +81 -81
  171. package/dist/{XerticaXLogo-uQgwns_E.cjs → LanguageSelector-Sw73qvWV.cjs} +84 -81
  172. package/dist/PasswordStrengthMeter-C8irs0G1.js +173 -0
  173. package/dist/PasswordStrengthMeter-MLzhax-d.cjs +179 -0
  174. package/dist/{ThemeContext-BgclCB35.js → ThemeContext-BGeh_Kdr.js} +290 -212
  175. package/dist/{ThemeContext-bSzuOit2.cjs → ThemeContext-CEY9pCn-.cjs} +15 -10
  176. package/dist/{ThemeContext-BblcjQup.cjs → ThemeContext-D50irx5T.cjs} +291 -205
  177. package/dist/{ThemeContext-RTy1m2Uq.js → ThemeContext-Dk3tH7Ym.js} +14 -13
  178. package/dist/{XerticaXLogo-CFuIlYFH.js → ThemeToggle-BpM_iHh8.js} +56 -93
  179. package/dist/{XerticaXLogo-CziKMQil.cjs → ThemeToggle-CZRlv0d4.cjs} +62 -85
  180. package/dist/{VerifyEmailPage-C_Zk6Gen.cjs → VerifyEmailPage-B6bOrybJ.cjs} +17 -17
  181. package/dist/VerifyEmailPage-BKiaw2YK.js +2954 -0
  182. package/dist/{VerifyEmailPage-B31mCrMc.js → VerifyEmailPage-BRE5Fz59.js} +8 -8
  183. package/dist/{VerifyEmailPage-Cyl55sJb.js → VerifyEmailPage-BcCluouh.js} +135 -65
  184. package/dist/VerifyEmailPage-Beaicc6V.cjs +2963 -0
  185. package/dist/VerifyEmailPage-BehPBp0F.cjs +2963 -0
  186. package/dist/VerifyEmailPage-Bx1sr0uQ.cjs +3298 -0
  187. package/dist/{VerifyEmailPage-Bci0bA-P.js → VerifyEmailPage-DUYyXCX9.js} +3 -3
  188. package/dist/VerifyEmailPage-DXXMHbzq.js +3289 -0
  189. package/dist/VerifyEmailPage-Djb6hthM.js +2954 -0
  190. package/dist/{VerifyEmailPage-u_Dn7t1U.cjs → VerifyEmailPage-DpYtFk35.cjs} +21 -21
  191. package/dist/{VerifyEmailPage-X14vhdyl.js → VerifyEmailPage-NXKWW3Dk.js} +12 -12
  192. package/dist/{VerifyEmailPage-C7KiYXrm.cjs → VerifyEmailPage-QrCz4Wqr.cjs} +11 -11
  193. package/dist/{VerifyEmailPage-Bv8Ah_TK.cjs → VerifyEmailPage-ZmQra0Wi.cjs} +144 -74
  194. package/dist/{XerticaProvider-BErr83Bg.js → XerticaProvider-BF3q_myt.js} +5 -4
  195. package/dist/{XerticaProvider-B8CaV7xu.cjs → XerticaProvider-BijpC_py.cjs} +18 -11
  196. package/dist/{XerticaProvider-DQtvJU7m.js → XerticaProvider-C2wDRQhT.js} +17 -13
  197. package/dist/{XerticaProvider-CeS5G_n5.cjs → XerticaProvider-C_JSOz1U.cjs} +2 -2
  198. package/dist/{XerticaProvider-CwOkHxiT.cjs → XerticaProvider-D5zjrqZq.cjs} +5 -4
  199. package/dist/{XerticaProvider-ra2NciRq.js → XerticaProvider-KTGl4Cn1.js} +2 -2
  200. package/dist/{alert-dialog-DqlRW_An.js → alert-dialog-BrpwXKwS.js} +3 -3
  201. package/dist/{alert-dialog-yckpaOpy.cjs → alert-dialog-BzIcTKn_.cjs} +173 -166
  202. package/dist/{alert-dialog-DhwPioBa.cjs → alert-dialog-CZU1Zase.cjs} +3 -3
  203. package/dist/{alert-dialog-KpoabzJb.js → alert-dialog-ClRoLH7l.js} +40 -56
  204. package/dist/{alert-dialog-B4M3vQKS.cjs → alert-dialog-DBBqvU-V.cjs} +53 -38
  205. package/dist/{alert-dialog-iDe5VE5o.js → alert-dialog-kty5pxQp.js} +160 -183
  206. package/dist/assistant.cjs.js +2 -2
  207. package/dist/assistant.es.js +2 -2
  208. package/dist/blocks.cjs.js +7 -3
  209. package/dist/blocks.es.js +2 -2
  210. package/dist/brand.cjs.js +7 -6
  211. package/dist/brand.es.js +3 -2
  212. package/dist/cli.js +63 -21
  213. package/dist/components/assistant/code-block/CodeBlock.d.ts +2 -1
  214. package/dist/components/assistant/formatted-document/FormattedDocument.d.ts +2 -1
  215. package/dist/components/assistant/markdown-message/MarkdownMessage.d.ts +2 -1
  216. package/dist/components/assistant/modern-chat-input/ModernChatInput.d.ts +2 -1
  217. package/dist/components/assistant/xertica-assistant/parts/AssistantCollapsedView.d.ts +2 -1
  218. package/dist/components/assistant/xertica-assistant/parts/AssistantConversationList.d.ts +2 -1
  219. package/dist/components/assistant/xertica-assistant/parts/AssistantDocumentEditor.d.ts +2 -1
  220. package/dist/components/assistant/xertica-assistant/parts/AssistantFeedbackDialog.d.ts +2 -1
  221. package/dist/components/assistant/xertica-assistant/parts/AssistantHeader.d.ts +2 -1
  222. package/dist/components/assistant/xertica-assistant/parts/AssistantMessageBubble.d.ts +2 -1
  223. package/dist/components/assistant/xertica-assistant/parts/AssistantTabBar.d.ts +2 -1
  224. package/dist/components/assistant/xertica-assistant/parts/AssistantTypingIndicator.d.ts +2 -1
  225. package/dist/components/assistant/xertica-assistant/parts/AssistantWelcomeScreen.d.ts +2 -1
  226. package/dist/components/assistant/xertica-assistant/types.d.ts +3 -2
  227. package/dist/components/assistant/xertica-assistant/use-assistant.d.ts +1 -1
  228. package/dist/components/assistant/xertica-assistant/xertica-assistant.d.ts +2 -1
  229. package/dist/components/blocks/auth/AuthPageShell.d.ts +12 -0
  230. package/dist/components/blocks/auth/PasswordStrengthMeter.d.ts +7 -0
  231. package/dist/components/blocks/auth/SocialLoginButtons.d.ts +8 -0
  232. package/dist/components/blocks/auth/index.d.ts +8 -0
  233. package/dist/components/blocks/auth/usePasswordStrength.d.ts +3 -0
  234. package/dist/components/blocks/card-patterns/ActivityCard.d.ts +1 -1
  235. package/dist/components/blocks/card-patterns/ActivityCardSkeleton.d.ts +1 -1
  236. package/dist/components/blocks/card-patterns/FeatureCard.d.ts +1 -1
  237. package/dist/components/blocks/card-patterns/FeatureCardSkeleton.d.ts +1 -1
  238. package/dist/components/blocks/card-patterns/NotificationCard.d.ts +1 -1
  239. package/dist/components/blocks/card-patterns/NotificationCardSkeleton.d.ts +1 -1
  240. package/dist/components/blocks/card-patterns/ProfileCard.d.ts +1 -1
  241. package/dist/components/blocks/card-patterns/ProfileCardSkeleton.d.ts +1 -1
  242. package/dist/components/blocks/card-patterns/ProjectCard.d.ts +1 -1
  243. package/dist/components/blocks/card-patterns/ProjectCardSkeleton.d.ts +1 -1
  244. package/dist/components/blocks/card-patterns/QuickActionCard.d.ts +1 -1
  245. package/dist/components/blocks/card-patterns/QuickActionCardSkeleton.d.ts +1 -1
  246. package/dist/components/blocks/index.d.ts +1 -0
  247. package/dist/components/brand/language-selector/LanguageSelector.d.ts +2 -1
  248. package/dist/components/brand/theme-toggle/ThemeToggle.d.ts +2 -1
  249. package/dist/components/brand/xertica-logo/XerticaLogo.d.ts +2 -1
  250. package/dist/components/brand/xertica-orbe/XerticaOrbe.d.ts +2 -1
  251. package/dist/components/brand/xertica-provider/XerticaProvider.d.ts +1 -1
  252. package/dist/components/brand/xertica-xlogo/XerticaXLogo.d.ts +2 -1
  253. package/dist/components/examples/DrawingMapExample.d.ts +2 -1
  254. package/dist/components/examples/FilterableMapExample.d.ts +1 -1
  255. package/dist/components/examples/MapShowcase.d.ts +2 -1
  256. package/dist/components/examples/SimpleFilterableMap.d.ts +2 -1
  257. package/dist/components/figma/ImageWithFallback.d.ts +1 -1
  258. package/dist/components/layout/header/header.d.ts +1 -1
  259. package/dist/components/layout/sidebar/sidebar.d.ts +25 -13
  260. package/dist/components/media/FloatingMediaWrapper.d.ts +1 -1
  261. package/dist/components/media/audio-player/AudioPlayer.d.ts +2 -1
  262. package/dist/components/media/video-player/VideoPlayer.d.ts +2 -1
  263. package/dist/components/pages/forgot-password-page/ForgotPasswordPage.d.ts +2 -1
  264. package/dist/components/pages/home-content/HomeContent.d.ts +2 -1
  265. package/dist/components/pages/home-page/HomePage.d.ts +2 -1
  266. package/dist/components/pages/login-page/LoginPage.d.ts +2 -1
  267. package/dist/components/pages/reset-password-page/ResetPasswordPage.d.ts +3 -2
  268. package/dist/components/pages/template-content/TemplateContent.d.ts +2 -1
  269. package/dist/components/pages/template-page/TemplatePage.d.ts +2 -1
  270. package/dist/components/pages/verify-email-page/VerifyEmailPage.d.ts +2 -1
  271. package/dist/components/shared/CustomTooltipContent.d.ts +1 -1
  272. package/dist/components/shared/assistant-utils.d.ts +3 -2
  273. package/dist/components/shared/error-boundary.d.ts +4 -4
  274. package/dist/components/shared/error-fallbacks.d.ts +4 -3
  275. package/dist/components/shared/navigation.d.ts +6 -2
  276. package/dist/components/ui/accordion/accordion.d.ts +4 -4
  277. package/dist/components/ui/alert/alert.d.ts +3 -3
  278. package/dist/components/ui/alert-dialog/alert-dialog.d.ts +10 -10
  279. package/dist/components/ui/aspect-ratio/aspect-ratio.d.ts +1 -1
  280. package/dist/components/ui/assistant-chart/AssistantChart.d.ts +4 -3
  281. package/dist/components/ui/avatar/avatar.d.ts +3 -3
  282. package/dist/components/ui/badge/badge.d.ts +1 -1
  283. package/dist/components/ui/breadcrumb/breadcrumb.d.ts +7 -7
  284. package/dist/components/ui/calendar/calendar.d.ts +2 -2
  285. package/dist/components/ui/card/card.d.ts +7 -7
  286. package/dist/components/ui/carousel/carousel.d.ts +5 -5
  287. package/dist/components/ui/chart/chart.d.ts +16 -16
  288. package/dist/components/ui/collapsible/collapsible.d.ts +3 -3
  289. package/dist/components/ui/command/command.d.ts +9 -9
  290. package/dist/components/ui/context-menu/context-menu.d.ts +15 -15
  291. package/dist/components/ui/dialog/dialog.d.ts +9 -9
  292. package/dist/components/ui/drawer/drawer.d.ts +10 -10
  293. package/dist/components/ui/dropdown-menu/dropdown-menu.d.ts +15 -15
  294. package/dist/components/ui/form/form.d.ts +7 -7
  295. package/dist/components/ui/google-maps-loader/google-maps-loader.d.ts +3 -3
  296. package/dist/components/ui/hover-card/hover-card.d.ts +3 -3
  297. package/dist/components/ui/input-otp/input-otp.d.ts +4 -4
  298. package/dist/components/ui/label/label.d.ts +1 -1
  299. package/dist/components/ui/menubar/menubar.d.ts +16 -16
  300. package/dist/components/ui/navigation-menu/navigation-menu.d.ts +8 -8
  301. package/dist/components/ui/page-header/page-header.d.ts +3 -3
  302. package/dist/components/ui/pagination/pagination.d.ts +7 -7
  303. package/dist/components/ui/popover/popover.d.ts +4 -4
  304. package/dist/components/ui/progress/progress.d.ts +1 -1
  305. package/dist/components/ui/resizable/resizable.d.ts +11 -7
  306. package/dist/components/ui/rich-text-editor/rich-text-editor.d.ts +1 -1
  307. package/dist/components/ui/scroll-area/scroll-area.d.ts +2 -2
  308. package/dist/components/ui/separator/separator.d.ts +1 -1
  309. package/dist/components/ui/sheet/sheet.d.ts +10 -10
  310. package/dist/components/ui/skeleton/skeleton.d.ts +1 -1
  311. package/dist/components/ui/sonner/sonner.d.ts +2 -1
  312. package/dist/components/ui/stats-card/stats-card-skeleton.d.ts +1 -1
  313. package/dist/components/ui/table/table.d.ts +8 -8
  314. package/dist/components/ui/tabs/tabs.d.ts +4 -4
  315. package/dist/components/ui/tooltip/tooltip.d.ts +4 -4
  316. package/dist/contexts/ApiKeyContext.d.ts +2 -2
  317. package/dist/contexts/AssistenteContext.d.ts +1 -1
  318. package/dist/contexts/AuthContext.d.ts +1 -1
  319. package/dist/contexts/LanguageContext.d.ts +2 -2
  320. package/dist/contexts/LayoutContext.d.ts +2 -2
  321. package/dist/contexts/ThemeContext.d.ts +1 -1
  322. package/dist/features/assistant/hooks/useAssistantConfig.d.ts +1 -1
  323. package/dist/features/home/hooks/useDashboardStats.d.ts +1 -1
  324. package/dist/features/home/hooks/useFeatureCards.d.ts +1 -1
  325. package/dist/features/home/hooks/useTeamMembers.d.ts +1 -1
  326. package/dist/{google-maps-loader-BCe58h9D.js → google-maps-loader-BOug999f.js} +0 -4
  327. package/dist/{google-maps-loader-casMyxlo.cjs → google-maps-loader-DfeBRI8Q.cjs} +0 -5
  328. package/dist/hooks.cjs.js +6 -5
  329. package/dist/hooks.es.js +3 -2
  330. package/dist/{index-9GWd0qxq.cjs → index-BQjx1Mdf.cjs} +1 -1
  331. package/dist/index-Bei5DzAL.js +6 -0
  332. package/dist/index.cjs.js +32 -27
  333. package/dist/index.es.js +15 -14
  334. package/dist/layout.cjs.js +1 -1
  335. package/dist/layout.es.js +1 -1
  336. package/dist/pages.cjs.js +1 -1
  337. package/dist/pages.es.js +1 -1
  338. package/dist/{rich-text-editor-B2CKz7nx.cjs → rich-text-editor-BuqSu718.cjs} +68 -3
  339. package/dist/{rich-text-editor-DloeW0wc.js → rich-text-editor-CV8FtA_l.js} +66 -4
  340. package/dist/{sidebar-B4ZWaMrE.js → sidebar-B7YzwAp3.js} +365 -6
  341. package/dist/{sidebar-CLmIjgNd.cjs → sidebar-BGJ0j567.cjs} +84 -67
  342. package/dist/{sidebar-Duermn32.js → sidebar-BI6mZlC8.js} +81 -64
  343. package/dist/{sidebar-BS1p2V7t.cjs → sidebar-BtmotOg-.cjs} +367 -9
  344. package/dist/{sidebar-DyYvgyBj.cjs → sidebar-Cb0U7NsV.cjs} +368 -9
  345. package/dist/{sidebar-QwCoeoZ_.js → sidebar-D3EgGVxF.js} +1 -1
  346. package/dist/{sidebar-CQn_rwxP.cjs → sidebar-DDTqMGnR.cjs} +4 -4
  347. package/dist/{sidebar-3XyzjVBw.js → sidebar-I7EXyf5D.js} +364 -6
  348. package/dist/ui.cjs.js +9 -9
  349. package/dist/ui.es.js +5 -5
  350. package/dist/{xertica-assistant-BbgzsgsG.js → xertica-assistant-8fh8KA2K.js} +60 -51
  351. package/dist/{xertica-assistant-d2qs2Vd9.cjs → xertica-assistant-BjfG3zyL.cjs} +63 -46
  352. package/dist/{xertica-assistant-B687qEPU.js → xertica-assistant-CJVnvMqU.js} +17 -8
  353. package/dist/{xertica-assistant-sOHwTgIP.cjs → xertica-assistant-CKPtc1z5.cjs} +19 -10
  354. package/dist/{xertica-assistant-QFUnv5I2.cjs → xertica-assistant-DER0brPQ.cjs} +6 -5
  355. package/dist/{xertica-assistant-CyikE3N_.js → xertica-assistant-DQCjpk5M.js} +4 -3
  356. package/dist/{xertica-assistant-B1NaSFFj.js → xertica-assistant-Dk-9AJzw.js} +6 -5
  357. package/dist/{xertica-assistant-CIaUlbIt.cjs → xertica-assistant-DxhIKvuY.cjs} +8 -7
  358. package/dist/xertica-ui.css +2 -2
  359. package/docs/components/audio-player.md +1 -1
  360. package/docs/components/sidebar.md +331 -331
  361. package/docs/components/stats-card.md +138 -138
  362. package/docs/components/toggle.md +12 -0
  363. package/docs/doc-audit.md +35 -229
  364. package/docs/i18n.md +33 -10
  365. package/docs/llms.md +6 -4
  366. package/llms-compact.txt +444 -444
  367. package/llms-full.txt +12144 -12144
  368. package/llms.txt +187 -187
  369. package/package.json +223 -219
  370. package/scripts/a11y-audit.mjs +162 -162
  371. package/scripts/fix-stories.cjs +19 -19
  372. package/styles/globals.css +0 -1
  373. package/styles/xertica/base.css +8 -0
  374. package/styles/xertica/tokens.css +240 -240
  375. package/templates/.env.example +2 -2
  376. package/templates/.prettierignore +4 -4
  377. package/templates/.prettierrc +10 -10
  378. package/templates/guidelines/Guidelines.md +865 -865
  379. package/templates/index.html +13 -13
  380. package/templates/package.json +69 -69
  381. package/templates/src/app/components/AuthGuard.tsx +11 -0
  382. package/templates/src/features/auth/ui/ForgotPasswordContent.tsx +1 -2
  383. package/templates/src/features/auth/ui/LoginContent.tsx +1 -2
  384. package/templates/src/features/auth/ui/ResetPasswordContent.tsx +3 -57
  385. package/templates/src/features/auth/ui/VerifyEmailContent.tsx +1 -2
  386. package/templates/src/features/home/index.ts +0 -10
  387. package/templates/src/features/home/ui/HomeContent.tsx +62 -116
  388. package/templates/src/features/settings/index.ts +2 -0
  389. package/templates/src/features/settings/ui/SettingsContent.tsx +134 -0
  390. package/templates/src/features/template/index.ts +0 -4
  391. package/templates/src/features/template/ui/TemplateContent.tsx +68 -1321
  392. package/templates/src/i18n.ts +36 -13
  393. package/templates/src/locales/en/pages/settings.json +15 -0
  394. package/templates/src/locales/en/pages/templates.json +5 -313
  395. package/templates/src/locales/es/pages/settings.json +15 -0
  396. package/templates/src/locales/es/pages/templates.json +5 -313
  397. package/templates/src/locales/pt-BR/pages/settings.json +15 -0
  398. package/templates/src/locales/pt-BR/pages/templates.json +5 -313
  399. package/templates/src/pages/SettingsPage.tsx +28 -0
  400. package/templates/src/shared/config/navigation.ts +2 -2
  401. package/templates/src/styles/xertica/tokens.css +240 -240
  402. package/utils/gemini.ts +2 -2
  403. package/dist/AssistantChart-9w31gdAb.cjs +0 -3363
  404. package/dist/AssistantChart-BAudAfne.cjs +0 -3591
  405. package/dist/AssistantChart-BAx9VQvb.cjs +0 -3374
  406. package/dist/AssistantChart-BP8upjMk.js +0 -3565
  407. package/dist/AssistantChart-CVko2A1W.js +0 -3348
  408. package/dist/AssistantChart-CVzmmhx4.js +0 -3337
  409. package/dist/AssistantChart-CeU2dIb6.js +0 -3469
  410. package/dist/AssistantChart-DO5UuX4J.cjs +0 -3495
  411. package/dist/AudioPlayer-1ypwE2Wh.cjs +0 -936
  412. package/dist/AudioPlayer-BJ2IuRQW.cjs +0 -1275
  413. package/dist/AudioPlayer-BWDW6TN9.cjs +0 -1279
  414. package/dist/AudioPlayer-CM962GEh.js +0 -1280
  415. package/dist/AudioPlayer-CbZId7cj.cjs +0 -998
  416. package/dist/AudioPlayer-DU8zt8-z.js +0 -999
  417. package/dist/AudioPlayer-DuKXrCfy.js +0 -937
  418. package/dist/AudioPlayer-Xxma6_-H.js +0 -1276
  419. package/dist/BrandColorsContext-DLVJgtmH.js +0 -666
  420. package/dist/BrandColorsContext-DZT7JjeD.js +0 -659
  421. package/dist/BrandColorsContext-GUyFmkX0.cjs +0 -665
  422. package/dist/BrandColorsContext-awnBCmC4.cjs +0 -666
  423. package/dist/CodeBlock-C2ZUXt8V.js +0 -219
  424. package/dist/CodeBlock-C5oYnLQp.cjs +0 -218
  425. package/dist/CodeBlock-DuxdtN-l.js +0 -219
  426. package/dist/CodeBlock-POVYONvC.cjs +0 -218
  427. package/dist/CustomTooltipContent-BT6brkVJ.cjs +0 -49
  428. package/dist/CustomTooltipContent-CfOfikhq.js +0 -33
  429. package/dist/CustomTooltipContent-DHjkY0ww.js +0 -40
  430. package/dist/CustomTooltipContent-c_K-DWRr.cjs +0 -56
  431. package/dist/FeatureCard-9GhnlgL3.js +0 -336
  432. package/dist/FeatureCard-Bcb3Zi54.js +0 -302
  433. package/dist/FeatureCard-CkPd2psQ.cjs +0 -301
  434. package/dist/FeatureCard-Dkp-xmzM.cjs +0 -335
  435. package/dist/FeatureCardSkeleton-CAgu8stE.js +0 -27
  436. package/dist/FeatureCardSkeleton-mO3Bdm_V.cjs +0 -26
  437. package/dist/LanguageContext-BwhwC3G2.js +0 -657
  438. package/dist/LanguageContext-DvUt5jBg.cjs +0 -656
  439. package/dist/LayoutContext-BDmcZfMH.cjs +0 -84
  440. package/dist/LayoutContext-C4kKN9RV.cjs +0 -93
  441. package/dist/LayoutContext-CGEe1oPq.js +0 -94
  442. package/dist/LayoutContext-CwT5KLiW.cjs +0 -104
  443. package/dist/LayoutContext-DVLCsoQn.js +0 -105
  444. package/dist/LayoutContext-dbQvdC4O.js +0 -85
  445. package/dist/ThemeContext-BbBNoFTG.js +0 -735
  446. package/dist/ThemeContext-Bo-W2WZH.js +0 -1856
  447. package/dist/ThemeContext-CP3a0jxy.cjs +0 -1855
  448. package/dist/ThemeContext-Cmr8Ex8H.cjs +0 -734
  449. package/dist/ThemeContext-CpqYShLq.cjs +0 -324
  450. package/dist/ThemeContext-D3LzacmG.js +0 -188
  451. package/dist/ThemeContext-DQUOeziy.cjs +0 -1855
  452. package/dist/ThemeContext-Du2nE1PL.js +0 -325
  453. package/dist/ThemeContext-GeEBTJ3q.cjs +0 -1621
  454. package/dist/ThemeContext-JyLK9B1o.js +0 -1622
  455. package/dist/ThemeContext-U4dEYc6C.cjs +0 -187
  456. package/dist/ThemeContext-ept8jhXI.js +0 -1856
  457. package/dist/VerifyEmailPage-BE-L9mB7.js +0 -2828
  458. package/dist/VerifyEmailPage-BIBOKV7Z.js +0 -3214
  459. package/dist/VerifyEmailPage-BJjAMUTW.js +0 -3223
  460. package/dist/VerifyEmailPage-BRSP-Pwt.cjs +0 -3196
  461. package/dist/VerifyEmailPage-Bae2cBXT.cjs +0 -2827
  462. package/dist/VerifyEmailPage-BiRm7Nh4.cjs +0 -3213
  463. package/dist/VerifyEmailPage-Bvfv8HVQ.js +0 -3214
  464. package/dist/VerifyEmailPage-CR7kb5df.cjs +0 -2827
  465. package/dist/VerifyEmailPage-C_ihbcth.js +0 -2828
  466. package/dist/VerifyEmailPage-CbgjOF0v.js +0 -2828
  467. package/dist/VerifyEmailPage-CdYPSJoO.js +0 -2828
  468. package/dist/VerifyEmailPage-CkBYfsNy.cjs +0 -3232
  469. package/dist/VerifyEmailPage-D-FRj5TU.cjs +0 -3213
  470. package/dist/VerifyEmailPage-DF2ilhum.cjs +0 -3210
  471. package/dist/VerifyEmailPage-DMBh4NM9.cjs +0 -2827
  472. package/dist/VerifyEmailPage-DTtFfC-J.js +0 -3197
  473. package/dist/VerifyEmailPage-Dt7zgA4w.cjs +0 -2827
  474. package/dist/VerifyEmailPage-EhudUdqF.js +0 -3211
  475. package/dist/VerifyEmailPage-RrUApqBN.js +0 -3214
  476. package/dist/VerifyEmailPage-VoMI7MYH.cjs +0 -3213
  477. package/dist/VerifyEmailPage-hdB8JQGv.cjs +0 -3213
  478. package/dist/VerifyEmailPage-vYHbYK3q.js +0 -3214
  479. package/dist/XerticaOrbe-BCUVSPPB.cjs +0 -1924
  480. package/dist/XerticaOrbe-BGIB1CTK.js +0 -1355
  481. package/dist/XerticaOrbe-By8eEvR-.js +0 -1925
  482. package/dist/XerticaOrbe-FWGjBj0U.cjs +0 -1354
  483. package/dist/XerticaProvider-AChwphCO.cjs +0 -48
  484. package/dist/XerticaProvider-AbWlr7Af.cjs +0 -48
  485. package/dist/XerticaProvider-BITjgC5p.js +0 -39
  486. package/dist/XerticaProvider-BSyFrmC0.js +0 -49
  487. package/dist/XerticaProvider-By8q3Roe.cjs +0 -38
  488. package/dist/XerticaProvider-CUYJZc32.js +0 -49
  489. package/dist/XerticaProvider-CW9hpCdF.cjs +0 -39
  490. package/dist/XerticaProvider-CWgby5mY.js +0 -49
  491. package/dist/XerticaProvider-CWs6EwNa.js +0 -49
  492. package/dist/XerticaProvider-CiNKjMx1.cjs +0 -48
  493. package/dist/XerticaProvider-CjQAQPcn.cjs +0 -48
  494. package/dist/XerticaProvider-D5lLumH-.js +0 -49
  495. package/dist/XerticaProvider-DUOJg9iX.js +0 -49
  496. package/dist/XerticaProvider-Dl_b72_l.cjs +0 -51
  497. package/dist/XerticaProvider-qQUDop71.cjs +0 -48
  498. package/dist/XerticaProvider-siSt9uG2.js +0 -40
  499. package/dist/XerticaXLogo-8TTzBjHw.cjs +0 -251
  500. package/dist/XerticaXLogo-B2svDGZh.cjs +0 -251
  501. package/dist/XerticaXLogo-BWaag64t.js +0 -252
  502. package/dist/XerticaXLogo-CU-U-GP4.cjs +0 -251
  503. package/dist/XerticaXLogo-ChryA6xj.js +0 -252
  504. package/dist/XerticaXLogo-CowGv7BC.js +0 -252
  505. package/dist/XerticaXLogo-D8jf0SNv.cjs +0 -214
  506. package/dist/XerticaXLogo-DHz5SugF.js +0 -252
  507. package/dist/XerticaXLogo-DTee_y8X.cjs +0 -251
  508. package/dist/XerticaXLogo-DfUvz-lD.js +0 -243
  509. package/dist/XerticaXLogo-DzXKZBtG.js +0 -231
  510. package/dist/XerticaXLogo-fAJMy3H4.js +0 -215
  511. package/dist/XerticaXLogo-kslQ8Tk_.cjs +0 -251
  512. package/dist/XerticaXLogo-xWbElCZj.cjs +0 -230
  513. package/dist/avatar-Cq5_E8r9.cjs +0 -78
  514. package/dist/avatar-CuYu2MKt.cjs +0 -72
  515. package/dist/avatar-Cxyofu1H.js +0 -56
  516. package/dist/avatar-DSBfJMRJ.js +0 -62
  517. package/dist/breadcrumb-CVzzzqfo.js +0 -103
  518. package/dist/breadcrumb-DIJ0X3Ot.js +0 -96
  519. package/dist/breadcrumb-DjSxkjlQ.cjs +0 -95
  520. package/dist/breadcrumb-DnDlQFg2.cjs +0 -102
  521. package/dist/button-0CXwCRTm.cjs +0 -85
  522. package/dist/button-Bnv9SvYK.cjs +0 -79
  523. package/dist/button-C6uvh0rV.js +0 -63
  524. package/dist/button-qkz7Icqf.js +0 -69
  525. package/dist/card-B8-Gl5DL.js +0 -73
  526. package/dist/card-Bv_c57KU.cjs +0 -72
  527. package/dist/card-DJTsYhLL.cjs +0 -89
  528. package/dist/card-fi8zAN-I.js +0 -90
  529. package/dist/dropdown-menu-B_uEXNc4.cjs +0 -220
  530. package/dist/dropdown-menu-CFuCssWA.js +0 -204
  531. package/dist/dropdown-menu-Ze1YZFyc.cjs +0 -247
  532. package/dist/dropdown-menu-nc_Lvg-u.js +0 -231
  533. package/dist/google-maps-loader-BVoVhUwA.js +0 -316
  534. package/dist/google-maps-loader-BqsYL48U.cjs +0 -285
  535. package/dist/google-maps-loader-DjJaYhV6.cjs +0 -316
  536. package/dist/google-maps-loader-t2IlYBzw.js +0 -286
  537. package/dist/index-B7A523O_.js +0 -8
  538. package/dist/index-BabBx2pa.js +0 -6
  539. package/dist/index-CkTUgOwX.js +0 -8
  540. package/dist/index-D3RLKRAs.cjs +0 -7
  541. package/dist/index-DmHhnb7a.cjs +0 -7
  542. package/dist/index.umd.js +0 -13712
  543. package/dist/input-CNFHVKvo.cjs +0 -145
  544. package/dist/input-CPl84zAx.cjs +0 -151
  545. package/dist/input-C_UiS2Py.cjs +0 -152
  546. package/dist/input-D-6IsAca.js +0 -133
  547. package/dist/input-cc-PTD4R.js +0 -123
  548. package/dist/input-wPYg0d0P.js +0 -127
  549. package/dist/progress-BLwqortq.cjs +0 -105
  550. package/dist/progress-D9mtsf6j.js +0 -89
  551. package/dist/rich-text-editor-1UW3Y2JK.js +0 -2891
  552. package/dist/rich-text-editor-BM-LPpTh.js +0 -2902
  553. package/dist/rich-text-editor-BmsjY03B.js +0 -2949
  554. package/dist/rich-text-editor-BrdC5evx.js +0 -2244
  555. package/dist/rich-text-editor-CyggTOLQ.cjs +0 -2908
  556. package/dist/rich-text-editor-DbV-S7Fi.cjs +0 -2261
  557. package/dist/rich-text-editor-DoCkKhzk.cjs +0 -2919
  558. package/dist/rich-text-editor-DqLICivI.js +0 -2832
  559. package/dist/rich-text-editor-DxO1Hz3a.cjs +0 -2903
  560. package/dist/rich-text-editor-GS2kpTAK.cjs +0 -2966
  561. package/dist/routes.d.ts +0 -10
  562. package/dist/select-Cmb8zF7w.cjs +0 -156
  563. package/dist/select-CprSNX0z.js +0 -139
  564. package/dist/select-DT7Zjn42.cjs +0 -162
  565. package/dist/select-En8DkoaQ.js +0 -145
  566. package/dist/sidebar-B3EYhli0.cjs +0 -800
  567. package/dist/sidebar-B9NR0lCe.cjs +0 -800
  568. package/dist/sidebar-BcJ6LxvX.cjs +0 -733
  569. package/dist/sidebar-BvF5I2Ue.cjs +0 -800
  570. package/dist/sidebar-C5B_LHek.cjs +0 -795
  571. package/dist/sidebar-CA6_ek3f.js +0 -801
  572. package/dist/sidebar-CRMiBtAi.js +0 -801
  573. package/dist/sidebar-CVUGHOS_.cjs +0 -756
  574. package/dist/sidebar-CZ2mWaMM.cjs +0 -800
  575. package/dist/sidebar-CmvwjnVb.js +0 -757
  576. package/dist/sidebar-CplprZpM.js +0 -801
  577. package/dist/sidebar-Dz7bd3zP.js +0 -796
  578. package/dist/sidebar-KIS0C2JH.js +0 -801
  579. package/dist/sidebar-OTO_up7Z.js +0 -801
  580. package/dist/sidebar-WoKvFWCN.js +0 -717
  581. package/dist/sidebar-zowjejT2.cjs +0 -800
  582. package/dist/skeleton-4zoHiFJ_.cjs +0 -83
  583. package/dist/skeleton-C4bHqUBf.js +0 -84
  584. package/dist/slider-C0JKA9k3.js +0 -56
  585. package/dist/slider-CPmwzm_3.cjs +0 -73
  586. package/dist/slider-DcJharR9.cjs +0 -73
  587. package/dist/slider-Pay3fka2.js +0 -56
  588. package/dist/sonner-DeUIj0Zg.cjs +0 -63
  589. package/dist/sonner-g9RIfi35.js +0 -47
  590. package/dist/tooltip-CNtsL5cB.cjs +0 -71
  591. package/dist/tooltip-CTyZCD8h.js +0 -55
  592. package/dist/tooltip-CwgSdy3h.cjs +0 -67
  593. package/dist/tooltip-D-OrYnKO.js +0 -51
  594. package/dist/use-audio-player-BJFV24XW.js +0 -188
  595. package/dist/use-audio-player-Bkh23vQ3.js +0 -177
  596. package/dist/use-audio-player-Dn1NR9xN.cjs +0 -176
  597. package/dist/use-audio-player-DyVYlOnR.cjs +0 -187
  598. package/dist/use-mobile-BB4M_4Ru.cjs +0 -47
  599. package/dist/use-mobile-CLu9nqGL.js +0 -31
  600. package/dist/use-mobile-CNwiBNxu.js +0 -33
  601. package/dist/use-mobile-Dlf74ufz.cjs +0 -49
  602. package/dist/xertica-assistant-B1IaHXnB.cjs +0 -1894
  603. package/dist/xertica-assistant-BMqdyRVi.js +0 -2082
  604. package/dist/xertica-assistant-Bj3vBCq_.cjs +0 -2081
  605. package/dist/xertica-assistant-CfBCo21g.js +0 -2148
  606. package/dist/xertica-assistant-DIM_Do1k.js +0 -1866
  607. package/dist/xertica-assistant-DPsESB6t.js +0 -1878
  608. package/dist/xertica-assistant-DrsRwla1.cjs +0 -2147
  609. package/dist/xertica-assistant-Qp3ydksa.cjs +0 -1882
  610. package/dist/xertica-assistant-gnCJdcZY.js +0 -1866
  611. package/dist/xertica-assistant-yyrR0JKl.cjs +0 -1882
  612. package/styles/xertica/app-overrides/scrollbar.css +0 -33
  613. package/templates/src/features/auth/ui/SocialLoginButtons.tsx +0 -78
  614. package/templates/src/features/home/data/mock.ts +0 -41
  615. package/templates/src/features/home/hooks/useFeatureCards.ts +0 -20
  616. package/templates/src/features/home/store/dashboardStore.ts +0 -25
  617. package/templates/src/features/template/ui/CrudTemplate.tsx +0 -112
  618. package/templates/src/features/template/ui/DashboardTemplate.tsx +0 -110
  619. package/templates/src/features/template/ui/FormTemplate.tsx +0 -117
  620. package/templates/src/features/template/ui/LoginTemplate.tsx +0 -59
  621. package/templates/src/locales/en/pages/crudTemplate.json +0 -25
  622. package/templates/src/locales/en/pages/dashboardTemplate.json +0 -20
  623. package/templates/src/locales/en/pages/formTemplate.json +0 -16
  624. package/templates/src/locales/en/pages/loginTemplate.json +0 -9
  625. package/templates/src/locales/es/pages/crudTemplate.json +0 -25
  626. package/templates/src/locales/es/pages/dashboardTemplate.json +0 -20
  627. package/templates/src/locales/es/pages/formTemplate.json +0 -16
  628. package/templates/src/locales/es/pages/loginTemplate.json +0 -9
  629. package/templates/src/locales/pt-BR/pages/crudTemplate.json +0 -25
  630. package/templates/src/locales/pt-BR/pages/dashboardTemplate.json +0 -20
  631. package/templates/src/locales/pt-BR/pages/formTemplate.json +0 -16
  632. package/templates/src/locales/pt-BR/pages/loginTemplate.json +0 -9
  633. package/templates/vite.config.d.ts +0 -2
  634. package/templates/vite.config.js +0 -20
  635. package/dist/{XerticaXLogo-qBPhwK3g.cjs → LanguageSelector-1cINLJ23.cjs} +80 -80
@@ -1,2245 +1,2252 @@
1
- 'use client';
2
-
3
- import * as React from 'react';
4
- import * as RechartsPrimitive from 'recharts';
5
- import type {
6
- Payload as TooltipPayloadItem,
7
- ValueType as TooltipValueType,
8
- NameType as TooltipNameType,
9
- } from 'recharts/types/component/DefaultTooltipContent';
10
- import type { Payload as LegendPayloadItem } from 'recharts/types/component/DefaultLegendContent';
11
-
12
- import { cn } from '../../shared/utils';
13
- import { Alert, AlertDescription, AlertTitle } from '../alert';
14
- import { Button } from '../button';
15
- import {
16
- Card,
17
- CardAction,
18
- CardContent,
19
- CardDescription,
20
- CardFooter,
21
- CardHeader,
22
- CardTitle,
23
- } from '../card';
24
- import { Empty, EmptyAction, EmptyDescription, EmptyIcon, EmptyTitle } from '../empty';
25
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../select';
26
- import { Skeleton } from '../skeleton';
27
- import { BarChart3, RefreshCw, WifiOff } from 'lucide-react';
28
-
29
- // Format: { THEME_NAME: CSS_SELECTOR }
30
- const THEMES = { light: '', dark: '.dark' } as const;
31
-
32
- export type ChartConfig = {
33
- [k in string]: {
34
- label?: React.ReactNode;
35
- icon?: React.ComponentType;
36
- } & (
37
- | { color?: string; theme?: never }
38
- | { color?: never; theme: Record<keyof typeof THEMES, string> }
39
- );
40
- };
41
-
42
- type ChartContextProps = {
43
- config: ChartConfig;
44
- };
45
-
46
- export type DashboardChartDatum = Record<string, string | number | null | undefined>;
47
-
48
- export type DashboardChartSeries = {
49
- key: string;
50
- label?: React.ReactNode;
51
- type?: 'bar' | 'line' | 'area';
52
- stackId?: string;
53
- yAxisId?: string;
54
- };
55
-
56
- export type DashboardChartColors = string[] | Record<string, string>;
57
-
58
- export type ChartPeriod = {
59
- value: string;
60
- label: string;
61
- };
62
-
63
- export type ChartBarSize = 'sm' | 'md' | 'lg' | 'xl' | number;
64
-
65
- /** Curve interpolation type for line and area charts */
66
- export type ChartCurveType =
67
- | 'monotone'
68
- | 'linear'
69
- | 'step'
70
- | 'stepBefore'
71
- | 'stepAfter'
72
- | 'natural'
73
- | 'basis';
74
-
75
- type ChartValueFormatter = (value: number | string) => string;
76
-
77
- export type ChartErrorState = boolean | string | Error | React.ReactNode;
78
-
79
- export type ChartStateProps = {
80
- isLoading?: boolean;
81
- error?: ChartErrorState;
82
- onRetry?: () => void;
83
- retryLabel?: React.ReactNode;
84
- emptyTitle?: React.ReactNode;
85
- emptyDescription?: React.ReactNode;
86
- errorTitle?: React.ReactNode;
87
- errorDescription?: React.ReactNode;
88
- loadingLabel?: React.ReactNode;
89
- stateClassName?: string;
90
- };
91
-
92
- const defaultPeriods: ChartPeriod[] = [
93
- { value: '7d', label: '7 days' },
94
- { value: '30d', label: '30 days' },
95
- { value: '90d', label: '90 days' },
96
- ];
97
-
98
- const defaultChartColors = [
99
- 'var(--chart-1)',
100
- 'var(--chart-2)',
101
- 'var(--chart-3)',
102
- 'var(--chart-4)',
103
- 'var(--chart-5)',
104
- 'var(--chart-6)',
105
- 'var(--chart-7)',
106
- 'var(--chart-8)',
107
- ];
108
-
109
- const chartBarSizes: Record<Exclude<ChartBarSize, number>, number> = {
110
- sm: 8,
111
- md: 14,
112
- lg: 22,
113
- xl: 32,
114
- };
115
-
116
- const ChartContext = React.createContext<ChartContextProps | null>(null);
117
-
118
- export function useChart() {
119
- const context = React.useContext(ChartContext);
120
-
121
- if (!context) {
122
- throw new Error('useChart must be used within a <ChartContainer />');
123
- }
124
-
125
- return context;
126
- }
127
-
128
- /**
129
- * Root container for Recharts-based charts with theme-aware color injection.
130
- *
131
- * @description
132
- * Wraps Recharts' `ResponsiveContainer` and injects CSS custom properties
133
- * (`--color-*`) from a `ChartConfig` object, enabling full dark-mode
134
- * support without hard-coded hex values in chart elements.
135
- *
136
- * @ai-rules
137
- * 1. NEVER pass hex colors directly to Recharts elements (e.g., `fill="#4F46E5"`).
138
- * Always use `fill="var(--color-keyName)"` referencing the injected CSS variables.
139
- * 2. This wrapper is REQUIRED to use `ChartTooltipContent` and `ChartLegendContent`.
140
- * 3. Set height via `className="h-[300px]"` on `ChartContainer`, not on Recharts components.
141
- * 4. Do NOT add another `<ResponsiveContainer>` — it is already included inside.
142
- */
143
- function ChartContainer({
144
- id,
145
- className,
146
- children,
147
- config,
148
- ...props
149
- }: React.ComponentProps<'div'> & {
150
- config: ChartConfig;
151
- children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>['children'];
152
- }) {
153
- const uniqueId = React.useId();
154
- const chartId = `chart-${id || uniqueId.replace(/:/g, '')}`;
155
-
156
- return (
157
- <ChartContext.Provider value={{ config }}>
158
- <div
159
- data-slot="chart"
160
- data-chart={chartId}
161
- className={cn(
162
- "[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border relative h-[300px] min-h-[200px] w-full min-w-0 overflow-hidden text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
163
- className
164
- )}
165
- {...props}
166
- >
167
- <ChartStyle id={chartId} config={config} />
168
- <RechartsPrimitive.ResponsiveContainer width="100%" height="100%">
169
- {children}
170
- </RechartsPrimitive.ResponsiveContainer>
171
- </div>
172
- </ChartContext.Provider>
173
- );
174
- }
175
-
176
- const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
177
- const colorConfig = Object.entries(config).filter(([, config]) => config.theme || config.color);
178
-
179
- if (!colorConfig.length) {
180
- return null;
181
- }
182
-
183
- return (
184
- <style
185
- dangerouslySetInnerHTML={{
186
- __html: Object.entries(THEMES)
187
- .map(
188
- ([theme, prefix]) => `
189
- ${prefix} [data-chart=${id}] {
190
- ${colorConfig
191
- .map(([key, itemConfig]) => {
192
- const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color;
193
- return color ? ` --color-${key}: ${color};` : null;
194
- })
195
- .join('\n')}
196
- }
197
- `
198
- )
199
- .join('\n'),
200
- }}
201
- />
202
- );
203
- };
204
-
205
- const ChartTooltip = RechartsPrimitive.Tooltip;
206
-
207
- function ChartTooltipContent({
208
- active,
209
- payload,
210
- className,
211
- indicator = 'dot',
212
- hideLabel = false,
213
- hideIndicator = false,
214
- label,
215
- labelFormatter,
216
- labelClassName,
217
- formatter,
218
- color,
219
- nameKey,
220
- labelKey,
221
- }: React.ComponentProps<'div'> & {
222
- active?: boolean;
223
- payload?: TooltipPayloadItem<TooltipValueType, TooltipNameType>[];
224
- label?: React.ReactNode;
225
- labelFormatter?: (label: React.ReactNode, payload: TooltipPayloadItem<TooltipValueType, TooltipNameType>[]) => React.ReactNode;
226
- labelClassName?: string;
227
- hideLabel?: boolean;
228
- hideIndicator?: boolean;
229
- indicator?: 'line' | 'dot' | 'dashed';
230
- nameKey?: string;
231
- labelKey?: string;
232
- color?: string;
233
- formatter?: RechartsPrimitive.DefaultTooltipContentProps<TooltipValueType, TooltipNameType>['formatter'];
234
- }) {
235
- const { config } = useChart();
236
-
237
- const tooltipLabel = React.useMemo(() => {
238
- if (hideLabel || !payload?.length) {
239
- return null;
240
- }
241
-
242
- const [item] = payload;
243
- const key = `${labelKey || item?.dataKey || item?.name || 'value'}`;
244
- const itemConfig = getPayloadConfigFromPayload(config, item, key);
245
- const value =
246
- !labelKey && typeof label === 'string'
247
- ? config[label as keyof typeof config]?.label || label
248
- : itemConfig?.label;
249
-
250
- if (labelFormatter) {
251
- return (
252
- <div className={cn('font-medium', labelClassName)}>{labelFormatter(value, payload)}</div>
253
- );
254
- }
255
-
256
- if (!value) {
257
- return null;
258
- }
259
-
260
- return <div className={cn('font-medium', labelClassName)}>{value}</div>;
261
- }, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey]);
262
-
263
- if (!active || !payload?.length) {
264
- return null;
265
- }
266
-
267
- const nestLabel = payload.length === 1 && indicator !== 'dot';
268
-
269
- return (
270
- <div
271
- className={cn(
272
- 'border-border/50 bg-background/95 backdrop-blur-sm grid min-w-[8rem] items-start gap-1.5 rounded-xl border px-3 py-2 text-xs shadow-xl',
273
- className
274
- )}
275
- >
276
- {!nestLabel ? tooltipLabel : null}
277
- <div className="grid gap-1.5">
278
- {payload.map((item: TooltipPayloadItem<TooltipValueType, TooltipNameType>, index: number) => {
279
- const key = `${nameKey || item.name || item.dataKey || 'value'}`;
280
- const itemConfig = getPayloadConfigFromPayload(config, item, key);
281
- const indicatorColor = color || item.payload?.fill || item.color;
282
-
283
- return (
284
- <div
285
- key={`${item.dataKey ?? index}`}
286
- className={cn(
287
- '[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5',
288
- indicator === 'dot' && 'items-center'
289
- )}
290
- >
291
- {formatter && item?.value !== undefined && item.name ? (
292
- formatter(item.value, item.name, item, index, item.payload)
293
- ) : (
294
- <>
295
- {itemConfig?.icon ? (
296
- <itemConfig.icon />
297
- ) : (
298
- !hideIndicator && (
299
- <div
300
- className={cn(
301
- 'shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)',
302
- {
303
- 'h-2.5 w-2.5': indicator === 'dot',
304
- 'w-1': indicator === 'line',
305
- 'w-0 border-[1.5px] border-dashed bg-transparent':
306
- indicator === 'dashed',
307
- 'my-0.5': nestLabel && indicator === 'dashed',
308
- }
309
- )}
310
- style={
311
- {
312
- '--color-bg': indicatorColor,
313
- '--color-border': indicatorColor,
314
- } as React.CSSProperties
315
- }
316
- />
317
- )
318
- )}
319
- <div
320
- className={cn(
321
- 'flex flex-1 justify-between leading-none',
322
- nestLabel ? 'items-end' : 'items-center'
323
- )}
324
- >
325
- <div className="grid gap-1.5">
326
- {nestLabel ? tooltipLabel : null}
327
- <span className="text-muted-foreground">
328
- {itemConfig?.label || item.name}
329
- </span>
330
- </div>
331
- {item.value && (
332
- <span className="text-foreground font-mono font-semibold tabular-nums">
333
- {item.value.toLocaleString()}
334
- </span>
335
- )}
336
- </div>
337
- </>
338
- )}
339
- </div>
340
- );
341
- })}
342
- </div>
343
- </div>
344
- );
345
- }
346
-
347
- const ChartLegend = RechartsPrimitive.Legend;
348
-
349
- function ChartLegendContent({
350
- className,
351
- hideIcon = false,
352
- payload,
353
- verticalAlign = 'bottom',
354
- nameKey,
355
- }: React.ComponentProps<'div'> & {
356
- payload?: LegendPayloadItem[];
357
- verticalAlign?: 'top' | 'middle' | 'bottom';
358
- hideIcon?: boolean;
359
- nameKey?: string;
360
- }) {
361
- const { config } = useChart();
362
-
363
- if (!payload?.length) {
364
- return null;
365
- }
366
-
367
- return (
368
- <div
369
- className={cn(
370
- 'flex items-center justify-center gap-4',
371
- verticalAlign === 'top' ? 'pb-3' : 'pt-3',
372
- className
373
- )}
374
- >
375
- {payload.map(item => {
376
- const key = `${nameKey ? item.value : item.dataKey || item.value || 'value'}`;
377
- const itemConfig = getPayloadConfigFromPayload(config, item, key);
378
-
379
- return (
380
- <div
381
- key={item.value}
382
- className={cn(
383
- '[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3'
384
- )}
385
- >
386
- {itemConfig?.icon && !hideIcon ? (
387
- <itemConfig.icon />
388
- ) : (
389
- <div
390
- className="h-2 w-2 shrink-0 rounded-full bg-(--color-bg)"
391
- style={
392
- {
393
- '--color-bg': item.color || `var(--color-${key})`,
394
- } as React.CSSProperties
395
- }
396
- />
397
- )}
398
- <span className="text-muted-foreground text-xs">{itemConfig?.label || item.value}</span>
399
- </div>
400
- );
401
- })}
402
- </div>
403
- );
404
- }
405
-
406
- // Helper to extract item config from a payload.
407
- function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {
408
- if (typeof payload !== 'object' || payload === null) {
409
- return undefined;
410
- }
411
-
412
- const payloadPayload =
413
- 'payload' in payload && typeof payload.payload === 'object' && payload.payload !== null
414
- ? payload.payload
415
- : undefined;
416
-
417
- let configLabelKey: string = key;
418
-
419
- if (key in payload && typeof payload[key as keyof typeof payload] === 'string') {
420
- configLabelKey = payload[key as keyof typeof payload] as string;
421
- } else if (
422
- payloadPayload &&
423
- key in payloadPayload &&
424
- typeof payloadPayload[key as keyof typeof payloadPayload] === 'string'
425
- ) {
426
- configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string;
427
- }
428
-
429
- return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config];
430
- }
431
-
432
- function getChartSeries(
433
- config: ChartConfig,
434
- series?: DashboardChartSeries[]
435
- ): DashboardChartSeries[] {
436
- if (series?.length) {
437
- return series;
438
- }
439
-
440
- return Object.entries(config).map(([key, item]) => ({
441
- key,
442
- label: item.label,
443
- }));
444
- }
445
-
446
- function getSeriesColor(key: string, index: number, colors?: DashboardChartColors) {
447
- if (Array.isArray(colors)) {
448
- return colors[index] || defaultChartColors[index % defaultChartColors.length];
449
- }
450
-
451
- return colors?.[key] || defaultChartColors[index % defaultChartColors.length];
452
- }
453
-
454
- function getChartConfigWithColors(
455
- config: ChartConfig,
456
- keys: string[],
457
- colors?: DashboardChartColors
458
- ): ChartConfig {
459
- return keys.reduce<ChartConfig>((nextConfig, key, index) => {
460
- const item = config[key];
461
-
462
- if (item?.theme && !colors) {
463
- nextConfig[key] = item;
464
- return nextConfig;
465
- }
466
-
467
- nextConfig[key] = {
468
- label: item?.label || key,
469
- icon: item?.icon,
470
- color: colors
471
- ? getSeriesColor(key, index, colors)
472
- : item?.color || getSeriesColor(key, index),
473
- };
474
-
475
- return nextConfig;
476
- }, {});
477
- }
478
-
479
- /** Build a ChartConfig from a list of keys (no pre-existing config needed). */
480
- function buildChartConfig(keys: string[], colors?: DashboardChartColors): ChartConfig {
481
- return keys.reduce<ChartConfig>((cfg, key, index) => {
482
- cfg[key] = {
483
- label: key,
484
- color: getSeriesColor(key, index, colors),
485
- };
486
- return cfg;
487
- }, {});
488
- }
489
-
490
- function formatTick(value: number | string) {
491
- if (typeof value !== 'number') {
492
- return value;
493
- }
494
-
495
- return Intl.NumberFormat('en', {
496
- notation: 'compact',
497
- maximumFractionDigits: 1,
498
- }).format(value);
499
- }
500
-
501
- function defaultFilterData(data: DashboardChartDatum[], period: string) {
502
- const match = period.match(/^(\d+)/);
503
-
504
- if (!match) {
505
- return data;
506
- }
507
-
508
- const limit = Number(match[1]);
509
- return data.slice(Math.max(data.length - limit, 0));
510
- }
511
-
512
- function getErrorDescription(error: ChartErrorState) {
513
- if (typeof error === 'string') {
514
- return error;
515
- }
516
-
517
- if (error instanceof Error) {
518
- return error.message;
519
- }
520
-
521
- return undefined;
522
- }
523
-
524
- function getBarSize(barSize: ChartBarSize = 'md') {
525
- return typeof barSize === 'number' ? barSize : chartBarSizes[barSize];
526
- }
527
-
528
- function hasChartData(data: DashboardChartDatum[], series: DashboardChartSeries[]) {
529
- if (!data.length || !series.length) {
530
- return false;
531
- }
532
-
533
- return data.some(item =>
534
- series.some(serie => {
535
- const value = item[serie.key];
536
- return value !== null && value !== undefined && value !== '';
537
- })
538
- );
539
- }
540
-
541
- function hasPieData(data: DashboardChartDatum[], nameKey: string, valueKey: string) {
542
- return data.some(item => {
543
- const name = item[nameKey];
544
- const value = item[valueKey];
545
- return (
546
- name !== null &&
547
- name !== undefined &&
548
- name !== '' &&
549
- value !== null &&
550
- value !== undefined &&
551
- value !== ''
552
- );
553
- });
554
- }
555
-
556
- function ChartState({
557
- type,
558
- className,
559
- error,
560
- onRetry,
561
- retryLabel = 'Try again',
562
- emptyTitle = 'No data available',
563
- emptyDescription = 'There is no data available for this chart yet.',
564
- errorTitle = 'Connection error',
565
- errorDescription,
566
- loadingLabel = 'Loading chart data',
567
- }: ChartStateProps & {
568
- type: 'empty' | 'error' | 'loading';
569
- className?: string;
570
- }) {
571
- if (type === 'loading') {
572
- return (
573
- <div
574
- className={cn(
575
- 'flex min-h-[240px] flex-col justify-end gap-3 rounded-[var(--radius-card)] border border-border p-6',
576
- className
577
- )}
578
- aria-label={typeof loadingLabel === 'string' ? loadingLabel : undefined}
579
- >
580
- <Skeleton className="h-8 w-2/5" />
581
- <Skeleton className="h-14 w-3/5" />
582
- <Skeleton className="h-24 w-4/5" />
583
- <Skeleton className="h-36 w-full" />
584
- </div>
585
- );
586
- }
587
-
588
- if (type === 'error') {
589
- return (
590
- <div
591
- className={cn(
592
- 'flex min-h-[240px] items-center justify-center rounded-[var(--radius-card)] border border-border p-6',
593
- className
594
- )}
595
- >
596
- <Alert variant="destructive" className="max-w-xl">
597
- <AlertTitle>{errorTitle}</AlertTitle>
598
- <AlertDescription>
599
- {errorDescription ||
600
- getErrorDescription(error) ||
601
- 'Unable to load chart data. Check your connection and try again.'}
602
- </AlertDescription>
603
- {onRetry ? (
604
- <div className="mt-3">
605
- <Button size="sm" variant="outline" onClick={onRetry}>
606
- <RefreshCw className="size-4" />
607
- {retryLabel}
608
- </Button>
609
- </div>
610
- ) : null}
611
- </Alert>
612
- </div>
613
- );
614
- }
615
-
616
- return (
617
- <Empty className={cn('min-h-[240px]', className)}>
618
- <EmptyIcon>
619
- <BarChart3 className="size-10 text-muted-foreground" />
620
- </EmptyIcon>
621
- <EmptyTitle>{emptyTitle}</EmptyTitle>
622
- <EmptyDescription>{emptyDescription}</EmptyDescription>
623
- {onRetry ? (
624
- <EmptyAction>
625
- <Button size="sm" variant="outline" onClick={onRetry}>
626
- <WifiOff className="size-4" />
627
- {retryLabel}
628
- </Button>
629
- </EmptyAction>
630
- ) : null}
631
- </Empty>
632
- );
633
- }
634
-
635
- function getChartState(state: ChartStateProps, hasData: boolean, className?: string) {
636
- if (state.isLoading) {
637
- return <ChartState {...state} type="loading" className={className} />;
638
- }
639
-
640
- if (state.error) {
641
- return <ChartState {...state} type="error" className={className} />;
642
- }
643
-
644
- if (!hasData) {
645
- return <ChartState {...state} type="empty" className={className} />;
646
- }
647
-
648
- return null;
649
- }
650
-
651
- export interface ChartCardProps extends Omit<React.ComponentProps<typeof Card>, 'title'> {
652
- title: React.ReactNode;
653
- description?: React.ReactNode;
654
- action?: React.ReactNode;
655
- footer?: React.ReactNode;
656
- contentClassName?: string;
657
- }
658
-
659
- function ChartCard({
660
- title,
661
- description,
662
- action,
663
- footer,
664
- children,
665
- className,
666
- contentClassName,
667
- ...props
668
- }: ChartCardProps) {
669
- return (
670
- <Card className={cn('w-full min-w-0 overflow-hidden', className)} {...props}>
671
- <CardHeader>
672
- <CardTitle>{title}</CardTitle>
673
- {description ? <CardDescription>{description}</CardDescription> : null}
674
- {action ? <CardAction>{action}</CardAction> : null}
675
- </CardHeader>
676
- <CardContent className={contentClassName}>{children}</CardContent>
677
- {footer ? <CardFooter>{footer}</CardFooter> : null}
678
- </Card>
679
- );
680
- }
681
-
682
- // ─── Gradient defs helper ────────────────────────────────────────────────────
683
-
684
- /**
685
- * Renders SVG `<defs>` with linear gradients for each series key.
686
- * Used internally by area charts when `gradientFill` is enabled.
687
- */
688
- function AreaGradientDefs({
689
- seriesKeys,
690
- opacity = 0.3,
691
- }: {
692
- seriesKeys: string[];
693
- opacity?: number;
694
- }) {
695
- return (
696
- <defs>
697
- {seriesKeys.map(key => (
698
- <linearGradient key={key} id={`gradient-${key}`} x1="0" y1="0" x2="0" y2="1">
699
- <stop offset="5%" stopColor={`var(--color-${key})`} stopOpacity={opacity} />
700
- <stop offset="95%" stopColor={`var(--color-${key})`} stopOpacity={0} />
701
- </linearGradient>
702
- ))}
703
- </defs>
704
- );
705
- }
706
-
707
- // ─── DashboardBarChart ────────────────────────────────────────────────────────
708
-
709
- export interface DashboardBarChartProps
710
- extends Omit<React.ComponentProps<typeof ChartContainer>, 'children'>, ChartStateProps {
711
- data: DashboardChartDatum[];
712
- indexKey?: string;
713
- series?: DashboardChartSeries[];
714
- colors?: DashboardChartColors;
715
- barSize?: ChartBarSize;
716
- stacked?: boolean;
717
- showGrid?: boolean;
718
- showLegend?: boolean;
719
- valueFormatter?: ChartValueFormatter;
720
- }
721
-
722
- function DashboardBarChart({
723
- data,
724
- config,
725
- indexKey = 'name',
726
- series,
727
- colors,
728
- barSize = 'md',
729
- stacked = false,
730
- showGrid = true,
731
- showLegend = true,
732
- valueFormatter = formatTick,
733
- isLoading,
734
- error,
735
- onRetry,
736
- retryLabel,
737
- emptyTitle,
738
- emptyDescription,
739
- errorTitle,
740
- errorDescription,
741
- loadingLabel,
742
- stateClassName,
743
- className,
744
- ...props
745
- }: DashboardBarChartProps) {
746
- const chartSeries = getChartSeries(config, series);
747
- const chartConfig = getChartConfigWithColors(
748
- config,
749
- chartSeries.map(item => item.key),
750
- colors
751
- );
752
- const chartState = getChartState(
753
- {
754
- isLoading,
755
- error,
756
- onRetry,
757
- retryLabel,
758
- emptyTitle,
759
- emptyDescription,
760
- errorTitle,
761
- errorDescription,
762
- loadingLabel,
763
- },
764
- hasChartData(data, chartSeries),
765
- cn('h-[320px] w-full', stateClassName)
766
- );
767
-
768
- const barElements = React.useMemo(() => {
769
- const topOfStack = new Set<string>();
770
- if (stacked) {
771
- const lastByStack = new Map<string, string>();
772
- chartSeries.forEach(s => lastByStack.set(s.stackId || 'total', s.key));
773
- lastByStack.forEach(key => topOfStack.add(key));
774
- }
775
- return chartSeries.map(item => {
776
- const isTop = !stacked || topOfStack.has(item.key);
777
- return (
778
- <RechartsPrimitive.Bar
779
- key={item.key}
780
- dataKey={item.key}
781
- fill={`var(--color-${item.key})`}
782
- radius={isTop ? [4, 4, 0, 0] : [0, 0, 0, 0]}
783
- barSize={getBarSize(barSize)}
784
- stackId={stacked ? item.stackId || 'total' : item.stackId}
785
- isAnimationActive
786
- animationDuration={600}
787
- animationEasing="ease-out"
788
- />
789
- );
790
- });
791
- }, [stacked, chartSeries]);
792
-
793
- if (chartState) {
794
- return chartState;
795
- }
796
-
797
- return (
798
- <ChartContainer config={chartConfig} className={cn('h-[320px] w-full', className)} {...props}>
799
- <RechartsPrimitive.BarChart data={data} accessibilityLayer barGap={4}>
800
- {showGrid ? (
801
- <RechartsPrimitive.CartesianGrid
802
- vertical={false}
803
- strokeDasharray="3 3"
804
- stroke="var(--border)"
805
- strokeOpacity={0.5}
806
- />
807
- ) : null}
808
- <RechartsPrimitive.XAxis
809
- dataKey={indexKey}
810
- tickLine={false}
811
- axisLine={false}
812
- tickMargin={8}
813
- />
814
- <RechartsPrimitive.YAxis
815
- tickLine={false}
816
- axisLine={false}
817
- tickMargin={8}
818
- tickFormatter={valueFormatter}
819
- />
820
- <ChartTooltip
821
- cursor={{ fill: 'var(--muted)', opacity: 0.4, radius: 4 }}
822
- content={<ChartTooltipContent />}
823
- />
824
- {showLegend ? <ChartLegend content={<ChartLegendContent />} /> : null}
825
- {barElements}
826
- </RechartsPrimitive.BarChart>
827
- </ChartContainer>
828
- );
829
- }
830
-
831
- // ─── DashboardLineChart ───────────────────────────────────────────────────────
832
-
833
- export interface DashboardLineChartProps
834
- extends Omit<React.ComponentProps<typeof ChartContainer>, 'children'>, ChartStateProps {
835
- data: DashboardChartDatum[];
836
- indexKey?: string;
837
- series?: DashboardChartSeries[];
838
- colors?: DashboardChartColors;
839
- showDots?: boolean;
840
- showGrid?: boolean;
841
- showLegend?: boolean;
842
- curveType?: ChartCurveType;
843
- strokeWidth?: number;
844
- valueFormatter?: ChartValueFormatter;
845
- }
846
-
847
- function DashboardLineChart({
848
- data,
849
- config,
850
- indexKey = 'name',
851
- series,
852
- colors,
853
- showDots = false,
854
- showGrid = true,
855
- showLegend = true,
856
- curveType = 'monotone',
857
- strokeWidth = 2,
858
- valueFormatter = formatTick,
859
- isLoading,
860
- error,
861
- onRetry,
862
- retryLabel,
863
- emptyTitle,
864
- emptyDescription,
865
- errorTitle,
866
- errorDescription,
867
- loadingLabel,
868
- stateClassName,
869
- className,
870
- ...props
871
- }: DashboardLineChartProps) {
872
- const chartSeries = getChartSeries(config, series);
873
- const chartConfig = getChartConfigWithColors(
874
- config,
875
- chartSeries.map(item => item.key),
876
- colors
877
- );
878
- const chartState = getChartState(
879
- {
880
- isLoading,
881
- error,
882
- onRetry,
883
- retryLabel,
884
- emptyTitle,
885
- emptyDescription,
886
- errorTitle,
887
- errorDescription,
888
- loadingLabel,
889
- },
890
- hasChartData(data, chartSeries),
891
- cn('h-[320px] w-full', stateClassName)
892
- );
893
-
894
- if (chartState) {
895
- return chartState;
896
- }
897
-
898
- return (
899
- <ChartContainer config={chartConfig} className={cn('h-[320px] w-full', className)} {...props}>
900
- <RechartsPrimitive.LineChart data={data} accessibilityLayer>
901
- {showGrid ? (
902
- <RechartsPrimitive.CartesianGrid
903
- vertical={false}
904
- strokeDasharray="3 3"
905
- stroke="var(--border)"
906
- strokeOpacity={0.5}
907
- />
908
- ) : null}
909
- <RechartsPrimitive.XAxis
910
- dataKey={indexKey}
911
- tickLine={false}
912
- axisLine={false}
913
- tickMargin={8}
914
- />
915
- <RechartsPrimitive.YAxis
916
- tickLine={false}
917
- axisLine={false}
918
- tickMargin={8}
919
- tickFormatter={valueFormatter}
920
- />
921
- <ChartTooltip content={<ChartTooltipContent indicator="line" />} />
922
- {showLegend ? <ChartLegend content={<ChartLegendContent />} /> : null}
923
- {chartSeries.map(item => (
924
- <RechartsPrimitive.Line
925
- key={item.key}
926
- type={curveType}
927
- dataKey={item.key}
928
- stroke={`var(--color-${item.key})`}
929
- strokeWidth={strokeWidth}
930
- dot={showDots ? { r: 3, fill: `var(--color-${item.key})`, strokeWidth: 0 } : false}
931
- activeDot={{ r: 5, strokeWidth: 0 }}
932
- yAxisId={item.yAxisId}
933
- isAnimationActive
934
- animationDuration={600}
935
- animationEasing="ease-out"
936
- />
937
- ))}
938
- </RechartsPrimitive.LineChart>
939
- </ChartContainer>
940
- );
941
- }
942
-
943
- // ─── HorizontalBarChart ───────────────────────────────────────────────────────
944
-
945
- export interface HorizontalBarChartProps
946
- extends Omit<React.ComponentProps<typeof ChartContainer>, 'children'>, ChartStateProps {
947
- data: DashboardChartDatum[];
948
- indexKey?: string;
949
- series?: DashboardChartSeries[];
950
- colors?: DashboardChartColors;
951
- barSize?: ChartBarSize;
952
- stacked?: boolean;
953
- categoryWidth?: number;
954
- showGrid?: boolean;
955
- showLegend?: boolean;
956
- valueFormatter?: ChartValueFormatter;
957
- }
958
-
959
- function HorizontalBarChart({
960
- data,
961
- config,
962
- indexKey = 'name',
963
- series,
964
- colors,
965
- barSize = 'md',
966
- stacked = false,
967
- categoryWidth = 96,
968
- showGrid = true,
969
- showLegend = true,
970
- valueFormatter = formatTick,
971
- isLoading,
972
- error,
973
- onRetry,
974
- retryLabel,
975
- emptyTitle,
976
- emptyDescription,
977
- errorTitle,
978
- errorDescription,
979
- loadingLabel,
980
- stateClassName,
981
- className,
982
- ...props
983
- }: HorizontalBarChartProps) {
984
- const chartSeries = getChartSeries(config, series);
985
- const chartConfig = getChartConfigWithColors(
986
- config,
987
- chartSeries.map(item => item.key),
988
- colors
989
- );
990
- const chartState = getChartState(
991
- {
992
- isLoading,
993
- error,
994
- onRetry,
995
- retryLabel,
996
- emptyTitle,
997
- emptyDescription,
998
- errorTitle,
999
- errorDescription,
1000
- loadingLabel,
1001
- },
1002
- hasChartData(data, chartSeries),
1003
- cn('h-[320px] w-full', stateClassName)
1004
- );
1005
-
1006
- if (chartState) {
1007
- return chartState;
1008
- }
1009
-
1010
- return (
1011
- <ChartContainer config={chartConfig} className={cn('h-[320px] w-full', className)} {...props}>
1012
- <RechartsPrimitive.BarChart
1013
- data={data}
1014
- layout="vertical"
1015
- accessibilityLayer
1016
- margin={{ left: 8, right: 16 }}
1017
- >
1018
- {showGrid ? (
1019
- <RechartsPrimitive.CartesianGrid
1020
- horizontal={false}
1021
- strokeDasharray="3 3"
1022
- stroke="var(--border)"
1023
- strokeOpacity={0.5}
1024
- />
1025
- ) : null}
1026
- <RechartsPrimitive.XAxis
1027
- type="number"
1028
- tickLine={false}
1029
- axisLine={false}
1030
- tickMargin={8}
1031
- tickFormatter={valueFormatter}
1032
- />
1033
- <RechartsPrimitive.YAxis
1034
- dataKey={indexKey}
1035
- type="category"
1036
- tickLine={false}
1037
- axisLine={false}
1038
- tickMargin={8}
1039
- width={categoryWidth}
1040
- />
1041
- <ChartTooltip content={<ChartTooltipContent />} />
1042
- {showLegend ? <ChartLegend content={<ChartLegendContent />} /> : null}
1043
- {chartSeries.map(item => (
1044
- <RechartsPrimitive.Bar
1045
- key={item.key}
1046
- dataKey={item.key}
1047
- fill={`var(--color-${item.key})`}
1048
- radius={[0, 4, 4, 0]}
1049
- barSize={getBarSize(barSize)}
1050
- stackId={stacked ? item.stackId || 'total' : item.stackId}
1051
- isAnimationActive
1052
- animationDuration={600}
1053
- animationEasing="ease-out"
1054
- />
1055
- ))}
1056
- </RechartsPrimitive.BarChart>
1057
- </ChartContainer>
1058
- );
1059
- }
1060
-
1061
- // ─── InteractiveTimeSeriesChart ───────────────────────────────────────────────
1062
-
1063
- export interface InteractiveTimeSeriesChartProps
1064
- extends Omit<React.ComponentProps<typeof ChartContainer>, 'children'>, ChartStateProps {
1065
- data: DashboardChartDatum[];
1066
- indexKey?: string;
1067
- series?: DashboardChartSeries[];
1068
- colors?: DashboardChartColors;
1069
- periods?: ChartPeriod[];
1070
- defaultPeriod?: string;
1071
- defaultSeries?: string;
1072
- filterData?: (data: DashboardChartDatum[], period: string) => DashboardChartDatum[];
1073
- showGrid?: boolean;
1074
- showLegend?: boolean;
1075
- showDots?: boolean;
1076
- gradientFill?: boolean;
1077
- curveType?: ChartCurveType;
1078
- strokeWidth?: number;
1079
- valueFormatter?: ChartValueFormatter;
1080
- }
1081
-
1082
- function InteractiveTimeSeriesChart({
1083
- data,
1084
- config,
1085
- indexKey = 'date',
1086
- series,
1087
- colors,
1088
- periods = defaultPeriods,
1089
- defaultPeriod = periods[1]?.value || periods[0]?.value || '30d',
1090
- defaultSeries,
1091
- filterData = defaultFilterData,
1092
- showGrid = true,
1093
- showLegend = false,
1094
- showDots = false,
1095
- gradientFill = true,
1096
- curveType = 'monotone',
1097
- strokeWidth = 2,
1098
- valueFormatter = formatTick,
1099
- isLoading,
1100
- error,
1101
- onRetry,
1102
- retryLabel,
1103
- emptyTitle,
1104
- emptyDescription,
1105
- errorTitle,
1106
- errorDescription,
1107
- loadingLabel,
1108
- stateClassName,
1109
- className,
1110
- ...props
1111
- }: InteractiveTimeSeriesChartProps) {
1112
- const chartSeries = getChartSeries(config, series);
1113
- const chartConfig = getChartConfigWithColors(
1114
- config,
1115
- chartSeries.map(item => item.key),
1116
- colors
1117
- );
1118
- const [period, setPeriod] = React.useState(defaultPeriod);
1119
- const [activeSeries, setActiveSeries] = React.useState(
1120
- defaultSeries || chartSeries[0]?.key || ''
1121
- );
1122
- const filteredData = React.useMemo(() => filterData(data, period), [data, filterData, period]);
1123
- const visibleSeries =
1124
- chartSeries.length > 1 ? chartSeries.filter(item => item.key === activeSeries) : chartSeries;
1125
- const chartState = getChartState(
1126
- {
1127
- isLoading,
1128
- error,
1129
- onRetry,
1130
- retryLabel,
1131
- emptyTitle,
1132
- emptyDescription,
1133
- errorTitle,
1134
- errorDescription,
1135
- loadingLabel,
1136
- },
1137
- hasChartData(filteredData, visibleSeries),
1138
- cn('h-[320px] w-full', stateClassName)
1139
- );
1140
-
1141
- return (
1142
- <div className="w-full space-y-4">
1143
- <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
1144
- {chartSeries.length > 1 ? (
1145
- <div
1146
- className="inline-flex h-10 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground"
1147
- role="group"
1148
- aria-label="Selecionar metrica do grafico"
1149
- >
1150
- {chartSeries.map(item => {
1151
- const label = item.label || config[item.key]?.label || item.key;
1152
-
1153
- return (
1154
- <Button
1155
- key={item.key}
1156
- type="button"
1157
- variant="ghost"
1158
- size="sm"
1159
- aria-pressed={activeSeries === item.key}
1160
- onClick={() => setActiveSeries(item.key)}
1161
- className={cn(
1162
- 'h-8 px-3 text-sm font-medium transition-all',
1163
- activeSeries === item.key
1164
- ? 'bg-background text-foreground shadow-sm'
1165
- : 'text-muted-foreground hover:text-foreground'
1166
- )}
1167
- >
1168
- {label}
1169
- </Button>
1170
- );
1171
- })}
1172
- </div>
1173
- ) : (
1174
- <div />
1175
- )}
1176
- <Select value={period} onValueChange={setPeriod}>
1177
- <SelectTrigger
1178
- className="w-full sm:w-[160px]"
1179
- size="sm"
1180
- aria-label="Selecionar periodo do grafico"
1181
- >
1182
- <SelectValue placeholder="Period" />
1183
- </SelectTrigger>
1184
- <SelectContent>
1185
- {periods.map(item => (
1186
- <SelectItem key={item.value} value={item.value}>
1187
- {item.label}
1188
- </SelectItem>
1189
- ))}
1190
- </SelectContent>
1191
- </Select>
1192
- </div>
1193
- {chartState || (
1194
- <ChartContainer
1195
- config={chartConfig}
1196
- className={cn('h-[320px] w-full', className)}
1197
- {...props}
1198
- >
1199
- <RechartsPrimitive.AreaChart data={filteredData} accessibilityLayer>
1200
- {gradientFill && (
1201
- <AreaGradientDefs seriesKeys={visibleSeries.map(s => s.key)} opacity={0.4} />
1202
- )}
1203
- {showGrid ? (
1204
- <RechartsPrimitive.CartesianGrid
1205
- vertical={false}
1206
- strokeDasharray="3 3"
1207
- stroke="var(--border)"
1208
- strokeOpacity={0.5}
1209
- />
1210
- ) : null}
1211
- <RechartsPrimitive.XAxis
1212
- dataKey={indexKey}
1213
- tickLine={false}
1214
- axisLine={false}
1215
- tickMargin={8}
1216
- />
1217
- <RechartsPrimitive.YAxis
1218
- tickLine={false}
1219
- axisLine={false}
1220
- tickMargin={8}
1221
- tickFormatter={valueFormatter}
1222
- />
1223
- <ChartTooltip content={<ChartTooltipContent indicator="line" />} />
1224
- {showLegend ? <ChartLegend content={<ChartLegendContent />} /> : null}
1225
- {visibleSeries.map(item => (
1226
- <RechartsPrimitive.Area
1227
- key={item.key}
1228
- type={curveType}
1229
- dataKey={item.key}
1230
- stroke={`var(--color-${item.key})`}
1231
- fill={gradientFill ? `url(#gradient-${item.key})` : `var(--color-${item.key})`}
1232
- fillOpacity={gradientFill ? 1 : 0.16}
1233
- strokeWidth={strokeWidth}
1234
- dot={showDots ? { r: 3, fill: `var(--color-${item.key})`, strokeWidth: 0 } : false}
1235
- activeDot={{ r: 5, strokeWidth: 0 }}
1236
- isAnimationActive
1237
- animationDuration={600}
1238
- animationEasing="ease-out"
1239
- />
1240
- ))}
1241
- </RechartsPrimitive.AreaChart>
1242
- </ChartContainer>
1243
- )}
1244
- </div>
1245
- );
1246
- }
1247
-
1248
- // ─── ComboMetricChart ─────────────────────────────────────────────────────────
1249
-
1250
- export interface ComboMetricChartProps
1251
- extends Omit<React.ComponentProps<typeof ChartContainer>, 'children'>, ChartStateProps {
1252
- data: DashboardChartDatum[];
1253
- indexKey?: string;
1254
- series?: DashboardChartSeries[];
1255
- colors?: DashboardChartColors;
1256
- barSize?: ChartBarSize;
1257
- showGrid?: boolean;
1258
- showLegend?: boolean;
1259
- gradientFill?: boolean;
1260
- curveType?: ChartCurveType;
1261
- valueFormatter?: ChartValueFormatter;
1262
- }
1263
-
1264
- function ComboMetricChart({
1265
- data,
1266
- config,
1267
- indexKey = 'name',
1268
- series,
1269
- colors,
1270
- barSize = 'md',
1271
- showGrid = true,
1272
- showLegend = true,
1273
- gradientFill = false,
1274
- curveType = 'monotone',
1275
- valueFormatter = formatTick,
1276
- isLoading,
1277
- error,
1278
- onRetry,
1279
- retryLabel,
1280
- emptyTitle,
1281
- emptyDescription,
1282
- errorTitle,
1283
- errorDescription,
1284
- loadingLabel,
1285
- stateClassName,
1286
- className,
1287
- ...props
1288
- }: ComboMetricChartProps) {
1289
- const chartSeries = getChartSeries(config, series);
1290
- const chartConfig = getChartConfigWithColors(
1291
- config,
1292
- chartSeries.map(item => item.key),
1293
- colors
1294
- );
1295
- const chartState = getChartState(
1296
- {
1297
- isLoading,
1298
- error,
1299
- onRetry,
1300
- retryLabel,
1301
- emptyTitle,
1302
- emptyDescription,
1303
- errorTitle,
1304
- errorDescription,
1305
- loadingLabel,
1306
- },
1307
- hasChartData(data, chartSeries),
1308
- cn('h-[340px] w-full', stateClassName)
1309
- );
1310
-
1311
- if (chartState) {
1312
- return chartState;
1313
- }
1314
-
1315
- const areaSeries = gradientFill ? chartSeries.filter(s => s.type === 'area').map(s => s.key) : [];
1316
-
1317
- return (
1318
- <ChartContainer config={chartConfig} className={cn('h-[340px] w-full', className)} {...props}>
1319
- <RechartsPrimitive.ComposedChart data={data} accessibilityLayer>
1320
- {gradientFill && areaSeries.length > 0 && (
1321
- <AreaGradientDefs seriesKeys={areaSeries} opacity={0.35} />
1322
- )}
1323
- {showGrid ? (
1324
- <RechartsPrimitive.CartesianGrid
1325
- vertical={false}
1326
- strokeDasharray="3 3"
1327
- stroke="var(--border)"
1328
- strokeOpacity={0.5}
1329
- />
1330
- ) : null}
1331
- <RechartsPrimitive.XAxis
1332
- dataKey={indexKey}
1333
- tickLine={false}
1334
- axisLine={false}
1335
- tickMargin={8}
1336
- />
1337
- <RechartsPrimitive.YAxis
1338
- tickLine={false}
1339
- axisLine={false}
1340
- tickMargin={8}
1341
- tickFormatter={valueFormatter}
1342
- />
1343
- <ChartTooltip content={<ChartTooltipContent />} />
1344
- {showLegend ? <ChartLegend content={<ChartLegendContent />} /> : null}
1345
- {chartSeries.map(item =>
1346
- item.type === 'line' ? (
1347
- <RechartsPrimitive.Line
1348
- key={item.key}
1349
- type={curveType}
1350
- dataKey={item.key}
1351
- stroke={`var(--color-${item.key})`}
1352
- strokeWidth={2}
1353
- dot={false}
1354
- activeDot={{ r: 5, strokeWidth: 0 }}
1355
- yAxisId={item.yAxisId}
1356
- isAnimationActive
1357
- animationDuration={600}
1358
- animationEasing="ease-out"
1359
- />
1360
- ) : item.type === 'area' ? (
1361
- <RechartsPrimitive.Area
1362
- key={item.key}
1363
- type={curveType}
1364
- dataKey={item.key}
1365
- stroke={`var(--color-${item.key})`}
1366
- fill={gradientFill ? `url(#gradient-${item.key})` : `var(--color-${item.key})`}
1367
- fillOpacity={gradientFill ? 1 : 0.12}
1368
- strokeWidth={2}
1369
- dot={false}
1370
- activeDot={{ r: 5, strokeWidth: 0 }}
1371
- yAxisId={item.yAxisId}
1372
- isAnimationActive
1373
- animationDuration={600}
1374
- animationEasing="ease-out"
1375
- />
1376
- ) : (
1377
- <RechartsPrimitive.Bar
1378
- key={item.key}
1379
- dataKey={item.key}
1380
- fill={`var(--color-${item.key})`}
1381
- radius={[4, 4, 0, 0]}
1382
- barSize={getBarSize(barSize)}
1383
- yAxisId={item.yAxisId}
1384
- isAnimationActive
1385
- animationDuration={600}
1386
- animationEasing="ease-out"
1387
- />
1388
- )
1389
- )}
1390
- </RechartsPrimitive.ComposedChart>
1391
- </ChartContainer>
1392
- );
1393
- }
1394
-
1395
- // ─── DonutBreakdownChart ──────────────────────────────────────────────────────
1396
-
1397
- export interface DonutBreakdownChartProps
1398
- extends Omit<React.ComponentProps<typeof ChartContainer>, 'children'>, ChartStateProps {
1399
- data: DashboardChartDatum[];
1400
- nameKey?: string;
1401
- valueKey?: string;
1402
- colors?: DashboardChartColors;
1403
- centerLabel?: React.ReactNode;
1404
- centerValue?: React.ReactNode;
1405
- showLegend?: boolean;
1406
- /** Inner radius as percentage string (e.g. "58%") or number */
1407
- innerRadius?: string | number;
1408
- /** Outer radius as percentage string (e.g. "82%") or number */
1409
- outerRadius?: string | number;
1410
- }
1411
-
1412
- function DonutBreakdownChart({
1413
- data,
1414
- config,
1415
- nameKey = 'name',
1416
- valueKey = 'value',
1417
- colors,
1418
- centerLabel,
1419
- centerValue,
1420
- showLegend = true,
1421
- innerRadius = '58%',
1422
- outerRadius = '82%',
1423
- isLoading,
1424
- error,
1425
- onRetry,
1426
- retryLabel,
1427
- emptyTitle,
1428
- emptyDescription,
1429
- errorTitle,
1430
- errorDescription,
1431
- loadingLabel,
1432
- stateClassName,
1433
- className,
1434
- ...props
1435
- }: DonutBreakdownChartProps) {
1436
- const [activeIndex, setActiveIndex] = React.useState(0);
1437
- const chartKeys = data.map((entry, index) => String(entry[nameKey] || `segment-${index}`));
1438
- const chartConfig = getChartConfigWithColors(config, chartKeys, colors);
1439
- const chartState = getChartState(
1440
- {
1441
- isLoading,
1442
- error,
1443
- onRetry,
1444
- retryLabel,
1445
- emptyTitle,
1446
- emptyDescription,
1447
- errorTitle,
1448
- errorDescription,
1449
- loadingLabel,
1450
- },
1451
- hasPieData(data, nameKey, valueKey),
1452
- cn('h-[320px] w-full', stateClassName)
1453
- );
1454
-
1455
- if (chartState) {
1456
- return chartState;
1457
- }
1458
-
1459
- return (
1460
- <ChartContainer config={chartConfig} className={cn('h-[320px] w-full', className)} {...props}>
1461
- <RechartsPrimitive.PieChart accessibilityLayer>
1462
- <ChartTooltip
1463
- cursor={false}
1464
- content={<ChartTooltipContent hideLabel nameKey={nameKey} />}
1465
- />
1466
- {showLegend ? (
1467
- <ChartLegend content={<ChartLegendContent nameKey={nameKey} />} verticalAlign="bottom" />
1468
- ) : null}
1469
- <RechartsPrimitive.Pie
1470
- data={data}
1471
- dataKey={valueKey}
1472
- nameKey={nameKey}
1473
- innerRadius={innerRadius}
1474
- outerRadius={outerRadius}
1475
- paddingAngle={3}
1476
- onMouseEnter={(_, index) => setActiveIndex(index)}
1477
- isAnimationActive
1478
- animationDuration={600}
1479
- animationEasing="ease-out"
1480
- >
1481
- {data.map((entry, index) => {
1482
- const key = String(entry[nameKey] || `segment-${index}`);
1483
-
1484
- return (
1485
- <RechartsPrimitive.Cell
1486
- key={key}
1487
- fill={`var(--color-${key})`}
1488
- opacity={index === activeIndex ? 1 : 0.7}
1489
- stroke="transparent"
1490
- />
1491
- );
1492
- })}
1493
- {centerValue || centerLabel ? (
1494
- <RechartsPrimitive.Label
1495
- position="center"
1496
- content={({ viewBox }) => {
1497
- if (!viewBox || !('cx' in viewBox) || !('cy' in viewBox)) {
1498
- return null;
1499
- }
1500
-
1501
- return (
1502
- <text x={viewBox.cx} y={viewBox.cy} textAnchor="middle" dominantBaseline="middle">
1503
- {centerValue ? (
1504
- <tspan
1505
- x={viewBox.cx}
1506
- y={viewBox.cy}
1507
- className="fill-foreground text-2xl font-semibold"
1508
- >
1509
- {centerValue}
1510
- </tspan>
1511
- ) : null}
1512
- {centerLabel ? (
1513
- <tspan
1514
- x={viewBox.cx}
1515
- y={Number(viewBox.cy) + 22}
1516
- className="fill-muted-foreground text-xs"
1517
- >
1518
- {centerLabel}
1519
- </tspan>
1520
- ) : null}
1521
- </text>
1522
- );
1523
- }}
1524
- />
1525
- ) : null}
1526
- </RechartsPrimitive.Pie>
1527
- </RechartsPrimitive.PieChart>
1528
- </ChartContainer>
1529
- );
1530
- }
1531
-
1532
- // ─── SparklineChart ───────────────────────────────────────────────────────────
1533
-
1534
- export interface SparklineChartProps {
1535
- /** Data array — each item must have the `dataKey` field */
1536
- data: DashboardChartDatum[];
1537
- /** Key to plot on the Y axis */
1538
- dataKey: string;
1539
- /** Color for the line/area — defaults to `var(--chart-1)` */
1540
- color?: string;
1541
- /** Show filled area under the line */
1542
- filled?: boolean;
1543
- /** Show the last data point as a dot */
1544
- showEndDot?: boolean;
1545
- /** Curve interpolation type */
1546
- curveType?: ChartCurveType;
1547
- /** Stroke width */
1548
- strokeWidth?: number;
1549
- className?: string;
1550
- }
1551
-
1552
- /**
1553
- * Minimal inline sparkline chart — no axes, no grid, no tooltip.
1554
- * Ideal for embedding inside stat cards or table cells.
1555
- *
1556
- * @ai-rules
1557
- * - Use `filled` for area-style sparklines.
1558
- * - Keep `className` height small (e.g. `h-12` or `h-16`).
1559
- * - Do NOT wrap in `ChartCard` — it is designed to be inline.
1560
- */
1561
- function SparklineChart({
1562
- data,
1563
- dataKey,
1564
- color = 'var(--chart-1)',
1565
- filled = true,
1566
- showEndDot = true,
1567
- curveType = 'monotone',
1568
- strokeWidth = 2,
1569
- className,
1570
- }: SparklineChartProps) {
1571
- const sparkId = React.useId().replace(/:/g, '');
1572
- const lastPoint = data[data.length - 1];
1573
- const lastValue = lastPoint ? lastPoint[dataKey] : undefined;
1574
-
1575
- return (
1576
- <div className={cn('relative h-12 w-full min-w-0', className)}>
1577
- <RechartsPrimitive.ResponsiveContainer width="100%" height="100%">
1578
- <RechartsPrimitive.AreaChart
1579
- data={data}
1580
- margin={{ top: 4, right: showEndDot ? 8 : 0, bottom: 0, left: 0 }}
1581
- >
1582
- {filled && (
1583
- <defs>
1584
- <linearGradient id={`spark-gradient-${sparkId}`} x1="0" y1="0" x2="0" y2="1">
1585
- <stop offset="5%" stopColor={color} stopOpacity={0.3} />
1586
- <stop offset="95%" stopColor={color} stopOpacity={0} />
1587
- </linearGradient>
1588
- </defs>
1589
- )}
1590
- <RechartsPrimitive.Area
1591
- type={curveType}
1592
- dataKey={dataKey}
1593
- stroke={color}
1594
- strokeWidth={strokeWidth}
1595
- fill={filled ? `url(#spark-gradient-${sparkId})` : 'none'}
1596
- fillOpacity={1}
1597
- dot={false}
1598
- activeDot={false}
1599
- isAnimationActive
1600
- animationDuration={600}
1601
- animationEasing="ease-out"
1602
- />
1603
- </RechartsPrimitive.AreaChart>
1604
- </RechartsPrimitive.ResponsiveContainer>
1605
- {showEndDot && lastValue !== undefined && lastValue !== null && (
1606
- <div
1607
- className="pointer-events-none absolute right-0 top-1/2 h-2.5 w-2.5 -translate-y-1/2 rounded-full ring-2 ring-background"
1608
- style={{ backgroundColor: color }}
1609
- />
1610
- )}
1611
- </div>
1612
- );
1613
- }
1614
-
1615
- // ─── RadarChart ───────────────────────────────────────────────────────────────
1616
-
1617
- export interface RadarMetricChartProps extends ChartStateProps {
1618
- /** Data array — each item is one axis point on the radar */
1619
- data: DashboardChartDatum[];
1620
- /** Key in each datum used as the axis label */
1621
- labelKey: string;
1622
- /**
1623
- * Series to render. Each entry maps to one `<Radar>` element.
1624
- * Use a single entry for a simple radar, multiple for comparison.
1625
- */
1626
- series: DashboardChartSeries[];
1627
- /** Override colors per series key or as an ordered array */
1628
- colors?: DashboardChartColors;
1629
- /** Fill the radar polygon (default: true) */
1630
- filled?: boolean;
1631
- /** Fill opacity when `filled` is true (default: 0.25) */
1632
- fillOpacity?: number;
1633
- /** Show dots on each axis point (default: false) */
1634
- showDots?: boolean;
1635
- /** Show the polar grid lines (default: true) */
1636
- showGrid?: boolean;
1637
- /** Show the legend (default: true when multiple series) */
1638
- showLegend?: boolean;
1639
- /** Format axis tick values */
1640
- valueFormatter?: ChartValueFormatter;
1641
- className?: string;
1642
- }
1643
-
1644
- function RadarMetricChart({
1645
- data,
1646
- labelKey,
1647
- series,
1648
- colors,
1649
- filled = true,
1650
- fillOpacity = 0.25,
1651
- showDots = false,
1652
- showGrid = true,
1653
- showLegend,
1654
- valueFormatter,
1655
- isLoading,
1656
- error,
1657
- onRetry,
1658
- retryLabel,
1659
- emptyTitle,
1660
- emptyDescription,
1661
- errorTitle,
1662
- errorDescription,
1663
- loadingLabel,
1664
- stateClassName,
1665
- className,
1666
- }: RadarMetricChartProps) {
1667
- const chartConfig = React.useMemo(
1668
- () =>
1669
- buildChartConfig(
1670
- series.map(s => s.key),
1671
- colors
1672
- ),
1673
- [series, colors]
1674
- );
1675
-
1676
- const hasData = data.length > 0 && series.length > 0;
1677
- const chartState = getChartState(
1678
- {
1679
- isLoading,
1680
- error,
1681
- emptyTitle,
1682
- emptyDescription,
1683
- errorTitle,
1684
- errorDescription,
1685
- loadingLabel,
1686
- stateClassName,
1687
- onRetry,
1688
- retryLabel,
1689
- },
1690
- hasData
1691
- );
1692
-
1693
- const displayLegend = showLegend ?? series.length > 1;
1694
-
1695
- return (
1696
- <ChartContainer config={chartConfig} className={cn('h-[300px] w-full', className)}>
1697
- {chartState ?? (
1698
- <RechartsPrimitive.RadarChart data={data} accessibilityLayer>
1699
- {showGrid && <RechartsPrimitive.PolarGrid stroke="var(--border)" strokeOpacity={0.6} />}
1700
- <RechartsPrimitive.PolarAngleAxis
1701
- dataKey={labelKey}
1702
- tick={{ fill: 'var(--muted-foreground)', fontSize: 12 }}
1703
- />
1704
- <RechartsPrimitive.PolarRadiusAxis
1705
- tick={false}
1706
- axisLine={false}
1707
- tickFormatter={valueFormatter}
1708
- />
1709
- <ChartTooltip
1710
- content={
1711
- <ChartTooltipContent
1712
- formatter={valueFormatter ? (v: TooltipValueType) => valueFormatter(v as number) : undefined}
1713
- />
1714
- }
1715
- />
1716
- {displayLegend && <ChartLegend content={<ChartLegendContent />} />}
1717
- {series.map(s => (
1718
- <RechartsPrimitive.Radar
1719
- key={s.key}
1720
- name={(s.label as string) ?? s.key}
1721
- dataKey={s.key}
1722
- stroke={`var(--color-${s.key})`}
1723
- fill={filled ? `var(--color-${s.key})` : 'none'}
1724
- fillOpacity={filled ? fillOpacity : 0}
1725
- dot={showDots ? { r: 3, fill: `var(--color-${s.key})` } : false}
1726
- isAnimationActive
1727
- animationDuration={600}
1728
- animationEasing="ease-out"
1729
- />
1730
- ))}
1731
- </RechartsPrimitive.RadarChart>
1732
- )}
1733
- </ChartContainer>
1734
- );
1735
- }
1736
-
1737
- // ─── PieMetricChart ───────────────────────────────────────────────────────────
1738
-
1739
- export interface PieMetricChartProps extends ChartStateProps {
1740
- /** Data array — each item is one slice */
1741
- data: DashboardChartDatum[];
1742
- /** Key in each datum used as the slice name/label */
1743
- nameKey: string;
1744
- /** Key in each datum used as the slice value */
1745
- valueKey: string;
1746
- /** Override colors as an ordered array or per-name map */
1747
- colors?: DashboardChartColors;
1748
- /** Outer radius of the pie (default: "80%") */
1749
- outerRadius?: number | string;
1750
- /** Inner radius — set > 0 to make a donut (default: 0) */
1751
- innerRadius?: number | string;
1752
- /** Show percentage labels inside/outside each slice (default: false) */
1753
- showLabels?: boolean;
1754
- /** Show the legend (default: true) */
1755
- showLegend?: boolean;
1756
- /**
1757
- * Index of the slice to "explode" (offset outward).
1758
- * Pass -1 or undefined to disable.
1759
- */
1760
- explodeIndex?: number;
1761
- /** Offset distance for the exploded slice in px (default: 12) */
1762
- explodeOffset?: number;
1763
- /** Format tooltip values */
1764
- valueFormatter?: ChartValueFormatter;
1765
- className?: string;
1766
- }
1767
-
1768
- function PieMetricChart({
1769
- data,
1770
- nameKey,
1771
- valueKey,
1772
- colors,
1773
- outerRadius = '80%',
1774
- innerRadius = 0,
1775
- showLabels = false,
1776
- showLegend = true,
1777
- explodeIndex,
1778
- explodeOffset = 12,
1779
- valueFormatter,
1780
- isLoading,
1781
- error,
1782
- onRetry,
1783
- retryLabel,
1784
- emptyTitle,
1785
- emptyDescription,
1786
- errorTitle,
1787
- errorDescription,
1788
- loadingLabel,
1789
- stateClassName,
1790
- className,
1791
- }: PieMetricChartProps) {
1792
- // Build config from unique name values
1793
- const names = React.useMemo(() => data.map(d => String(d[nameKey] ?? '')), [data, nameKey]);
1794
-
1795
- const chartConfig = React.useMemo(() => buildChartConfig(names, colors), [names, colors]);
1796
-
1797
- const hasData = hasPieData(data, nameKey, valueKey);
1798
- const chartState = getChartState(
1799
- {
1800
- isLoading,
1801
- error,
1802
- emptyTitle,
1803
- emptyDescription,
1804
- errorTitle,
1805
- errorDescription,
1806
- loadingLabel,
1807
- stateClassName,
1808
- onRetry,
1809
- retryLabel,
1810
- },
1811
- hasData
1812
- );
1813
-
1814
- return (
1815
- <ChartContainer config={chartConfig} className={cn('h-[300px] w-full', className)}>
1816
- {chartState ?? (
1817
- <RechartsPrimitive.PieChart accessibilityLayer>
1818
- <ChartTooltip
1819
- content={
1820
- <ChartTooltipContent
1821
- nameKey={nameKey}
1822
- formatter={valueFormatter ? (v: TooltipValueType) => valueFormatter(v as number) : undefined}
1823
- />
1824
- }
1825
- />
1826
- {showLegend && <ChartLegend content={<ChartLegendContent nameKey={nameKey} />} />}
1827
- <RechartsPrimitive.Pie
1828
- data={data}
1829
- dataKey={valueKey}
1830
- nameKey={nameKey}
1831
- outerRadius={outerRadius}
1832
- innerRadius={innerRadius}
1833
- paddingAngle={2}
1834
- label={
1835
- showLabels
1836
- ? ({ cx, cy, midAngle, innerRadius: ir, outerRadius: or, percent }) => {
1837
- if (midAngle == null || percent == null) return null;
1838
- const RADIAN = Math.PI / 180;
1839
- const radius = Number(ir) + (Number(or) - Number(ir)) * 1.35;
1840
- const x = Number(cx) + radius * Math.cos(-midAngle * RADIAN);
1841
- const y = Number(cy) + radius * Math.sin(-midAngle * RADIAN);
1842
- return percent > 0.04 ? (
1843
- <text
1844
- x={x}
1845
- y={y}
1846
- fill="var(--foreground)"
1847
- textAnchor={x > Number(cx) ? 'start' : 'end'}
1848
- dominantBaseline="central"
1849
- fontSize={12}
1850
- fontWeight={500}
1851
- >
1852
- {`${(percent * 100).toFixed(0)}%`}
1853
- </text>
1854
- ) : null;
1855
- }
1856
- : false
1857
- }
1858
- labelLine={false}
1859
- isAnimationActive
1860
- animationDuration={600}
1861
- animationEasing="ease-out"
1862
- >
1863
- {data.map((entry, index) => {
1864
- const name = String(entry[nameKey] ?? '');
1865
- const isExploded = index === explodeIndex;
1866
- return (
1867
- <RechartsPrimitive.Cell
1868
- key={`cell-${index}`}
1869
- fill={chartConfig[name]?.color ?? `var(--chart-${(index % 8) + 1})`}
1870
- stroke="var(--background)"
1871
- strokeWidth={2}
1872
- style={
1873
- isExploded
1874
- ? {
1875
- filter: `drop-shadow(0 4px 8px color-mix(in srgb, ${chartConfig[name]?.color ?? 'var(--chart-1)'} 40%, transparent))`,
1876
- }
1877
- : undefined
1878
- }
1879
- />
1880
- );
1881
- })}
1882
- </RechartsPrimitive.Pie>
1883
- </RechartsPrimitive.PieChart>
1884
- )}
1885
- </ChartContainer>
1886
- );
1887
- }
1888
-
1889
- // ─── RadialBarMetricChart ─────────────────────────────────────────────────────
1890
-
1891
- export interface RadialBarMetricChartProps extends ChartStateProps {
1892
- /**
1893
- * Data array. Each item should have a `name` field (for labels) and
1894
- * the `dataKey` field (for values, 0–100 for percentage-based display).
1895
- */
1896
- data: DashboardChartDatum[];
1897
- /** Key in each datum used as the bar value (default: "value") */
1898
- dataKey?: string;
1899
- /** Key in each datum used as the bar label (default: "name") */
1900
- nameKey?: string;
1901
- /** Override colors as an ordered array or per-name map */
1902
- colors?: DashboardChartColors;
1903
- /** Inner radius of the radial bar (default: "30%") */
1904
- innerRadius?: number | string;
1905
- /** Outer radius of the radial bar (default: "100%") */
1906
- outerRadius?: number | string;
1907
- /** Start angle in degrees (default: 90 — top) */
1908
- startAngle?: number;
1909
- /** End angle in degrees (default: -270 — full circle) */
1910
- endAngle?: number;
1911
- /** Show background track behind each bar (default: true) */
1912
- showBackground?: boolean;
1913
- /** Show the legend (default: true) */
1914
- showLegend?: boolean;
1915
- /** Format tooltip values */
1916
- valueFormatter?: ChartValueFormatter;
1917
- className?: string;
1918
- }
1919
-
1920
- function RadialBarMetricChart({
1921
- data,
1922
- dataKey = 'value',
1923
- nameKey = 'name',
1924
- colors,
1925
- innerRadius = '30%',
1926
- outerRadius = '100%',
1927
- startAngle = 90,
1928
- endAngle = -270,
1929
- showBackground = true,
1930
- showLegend = true,
1931
- valueFormatter,
1932
- isLoading,
1933
- error,
1934
- onRetry,
1935
- retryLabel,
1936
- emptyTitle,
1937
- emptyDescription,
1938
- errorTitle,
1939
- errorDescription,
1940
- loadingLabel,
1941
- stateClassName,
1942
- className,
1943
- }: RadialBarMetricChartProps) {
1944
- const names = React.useMemo(() => data.map(d => String(d[nameKey] ?? '')), [data, nameKey]);
1945
-
1946
- const chartConfig = React.useMemo(() => buildChartConfig(names, colors), [names, colors]);
1947
-
1948
- const hasData = data.length > 0;
1949
- const chartState = getChartState(
1950
- {
1951
- isLoading,
1952
- error,
1953
- emptyTitle,
1954
- emptyDescription,
1955
- errorTitle,
1956
- errorDescription,
1957
- loadingLabel,
1958
- stateClassName,
1959
- onRetry,
1960
- retryLabel,
1961
- },
1962
- hasData
1963
- );
1964
-
1965
- // Inject per-item fill color
1966
- const coloredData = React.useMemo(
1967
- () =>
1968
- data.map((item, index) => {
1969
- const name = String(item[nameKey] ?? '');
1970
- return {
1971
- ...item,
1972
- fill: chartConfig[name]?.color ?? `var(--chart-${(index % 8) + 1})`,
1973
- };
1974
- }),
1975
- [data, nameKey, chartConfig]
1976
- );
1977
-
1978
- return (
1979
- <div className={cn('flex w-full flex-col gap-3', className)}>
1980
- <ChartContainer config={chartConfig} className="h-[260px] w-full">
1981
- {chartState ?? (
1982
- <RechartsPrimitive.RadialBarChart
1983
- data={coloredData}
1984
- innerRadius={innerRadius}
1985
- outerRadius={outerRadius}
1986
- startAngle={startAngle}
1987
- endAngle={endAngle}
1988
- accessibilityLayer
1989
- >
1990
- <ChartTooltip
1991
- cursor={false}
1992
- content={
1993
- <ChartTooltipContent
1994
- nameKey={nameKey}
1995
- formatter={valueFormatter ? (v: TooltipValueType) => valueFormatter(v as number) : undefined}
1996
- />
1997
- }
1998
- />
1999
- <RechartsPrimitive.RadialBar
2000
- dataKey={dataKey}
2001
- background={showBackground ? { fill: 'var(--muted)' } : false}
2002
- cornerRadius={6}
2003
- isAnimationActive
2004
- animationDuration={700}
2005
- animationEasing="ease-out"
2006
- />
2007
- </RechartsPrimitive.RadialBarChart>
2008
- )}
2009
- </ChartContainer>
2010
- {showLegend && !chartState && (
2011
- <div className="flex flex-wrap justify-center gap-x-4 gap-y-1.5 px-2">
2012
- {coloredData.map((item, index) => {
2013
- const d = item as DashboardChartDatum & { fill: string };
2014
- const name = String(d[nameKey] ?? '');
2015
- const fillColor = d.fill ?? `var(--chart-${(index % 8) + 1})`;
2016
- const val = d[dataKey];
2017
- return (
2018
- <div key={name} className="flex items-center gap-1.5">
2019
- <span
2020
- className="inline-block h-2.5 w-2.5 shrink-0 rounded-full"
2021
- style={{ backgroundColor: fillColor }}
2022
- />
2023
- <span className="text-xs text-muted-foreground">
2024
- {name}
2025
- {val !== undefined && val !== null && (
2026
- <span className="ml-1 font-medium text-foreground">
2027
- {valueFormatter ? valueFormatter(val as number) : String(val)}
2028
- </span>
2029
- )}
2030
- </span>
2031
- </div>
2032
- );
2033
- })}
2034
- </div>
2035
- )}
2036
- </div>
2037
- );
2038
- }
2039
-
2040
- // ─── GaugeChart ───────────────────────────────────────────────────────────────
2041
-
2042
- export interface GaugeChartThreshold {
2043
- /** Upper bound of this zone (0–100) */
2044
- value: number;
2045
- /** Color for this zone */
2046
- color: string;
2047
- /** Optional label for this zone */
2048
- label?: string;
2049
- }
2050
-
2051
- export interface GaugeChartProps {
2052
- /** Current value (must be within [min, max]) */
2053
- value: number;
2054
- /** Minimum value (default: 0) */
2055
- min?: number;
2056
- /** Maximum value (default: 100) */
2057
- max?: number;
2058
- /**
2059
- * Color zones. Each threshold defines the upper bound of a zone.
2060
- * Zones are evaluated in order; the first zone whose `value >= current %`
2061
- * determines the active color.
2062
- * If omitted, uses `--chart-1`.
2063
- */
2064
- thresholds?: GaugeChartThreshold[];
2065
- /** Label shown below the value (e.g. "CPU Usage") */
2066
- label?: React.ReactNode;
2067
- /** Format the center value text (default: shows percentage) */
2068
- valueFormatter?: (value: number, percent: number) => string;
2069
- /** Show the needle indicator (default: true) */
2070
- showNeedle?: boolean;
2071
- className?: string;
2072
- }
2073
-
2074
- function GaugeChart({
2075
- value,
2076
- min = 0,
2077
- max = 100,
2078
- thresholds,
2079
- label,
2080
- valueFormatter,
2081
- showNeedle = true,
2082
- className,
2083
- }: GaugeChartProps) {
2084
- const percent = Math.min(1, Math.max(0, (value - min) / (max - min)));
2085
- const percentInt = Math.round(percent * 100);
2086
-
2087
- // Determine active color from thresholds
2088
- const activeColor = React.useMemo(() => {
2089
- if (!thresholds || thresholds.length === 0) return 'var(--chart-1)';
2090
- for (const t of thresholds) {
2091
- if (percentInt <= t.value) return t.color;
2092
- }
2093
- return thresholds[thresholds.length - 1].color;
2094
- }, [thresholds, percentInt]);
2095
-
2096
- const displayText = valueFormatter ? valueFormatter(value, percentInt) : `${percentInt}%`;
2097
-
2098
- // SVG gauge constants viewBox is 200×110, arc center at (100, 100)
2099
- const cx = 100;
2100
- const cy = 100;
2101
- const R = 80; // outer radius
2102
- const r = 54; // inner radius (track width = 26)
2103
-
2104
- // Helper: polar → cartesian (angle in degrees, 0° = right, CCW)
2105
- const polar = (angleDeg: number, radius: number) => {
2106
- const rad = (angleDeg * Math.PI) / 180;
2107
- return {
2108
- x: cx + radius * Math.cos(rad),
2109
- y: cy - radius * Math.sin(rad),
2110
- };
2111
- };
2112
-
2113
- // Semicircle: from 180° (left) to 0° (right)
2114
- // Track background arc path
2115
- const trackStart = polar(180, R);
2116
- const trackEnd = polar(0, R);
2117
- const trackStartI = polar(180, r);
2118
- const trackEndI = polar(0, r);
2119
- const trackPath = [
2120
- `M ${trackStart.x} ${trackStart.y}`,
2121
- `A ${R} ${R} 0 0 1 ${trackEnd.x} ${trackEnd.y}`,
2122
- `L ${trackEndI.x} ${trackEndI.y}`,
2123
- `A ${r} ${r} 0 0 0 ${trackStartI.x} ${trackStartI.y}`,
2124
- 'Z',
2125
- ].join(' ');
2126
-
2127
- // Value arc: from 180° (left) clockwise to valueAngle
2128
- // The gauge is a semicircle (max 180°), so largeArc is always 0.
2129
- // largeArc=1 would sweep the reflex arc (>180°) which creates the filled-blob bug.
2130
- const valueAngle = 180 - percent * 180;
2131
- const valueEnd = polar(valueAngle, R);
2132
- const valueEndI = polar(valueAngle, r);
2133
- const valuePath =
2134
- percent <= 0
2135
- ? ''
2136
- : percent >= 1
2137
- ? trackPath // full arc = same as track
2138
- : [
2139
- `M ${trackStart.x} ${trackStart.y}`,
2140
- `A ${R} ${R} 0 0 1 ${valueEnd.x} ${valueEnd.y}`,
2141
- `L ${valueEndI.x} ${valueEndI.y}`,
2142
- `A ${r} ${r} 0 0 0 ${trackStartI.x} ${trackStartI.y}`,
2143
- 'Z',
2144
- ].join(' ');
2145
-
2146
- // Needle: rotates from -90° (left, 0%) to +90° (right, 100%)
2147
- // In our coordinate system: 180° at 0%, 0° at 100%
2148
- const needleAngle = 180 - percent * 180;
2149
- const needleTip = polar(needleAngle, r - 6);
2150
- const needleBase1 = polar(needleAngle + 90, 5);
2151
- const needleBase2 = polar(needleAngle - 90, 5);
2152
-
2153
- return (
2154
- <div className={cn('flex flex-col items-center gap-2', className)}>
2155
- <div className="relative w-full max-w-[260px]">
2156
- <svg viewBox="0 0 200 130" className="w-full" aria-label={`Gauge: ${displayText}`}>
2157
- {/* Background track */}
2158
- <path d={trackPath} fill="var(--muted)" />
2159
-
2160
- {/* Value arc */}
2161
- {valuePath && <path d={valuePath} fill={activeColor} />}
2162
-
2163
- {/* Needle */}
2164
- {showNeedle && (
2165
- <g>
2166
- <polygon
2167
- points={`${needleTip.x},${needleTip.y} ${needleBase1.x},${needleBase1.y} ${needleBase2.x},${needleBase2.y}`}
2168
- fill="var(--foreground)"
2169
- opacity={0.85}
2170
- />
2171
- <circle cx={cx} cy={cy} r={5} fill="var(--foreground)" />
2172
- </g>
2173
- )}
2174
-
2175
- {/* Value text — placed below the arc baseline (cy=100) to avoid needle overlap */}
2176
- <text
2177
- x={cx}
2178
- y={cy + 18}
2179
- textAnchor="middle"
2180
- dominantBaseline="middle"
2181
- fontSize={22}
2182
- fontWeight={700}
2183
- fill="currentColor"
2184
- className="fill-foreground"
2185
- >
2186
- {displayText}
2187
- </text>
2188
-
2189
- {/* Label below value */}
2190
- {label && (
2191
- <text
2192
- x={cx}
2193
- y={cy + 36}
2194
- textAnchor="middle"
2195
- dominantBaseline="middle"
2196
- fontSize={10}
2197
- fill="currentColor"
2198
- className="fill-muted-foreground"
2199
- >
2200
- {label}
2201
- </text>
2202
- )}
2203
- </svg>
2204
- </div>
2205
-
2206
- {/* Threshold legend */}
2207
- {thresholds && thresholds.length > 0 && (
2208
- <div className="flex flex-wrap justify-center gap-x-3 gap-y-1">
2209
- {thresholds.map((t, i) => (
2210
- <div key={i} className="flex items-center gap-1.5">
2211
- <span
2212
- className="inline-block h-2 w-2 shrink-0 rounded-full"
2213
- style={{ backgroundColor: t.color }}
2214
- />
2215
- <span className="text-xs text-muted-foreground">{t.label}</span>
2216
- </div>
2217
- ))}
2218
- </div>
2219
- )}
2220
- </div>
2221
- );
2222
- }
2223
-
2224
- // ─── Exports ──────────────────────────────────────────────────────────────────
2225
-
2226
- export {
2227
- ChartContainer,
2228
- ChartTooltip,
2229
- ChartTooltipContent,
2230
- ChartLegend,
2231
- ChartLegendContent,
2232
- ChartStyle,
2233
- ChartCard,
2234
- DashboardBarChart,
2235
- DashboardLineChart,
2236
- HorizontalBarChart,
2237
- InteractiveTimeSeriesChart,
2238
- ComboMetricChart,
2239
- DonutBreakdownChart,
2240
- SparklineChart,
2241
- RadarMetricChart,
2242
- PieMetricChart,
2243
- RadialBarMetricChart,
2244
- GaugeChart,
2245
- };
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import * as RechartsPrimitive from 'recharts';
5
+ import type {
6
+ Payload as TooltipPayloadItem,
7
+ ValueType as TooltipValueType,
8
+ NameType as TooltipNameType,
9
+ } from 'recharts/types/component/DefaultTooltipContent';
10
+ import type { Payload as LegendPayloadItem } from 'recharts/types/component/DefaultLegendContent';
11
+
12
+ import { cn } from '../../shared/utils';
13
+ import { Alert, AlertDescription, AlertTitle } from '../alert';
14
+ import { Button } from '../button';
15
+ import {
16
+ Card,
17
+ CardAction,
18
+ CardContent,
19
+ CardDescription,
20
+ CardFooter,
21
+ CardHeader,
22
+ CardTitle,
23
+ } from '../card';
24
+ import { Empty, EmptyAction, EmptyDescription, EmptyIcon, EmptyTitle } from '../empty';
25
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../select';
26
+ import { Skeleton } from '../skeleton';
27
+ import { BarChart3, RefreshCw, WifiOff } from 'lucide-react';
28
+
29
+ // Format: { THEME_NAME: CSS_SELECTOR }
30
+ const THEMES = { light: '', dark: '.dark' } as const;
31
+
32
+ export type ChartConfig = {
33
+ [k in string]: {
34
+ label?: React.ReactNode;
35
+ icon?: React.ComponentType;
36
+ } & (
37
+ | { color?: string; theme?: never }
38
+ | { color?: never; theme: Record<keyof typeof THEMES, string> }
39
+ );
40
+ };
41
+
42
+ type ChartContextProps = {
43
+ config: ChartConfig;
44
+ };
45
+
46
+ export type DashboardChartDatum = Record<string, string | number | null | undefined>;
47
+
48
+ export type DashboardChartSeries = {
49
+ key: string;
50
+ label?: React.ReactNode;
51
+ type?: 'bar' | 'line' | 'area';
52
+ stackId?: string;
53
+ yAxisId?: string;
54
+ };
55
+
56
+ export type DashboardChartColors = string[] | Record<string, string>;
57
+
58
+ export type ChartPeriod = {
59
+ value: string;
60
+ label: string;
61
+ };
62
+
63
+ export type ChartBarSize = 'sm' | 'md' | 'lg' | 'xl' | number;
64
+
65
+ /** Curve interpolation type for line and area charts */
66
+ export type ChartCurveType =
67
+ | 'monotone'
68
+ | 'linear'
69
+ | 'step'
70
+ | 'stepBefore'
71
+ | 'stepAfter'
72
+ | 'natural'
73
+ | 'basis';
74
+
75
+ type ChartValueFormatter = (value: number | string) => string;
76
+
77
+ export type ChartErrorState = boolean | string | Error | React.ReactNode;
78
+
79
+ export type ChartStateProps = {
80
+ isLoading?: boolean;
81
+ error?: ChartErrorState;
82
+ onRetry?: () => void;
83
+ retryLabel?: React.ReactNode;
84
+ emptyTitle?: React.ReactNode;
85
+ emptyDescription?: React.ReactNode;
86
+ errorTitle?: React.ReactNode;
87
+ errorDescription?: React.ReactNode;
88
+ loadingLabel?: React.ReactNode;
89
+ stateClassName?: string;
90
+ };
91
+
92
+ const defaultPeriods: ChartPeriod[] = [
93
+ { value: '7d', label: '7 days' },
94
+ { value: '30d', label: '30 days' },
95
+ { value: '90d', label: '90 days' },
96
+ ];
97
+
98
+ const defaultChartColors = [
99
+ 'var(--chart-1)',
100
+ 'var(--chart-2)',
101
+ 'var(--chart-3)',
102
+ 'var(--chart-4)',
103
+ 'var(--chart-5)',
104
+ 'var(--chart-6)',
105
+ 'var(--chart-7)',
106
+ 'var(--chart-8)',
107
+ ];
108
+
109
+ const chartBarSizes: Record<Exclude<ChartBarSize, number>, number> = {
110
+ sm: 8,
111
+ md: 14,
112
+ lg: 22,
113
+ xl: 32,
114
+ };
115
+
116
+ const ChartContext = React.createContext<ChartContextProps | null>(null);
117
+
118
+ export function useChart() {
119
+ const context = React.useContext(ChartContext);
120
+
121
+ if (!context) {
122
+ throw new Error('useChart must be used within a <ChartContainer />');
123
+ }
124
+
125
+ return context;
126
+ }
127
+
128
+ /**
129
+ * Root container for Recharts-based charts with theme-aware color injection.
130
+ *
131
+ * @description
132
+ * Wraps Recharts' `ResponsiveContainer` and injects CSS custom properties
133
+ * (`--color-*`) from a `ChartConfig` object, enabling full dark-mode
134
+ * support without hard-coded hex values in chart elements.
135
+ *
136
+ * @ai-rules
137
+ * 1. NEVER pass hex colors directly to Recharts elements (e.g., `fill="#4F46E5"`).
138
+ * Always use `fill="var(--color-keyName)"` referencing the injected CSS variables.
139
+ * 2. This wrapper is REQUIRED to use `ChartTooltipContent` and `ChartLegendContent`.
140
+ * 3. Set height via `className="h-[300px]"` on `ChartContainer`, not on Recharts components.
141
+ * 4. Do NOT add another `<ResponsiveContainer>` — it is already included inside.
142
+ */
143
+ function ChartContainer({
144
+ id,
145
+ className,
146
+ children,
147
+ config,
148
+ ...props
149
+ }: React.ComponentProps<'div'> & {
150
+ config: ChartConfig;
151
+ children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>['children'];
152
+ }) {
153
+ const uniqueId = React.useId();
154
+ const chartId = `chart-${id || uniqueId.replace(/:/g, '')}`;
155
+
156
+ return (
157
+ <ChartContext.Provider value={{ config }}>
158
+ <div
159
+ data-slot="chart"
160
+ data-chart={chartId}
161
+ className={cn(
162
+ "[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border relative h-[300px] min-h-[200px] w-full min-w-0 overflow-hidden text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
163
+ className
164
+ )}
165
+ {...props}
166
+ >
167
+ <ChartStyle id={chartId} config={config} />
168
+ <RechartsPrimitive.ResponsiveContainer width="100%" height="100%">
169
+ {children}
170
+ </RechartsPrimitive.ResponsiveContainer>
171
+ </div>
172
+ </ChartContext.Provider>
173
+ );
174
+ }
175
+
176
+ // Matches hex, rgb()/rgba(), hsl()/hsla(), var(), and CSS named/keyword colors.
177
+ // Anything else (e.g. attempting to break out of the declaration) is dropped.
178
+ const SAFE_CSS_COLOR = /^(#[0-9a-fA-F]{3,8}|(rgb|rgba|hsl|hsla|var)\([^;{}<>]*\)|[a-zA-Z-]+)$/;
179
+
180
+ const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
181
+ const colorConfig = Object.entries(config).filter(([, config]) => config.theme || config.color);
182
+
183
+ if (!colorConfig.length) {
184
+ return null;
185
+ }
186
+
187
+ return (
188
+ <style
189
+ dangerouslySetInnerHTML={{
190
+ __html: Object.entries(THEMES)
191
+ .map(
192
+ ([theme, prefix]) => `
193
+ ${prefix} [data-chart=${id}] {
194
+ ${colorConfig
195
+ .map(([key, itemConfig]) => {
196
+ const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color;
197
+ if (!color || !SAFE_CSS_COLOR.test(color.trim())) {
198
+ return null;
199
+ }
200
+ return ` --color-${key}: ${color};`;
201
+ })
202
+ .join('\n')}
203
+ }
204
+ `
205
+ )
206
+ .join('\n'),
207
+ }}
208
+ />
209
+ );
210
+ };
211
+
212
+ const ChartTooltip = RechartsPrimitive.Tooltip;
213
+
214
+ function ChartTooltipContent({
215
+ active,
216
+ payload,
217
+ className,
218
+ indicator = 'dot',
219
+ hideLabel = false,
220
+ hideIndicator = false,
221
+ label,
222
+ labelFormatter,
223
+ labelClassName,
224
+ formatter,
225
+ color,
226
+ nameKey,
227
+ labelKey,
228
+ }: React.ComponentProps<'div'> & {
229
+ active?: boolean;
230
+ payload?: TooltipPayloadItem<TooltipValueType, TooltipNameType>[];
231
+ label?: React.ReactNode;
232
+ labelFormatter?: (label: React.ReactNode, payload: TooltipPayloadItem<TooltipValueType, TooltipNameType>[]) => React.ReactNode;
233
+ labelClassName?: string;
234
+ hideLabel?: boolean;
235
+ hideIndicator?: boolean;
236
+ indicator?: 'line' | 'dot' | 'dashed';
237
+ nameKey?: string;
238
+ labelKey?: string;
239
+ color?: string;
240
+ formatter?: RechartsPrimitive.DefaultTooltipContentProps<TooltipValueType, TooltipNameType>['formatter'];
241
+ }) {
242
+ const { config } = useChart();
243
+
244
+ const tooltipLabel = React.useMemo(() => {
245
+ if (hideLabel || !payload?.length) {
246
+ return null;
247
+ }
248
+
249
+ const [item] = payload;
250
+ const key = `${labelKey || item?.dataKey || item?.name || 'value'}`;
251
+ const itemConfig = getPayloadConfigFromPayload(config, item, key);
252
+ const value =
253
+ !labelKey && typeof label === 'string'
254
+ ? config[label as keyof typeof config]?.label || label
255
+ : itemConfig?.label;
256
+
257
+ if (labelFormatter) {
258
+ return (
259
+ <div className={cn('font-medium', labelClassName)}>{labelFormatter(value, payload)}</div>
260
+ );
261
+ }
262
+
263
+ if (!value) {
264
+ return null;
265
+ }
266
+
267
+ return <div className={cn('font-medium', labelClassName)}>{value}</div>;
268
+ }, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey]);
269
+
270
+ if (!active || !payload?.length) {
271
+ return null;
272
+ }
273
+
274
+ const nestLabel = payload.length === 1 && indicator !== 'dot';
275
+
276
+ return (
277
+ <div
278
+ className={cn(
279
+ 'border-border/50 bg-background/95 backdrop-blur-sm grid min-w-[8rem] items-start gap-1.5 rounded-xl border px-3 py-2 text-xs shadow-xl',
280
+ className
281
+ )}
282
+ >
283
+ {!nestLabel ? tooltipLabel : null}
284
+ <div className="grid gap-1.5">
285
+ {payload.map((item: TooltipPayloadItem<TooltipValueType, TooltipNameType>, index: number) => {
286
+ const key = `${nameKey || item.name || item.dataKey || 'value'}`;
287
+ const itemConfig = getPayloadConfigFromPayload(config, item, key);
288
+ const indicatorColor = color || item.payload?.fill || item.color;
289
+
290
+ return (
291
+ <div
292
+ key={`${item.dataKey ?? index}`}
293
+ className={cn(
294
+ '[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5',
295
+ indicator === 'dot' && 'items-center'
296
+ )}
297
+ >
298
+ {formatter && item?.value !== undefined && item.name ? (
299
+ formatter(item.value, item.name, item, index, item.payload)
300
+ ) : (
301
+ <>
302
+ {itemConfig?.icon ? (
303
+ <itemConfig.icon />
304
+ ) : (
305
+ !hideIndicator && (
306
+ <div
307
+ className={cn(
308
+ 'shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)',
309
+ {
310
+ 'h-2.5 w-2.5': indicator === 'dot',
311
+ 'w-1': indicator === 'line',
312
+ 'w-0 border-[1.5px] border-dashed bg-transparent':
313
+ indicator === 'dashed',
314
+ 'my-0.5': nestLabel && indicator === 'dashed',
315
+ }
316
+ )}
317
+ style={
318
+ {
319
+ '--color-bg': indicatorColor,
320
+ '--color-border': indicatorColor,
321
+ } as React.CSSProperties
322
+ }
323
+ />
324
+ )
325
+ )}
326
+ <div
327
+ className={cn(
328
+ 'flex flex-1 justify-between leading-none',
329
+ nestLabel ? 'items-end' : 'items-center'
330
+ )}
331
+ >
332
+ <div className="grid gap-1.5">
333
+ {nestLabel ? tooltipLabel : null}
334
+ <span className="text-muted-foreground">
335
+ {itemConfig?.label || item.name}
336
+ </span>
337
+ </div>
338
+ {item.value && (
339
+ <span className="text-foreground font-mono font-semibold tabular-nums">
340
+ {item.value.toLocaleString()}
341
+ </span>
342
+ )}
343
+ </div>
344
+ </>
345
+ )}
346
+ </div>
347
+ );
348
+ })}
349
+ </div>
350
+ </div>
351
+ );
352
+ }
353
+
354
+ const ChartLegend = RechartsPrimitive.Legend;
355
+
356
+ function ChartLegendContent({
357
+ className,
358
+ hideIcon = false,
359
+ payload,
360
+ verticalAlign = 'bottom',
361
+ nameKey,
362
+ }: React.ComponentProps<'div'> & {
363
+ payload?: LegendPayloadItem[];
364
+ verticalAlign?: 'top' | 'middle' | 'bottom';
365
+ hideIcon?: boolean;
366
+ nameKey?: string;
367
+ }) {
368
+ const { config } = useChart();
369
+
370
+ if (!payload?.length) {
371
+ return null;
372
+ }
373
+
374
+ return (
375
+ <div
376
+ className={cn(
377
+ 'flex items-center justify-center gap-4',
378
+ verticalAlign === 'top' ? 'pb-3' : 'pt-3',
379
+ className
380
+ )}
381
+ >
382
+ {payload.map(item => {
383
+ const key = `${nameKey ? item.value : item.dataKey || item.value || 'value'}`;
384
+ const itemConfig = getPayloadConfigFromPayload(config, item, key);
385
+
386
+ return (
387
+ <div
388
+ key={item.value}
389
+ className={cn(
390
+ '[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3'
391
+ )}
392
+ >
393
+ {itemConfig?.icon && !hideIcon ? (
394
+ <itemConfig.icon />
395
+ ) : (
396
+ <div
397
+ className="h-2 w-2 shrink-0 rounded-full bg-(--color-bg)"
398
+ style={
399
+ {
400
+ '--color-bg': item.color || `var(--color-${key})`,
401
+ } as React.CSSProperties
402
+ }
403
+ />
404
+ )}
405
+ <span className="text-muted-foreground text-xs">{itemConfig?.label || item.value}</span>
406
+ </div>
407
+ );
408
+ })}
409
+ </div>
410
+ );
411
+ }
412
+
413
+ // Helper to extract item config from a payload.
414
+ function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {
415
+ if (typeof payload !== 'object' || payload === null) {
416
+ return undefined;
417
+ }
418
+
419
+ const payloadPayload =
420
+ 'payload' in payload && typeof payload.payload === 'object' && payload.payload !== null
421
+ ? payload.payload
422
+ : undefined;
423
+
424
+ let configLabelKey: string = key;
425
+
426
+ if (key in payload && typeof payload[key as keyof typeof payload] === 'string') {
427
+ configLabelKey = payload[key as keyof typeof payload] as string;
428
+ } else if (
429
+ payloadPayload &&
430
+ key in payloadPayload &&
431
+ typeof payloadPayload[key as keyof typeof payloadPayload] === 'string'
432
+ ) {
433
+ configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string;
434
+ }
435
+
436
+ return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config];
437
+ }
438
+
439
+ function getChartSeries(
440
+ config: ChartConfig,
441
+ series?: DashboardChartSeries[]
442
+ ): DashboardChartSeries[] {
443
+ if (series?.length) {
444
+ return series;
445
+ }
446
+
447
+ return Object.entries(config).map(([key, item]) => ({
448
+ key,
449
+ label: item.label,
450
+ }));
451
+ }
452
+
453
+ function getSeriesColor(key: string, index: number, colors?: DashboardChartColors) {
454
+ if (Array.isArray(colors)) {
455
+ return colors[index] || defaultChartColors[index % defaultChartColors.length];
456
+ }
457
+
458
+ return colors?.[key] || defaultChartColors[index % defaultChartColors.length];
459
+ }
460
+
461
+ function getChartConfigWithColors(
462
+ config: ChartConfig,
463
+ keys: string[],
464
+ colors?: DashboardChartColors
465
+ ): ChartConfig {
466
+ return keys.reduce<ChartConfig>((nextConfig, key, index) => {
467
+ const item = config[key];
468
+
469
+ if (item?.theme && !colors) {
470
+ nextConfig[key] = item;
471
+ return nextConfig;
472
+ }
473
+
474
+ nextConfig[key] = {
475
+ label: item?.label || key,
476
+ icon: item?.icon,
477
+ color: colors
478
+ ? getSeriesColor(key, index, colors)
479
+ : item?.color || getSeriesColor(key, index),
480
+ };
481
+
482
+ return nextConfig;
483
+ }, {});
484
+ }
485
+
486
+ /** Build a ChartConfig from a list of keys (no pre-existing config needed). */
487
+ function buildChartConfig(keys: string[], colors?: DashboardChartColors): ChartConfig {
488
+ return keys.reduce<ChartConfig>((cfg, key, index) => {
489
+ cfg[key] = {
490
+ label: key,
491
+ color: getSeriesColor(key, index, colors),
492
+ };
493
+ return cfg;
494
+ }, {});
495
+ }
496
+
497
+ function formatTick(value: number | string) {
498
+ if (typeof value !== 'number') {
499
+ return value;
500
+ }
501
+
502
+ return Intl.NumberFormat('en', {
503
+ notation: 'compact',
504
+ maximumFractionDigits: 1,
505
+ }).format(value);
506
+ }
507
+
508
+ function defaultFilterData(data: DashboardChartDatum[], period: string) {
509
+ const match = period.match(/^(\d+)/);
510
+
511
+ if (!match) {
512
+ return data;
513
+ }
514
+
515
+ const limit = Number(match[1]);
516
+ return data.slice(Math.max(data.length - limit, 0));
517
+ }
518
+
519
+ function getErrorDescription(error: ChartErrorState) {
520
+ if (typeof error === 'string') {
521
+ return error;
522
+ }
523
+
524
+ if (error instanceof Error) {
525
+ return error.message;
526
+ }
527
+
528
+ return undefined;
529
+ }
530
+
531
+ function getBarSize(barSize: ChartBarSize = 'md') {
532
+ return typeof barSize === 'number' ? barSize : chartBarSizes[barSize];
533
+ }
534
+
535
+ function hasChartData(data: DashboardChartDatum[], series: DashboardChartSeries[]) {
536
+ if (!data.length || !series.length) {
537
+ return false;
538
+ }
539
+
540
+ return data.some(item =>
541
+ series.some(serie => {
542
+ const value = item[serie.key];
543
+ return value !== null && value !== undefined && value !== '';
544
+ })
545
+ );
546
+ }
547
+
548
+ function hasPieData(data: DashboardChartDatum[], nameKey: string, valueKey: string) {
549
+ return data.some(item => {
550
+ const name = item[nameKey];
551
+ const value = item[valueKey];
552
+ return (
553
+ name !== null &&
554
+ name !== undefined &&
555
+ name !== '' &&
556
+ value !== null &&
557
+ value !== undefined &&
558
+ value !== ''
559
+ );
560
+ });
561
+ }
562
+
563
+ function ChartState({
564
+ type,
565
+ className,
566
+ error,
567
+ onRetry,
568
+ retryLabel = 'Try again',
569
+ emptyTitle = 'No data available',
570
+ emptyDescription = 'There is no data available for this chart yet.',
571
+ errorTitle = 'Connection error',
572
+ errorDescription,
573
+ loadingLabel = 'Loading chart data',
574
+ }: ChartStateProps & {
575
+ type: 'empty' | 'error' | 'loading';
576
+ className?: string;
577
+ }) {
578
+ if (type === 'loading') {
579
+ return (
580
+ <div
581
+ className={cn(
582
+ 'flex min-h-[240px] flex-col justify-end gap-3 rounded-[var(--radius-card)] border border-border p-6',
583
+ className
584
+ )}
585
+ aria-label={typeof loadingLabel === 'string' ? loadingLabel : undefined}
586
+ >
587
+ <Skeleton className="h-8 w-2/5" />
588
+ <Skeleton className="h-14 w-3/5" />
589
+ <Skeleton className="h-24 w-4/5" />
590
+ <Skeleton className="h-36 w-full" />
591
+ </div>
592
+ );
593
+ }
594
+
595
+ if (type === 'error') {
596
+ return (
597
+ <div
598
+ className={cn(
599
+ 'flex min-h-[240px] items-center justify-center rounded-[var(--radius-card)] border border-border p-6',
600
+ className
601
+ )}
602
+ >
603
+ <Alert variant="destructive" className="max-w-xl">
604
+ <AlertTitle>{errorTitle}</AlertTitle>
605
+ <AlertDescription>
606
+ {errorDescription ||
607
+ getErrorDescription(error) ||
608
+ 'Unable to load chart data. Check your connection and try again.'}
609
+ </AlertDescription>
610
+ {onRetry ? (
611
+ <div className="mt-3">
612
+ <Button size="sm" variant="outline" onClick={onRetry}>
613
+ <RefreshCw className="size-4" />
614
+ {retryLabel}
615
+ </Button>
616
+ </div>
617
+ ) : null}
618
+ </Alert>
619
+ </div>
620
+ );
621
+ }
622
+
623
+ return (
624
+ <Empty className={cn('min-h-[240px]', className)}>
625
+ <EmptyIcon>
626
+ <BarChart3 className="size-10 text-muted-foreground" />
627
+ </EmptyIcon>
628
+ <EmptyTitle>{emptyTitle}</EmptyTitle>
629
+ <EmptyDescription>{emptyDescription}</EmptyDescription>
630
+ {onRetry ? (
631
+ <EmptyAction>
632
+ <Button size="sm" variant="outline" onClick={onRetry}>
633
+ <WifiOff className="size-4" />
634
+ {retryLabel}
635
+ </Button>
636
+ </EmptyAction>
637
+ ) : null}
638
+ </Empty>
639
+ );
640
+ }
641
+
642
+ function getChartState(state: ChartStateProps, hasData: boolean, className?: string) {
643
+ if (state.isLoading) {
644
+ return <ChartState {...state} type="loading" className={className} />;
645
+ }
646
+
647
+ if (state.error) {
648
+ return <ChartState {...state} type="error" className={className} />;
649
+ }
650
+
651
+ if (!hasData) {
652
+ return <ChartState {...state} type="empty" className={className} />;
653
+ }
654
+
655
+ return null;
656
+ }
657
+
658
+ export interface ChartCardProps extends Omit<React.ComponentProps<typeof Card>, 'title'> {
659
+ title: React.ReactNode;
660
+ description?: React.ReactNode;
661
+ action?: React.ReactNode;
662
+ footer?: React.ReactNode;
663
+ contentClassName?: string;
664
+ }
665
+
666
+ function ChartCard({
667
+ title,
668
+ description,
669
+ action,
670
+ footer,
671
+ children,
672
+ className,
673
+ contentClassName,
674
+ ...props
675
+ }: ChartCardProps) {
676
+ return (
677
+ <Card className={cn('w-full min-w-0 overflow-hidden', className)} {...props}>
678
+ <CardHeader>
679
+ <CardTitle>{title}</CardTitle>
680
+ {description ? <CardDescription>{description}</CardDescription> : null}
681
+ {action ? <CardAction>{action}</CardAction> : null}
682
+ </CardHeader>
683
+ <CardContent className={contentClassName}>{children}</CardContent>
684
+ {footer ? <CardFooter>{footer}</CardFooter> : null}
685
+ </Card>
686
+ );
687
+ }
688
+
689
+ // ─── Gradient defs helper ────────────────────────────────────────────────────
690
+
691
+ /**
692
+ * Renders SVG `<defs>` with linear gradients for each series key.
693
+ * Used internally by area charts when `gradientFill` is enabled.
694
+ */
695
+ function AreaGradientDefs({
696
+ seriesKeys,
697
+ opacity = 0.3,
698
+ }: {
699
+ seriesKeys: string[];
700
+ opacity?: number;
701
+ }) {
702
+ return (
703
+ <defs>
704
+ {seriesKeys.map(key => (
705
+ <linearGradient key={key} id={`gradient-${key}`} x1="0" y1="0" x2="0" y2="1">
706
+ <stop offset="5%" stopColor={`var(--color-${key})`} stopOpacity={opacity} />
707
+ <stop offset="95%" stopColor={`var(--color-${key})`} stopOpacity={0} />
708
+ </linearGradient>
709
+ ))}
710
+ </defs>
711
+ );
712
+ }
713
+
714
+ // ─── DashboardBarChart ────────────────────────────────────────────────────────
715
+
716
+ export interface DashboardBarChartProps
717
+ extends Omit<React.ComponentProps<typeof ChartContainer>, 'children'>, ChartStateProps {
718
+ data: DashboardChartDatum[];
719
+ indexKey?: string;
720
+ series?: DashboardChartSeries[];
721
+ colors?: DashboardChartColors;
722
+ barSize?: ChartBarSize;
723
+ stacked?: boolean;
724
+ showGrid?: boolean;
725
+ showLegend?: boolean;
726
+ valueFormatter?: ChartValueFormatter;
727
+ }
728
+
729
+ function DashboardBarChart({
730
+ data,
731
+ config,
732
+ indexKey = 'name',
733
+ series,
734
+ colors,
735
+ barSize = 'md',
736
+ stacked = false,
737
+ showGrid = true,
738
+ showLegend = true,
739
+ valueFormatter = formatTick,
740
+ isLoading,
741
+ error,
742
+ onRetry,
743
+ retryLabel,
744
+ emptyTitle,
745
+ emptyDescription,
746
+ errorTitle,
747
+ errorDescription,
748
+ loadingLabel,
749
+ stateClassName,
750
+ className,
751
+ ...props
752
+ }: DashboardBarChartProps) {
753
+ const chartSeries = getChartSeries(config, series);
754
+ const chartConfig = getChartConfigWithColors(
755
+ config,
756
+ chartSeries.map(item => item.key),
757
+ colors
758
+ );
759
+ const chartState = getChartState(
760
+ {
761
+ isLoading,
762
+ error,
763
+ onRetry,
764
+ retryLabel,
765
+ emptyTitle,
766
+ emptyDescription,
767
+ errorTitle,
768
+ errorDescription,
769
+ loadingLabel,
770
+ },
771
+ hasChartData(data, chartSeries),
772
+ cn('h-[320px] w-full', stateClassName)
773
+ );
774
+
775
+ const barElements = React.useMemo(() => {
776
+ const topOfStack = new Set<string>();
777
+ if (stacked) {
778
+ const lastByStack = new Map<string, string>();
779
+ chartSeries.forEach(s => lastByStack.set(s.stackId || 'total', s.key));
780
+ lastByStack.forEach(key => topOfStack.add(key));
781
+ }
782
+ return chartSeries.map(item => {
783
+ const isTop = !stacked || topOfStack.has(item.key);
784
+ return (
785
+ <RechartsPrimitive.Bar
786
+ key={item.key}
787
+ dataKey={item.key}
788
+ fill={`var(--color-${item.key})`}
789
+ radius={isTop ? [4, 4, 0, 0] : [0, 0, 0, 0]}
790
+ barSize={getBarSize(barSize)}
791
+ stackId={stacked ? item.stackId || 'total' : item.stackId}
792
+ isAnimationActive
793
+ animationDuration={600}
794
+ animationEasing="ease-out"
795
+ />
796
+ );
797
+ });
798
+ }, [stacked, chartSeries]);
799
+
800
+ if (chartState) {
801
+ return chartState;
802
+ }
803
+
804
+ return (
805
+ <ChartContainer config={chartConfig} className={cn('h-[320px] w-full', className)} {...props}>
806
+ <RechartsPrimitive.BarChart data={data} accessibilityLayer barGap={4}>
807
+ {showGrid ? (
808
+ <RechartsPrimitive.CartesianGrid
809
+ vertical={false}
810
+ strokeDasharray="3 3"
811
+ stroke="var(--border)"
812
+ strokeOpacity={0.5}
813
+ />
814
+ ) : null}
815
+ <RechartsPrimitive.XAxis
816
+ dataKey={indexKey}
817
+ tickLine={false}
818
+ axisLine={false}
819
+ tickMargin={8}
820
+ />
821
+ <RechartsPrimitive.YAxis
822
+ tickLine={false}
823
+ axisLine={false}
824
+ tickMargin={8}
825
+ tickFormatter={valueFormatter}
826
+ />
827
+ <ChartTooltip
828
+ cursor={{ fill: 'var(--muted)', opacity: 0.4, radius: 4 }}
829
+ content={<ChartTooltipContent />}
830
+ />
831
+ {showLegend ? <ChartLegend content={<ChartLegendContent />} /> : null}
832
+ {barElements}
833
+ </RechartsPrimitive.BarChart>
834
+ </ChartContainer>
835
+ );
836
+ }
837
+
838
+ // ─── DashboardLineChart ───────────────────────────────────────────────────────
839
+
840
+ export interface DashboardLineChartProps
841
+ extends Omit<React.ComponentProps<typeof ChartContainer>, 'children'>, ChartStateProps {
842
+ data: DashboardChartDatum[];
843
+ indexKey?: string;
844
+ series?: DashboardChartSeries[];
845
+ colors?: DashboardChartColors;
846
+ showDots?: boolean;
847
+ showGrid?: boolean;
848
+ showLegend?: boolean;
849
+ curveType?: ChartCurveType;
850
+ strokeWidth?: number;
851
+ valueFormatter?: ChartValueFormatter;
852
+ }
853
+
854
+ function DashboardLineChart({
855
+ data,
856
+ config,
857
+ indexKey = 'name',
858
+ series,
859
+ colors,
860
+ showDots = false,
861
+ showGrid = true,
862
+ showLegend = true,
863
+ curveType = 'monotone',
864
+ strokeWidth = 2,
865
+ valueFormatter = formatTick,
866
+ isLoading,
867
+ error,
868
+ onRetry,
869
+ retryLabel,
870
+ emptyTitle,
871
+ emptyDescription,
872
+ errorTitle,
873
+ errorDescription,
874
+ loadingLabel,
875
+ stateClassName,
876
+ className,
877
+ ...props
878
+ }: DashboardLineChartProps) {
879
+ const chartSeries = getChartSeries(config, series);
880
+ const chartConfig = getChartConfigWithColors(
881
+ config,
882
+ chartSeries.map(item => item.key),
883
+ colors
884
+ );
885
+ const chartState = getChartState(
886
+ {
887
+ isLoading,
888
+ error,
889
+ onRetry,
890
+ retryLabel,
891
+ emptyTitle,
892
+ emptyDescription,
893
+ errorTitle,
894
+ errorDescription,
895
+ loadingLabel,
896
+ },
897
+ hasChartData(data, chartSeries),
898
+ cn('h-[320px] w-full', stateClassName)
899
+ );
900
+
901
+ if (chartState) {
902
+ return chartState;
903
+ }
904
+
905
+ return (
906
+ <ChartContainer config={chartConfig} className={cn('h-[320px] w-full', className)} {...props}>
907
+ <RechartsPrimitive.LineChart data={data} accessibilityLayer>
908
+ {showGrid ? (
909
+ <RechartsPrimitive.CartesianGrid
910
+ vertical={false}
911
+ strokeDasharray="3 3"
912
+ stroke="var(--border)"
913
+ strokeOpacity={0.5}
914
+ />
915
+ ) : null}
916
+ <RechartsPrimitive.XAxis
917
+ dataKey={indexKey}
918
+ tickLine={false}
919
+ axisLine={false}
920
+ tickMargin={8}
921
+ />
922
+ <RechartsPrimitive.YAxis
923
+ tickLine={false}
924
+ axisLine={false}
925
+ tickMargin={8}
926
+ tickFormatter={valueFormatter}
927
+ />
928
+ <ChartTooltip content={<ChartTooltipContent indicator="line" />} />
929
+ {showLegend ? <ChartLegend content={<ChartLegendContent />} /> : null}
930
+ {chartSeries.map(item => (
931
+ <RechartsPrimitive.Line
932
+ key={item.key}
933
+ type={curveType}
934
+ dataKey={item.key}
935
+ stroke={`var(--color-${item.key})`}
936
+ strokeWidth={strokeWidth}
937
+ dot={showDots ? { r: 3, fill: `var(--color-${item.key})`, strokeWidth: 0 } : false}
938
+ activeDot={{ r: 5, strokeWidth: 0 }}
939
+ yAxisId={item.yAxisId}
940
+ isAnimationActive
941
+ animationDuration={600}
942
+ animationEasing="ease-out"
943
+ />
944
+ ))}
945
+ </RechartsPrimitive.LineChart>
946
+ </ChartContainer>
947
+ );
948
+ }
949
+
950
+ // ─── HorizontalBarChart ───────────────────────────────────────────────────────
951
+
952
+ export interface HorizontalBarChartProps
953
+ extends Omit<React.ComponentProps<typeof ChartContainer>, 'children'>, ChartStateProps {
954
+ data: DashboardChartDatum[];
955
+ indexKey?: string;
956
+ series?: DashboardChartSeries[];
957
+ colors?: DashboardChartColors;
958
+ barSize?: ChartBarSize;
959
+ stacked?: boolean;
960
+ categoryWidth?: number;
961
+ showGrid?: boolean;
962
+ showLegend?: boolean;
963
+ valueFormatter?: ChartValueFormatter;
964
+ }
965
+
966
+ function HorizontalBarChart({
967
+ data,
968
+ config,
969
+ indexKey = 'name',
970
+ series,
971
+ colors,
972
+ barSize = 'md',
973
+ stacked = false,
974
+ categoryWidth = 96,
975
+ showGrid = true,
976
+ showLegend = true,
977
+ valueFormatter = formatTick,
978
+ isLoading,
979
+ error,
980
+ onRetry,
981
+ retryLabel,
982
+ emptyTitle,
983
+ emptyDescription,
984
+ errorTitle,
985
+ errorDescription,
986
+ loadingLabel,
987
+ stateClassName,
988
+ className,
989
+ ...props
990
+ }: HorizontalBarChartProps) {
991
+ const chartSeries = getChartSeries(config, series);
992
+ const chartConfig = getChartConfigWithColors(
993
+ config,
994
+ chartSeries.map(item => item.key),
995
+ colors
996
+ );
997
+ const chartState = getChartState(
998
+ {
999
+ isLoading,
1000
+ error,
1001
+ onRetry,
1002
+ retryLabel,
1003
+ emptyTitle,
1004
+ emptyDescription,
1005
+ errorTitle,
1006
+ errorDescription,
1007
+ loadingLabel,
1008
+ },
1009
+ hasChartData(data, chartSeries),
1010
+ cn('h-[320px] w-full', stateClassName)
1011
+ );
1012
+
1013
+ if (chartState) {
1014
+ return chartState;
1015
+ }
1016
+
1017
+ return (
1018
+ <ChartContainer config={chartConfig} className={cn('h-[320px] w-full', className)} {...props}>
1019
+ <RechartsPrimitive.BarChart
1020
+ data={data}
1021
+ layout="vertical"
1022
+ accessibilityLayer
1023
+ margin={{ left: 8, right: 16 }}
1024
+ >
1025
+ {showGrid ? (
1026
+ <RechartsPrimitive.CartesianGrid
1027
+ horizontal={false}
1028
+ strokeDasharray="3 3"
1029
+ stroke="var(--border)"
1030
+ strokeOpacity={0.5}
1031
+ />
1032
+ ) : null}
1033
+ <RechartsPrimitive.XAxis
1034
+ type="number"
1035
+ tickLine={false}
1036
+ axisLine={false}
1037
+ tickMargin={8}
1038
+ tickFormatter={valueFormatter}
1039
+ />
1040
+ <RechartsPrimitive.YAxis
1041
+ dataKey={indexKey}
1042
+ type="category"
1043
+ tickLine={false}
1044
+ axisLine={false}
1045
+ tickMargin={8}
1046
+ width={categoryWidth}
1047
+ />
1048
+ <ChartTooltip content={<ChartTooltipContent />} />
1049
+ {showLegend ? <ChartLegend content={<ChartLegendContent />} /> : null}
1050
+ {chartSeries.map(item => (
1051
+ <RechartsPrimitive.Bar
1052
+ key={item.key}
1053
+ dataKey={item.key}
1054
+ fill={`var(--color-${item.key})`}
1055
+ radius={[0, 4, 4, 0]}
1056
+ barSize={getBarSize(barSize)}
1057
+ stackId={stacked ? item.stackId || 'total' : item.stackId}
1058
+ isAnimationActive
1059
+ animationDuration={600}
1060
+ animationEasing="ease-out"
1061
+ />
1062
+ ))}
1063
+ </RechartsPrimitive.BarChart>
1064
+ </ChartContainer>
1065
+ );
1066
+ }
1067
+
1068
+ // ─── InteractiveTimeSeriesChart ───────────────────────────────────────────────
1069
+
1070
+ export interface InteractiveTimeSeriesChartProps
1071
+ extends Omit<React.ComponentProps<typeof ChartContainer>, 'children'>, ChartStateProps {
1072
+ data: DashboardChartDatum[];
1073
+ indexKey?: string;
1074
+ series?: DashboardChartSeries[];
1075
+ colors?: DashboardChartColors;
1076
+ periods?: ChartPeriod[];
1077
+ defaultPeriod?: string;
1078
+ defaultSeries?: string;
1079
+ filterData?: (data: DashboardChartDatum[], period: string) => DashboardChartDatum[];
1080
+ showGrid?: boolean;
1081
+ showLegend?: boolean;
1082
+ showDots?: boolean;
1083
+ gradientFill?: boolean;
1084
+ curveType?: ChartCurveType;
1085
+ strokeWidth?: number;
1086
+ valueFormatter?: ChartValueFormatter;
1087
+ }
1088
+
1089
+ function InteractiveTimeSeriesChart({
1090
+ data,
1091
+ config,
1092
+ indexKey = 'date',
1093
+ series,
1094
+ colors,
1095
+ periods = defaultPeriods,
1096
+ defaultPeriod = periods[1]?.value || periods[0]?.value || '30d',
1097
+ defaultSeries,
1098
+ filterData = defaultFilterData,
1099
+ showGrid = true,
1100
+ showLegend = false,
1101
+ showDots = false,
1102
+ gradientFill = true,
1103
+ curveType = 'monotone',
1104
+ strokeWidth = 2,
1105
+ valueFormatter = formatTick,
1106
+ isLoading,
1107
+ error,
1108
+ onRetry,
1109
+ retryLabel,
1110
+ emptyTitle,
1111
+ emptyDescription,
1112
+ errorTitle,
1113
+ errorDescription,
1114
+ loadingLabel,
1115
+ stateClassName,
1116
+ className,
1117
+ ...props
1118
+ }: InteractiveTimeSeriesChartProps) {
1119
+ const chartSeries = getChartSeries(config, series);
1120
+ const chartConfig = getChartConfigWithColors(
1121
+ config,
1122
+ chartSeries.map(item => item.key),
1123
+ colors
1124
+ );
1125
+ const [period, setPeriod] = React.useState(defaultPeriod);
1126
+ const [activeSeries, setActiveSeries] = React.useState(
1127
+ defaultSeries || chartSeries[0]?.key || ''
1128
+ );
1129
+ const filteredData = React.useMemo(() => filterData(data, period), [data, filterData, period]);
1130
+ const visibleSeries =
1131
+ chartSeries.length > 1 ? chartSeries.filter(item => item.key === activeSeries) : chartSeries;
1132
+ const chartState = getChartState(
1133
+ {
1134
+ isLoading,
1135
+ error,
1136
+ onRetry,
1137
+ retryLabel,
1138
+ emptyTitle,
1139
+ emptyDescription,
1140
+ errorTitle,
1141
+ errorDescription,
1142
+ loadingLabel,
1143
+ },
1144
+ hasChartData(filteredData, visibleSeries),
1145
+ cn('h-[320px] w-full', stateClassName)
1146
+ );
1147
+
1148
+ return (
1149
+ <div className="w-full space-y-4">
1150
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
1151
+ {chartSeries.length > 1 ? (
1152
+ <div
1153
+ className="inline-flex h-10 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground"
1154
+ role="group"
1155
+ aria-label="Selecionar metrica do grafico"
1156
+ >
1157
+ {chartSeries.map(item => {
1158
+ const label = item.label || config[item.key]?.label || item.key;
1159
+
1160
+ return (
1161
+ <Button
1162
+ key={item.key}
1163
+ type="button"
1164
+ variant="ghost"
1165
+ size="sm"
1166
+ aria-pressed={activeSeries === item.key}
1167
+ onClick={() => setActiveSeries(item.key)}
1168
+ className={cn(
1169
+ 'h-8 px-3 text-sm font-medium transition-all',
1170
+ activeSeries === item.key
1171
+ ? 'bg-background text-foreground shadow-sm'
1172
+ : 'text-muted-foreground hover:text-foreground'
1173
+ )}
1174
+ >
1175
+ {label}
1176
+ </Button>
1177
+ );
1178
+ })}
1179
+ </div>
1180
+ ) : (
1181
+ <div />
1182
+ )}
1183
+ <Select value={period} onValueChange={setPeriod}>
1184
+ <SelectTrigger
1185
+ className="w-full sm:w-[160px]"
1186
+ size="sm"
1187
+ aria-label="Selecionar periodo do grafico"
1188
+ >
1189
+ <SelectValue placeholder="Period" />
1190
+ </SelectTrigger>
1191
+ <SelectContent>
1192
+ {periods.map(item => (
1193
+ <SelectItem key={item.value} value={item.value}>
1194
+ {item.label}
1195
+ </SelectItem>
1196
+ ))}
1197
+ </SelectContent>
1198
+ </Select>
1199
+ </div>
1200
+ {chartState || (
1201
+ <ChartContainer
1202
+ config={chartConfig}
1203
+ className={cn('h-[320px] w-full', className)}
1204
+ {...props}
1205
+ >
1206
+ <RechartsPrimitive.AreaChart data={filteredData} accessibilityLayer>
1207
+ {gradientFill && (
1208
+ <AreaGradientDefs seriesKeys={visibleSeries.map(s => s.key)} opacity={0.4} />
1209
+ )}
1210
+ {showGrid ? (
1211
+ <RechartsPrimitive.CartesianGrid
1212
+ vertical={false}
1213
+ strokeDasharray="3 3"
1214
+ stroke="var(--border)"
1215
+ strokeOpacity={0.5}
1216
+ />
1217
+ ) : null}
1218
+ <RechartsPrimitive.XAxis
1219
+ dataKey={indexKey}
1220
+ tickLine={false}
1221
+ axisLine={false}
1222
+ tickMargin={8}
1223
+ />
1224
+ <RechartsPrimitive.YAxis
1225
+ tickLine={false}
1226
+ axisLine={false}
1227
+ tickMargin={8}
1228
+ tickFormatter={valueFormatter}
1229
+ />
1230
+ <ChartTooltip content={<ChartTooltipContent indicator="line" />} />
1231
+ {showLegend ? <ChartLegend content={<ChartLegendContent />} /> : null}
1232
+ {visibleSeries.map(item => (
1233
+ <RechartsPrimitive.Area
1234
+ key={item.key}
1235
+ type={curveType}
1236
+ dataKey={item.key}
1237
+ stroke={`var(--color-${item.key})`}
1238
+ fill={gradientFill ? `url(#gradient-${item.key})` : `var(--color-${item.key})`}
1239
+ fillOpacity={gradientFill ? 1 : 0.16}
1240
+ strokeWidth={strokeWidth}
1241
+ dot={showDots ? { r: 3, fill: `var(--color-${item.key})`, strokeWidth: 0 } : false}
1242
+ activeDot={{ r: 5, strokeWidth: 0 }}
1243
+ isAnimationActive
1244
+ animationDuration={600}
1245
+ animationEasing="ease-out"
1246
+ />
1247
+ ))}
1248
+ </RechartsPrimitive.AreaChart>
1249
+ </ChartContainer>
1250
+ )}
1251
+ </div>
1252
+ );
1253
+ }
1254
+
1255
+ // ─── ComboMetricChart ─────────────────────────────────────────────────────────
1256
+
1257
+ export interface ComboMetricChartProps
1258
+ extends Omit<React.ComponentProps<typeof ChartContainer>, 'children'>, ChartStateProps {
1259
+ data: DashboardChartDatum[];
1260
+ indexKey?: string;
1261
+ series?: DashboardChartSeries[];
1262
+ colors?: DashboardChartColors;
1263
+ barSize?: ChartBarSize;
1264
+ showGrid?: boolean;
1265
+ showLegend?: boolean;
1266
+ gradientFill?: boolean;
1267
+ curveType?: ChartCurveType;
1268
+ valueFormatter?: ChartValueFormatter;
1269
+ }
1270
+
1271
+ function ComboMetricChart({
1272
+ data,
1273
+ config,
1274
+ indexKey = 'name',
1275
+ series,
1276
+ colors,
1277
+ barSize = 'md',
1278
+ showGrid = true,
1279
+ showLegend = true,
1280
+ gradientFill = false,
1281
+ curveType = 'monotone',
1282
+ valueFormatter = formatTick,
1283
+ isLoading,
1284
+ error,
1285
+ onRetry,
1286
+ retryLabel,
1287
+ emptyTitle,
1288
+ emptyDescription,
1289
+ errorTitle,
1290
+ errorDescription,
1291
+ loadingLabel,
1292
+ stateClassName,
1293
+ className,
1294
+ ...props
1295
+ }: ComboMetricChartProps) {
1296
+ const chartSeries = getChartSeries(config, series);
1297
+ const chartConfig = getChartConfigWithColors(
1298
+ config,
1299
+ chartSeries.map(item => item.key),
1300
+ colors
1301
+ );
1302
+ const chartState = getChartState(
1303
+ {
1304
+ isLoading,
1305
+ error,
1306
+ onRetry,
1307
+ retryLabel,
1308
+ emptyTitle,
1309
+ emptyDescription,
1310
+ errorTitle,
1311
+ errorDescription,
1312
+ loadingLabel,
1313
+ },
1314
+ hasChartData(data, chartSeries),
1315
+ cn('h-[340px] w-full', stateClassName)
1316
+ );
1317
+
1318
+ if (chartState) {
1319
+ return chartState;
1320
+ }
1321
+
1322
+ const areaSeries = gradientFill ? chartSeries.filter(s => s.type === 'area').map(s => s.key) : [];
1323
+
1324
+ return (
1325
+ <ChartContainer config={chartConfig} className={cn('h-[340px] w-full', className)} {...props}>
1326
+ <RechartsPrimitive.ComposedChart data={data} accessibilityLayer>
1327
+ {gradientFill && areaSeries.length > 0 && (
1328
+ <AreaGradientDefs seriesKeys={areaSeries} opacity={0.35} />
1329
+ )}
1330
+ {showGrid ? (
1331
+ <RechartsPrimitive.CartesianGrid
1332
+ vertical={false}
1333
+ strokeDasharray="3 3"
1334
+ stroke="var(--border)"
1335
+ strokeOpacity={0.5}
1336
+ />
1337
+ ) : null}
1338
+ <RechartsPrimitive.XAxis
1339
+ dataKey={indexKey}
1340
+ tickLine={false}
1341
+ axisLine={false}
1342
+ tickMargin={8}
1343
+ />
1344
+ <RechartsPrimitive.YAxis
1345
+ tickLine={false}
1346
+ axisLine={false}
1347
+ tickMargin={8}
1348
+ tickFormatter={valueFormatter}
1349
+ />
1350
+ <ChartTooltip content={<ChartTooltipContent />} />
1351
+ {showLegend ? <ChartLegend content={<ChartLegendContent />} /> : null}
1352
+ {chartSeries.map(item =>
1353
+ item.type === 'line' ? (
1354
+ <RechartsPrimitive.Line
1355
+ key={item.key}
1356
+ type={curveType}
1357
+ dataKey={item.key}
1358
+ stroke={`var(--color-${item.key})`}
1359
+ strokeWidth={2}
1360
+ dot={false}
1361
+ activeDot={{ r: 5, strokeWidth: 0 }}
1362
+ yAxisId={item.yAxisId}
1363
+ isAnimationActive
1364
+ animationDuration={600}
1365
+ animationEasing="ease-out"
1366
+ />
1367
+ ) : item.type === 'area' ? (
1368
+ <RechartsPrimitive.Area
1369
+ key={item.key}
1370
+ type={curveType}
1371
+ dataKey={item.key}
1372
+ stroke={`var(--color-${item.key})`}
1373
+ fill={gradientFill ? `url(#gradient-${item.key})` : `var(--color-${item.key})`}
1374
+ fillOpacity={gradientFill ? 1 : 0.12}
1375
+ strokeWidth={2}
1376
+ dot={false}
1377
+ activeDot={{ r: 5, strokeWidth: 0 }}
1378
+ yAxisId={item.yAxisId}
1379
+ isAnimationActive
1380
+ animationDuration={600}
1381
+ animationEasing="ease-out"
1382
+ />
1383
+ ) : (
1384
+ <RechartsPrimitive.Bar
1385
+ key={item.key}
1386
+ dataKey={item.key}
1387
+ fill={`var(--color-${item.key})`}
1388
+ radius={[4, 4, 0, 0]}
1389
+ barSize={getBarSize(barSize)}
1390
+ yAxisId={item.yAxisId}
1391
+ isAnimationActive
1392
+ animationDuration={600}
1393
+ animationEasing="ease-out"
1394
+ />
1395
+ )
1396
+ )}
1397
+ </RechartsPrimitive.ComposedChart>
1398
+ </ChartContainer>
1399
+ );
1400
+ }
1401
+
1402
+ // ─── DonutBreakdownChart ──────────────────────────────────────────────────────
1403
+
1404
+ export interface DonutBreakdownChartProps
1405
+ extends Omit<React.ComponentProps<typeof ChartContainer>, 'children'>, ChartStateProps {
1406
+ data: DashboardChartDatum[];
1407
+ nameKey?: string;
1408
+ valueKey?: string;
1409
+ colors?: DashboardChartColors;
1410
+ centerLabel?: React.ReactNode;
1411
+ centerValue?: React.ReactNode;
1412
+ showLegend?: boolean;
1413
+ /** Inner radius as percentage string (e.g. "58%") or number */
1414
+ innerRadius?: string | number;
1415
+ /** Outer radius as percentage string (e.g. "82%") or number */
1416
+ outerRadius?: string | number;
1417
+ }
1418
+
1419
+ function DonutBreakdownChart({
1420
+ data,
1421
+ config,
1422
+ nameKey = 'name',
1423
+ valueKey = 'value',
1424
+ colors,
1425
+ centerLabel,
1426
+ centerValue,
1427
+ showLegend = true,
1428
+ innerRadius = '58%',
1429
+ outerRadius = '82%',
1430
+ isLoading,
1431
+ error,
1432
+ onRetry,
1433
+ retryLabel,
1434
+ emptyTitle,
1435
+ emptyDescription,
1436
+ errorTitle,
1437
+ errorDescription,
1438
+ loadingLabel,
1439
+ stateClassName,
1440
+ className,
1441
+ ...props
1442
+ }: DonutBreakdownChartProps) {
1443
+ const [activeIndex, setActiveIndex] = React.useState(0);
1444
+ const chartKeys = data.map((entry, index) => String(entry[nameKey] || `segment-${index}`));
1445
+ const chartConfig = getChartConfigWithColors(config, chartKeys, colors);
1446
+ const chartState = getChartState(
1447
+ {
1448
+ isLoading,
1449
+ error,
1450
+ onRetry,
1451
+ retryLabel,
1452
+ emptyTitle,
1453
+ emptyDescription,
1454
+ errorTitle,
1455
+ errorDescription,
1456
+ loadingLabel,
1457
+ },
1458
+ hasPieData(data, nameKey, valueKey),
1459
+ cn('h-[320px] w-full', stateClassName)
1460
+ );
1461
+
1462
+ if (chartState) {
1463
+ return chartState;
1464
+ }
1465
+
1466
+ return (
1467
+ <ChartContainer config={chartConfig} className={cn('h-[320px] w-full', className)} {...props}>
1468
+ <RechartsPrimitive.PieChart accessibilityLayer>
1469
+ <ChartTooltip
1470
+ cursor={false}
1471
+ content={<ChartTooltipContent hideLabel nameKey={nameKey} />}
1472
+ />
1473
+ {showLegend ? (
1474
+ <ChartLegend content={<ChartLegendContent nameKey={nameKey} />} verticalAlign="bottom" />
1475
+ ) : null}
1476
+ <RechartsPrimitive.Pie
1477
+ data={data}
1478
+ dataKey={valueKey}
1479
+ nameKey={nameKey}
1480
+ innerRadius={innerRadius}
1481
+ outerRadius={outerRadius}
1482
+ paddingAngle={3}
1483
+ onMouseEnter={(_, index) => setActiveIndex(index)}
1484
+ isAnimationActive
1485
+ animationDuration={600}
1486
+ animationEasing="ease-out"
1487
+ >
1488
+ {data.map((entry, index) => {
1489
+ const key = String(entry[nameKey] || `segment-${index}`);
1490
+
1491
+ return (
1492
+ <RechartsPrimitive.Cell
1493
+ key={key}
1494
+ fill={`var(--color-${key})`}
1495
+ opacity={index === activeIndex ? 1 : 0.7}
1496
+ stroke="transparent"
1497
+ />
1498
+ );
1499
+ })}
1500
+ {centerValue || centerLabel ? (
1501
+ <RechartsPrimitive.Label
1502
+ position="center"
1503
+ content={({ viewBox }) => {
1504
+ if (!viewBox || !('cx' in viewBox) || !('cy' in viewBox)) {
1505
+ return null;
1506
+ }
1507
+
1508
+ return (
1509
+ <text x={viewBox.cx} y={viewBox.cy} textAnchor="middle" dominantBaseline="middle">
1510
+ {centerValue ? (
1511
+ <tspan
1512
+ x={viewBox.cx}
1513
+ y={viewBox.cy}
1514
+ className="fill-foreground text-2xl font-semibold"
1515
+ >
1516
+ {centerValue}
1517
+ </tspan>
1518
+ ) : null}
1519
+ {centerLabel ? (
1520
+ <tspan
1521
+ x={viewBox.cx}
1522
+ y={Number(viewBox.cy) + 22}
1523
+ className="fill-muted-foreground text-xs"
1524
+ >
1525
+ {centerLabel}
1526
+ </tspan>
1527
+ ) : null}
1528
+ </text>
1529
+ );
1530
+ }}
1531
+ />
1532
+ ) : null}
1533
+ </RechartsPrimitive.Pie>
1534
+ </RechartsPrimitive.PieChart>
1535
+ </ChartContainer>
1536
+ );
1537
+ }
1538
+
1539
+ // ─── SparklineChart ───────────────────────────────────────────────────────────
1540
+
1541
+ export interface SparklineChartProps {
1542
+ /** Data array — each item must have the `dataKey` field */
1543
+ data: DashboardChartDatum[];
1544
+ /** Key to plot on the Y axis */
1545
+ dataKey: string;
1546
+ /** Color for the line/area — defaults to `var(--chart-1)` */
1547
+ color?: string;
1548
+ /** Show filled area under the line */
1549
+ filled?: boolean;
1550
+ /** Show the last data point as a dot */
1551
+ showEndDot?: boolean;
1552
+ /** Curve interpolation type */
1553
+ curveType?: ChartCurveType;
1554
+ /** Stroke width */
1555
+ strokeWidth?: number;
1556
+ className?: string;
1557
+ }
1558
+
1559
+ /**
1560
+ * Minimal inline sparkline chart — no axes, no grid, no tooltip.
1561
+ * Ideal for embedding inside stat cards or table cells.
1562
+ *
1563
+ * @ai-rules
1564
+ * - Use `filled` for area-style sparklines.
1565
+ * - Keep `className` height small (e.g. `h-12` or `h-16`).
1566
+ * - Do NOT wrap in `ChartCard` — it is designed to be inline.
1567
+ */
1568
+ function SparklineChart({
1569
+ data,
1570
+ dataKey,
1571
+ color = 'var(--chart-1)',
1572
+ filled = true,
1573
+ showEndDot = true,
1574
+ curveType = 'monotone',
1575
+ strokeWidth = 2,
1576
+ className,
1577
+ }: SparklineChartProps) {
1578
+ const sparkId = React.useId().replace(/:/g, '');
1579
+ const lastPoint = data[data.length - 1];
1580
+ const lastValue = lastPoint ? lastPoint[dataKey] : undefined;
1581
+
1582
+ return (
1583
+ <div className={cn('relative h-12 w-full min-w-0', className)}>
1584
+ <RechartsPrimitive.ResponsiveContainer width="100%" height="100%">
1585
+ <RechartsPrimitive.AreaChart
1586
+ data={data}
1587
+ margin={{ top: 4, right: showEndDot ? 8 : 0, bottom: 0, left: 0 }}
1588
+ >
1589
+ {filled && (
1590
+ <defs>
1591
+ <linearGradient id={`spark-gradient-${sparkId}`} x1="0" y1="0" x2="0" y2="1">
1592
+ <stop offset="5%" stopColor={color} stopOpacity={0.3} />
1593
+ <stop offset="95%" stopColor={color} stopOpacity={0} />
1594
+ </linearGradient>
1595
+ </defs>
1596
+ )}
1597
+ <RechartsPrimitive.Area
1598
+ type={curveType}
1599
+ dataKey={dataKey}
1600
+ stroke={color}
1601
+ strokeWidth={strokeWidth}
1602
+ fill={filled ? `url(#spark-gradient-${sparkId})` : 'none'}
1603
+ fillOpacity={1}
1604
+ dot={false}
1605
+ activeDot={false}
1606
+ isAnimationActive
1607
+ animationDuration={600}
1608
+ animationEasing="ease-out"
1609
+ />
1610
+ </RechartsPrimitive.AreaChart>
1611
+ </RechartsPrimitive.ResponsiveContainer>
1612
+ {showEndDot && lastValue !== undefined && lastValue !== null && (
1613
+ <div
1614
+ className="pointer-events-none absolute right-0 top-1/2 h-2.5 w-2.5 -translate-y-1/2 rounded-full ring-2 ring-background"
1615
+ style={{ backgroundColor: color }}
1616
+ />
1617
+ )}
1618
+ </div>
1619
+ );
1620
+ }
1621
+
1622
+ // ─── RadarChart ───────────────────────────────────────────────────────────────
1623
+
1624
+ export interface RadarMetricChartProps extends ChartStateProps {
1625
+ /** Data array — each item is one axis point on the radar */
1626
+ data: DashboardChartDatum[];
1627
+ /** Key in each datum used as the axis label */
1628
+ labelKey: string;
1629
+ /**
1630
+ * Series to render. Each entry maps to one `<Radar>` element.
1631
+ * Use a single entry for a simple radar, multiple for comparison.
1632
+ */
1633
+ series: DashboardChartSeries[];
1634
+ /** Override colors per series key or as an ordered array */
1635
+ colors?: DashboardChartColors;
1636
+ /** Fill the radar polygon (default: true) */
1637
+ filled?: boolean;
1638
+ /** Fill opacity when `filled` is true (default: 0.25) */
1639
+ fillOpacity?: number;
1640
+ /** Show dots on each axis point (default: false) */
1641
+ showDots?: boolean;
1642
+ /** Show the polar grid lines (default: true) */
1643
+ showGrid?: boolean;
1644
+ /** Show the legend (default: true when multiple series) */
1645
+ showLegend?: boolean;
1646
+ /** Format axis tick values */
1647
+ valueFormatter?: ChartValueFormatter;
1648
+ className?: string;
1649
+ }
1650
+
1651
+ function RadarMetricChart({
1652
+ data,
1653
+ labelKey,
1654
+ series,
1655
+ colors,
1656
+ filled = true,
1657
+ fillOpacity = 0.25,
1658
+ showDots = false,
1659
+ showGrid = true,
1660
+ showLegend,
1661
+ valueFormatter,
1662
+ isLoading,
1663
+ error,
1664
+ onRetry,
1665
+ retryLabel,
1666
+ emptyTitle,
1667
+ emptyDescription,
1668
+ errorTitle,
1669
+ errorDescription,
1670
+ loadingLabel,
1671
+ stateClassName,
1672
+ className,
1673
+ }: RadarMetricChartProps) {
1674
+ const chartConfig = React.useMemo(
1675
+ () =>
1676
+ buildChartConfig(
1677
+ series.map(s => s.key),
1678
+ colors
1679
+ ),
1680
+ [series, colors]
1681
+ );
1682
+
1683
+ const hasData = data.length > 0 && series.length > 0;
1684
+ const chartState = getChartState(
1685
+ {
1686
+ isLoading,
1687
+ error,
1688
+ emptyTitle,
1689
+ emptyDescription,
1690
+ errorTitle,
1691
+ errorDescription,
1692
+ loadingLabel,
1693
+ stateClassName,
1694
+ onRetry,
1695
+ retryLabel,
1696
+ },
1697
+ hasData
1698
+ );
1699
+
1700
+ const displayLegend = showLegend ?? series.length > 1;
1701
+
1702
+ return (
1703
+ <ChartContainer config={chartConfig} className={cn('h-[300px] w-full', className)}>
1704
+ {chartState ?? (
1705
+ <RechartsPrimitive.RadarChart data={data} accessibilityLayer>
1706
+ {showGrid && <RechartsPrimitive.PolarGrid stroke="var(--border)" strokeOpacity={0.6} />}
1707
+ <RechartsPrimitive.PolarAngleAxis
1708
+ dataKey={labelKey}
1709
+ tick={{ fill: 'var(--muted-foreground)', fontSize: 12 }}
1710
+ />
1711
+ <RechartsPrimitive.PolarRadiusAxis
1712
+ tick={false}
1713
+ axisLine={false}
1714
+ tickFormatter={valueFormatter}
1715
+ />
1716
+ <ChartTooltip
1717
+ content={
1718
+ <ChartTooltipContent
1719
+ formatter={valueFormatter ? (v: TooltipValueType) => valueFormatter(v as number) : undefined}
1720
+ />
1721
+ }
1722
+ />
1723
+ {displayLegend && <ChartLegend content={<ChartLegendContent />} />}
1724
+ {series.map(s => (
1725
+ <RechartsPrimitive.Radar
1726
+ key={s.key}
1727
+ name={(s.label as string) ?? s.key}
1728
+ dataKey={s.key}
1729
+ stroke={`var(--color-${s.key})`}
1730
+ fill={filled ? `var(--color-${s.key})` : 'none'}
1731
+ fillOpacity={filled ? fillOpacity : 0}
1732
+ dot={showDots ? { r: 3, fill: `var(--color-${s.key})` } : false}
1733
+ isAnimationActive
1734
+ animationDuration={600}
1735
+ animationEasing="ease-out"
1736
+ />
1737
+ ))}
1738
+ </RechartsPrimitive.RadarChart>
1739
+ )}
1740
+ </ChartContainer>
1741
+ );
1742
+ }
1743
+
1744
+ // ─── PieMetricChart ───────────────────────────────────────────────────────────
1745
+
1746
+ export interface PieMetricChartProps extends ChartStateProps {
1747
+ /** Data array — each item is one slice */
1748
+ data: DashboardChartDatum[];
1749
+ /** Key in each datum used as the slice name/label */
1750
+ nameKey: string;
1751
+ /** Key in each datum used as the slice value */
1752
+ valueKey: string;
1753
+ /** Override colors as an ordered array or per-name map */
1754
+ colors?: DashboardChartColors;
1755
+ /** Outer radius of the pie (default: "80%") */
1756
+ outerRadius?: number | string;
1757
+ /** Inner radius set > 0 to make a donut (default: 0) */
1758
+ innerRadius?: number | string;
1759
+ /** Show percentage labels inside/outside each slice (default: false) */
1760
+ showLabels?: boolean;
1761
+ /** Show the legend (default: true) */
1762
+ showLegend?: boolean;
1763
+ /**
1764
+ * Index of the slice to "explode" (offset outward).
1765
+ * Pass -1 or undefined to disable.
1766
+ */
1767
+ explodeIndex?: number;
1768
+ /** Offset distance for the exploded slice in px (default: 12) */
1769
+ explodeOffset?: number;
1770
+ /** Format tooltip values */
1771
+ valueFormatter?: ChartValueFormatter;
1772
+ className?: string;
1773
+ }
1774
+
1775
+ function PieMetricChart({
1776
+ data,
1777
+ nameKey,
1778
+ valueKey,
1779
+ colors,
1780
+ outerRadius = '80%',
1781
+ innerRadius = 0,
1782
+ showLabels = false,
1783
+ showLegend = true,
1784
+ explodeIndex,
1785
+ explodeOffset = 12,
1786
+ valueFormatter,
1787
+ isLoading,
1788
+ error,
1789
+ onRetry,
1790
+ retryLabel,
1791
+ emptyTitle,
1792
+ emptyDescription,
1793
+ errorTitle,
1794
+ errorDescription,
1795
+ loadingLabel,
1796
+ stateClassName,
1797
+ className,
1798
+ }: PieMetricChartProps) {
1799
+ // Build config from unique name values
1800
+ const names = React.useMemo(() => data.map(d => String(d[nameKey] ?? '')), [data, nameKey]);
1801
+
1802
+ const chartConfig = React.useMemo(() => buildChartConfig(names, colors), [names, colors]);
1803
+
1804
+ const hasData = hasPieData(data, nameKey, valueKey);
1805
+ const chartState = getChartState(
1806
+ {
1807
+ isLoading,
1808
+ error,
1809
+ emptyTitle,
1810
+ emptyDescription,
1811
+ errorTitle,
1812
+ errorDescription,
1813
+ loadingLabel,
1814
+ stateClassName,
1815
+ onRetry,
1816
+ retryLabel,
1817
+ },
1818
+ hasData
1819
+ );
1820
+
1821
+ return (
1822
+ <ChartContainer config={chartConfig} className={cn('h-[300px] w-full', className)}>
1823
+ {chartState ?? (
1824
+ <RechartsPrimitive.PieChart accessibilityLayer>
1825
+ <ChartTooltip
1826
+ content={
1827
+ <ChartTooltipContent
1828
+ nameKey={nameKey}
1829
+ formatter={valueFormatter ? (v: TooltipValueType) => valueFormatter(v as number) : undefined}
1830
+ />
1831
+ }
1832
+ />
1833
+ {showLegend && <ChartLegend content={<ChartLegendContent nameKey={nameKey} />} />}
1834
+ <RechartsPrimitive.Pie
1835
+ data={data}
1836
+ dataKey={valueKey}
1837
+ nameKey={nameKey}
1838
+ outerRadius={outerRadius}
1839
+ innerRadius={innerRadius}
1840
+ paddingAngle={2}
1841
+ label={
1842
+ showLabels
1843
+ ? ({ cx, cy, midAngle, innerRadius: ir, outerRadius: or, percent }) => {
1844
+ if (midAngle == null || percent == null) return null;
1845
+ const RADIAN = Math.PI / 180;
1846
+ const radius = Number(ir) + (Number(or) - Number(ir)) * 1.35;
1847
+ const x = Number(cx) + radius * Math.cos(-midAngle * RADIAN);
1848
+ const y = Number(cy) + radius * Math.sin(-midAngle * RADIAN);
1849
+ return percent > 0.04 ? (
1850
+ <text
1851
+ x={x}
1852
+ y={y}
1853
+ fill="var(--foreground)"
1854
+ textAnchor={x > Number(cx) ? 'start' : 'end'}
1855
+ dominantBaseline="central"
1856
+ fontSize={12}
1857
+ fontWeight={500}
1858
+ >
1859
+ {`${(percent * 100).toFixed(0)}%`}
1860
+ </text>
1861
+ ) : null;
1862
+ }
1863
+ : false
1864
+ }
1865
+ labelLine={false}
1866
+ isAnimationActive
1867
+ animationDuration={600}
1868
+ animationEasing="ease-out"
1869
+ >
1870
+ {data.map((entry, index) => {
1871
+ const name = String(entry[nameKey] ?? '');
1872
+ const isExploded = index === explodeIndex;
1873
+ return (
1874
+ <RechartsPrimitive.Cell
1875
+ key={`cell-${index}`}
1876
+ fill={chartConfig[name]?.color ?? `var(--chart-${(index % 8) + 1})`}
1877
+ stroke="var(--background)"
1878
+ strokeWidth={2}
1879
+ style={
1880
+ isExploded
1881
+ ? {
1882
+ filter: `drop-shadow(0 4px 8px color-mix(in srgb, ${chartConfig[name]?.color ?? 'var(--chart-1)'} 40%, transparent))`,
1883
+ }
1884
+ : undefined
1885
+ }
1886
+ />
1887
+ );
1888
+ })}
1889
+ </RechartsPrimitive.Pie>
1890
+ </RechartsPrimitive.PieChart>
1891
+ )}
1892
+ </ChartContainer>
1893
+ );
1894
+ }
1895
+
1896
+ // ─── RadialBarMetricChart ─────────────────────────────────────────────────────
1897
+
1898
+ export interface RadialBarMetricChartProps extends ChartStateProps {
1899
+ /**
1900
+ * Data array. Each item should have a `name` field (for labels) and
1901
+ * the `dataKey` field (for values, 0–100 for percentage-based display).
1902
+ */
1903
+ data: DashboardChartDatum[];
1904
+ /** Key in each datum used as the bar value (default: "value") */
1905
+ dataKey?: string;
1906
+ /** Key in each datum used as the bar label (default: "name") */
1907
+ nameKey?: string;
1908
+ /** Override colors as an ordered array or per-name map */
1909
+ colors?: DashboardChartColors;
1910
+ /** Inner radius of the radial bar (default: "30%") */
1911
+ innerRadius?: number | string;
1912
+ /** Outer radius of the radial bar (default: "100%") */
1913
+ outerRadius?: number | string;
1914
+ /** Start angle in degrees (default: 90 — top) */
1915
+ startAngle?: number;
1916
+ /** End angle in degrees (default: -270 — full circle) */
1917
+ endAngle?: number;
1918
+ /** Show background track behind each bar (default: true) */
1919
+ showBackground?: boolean;
1920
+ /** Show the legend (default: true) */
1921
+ showLegend?: boolean;
1922
+ /** Format tooltip values */
1923
+ valueFormatter?: ChartValueFormatter;
1924
+ className?: string;
1925
+ }
1926
+
1927
+ function RadialBarMetricChart({
1928
+ data,
1929
+ dataKey = 'value',
1930
+ nameKey = 'name',
1931
+ colors,
1932
+ innerRadius = '30%',
1933
+ outerRadius = '100%',
1934
+ startAngle = 90,
1935
+ endAngle = -270,
1936
+ showBackground = true,
1937
+ showLegend = true,
1938
+ valueFormatter,
1939
+ isLoading,
1940
+ error,
1941
+ onRetry,
1942
+ retryLabel,
1943
+ emptyTitle,
1944
+ emptyDescription,
1945
+ errorTitle,
1946
+ errorDescription,
1947
+ loadingLabel,
1948
+ stateClassName,
1949
+ className,
1950
+ }: RadialBarMetricChartProps) {
1951
+ const names = React.useMemo(() => data.map(d => String(d[nameKey] ?? '')), [data, nameKey]);
1952
+
1953
+ const chartConfig = React.useMemo(() => buildChartConfig(names, colors), [names, colors]);
1954
+
1955
+ const hasData = data.length > 0;
1956
+ const chartState = getChartState(
1957
+ {
1958
+ isLoading,
1959
+ error,
1960
+ emptyTitle,
1961
+ emptyDescription,
1962
+ errorTitle,
1963
+ errorDescription,
1964
+ loadingLabel,
1965
+ stateClassName,
1966
+ onRetry,
1967
+ retryLabel,
1968
+ },
1969
+ hasData
1970
+ );
1971
+
1972
+ // Inject per-item fill color
1973
+ const coloredData = React.useMemo(
1974
+ () =>
1975
+ data.map((item, index) => {
1976
+ const name = String(item[nameKey] ?? '');
1977
+ return {
1978
+ ...item,
1979
+ fill: chartConfig[name]?.color ?? `var(--chart-${(index % 8) + 1})`,
1980
+ };
1981
+ }),
1982
+ [data, nameKey, chartConfig]
1983
+ );
1984
+
1985
+ return (
1986
+ <div className={cn('flex w-full flex-col gap-3', className)}>
1987
+ <ChartContainer config={chartConfig} className="h-[260px] w-full">
1988
+ {chartState ?? (
1989
+ <RechartsPrimitive.RadialBarChart
1990
+ data={coloredData}
1991
+ innerRadius={innerRadius}
1992
+ outerRadius={outerRadius}
1993
+ startAngle={startAngle}
1994
+ endAngle={endAngle}
1995
+ accessibilityLayer
1996
+ >
1997
+ <ChartTooltip
1998
+ cursor={false}
1999
+ content={
2000
+ <ChartTooltipContent
2001
+ nameKey={nameKey}
2002
+ formatter={valueFormatter ? (v: TooltipValueType) => valueFormatter(v as number) : undefined}
2003
+ />
2004
+ }
2005
+ />
2006
+ <RechartsPrimitive.RadialBar
2007
+ dataKey={dataKey}
2008
+ background={showBackground ? { fill: 'var(--muted)' } : false}
2009
+ cornerRadius={6}
2010
+ isAnimationActive
2011
+ animationDuration={700}
2012
+ animationEasing="ease-out"
2013
+ />
2014
+ </RechartsPrimitive.RadialBarChart>
2015
+ )}
2016
+ </ChartContainer>
2017
+ {showLegend && !chartState && (
2018
+ <div className="flex flex-wrap justify-center gap-x-4 gap-y-1.5 px-2">
2019
+ {coloredData.map((item, index) => {
2020
+ const d = item as DashboardChartDatum & { fill: string };
2021
+ const name = String(d[nameKey] ?? '');
2022
+ const fillColor = d.fill ?? `var(--chart-${(index % 8) + 1})`;
2023
+ const val = d[dataKey];
2024
+ return (
2025
+ <div key={name} className="flex items-center gap-1.5">
2026
+ <span
2027
+ className="inline-block h-2.5 w-2.5 shrink-0 rounded-full"
2028
+ style={{ backgroundColor: fillColor }}
2029
+ />
2030
+ <span className="text-xs text-muted-foreground">
2031
+ {name}
2032
+ {val !== undefined && val !== null && (
2033
+ <span className="ml-1 font-medium text-foreground">
2034
+ {valueFormatter ? valueFormatter(val as number) : String(val)}
2035
+ </span>
2036
+ )}
2037
+ </span>
2038
+ </div>
2039
+ );
2040
+ })}
2041
+ </div>
2042
+ )}
2043
+ </div>
2044
+ );
2045
+ }
2046
+
2047
+ // ─── GaugeChart ───────────────────────────────────────────────────────────────
2048
+
2049
+ export interface GaugeChartThreshold {
2050
+ /** Upper bound of this zone (0–100) */
2051
+ value: number;
2052
+ /** Color for this zone */
2053
+ color: string;
2054
+ /** Optional label for this zone */
2055
+ label?: string;
2056
+ }
2057
+
2058
+ export interface GaugeChartProps {
2059
+ /** Current value (must be within [min, max]) */
2060
+ value: number;
2061
+ /** Minimum value (default: 0) */
2062
+ min?: number;
2063
+ /** Maximum value (default: 100) */
2064
+ max?: number;
2065
+ /**
2066
+ * Color zones. Each threshold defines the upper bound of a zone.
2067
+ * Zones are evaluated in order; the first zone whose `value >= current %`
2068
+ * determines the active color.
2069
+ * If omitted, uses `--chart-1`.
2070
+ */
2071
+ thresholds?: GaugeChartThreshold[];
2072
+ /** Label shown below the value (e.g. "CPU Usage") */
2073
+ label?: React.ReactNode;
2074
+ /** Format the center value text (default: shows percentage) */
2075
+ valueFormatter?: (value: number, percent: number) => string;
2076
+ /** Show the needle indicator (default: true) */
2077
+ showNeedle?: boolean;
2078
+ className?: string;
2079
+ }
2080
+
2081
+ function GaugeChart({
2082
+ value,
2083
+ min = 0,
2084
+ max = 100,
2085
+ thresholds,
2086
+ label,
2087
+ valueFormatter,
2088
+ showNeedle = true,
2089
+ className,
2090
+ }: GaugeChartProps) {
2091
+ const percent = Math.min(1, Math.max(0, (value - min) / (max - min)));
2092
+ const percentInt = Math.round(percent * 100);
2093
+
2094
+ // Determine active color from thresholds
2095
+ const activeColor = React.useMemo(() => {
2096
+ if (!thresholds || thresholds.length === 0) return 'var(--chart-1)';
2097
+ for (const t of thresholds) {
2098
+ if (percentInt <= t.value) return t.color;
2099
+ }
2100
+ return thresholds[thresholds.length - 1].color;
2101
+ }, [thresholds, percentInt]);
2102
+
2103
+ const displayText = valueFormatter ? valueFormatter(value, percentInt) : `${percentInt}%`;
2104
+
2105
+ // SVG gauge constants — viewBox is 200×110, arc center at (100, 100)
2106
+ const cx = 100;
2107
+ const cy = 100;
2108
+ const R = 80; // outer radius
2109
+ const r = 54; // inner radius (track width = 26)
2110
+
2111
+ // Helper: polar → cartesian (angle in degrees, 0° = right, CCW)
2112
+ const polar = (angleDeg: number, radius: number) => {
2113
+ const rad = (angleDeg * Math.PI) / 180;
2114
+ return {
2115
+ x: cx + radius * Math.cos(rad),
2116
+ y: cy - radius * Math.sin(rad),
2117
+ };
2118
+ };
2119
+
2120
+ // Semicircle: from 180° (left) to 0° (right)
2121
+ // Track background arc path
2122
+ const trackStart = polar(180, R);
2123
+ const trackEnd = polar(0, R);
2124
+ const trackStartI = polar(180, r);
2125
+ const trackEndI = polar(0, r);
2126
+ const trackPath = [
2127
+ `M ${trackStart.x} ${trackStart.y}`,
2128
+ `A ${R} ${R} 0 0 1 ${trackEnd.x} ${trackEnd.y}`,
2129
+ `L ${trackEndI.x} ${trackEndI.y}`,
2130
+ `A ${r} ${r} 0 0 0 ${trackStartI.x} ${trackStartI.y}`,
2131
+ 'Z',
2132
+ ].join(' ');
2133
+
2134
+ // Value arc: from 180° (left) clockwise to valueAngle
2135
+ // The gauge is a semicircle (max 180°), so largeArc is always 0.
2136
+ // largeArc=1 would sweep the reflex arc (>180°) which creates the filled-blob bug.
2137
+ const valueAngle = 180 - percent * 180;
2138
+ const valueEnd = polar(valueAngle, R);
2139
+ const valueEndI = polar(valueAngle, r);
2140
+ const valuePath =
2141
+ percent <= 0
2142
+ ? ''
2143
+ : percent >= 1
2144
+ ? trackPath // full arc = same as track
2145
+ : [
2146
+ `M ${trackStart.x} ${trackStart.y}`,
2147
+ `A ${R} ${R} 0 0 1 ${valueEnd.x} ${valueEnd.y}`,
2148
+ `L ${valueEndI.x} ${valueEndI.y}`,
2149
+ `A ${r} ${r} 0 0 0 ${trackStartI.x} ${trackStartI.y}`,
2150
+ 'Z',
2151
+ ].join(' ');
2152
+
2153
+ // Needle: rotates from -90° (left, 0%) to +90° (right, 100%)
2154
+ // In our coordinate system: 180° at 0%, 0° at 100%
2155
+ const needleAngle = 180 - percent * 180;
2156
+ const needleTip = polar(needleAngle, r - 6);
2157
+ const needleBase1 = polar(needleAngle + 90, 5);
2158
+ const needleBase2 = polar(needleAngle - 90, 5);
2159
+
2160
+ return (
2161
+ <div className={cn('flex flex-col items-center gap-2', className)}>
2162
+ <div className="relative w-full max-w-[260px]">
2163
+ <svg viewBox="0 0 200 130" className="w-full" aria-label={`Gauge: ${displayText}`}>
2164
+ {/* Background track */}
2165
+ <path d={trackPath} fill="var(--muted)" />
2166
+
2167
+ {/* Value arc */}
2168
+ {valuePath && <path d={valuePath} fill={activeColor} />}
2169
+
2170
+ {/* Needle */}
2171
+ {showNeedle && (
2172
+ <g>
2173
+ <polygon
2174
+ points={`${needleTip.x},${needleTip.y} ${needleBase1.x},${needleBase1.y} ${needleBase2.x},${needleBase2.y}`}
2175
+ fill="var(--foreground)"
2176
+ opacity={0.85}
2177
+ />
2178
+ <circle cx={cx} cy={cy} r={5} fill="var(--foreground)" />
2179
+ </g>
2180
+ )}
2181
+
2182
+ {/* Value text — placed below the arc baseline (cy=100) to avoid needle overlap */}
2183
+ <text
2184
+ x={cx}
2185
+ y={cy + 18}
2186
+ textAnchor="middle"
2187
+ dominantBaseline="middle"
2188
+ fontSize={22}
2189
+ fontWeight={700}
2190
+ fill="currentColor"
2191
+ className="fill-foreground"
2192
+ >
2193
+ {displayText}
2194
+ </text>
2195
+
2196
+ {/* Label below value */}
2197
+ {label && (
2198
+ <text
2199
+ x={cx}
2200
+ y={cy + 36}
2201
+ textAnchor="middle"
2202
+ dominantBaseline="middle"
2203
+ fontSize={10}
2204
+ fill="currentColor"
2205
+ className="fill-muted-foreground"
2206
+ >
2207
+ {label}
2208
+ </text>
2209
+ )}
2210
+ </svg>
2211
+ </div>
2212
+
2213
+ {/* Threshold legend */}
2214
+ {thresholds && thresholds.length > 0 && (
2215
+ <div className="flex flex-wrap justify-center gap-x-3 gap-y-1">
2216
+ {thresholds.map((t, i) => (
2217
+ <div key={i} className="flex items-center gap-1.5">
2218
+ <span
2219
+ className="inline-block h-2 w-2 shrink-0 rounded-full"
2220
+ style={{ backgroundColor: t.color }}
2221
+ />
2222
+ <span className="text-xs text-muted-foreground">{t.label}</span>
2223
+ </div>
2224
+ ))}
2225
+ </div>
2226
+ )}
2227
+ </div>
2228
+ );
2229
+ }
2230
+
2231
+ // ─── Exports ──────────────────────────────────────────────────────────────────
2232
+
2233
+ export {
2234
+ ChartContainer,
2235
+ ChartTooltip,
2236
+ ChartTooltipContent,
2237
+ ChartLegend,
2238
+ ChartLegendContent,
2239
+ ChartStyle,
2240
+ ChartCard,
2241
+ DashboardBarChart,
2242
+ DashboardLineChart,
2243
+ HorizontalBarChart,
2244
+ InteractiveTimeSeriesChart,
2245
+ ComboMetricChart,
2246
+ DonutBreakdownChart,
2247
+ SparklineChart,
2248
+ RadarMetricChart,
2249
+ PieMetricChart,
2250
+ RadialBarMetricChart,
2251
+ GaugeChart,
2252
+ };