xertica-ui 2.1.2 → 2.1.3

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 (402) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +1 -1
  3. package/assets/xertica-logo.svg +37 -37
  4. package/assets/xertica-x-logo.svg +20 -20
  5. package/bin/cli.ts +1 -1
  6. package/bin/generate-tokens.ts +13 -7
  7. package/components/assistant/index.ts +6 -6
  8. package/components/assistant/xertica-assistant/index.ts +2 -0
  9. package/components/assistant/xertica-assistant/use-assistant.ts +564 -0
  10. package/components/assistant/xertica-assistant/xertica-assistant.stories.tsx +200 -0
  11. package/components/assistant/xertica-assistant/xertica-assistant.tsx +183 -474
  12. package/components/blocks/card-patterns/ActivityCard.tsx +72 -72
  13. package/components/blocks/card-patterns/FeatureCard.tsx +100 -100
  14. package/components/blocks/card-patterns/NotificationCard.tsx +127 -127
  15. package/components/blocks/card-patterns/ProfileCard.tsx +84 -84
  16. package/components/blocks/card-patterns/ProjectCard.tsx +89 -89
  17. package/components/blocks/card-patterns/QuickActionCard.tsx +62 -62
  18. package/components/blocks/card-patterns/card-patterns.stories.tsx +445 -445
  19. package/components/blocks/card-patterns/index.ts +17 -17
  20. package/components/blocks/index.ts +1 -1
  21. package/components/brand/index.ts +6 -6
  22. package/components/brand/xertica-provider/XerticaProvider.tsx +48 -48
  23. package/components/brand/xertica-provider/xertica-provider.mdx +61 -61
  24. package/components/brand/xertica-provider/xertica-provider.test.tsx +52 -52
  25. package/components/examples/MapExamples.tsx +282 -282
  26. package/components/examples/SimpleFilterableMap.tsx +191 -191
  27. package/components/examples/index.ts +51 -51
  28. package/components/figma/ImageWithFallback.tsx +27 -27
  29. package/components/hooks/index.ts +7 -7
  30. package/components/index.ts +5 -5
  31. package/components/layout/header/header.test.tsx +8 -8
  32. package/components/layout/header/header.tsx +4 -4
  33. package/components/layout/index.ts +2 -2
  34. package/components/layout/sidebar/index.ts +2 -0
  35. package/components/layout/sidebar/sidebar.mdx +1 -1
  36. package/components/layout/sidebar/sidebar.stories.tsx +160 -8
  37. package/components/layout/sidebar/sidebar.test.tsx +11 -11
  38. package/components/layout/sidebar/sidebar.tsx +610 -471
  39. package/components/layout/sidebar/use-sidebar.ts +113 -0
  40. package/components/media/FloatingMediaWrapper.tsx +11 -11
  41. package/components/media/audio-player/AudioPlayer.tsx +22 -22
  42. package/components/media/index.ts +3 -3
  43. package/components/pages/home-content/HomeContent.tsx +7 -7
  44. package/components/pages/home-content/home-content.mdx +62 -62
  45. package/components/pages/home-page/HomePage.stories.tsx +39 -39
  46. package/components/pages/home-page/HomePage.tsx +1 -1
  47. package/components/pages/home-page/home-page.mdx +53 -53
  48. package/components/pages/index.ts +8 -8
  49. package/components/pages/template-content/TemplateContent.tsx +5 -5
  50. package/components/pages/template-content/template-content.mdx +61 -61
  51. package/components/pages/template-page/TemplatePage.stories.tsx +39 -39
  52. package/components/pages/template-page/TemplatePage.tsx +5 -5
  53. package/components/pages/template-page/template-page.mdx +53 -53
  54. package/components/public-api-smoke.test.tsx +52 -52
  55. package/components/shared/assistant-utils.ts +43 -43
  56. package/components/shared/layout-constants.ts +1 -1
  57. package/components/ui/accordion/accordion.mdx +8 -8
  58. package/components/ui/accordion/accordion.stories.tsx +53 -53
  59. package/components/ui/alert/alert.mdx +8 -8
  60. package/components/ui/alert-dialog/alert-dialog.mdx +8 -8
  61. package/components/ui/aspect-ratio/aspect-ratio.mdx +8 -8
  62. package/components/ui/assistant-chart/assistant-chart.mdx +8 -8
  63. package/components/ui/avatar/avatar.mdx +8 -8
  64. package/components/ui/badge/badge.mdx +8 -8
  65. package/components/ui/badge/badge.tsx +9 -9
  66. package/components/ui/breadcrumb/breadcrumb.mdx +8 -8
  67. package/components/ui/button/button.mdx +8 -8
  68. package/components/ui/calendar/calendar.mdx +8 -8
  69. package/components/ui/calendar/calendar.tsx +258 -258
  70. package/components/ui/card/card.mdx +8 -8
  71. package/components/ui/card/card.stories.tsx +245 -245
  72. package/components/ui/carousel/carousel.mdx +8 -8
  73. package/components/ui/chart/chart.mdx +8 -8
  74. package/components/ui/chart/chart.stories.tsx +1303 -344
  75. package/components/ui/chart/chart.test.tsx +154 -154
  76. package/components/ui/chart/chart.tsx +2367 -1494
  77. package/components/ui/checkbox/checkbox.mdx +8 -8
  78. package/components/ui/checkbox/checkbox.stories.tsx +20 -20
  79. package/components/ui/collapsible/collapsible.mdx +8 -8
  80. package/components/ui/command/command.mdx +8 -8
  81. package/components/ui/context-menu/context-menu.mdx +8 -8
  82. package/components/ui/dialog/dialog.mdx +8 -8
  83. package/components/ui/drawer/drawer.mdx +8 -8
  84. package/components/ui/dropdown-menu/dropdown-menu.mdx +8 -8
  85. package/components/ui/empty/empty.mdx +8 -8
  86. package/components/ui/file-upload/file-upload.mdx +8 -8
  87. package/components/ui/file-upload/file-upload.stories.tsx +100 -0
  88. package/components/ui/file-upload/file-upload.tsx +14 -74
  89. package/components/ui/file-upload/index.ts +1 -0
  90. package/components/ui/file-upload/use-file-upload.ts +181 -0
  91. package/components/ui/google-maps-loader/google-maps-loader.tsx +2 -2
  92. package/components/ui/hover-card/hover-card.mdx +8 -8
  93. package/components/ui/input/input.mdx +8 -8
  94. package/components/ui/input-otp/input-otp.mdx +8 -8
  95. package/components/ui/input-otp/input-otp.stories.tsx +6 -6
  96. package/components/ui/label/label.mdx +8 -8
  97. package/components/ui/map/map.mdx +8 -8
  98. package/components/ui/map/map.stories.tsx +51 -51
  99. package/components/ui/map/map.tsx +2 -2
  100. package/components/ui/menubar/menubar.mdx +8 -8
  101. package/components/ui/navigation-menu/navigation-menu.mdx +8 -8
  102. package/components/ui/notification-badge/notification-badge.mdx +8 -8
  103. package/components/ui/pagination/index.ts +2 -0
  104. package/components/ui/pagination/pagination.mdx +8 -8
  105. package/components/ui/pagination/pagination.stories.tsx +94 -0
  106. package/components/ui/pagination/use-pagination.ts +194 -0
  107. package/components/ui/popover/popover.mdx +8 -8
  108. package/components/ui/progress/progress.mdx +8 -8
  109. package/components/ui/progress/progress.tsx +68 -68
  110. package/components/ui/radio-group/radio-group.mdx +8 -8
  111. package/components/ui/rating/rating.mdx +8 -8
  112. package/components/ui/resizable/resizable.mdx +8 -8
  113. package/components/ui/rich-text-editor/index.ts +2 -0
  114. package/components/ui/rich-text-editor/rich-text-editor.stories.tsx +129 -1
  115. package/components/ui/rich-text-editor/rich-text-editor.tsx +86 -305
  116. package/components/ui/rich-text-editor/use-rich-text-editor.ts +439 -0
  117. package/components/ui/route-map/route-map.mdx +8 -8
  118. package/components/ui/route-map/route-map.stories.tsx +54 -54
  119. package/components/ui/route-map/route-map.tsx +2 -2
  120. package/components/ui/scroll-area/scroll-area.mdx +8 -8
  121. package/components/ui/search/search.mdx +8 -8
  122. package/components/ui/select/select.mdx +8 -8
  123. package/components/ui/select/select.stories.tsx +9 -9
  124. package/components/ui/separator/separator.mdx +8 -8
  125. package/components/ui/sheet/sheet.mdx +8 -8
  126. package/components/ui/sheet/sheet.stories.tsx +95 -95
  127. package/components/ui/simple-map/simple-map.mdx +8 -8
  128. package/components/ui/simple-map/simple-map.stories.tsx +48 -48
  129. package/components/ui/skeleton/skeleton.mdx +8 -8
  130. package/components/ui/slider/slider.mdx +8 -8
  131. package/components/ui/sonner/sonner.mdx +8 -8
  132. package/components/ui/stats-card/stats-card.mdx +8 -8
  133. package/components/ui/stepper/index.ts +3 -1
  134. package/components/ui/stepper/stepper.mdx +8 -8
  135. package/components/ui/stepper/stepper.stories.tsx +116 -0
  136. package/components/ui/stepper/stepper.tsx +4 -4
  137. package/components/ui/stepper/use-stepper.ts +137 -0
  138. package/components/ui/switch/switch.mdx +8 -8
  139. package/components/ui/switch/switch.stories.tsx +20 -20
  140. package/components/ui/table/table.mdx +8 -8
  141. package/components/ui/tabs/tabs.mdx +8 -8
  142. package/components/ui/tabs/tabs.stories.tsx +26 -26
  143. package/components/ui/textarea/textarea.mdx +8 -8
  144. package/components/ui/timeline/timeline.mdx +8 -8
  145. package/components/ui/toggle/toggle.mdx +8 -8
  146. package/components/ui/toggle-group/toggle-group.mdx +8 -8
  147. package/components/ui/tooltip/tooltip.mdx +8 -8
  148. package/components/ui/tree-view/index.ts +4 -1
  149. package/components/ui/tree-view/tree-view.mdx +8 -8
  150. package/components/ui/tree-view/tree-view.stories.tsx +110 -4
  151. package/components/ui/tree-view/tree-view.tsx +17 -125
  152. package/components/ui/tree-view/use-tree-view.ts +229 -0
  153. package/components.json +1512 -1512
  154. package/contexts/ApiKeyContext.tsx +72 -72
  155. package/contexts/BrandColorsContext.tsx +26 -26
  156. package/contexts/LanguageContext.tsx +13 -13
  157. package/contexts/LayoutContext.test.tsx +11 -11
  158. package/contexts/LayoutContext.tsx +29 -29
  159. package/contexts/ThemeContext.tsx +26 -26
  160. package/contexts/theme-data.ts +4 -4
  161. package/dist/{AssistantChart-CVzmmhx4.js → AssistantChart-C_hwFRRr.js} +4 -4
  162. package/dist/{AssistantChart-BAx9VQvb.cjs → AssistantChart-CldVCVDe.cjs} +311 -94
  163. package/dist/{AssistantChart-CVko2A1W.js → AssistantChart-Cu3m7RBo.js} +314 -97
  164. package/dist/{AssistantChart-9w31gdAb.cjs → AssistantChart-DoZCyS5r.cjs} +4 -4
  165. package/dist/AudioPlayer-B1lt5cPl.cjs +989 -0
  166. package/dist/AudioPlayer-BZ7bibzU.cjs +982 -0
  167. package/dist/AudioPlayer-C12BjQBV.cjs +997 -0
  168. package/dist/AudioPlayer-DMcG_c7L.js +990 -0
  169. package/dist/AudioPlayer-DcFKRJE_.js +998 -0
  170. package/dist/AudioPlayer-e8LfNoqO.js +983 -0
  171. package/dist/{xertica-assistant-Qp3ydksa.cjs → CodeBlock-7TTgmdGG.cjs} +263 -51
  172. package/dist/{xertica-assistant-gnCJdcZY.js → CodeBlock-BeSt1h5P.js} +219 -7
  173. package/dist/CodeBlock-BgfYL_rD.cjs +2094 -0
  174. package/dist/CodeBlock-BlcqlA9M.cjs +2094 -0
  175. package/dist/CodeBlock-Bnmeu5ez.cjs +2094 -0
  176. package/dist/CodeBlock-BtfPlbAI.js +2078 -0
  177. package/dist/CodeBlock-CIySIuYr.js +2078 -0
  178. package/dist/CodeBlock-CuPtUM-7.cjs +2094 -0
  179. package/dist/CodeBlock-D6ffWXgc.js +2078 -0
  180. package/dist/CodeBlock-D8dcwbit.cjs +2094 -0
  181. package/dist/CodeBlock-DMZrFnlw.cjs +2094 -0
  182. package/dist/CodeBlock-DlBehYN8.js +2078 -0
  183. package/dist/CodeBlock-DnYNI8rQ.js +2078 -0
  184. package/dist/CodeBlock-DvKWbSnE.cjs +2094 -0
  185. package/dist/CodeBlock-DwMCfkFY.js +2078 -0
  186. package/dist/CodeBlock-Dy6CNYyj.js +2078 -0
  187. package/dist/CodeBlock-U1pPOQI7.cjs +2094 -0
  188. package/dist/CodeBlock-f_GpNhEB.js +2078 -0
  189. package/dist/CodeBlock-oB6u8nI1.js +2078 -0
  190. package/dist/CodeBlock-tZC31B73.cjs +2094 -0
  191. package/dist/ImageWithFallback-CGtidP6B.cjs +4542 -0
  192. package/dist/ImageWithFallback-lsg3pdFg.js +4508 -0
  193. package/dist/LanguageSelector-B5YfbHra.js +231 -0
  194. package/dist/LanguageSelector-D6uacAIM.cjs +230 -0
  195. package/dist/LayoutContext-BAql6ZRY.js +97 -0
  196. package/dist/LayoutContext-BvK-ggDa.cjs +96 -0
  197. package/dist/ThemeContext-BoH4NLfN.js +734 -0
  198. package/dist/{ThemeContext-BbBNoFTG.js → ThemeContext-C2EwAPDt.js} +2 -2
  199. package/dist/ThemeContext-r69W20Xg.cjs +733 -0
  200. package/dist/{ThemeContext-Cmr8Ex8H.cjs → ThemeContext-vTjumZeM.cjs} +2 -2
  201. package/dist/{VerifyEmailPage-BE-L9mB7.js → VerifyEmailPage-C0c2e5n0.js} +7 -7
  202. package/dist/{VerifyEmailPage-DMBh4NM9.cjs → VerifyEmailPage-CYXtbKi3.cjs} +1 -1
  203. package/dist/{VerifyEmailPage-CR7kb5df.cjs → VerifyEmailPage-Cwi3kbol.cjs} +7 -7
  204. package/dist/{VerifyEmailPage-CdYPSJoO.js → VerifyEmailPage-DvMLZgFt.js} +1 -1
  205. package/dist/XerticaOrbe-KL1RBHzw.cjs +1354 -0
  206. package/dist/XerticaOrbe-zwS1p2a8.js +1355 -0
  207. package/dist/XerticaProvider-6btlAlzc.js +17 -0
  208. package/dist/XerticaProvider-BNoNOxQ5.cjs +16 -0
  209. package/dist/XerticaProvider-BlY2limY.cjs +38 -0
  210. package/dist/{XerticaProvider-BITjgC5p.js → XerticaProvider-CEoWMTxu.js} +2 -2
  211. package/dist/{XerticaProvider-By8q3Roe.cjs → XerticaProvider-CllrbMEJ.cjs} +2 -2
  212. package/dist/XerticaProvider-DDuiIcKo.js +39 -0
  213. package/dist/XerticaProvider-cI9hSs27.cjs +38 -0
  214. package/dist/XerticaProvider-hSwhNQex.js +39 -0
  215. package/dist/{alert-dialog-yckpaOpy.cjs → alert-dialog-DSKByiKZ.cjs} +3 -3
  216. package/dist/{alert-dialog-iDe5VE5o.js → alert-dialog-s-vmNkJ_.js} +3 -3
  217. package/dist/assistant.cjs.js +2 -1
  218. package/dist/assistant.es.js +3 -2
  219. package/dist/brand.cjs.js +1 -1
  220. package/dist/brand.es.js +1 -1
  221. package/dist/breadcrumb-CqJ7bHY5.js +161 -0
  222. package/dist/breadcrumb-m9Hb2_XN.cjs +177 -0
  223. package/dist/cli.js +14 -8
  224. package/dist/components/assistant/xertica-assistant/index.d.ts +2 -0
  225. package/dist/components/assistant/xertica-assistant/use-assistant.d.ts +126 -0
  226. package/dist/components/assistant/xertica-assistant/xertica-assistant.d.ts +7 -2
  227. package/dist/components/blocks/audio-player/AudioPlayer.d.ts +35 -0
  228. package/dist/components/blocks/audio-player/index.d.ts +1 -0
  229. package/dist/components/blocks/document-editor/DocumentEditor.d.ts +26 -0
  230. package/dist/components/blocks/document-editor/index.d.ts +1 -0
  231. package/dist/components/blocks/podcast-player/PodcastPlayer.d.ts +41 -0
  232. package/dist/components/blocks/podcast-player/index.d.ts +1 -0
  233. package/dist/components/layout/sidebar/index.d.ts +2 -0
  234. package/dist/components/layout/sidebar/sidebar.d.ts +80 -0
  235. package/dist/components/layout/sidebar/use-sidebar.d.ts +22 -0
  236. package/dist/components/shared/layout-constants.d.ts +1 -1
  237. package/dist/components/ui/button/button.d.ts +1 -1
  238. package/dist/components/ui/chart/chart.d.ts +162 -5
  239. package/dist/components/ui/file-upload/file-upload.d.ts +2 -0
  240. package/dist/components/ui/file-upload/index.d.ts +1 -0
  241. package/dist/components/ui/file-upload/use-file-upload.d.ts +49 -0
  242. package/dist/components/ui/pagination/index.d.ts +2 -0
  243. package/dist/components/ui/pagination/use-pagination.d.ts +78 -0
  244. package/dist/components/ui/rich-text-editor/index.d.ts +2 -0
  245. package/dist/components/ui/rich-text-editor/use-rich-text-editor.d.ts +107 -0
  246. package/dist/components/ui/stepper/index.d.ts +3 -1
  247. package/dist/components/ui/stepper/stepper.d.ts +2 -2
  248. package/dist/components/ui/stepper/use-stepper.d.ts +60 -0
  249. package/dist/components/ui/tree-view/index.d.ts +4 -1
  250. package/dist/components/ui/tree-view/tree-view.d.ts +4 -6
  251. package/dist/components/ui/tree-view/use-tree-view.d.ts +60 -0
  252. package/dist/dropdown-menu-BDB5CmQs.cjs +247 -0
  253. package/dist/dropdown-menu-DQidbKBD.js +231 -0
  254. package/dist/google-maps-loader-BFWp6VPd.js +287 -0
  255. package/dist/{google-maps-loader-t2IlYBzw.js → google-maps-loader-CTYySAun.js} +4 -0
  256. package/dist/{google-maps-loader-BqsYL48U.cjs → google-maps-loader-Y-QkD-Li.cjs} +5 -0
  257. package/dist/google-maps-loader-eS3uQ5TA.cjs +287 -0
  258. package/dist/header-Cgy6vYPk.cjs +731 -0
  259. package/dist/header-DRlT4jgI.js +715 -0
  260. package/dist/header-Dux00SI4.cjs +731 -0
  261. package/dist/header-EkGKXPsD.js +715 -0
  262. package/dist/header-WfEywpyc.cjs +731 -0
  263. package/dist/header-tifNQn2U.js +715 -0
  264. package/dist/hooks.cjs.js +1 -1
  265. package/dist/hooks.es.js +1 -1
  266. package/dist/index-BhapVLVj.js +8 -0
  267. package/dist/{index-D3RLKRAs.cjs → index-COtD8bRW.cjs} +1 -1
  268. package/dist/index-D6fxYEY8.cjs +7 -0
  269. package/dist/index-DW5tYe26.js +8 -0
  270. package/dist/index.cjs.js +19 -8
  271. package/dist/index.es.js +39 -28
  272. package/dist/index.umd.js +1043 -470
  273. package/dist/input-2R4loU86.js +127 -0
  274. package/dist/input-DWANSKGb.cjs +145 -0
  275. package/dist/layout.cjs.js +89 -1
  276. package/dist/layout.es.js +90 -2
  277. package/dist/pages.cjs.js +1 -1
  278. package/dist/pages.es.js +1 -1
  279. package/dist/rich-text-editor-0mraWT5y.cjs +2376 -0
  280. package/dist/rich-text-editor-B6jMRLzk.cjs +1939 -0
  281. package/dist/rich-text-editor-B8_oYcIR.js +1730 -0
  282. package/dist/rich-text-editor-B9UbSXNb.js +1203 -0
  283. package/dist/rich-text-editor-BYuRBNBU.js +2373 -0
  284. package/dist/rich-text-editor-Bb9pySTs.cjs +2374 -0
  285. package/dist/rich-text-editor-BcL6L3cm.cjs +2374 -0
  286. package/dist/rich-text-editor-BoVZYtTs.cjs +2391 -0
  287. package/dist/rich-text-editor-CPV1lEPH.cjs +1748 -0
  288. package/dist/rich-text-editor-CoKqbCtu.cjs +1799 -0
  289. package/dist/rich-text-editor-Cw56T_mB.js +2356 -0
  290. package/dist/rich-text-editor-Cyt8qs2b.js +1921 -0
  291. package/dist/rich-text-editor-D6H84OcX.cjs +1220 -0
  292. package/dist/rich-text-editor-D76gD-QI.js +2328 -0
  293. package/dist/rich-text-editor-DKkokOnA.js +1781 -0
  294. package/dist/rich-text-editor-DNsdpN64.cjs +2359 -0
  295. package/dist/rich-text-editor-DfG8bCyY.js +2358 -0
  296. package/dist/rich-text-editor-DgF8s7xW.js +2949 -0
  297. package/dist/rich-text-editor-Dxjw31Z4.js +2341 -0
  298. package/dist/rich-text-editor-DzP0Epmb.js +2356 -0
  299. package/dist/rich-text-editor-mWoaSCE4.cjs +2966 -0
  300. package/dist/rich-text-editor-skplNlBM.cjs +2345 -0
  301. package/dist/select-Bkbr0f-Z.cjs +162 -0
  302. package/dist/select-CvIVdX2n.js +145 -0
  303. package/dist/sidebar-CK_0ZQHj.cjs +803 -0
  304. package/dist/sidebar-CUuOvYhK.js +787 -0
  305. package/dist/slider-Bc5Hd0y1.js +56 -0
  306. package/dist/slider-N7hFFj6X.cjs +73 -0
  307. package/dist/tooltip-Ded96neP.cjs +137 -0
  308. package/dist/tooltip-HDOoD2-0.js +120 -0
  309. package/dist/ui.cjs.js +14 -4
  310. package/dist/ui.es.js +26 -16
  311. package/dist/use-mobile-B0hNy_Y6.cjs +4303 -0
  312. package/dist/use-mobile-BXuYROXM.js +4202 -0
  313. package/dist/use-mobile-Bbd51ASU.cjs +4392 -0
  314. package/dist/use-mobile-Bk6CX-TC.js +4359 -0
  315. package/dist/use-mobile-BvYdisLP.js +4202 -0
  316. package/dist/use-mobile-BzuxjzNX.cjs +4392 -0
  317. package/dist/use-mobile-CG2-SdXV.cjs +4235 -0
  318. package/dist/use-mobile-CKb5pqTs.js +4269 -0
  319. package/dist/use-mobile-CYuAuGDl.js +4202 -0
  320. package/dist/use-mobile-CaENcqm-.js +4508 -0
  321. package/dist/use-mobile-CbrYgJGJ.js +4203 -0
  322. package/dist/use-mobile-DMOvImGQ.cjs +4542 -0
  323. package/dist/use-mobile-DRB3BQgD.cjs +4235 -0
  324. package/dist/use-mobile-DZvv7QMR.js +4359 -0
  325. package/dist/use-mobile-DdI_TXam.cjs +4235 -0
  326. package/dist/use-mobile-DlceKf8a.js +4359 -0
  327. package/dist/use-mobile-DsOnow1o.cjs +4236 -0
  328. package/dist/use-mobile-Kcj6jSnK.cjs +4392 -0
  329. package/dist/use-mobile-bnKcua_i.js +4202 -0
  330. package/dist/use-mobile-ncXBeE2z.cjs +4235 -0
  331. package/dist/{xertica-assistant-B1IaHXnB.cjs → xertica-assistant-dyP7KHM5.cjs} +533 -392
  332. package/dist/{xertica-assistant-DPsESB6t.js → xertica-assistant-yX1CFBBo.js} +535 -394
  333. package/dist/xertica-ui.css +2 -2
  334. package/docs/ai-usage.md +28 -10
  335. package/docs/architecture.md +76 -0
  336. package/docs/components/assistant.md +159 -0
  337. package/docs/components/calendar.md +154 -154
  338. package/docs/components/card-patterns.md +337 -337
  339. package/docs/components/card.md +235 -235
  340. package/docs/components/chart.md +354 -39
  341. package/docs/components/file-upload.md +119 -2
  342. package/docs/components/map.md +84 -84
  343. package/docs/components/pagination.md +187 -0
  344. package/docs/components/rich-text-editor.md +164 -0
  345. package/docs/components/sidebar.md +153 -4
  346. package/docs/components/stepper.md +157 -12
  347. package/docs/components/tree-view.md +164 -6
  348. package/docs/components/xertica-provider.md +24 -24
  349. package/docs/decision-tree.md +287 -287
  350. package/docs/getting-started.md +1 -1
  351. package/docs/guidelines.md +14 -8
  352. package/docs/layout.md +2 -2
  353. package/docs/llms.md +4 -4
  354. package/guidelines/Guidelines.md +252 -250
  355. package/hooks/useTheme.ts +3 -3
  356. package/imports/Podcast.tsx +388 -388
  357. package/imports/XerticaAi.tsx +45 -45
  358. package/imports/XerticaX.tsx +19 -19
  359. package/imports/svg-aueiaqngck.ts +11 -11
  360. package/imports/svg-v9krss1ozd.ts +16 -16
  361. package/imports/svg-vhrdofe3qe.ts +5 -5
  362. package/llms-compact.txt +327 -327
  363. package/llms.txt +160 -160
  364. package/package.json +203 -203
  365. package/styles/xertica/app-overrides/chat.css +61 -61
  366. package/styles/xertica/app-overrides/scrollbar.css +33 -33
  367. package/styles/xertica/integrations/google-maps.css +76 -76
  368. package/styles/xertica/integrations/sonner.css +73 -73
  369. package/styles/xertica/tokens.css +41 -12
  370. package/templates/CLAUDE.md +182 -172
  371. package/templates/guidelines/Guidelines.md +325 -313
  372. package/templates/package.json +3 -3
  373. package/templates/src/features/auth/index.ts +4 -4
  374. package/templates/src/features/auth/ui/AuthPageShell.tsx +34 -34
  375. package/templates/src/features/auth/ui/ForgotPasswordContent.tsx +70 -70
  376. package/templates/src/features/auth/ui/LoginContent.tsx +90 -90
  377. package/templates/src/features/auth/ui/ResetPasswordContent.tsx +151 -151
  378. package/templates/src/features/auth/ui/SocialLoginButtons.tsx +59 -59
  379. package/templates/src/features/auth/ui/VerifyEmailContent.tsx +82 -82
  380. package/templates/src/features/home/index.ts +1 -1
  381. package/templates/src/features/home/ui/HomeContent.tsx +100 -100
  382. package/templates/src/features/template/index.ts +5 -5
  383. package/templates/src/features/template/ui/CrudTemplate.tsx +3 -3
  384. package/templates/src/features/template/ui/DashboardTemplate.tsx +3 -3
  385. package/templates/src/features/template/ui/FormTemplate.tsx +120 -120
  386. package/templates/src/features/template/ui/LoginTemplate.tsx +3 -3
  387. package/templates/src/pages/AssistantPage.tsx +328 -328
  388. package/templates/src/pages/ForgotPasswordPage.tsx +6 -6
  389. package/templates/src/pages/HomePage.tsx +57 -57
  390. package/templates/src/pages/LoginPage.tsx +10 -10
  391. package/templates/src/pages/ResetPasswordPage.tsx +6 -6
  392. package/templates/src/pages/TemplatePage.tsx +30 -30
  393. package/templates/src/pages/VerifyEmailPage.tsx +6 -6
  394. package/templates/src/shared/config/navigation.ts +20 -20
  395. package/templates/src/shared/lib/auth.ts +20 -20
  396. package/templates/src/shared/types/auth.ts +3 -3
  397. package/templates/src/styles/xertica/tokens.css +39 -10
  398. package/templates/tsconfig.json +5 -5
  399. package/utils/gemini.ts +140 -140
  400. package/dist/VerifyEmailPage-Bae2cBXT.cjs +0 -2827
  401. package/dist/VerifyEmailPage-CbgjOF0v.js +0 -2828
  402. package/dist/index-CkTUgOwX.js +0 -8
@@ -1,1494 +1,2367 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import * as RechartsPrimitive from "recharts";
5
-
6
- import { cn } from "../../shared/utils";
7
- import { Alert, AlertDescription, AlertTitle } from "../alert";
8
- import { Button } from "../button";
9
- import {
10
- Card,
11
- CardAction,
12
- CardContent,
13
- CardDescription,
14
- CardFooter,
15
- CardHeader,
16
- CardTitle,
17
- } from "../card";
18
- import {
19
- Empty,
20
- EmptyAction,
21
- EmptyDescription,
22
- EmptyIcon,
23
- EmptyTitle,
24
- } from "../empty";
25
- import {
26
- Select,
27
- SelectContent,
28
- SelectItem,
29
- SelectTrigger,
30
- SelectValue,
31
- } from "../select";
32
- import { Skeleton } from "../skeleton";
33
- import { BarChart3, RefreshCw, WifiOff } from "lucide-react";
34
-
35
- // Format: { THEME_NAME: CSS_SELECTOR }
36
- const THEMES = { light: "", dark: ".dark" } as const;
37
-
38
- export type ChartConfig = {
39
- [k in string]: {
40
- label?: React.ReactNode;
41
- icon?: React.ComponentType;
42
- } & (
43
- | { color?: string; theme?: never }
44
- | { color?: never; theme: Record<keyof typeof THEMES, string> }
45
- );
46
- };
47
-
48
- type ChartContextProps = {
49
- config: ChartConfig;
50
- };
51
-
52
- export type DashboardChartDatum = Record<
53
- string,
54
- string | number | null | undefined
55
- >;
56
-
57
- export type DashboardChartSeries = {
58
- key: string;
59
- label?: React.ReactNode;
60
- type?: "bar" | "line" | "area";
61
- stackId?: string;
62
- yAxisId?: string;
63
- };
64
-
65
- export type DashboardChartColors = string[] | Record<string, string>;
66
-
67
- export type ChartPeriod = {
68
- value: string;
69
- label: string;
70
- };
71
-
72
- export type ChartBarSize = "sm" | "md" | "lg" | "xl" | number;
73
-
74
- type ChartValueFormatter = (value: number | string) => string;
75
-
76
- export type ChartErrorState = boolean | string | Error | React.ReactNode;
77
-
78
- export type ChartStateProps = {
79
- isLoading?: boolean;
80
- error?: ChartErrorState;
81
- onRetry?: () => void;
82
- retryLabel?: React.ReactNode;
83
- emptyTitle?: React.ReactNode;
84
- emptyDescription?: React.ReactNode;
85
- errorTitle?: React.ReactNode;
86
- errorDescription?: React.ReactNode;
87
- loadingLabel?: React.ReactNode;
88
- stateClassName?: string;
89
- };
90
-
91
- const defaultPeriods: ChartPeriod[] = [
92
- { value: "7d", label: "7 days" },
93
- { value: "30d", label: "30 days" },
94
- { value: "90d", label: "90 days" },
95
- ];
96
-
97
- const defaultChartColors = [
98
- "var(--chart-1)",
99
- "var(--chart-2)",
100
- "var(--chart-3)",
101
- "var(--chart-4)",
102
- "var(--chart-5)",
103
- ];
104
-
105
- const chartBarSizes: Record<Exclude<ChartBarSize, number>, number> = {
106
- sm: 8,
107
- md: 14,
108
- lg: 22,
109
- xl: 32,
110
- };
111
-
112
- const ChartContext = React.createContext<ChartContextProps | null>(null);
113
-
114
- export function useChart() {
115
- const context = React.useContext(ChartContext);
116
-
117
- if (!context) {
118
- throw new Error("useChart must be used within a <ChartContainer />");
119
- }
120
-
121
- return context;
122
- }
123
-
124
- /**
125
- * Root container for Recharts-based charts with theme-aware color injection.
126
- *
127
- * @description
128
- * Wraps Recharts' `ResponsiveContainer` and injects CSS custom properties
129
- * (`--color-*`) from a `ChartConfig` object, enabling full dark-mode
130
- * support without hard-coded hex values in chart elements.
131
- *
132
- * @ai-rules
133
- * 1. NEVER pass hex colors directly to Recharts elements (e.g., `fill="#4F46E5"`).
134
- * Always use `fill="var(--color-keyName)"` referencing the injected CSS variables.
135
- * 2. This wrapper is REQUIRED to use `ChartTooltipContent` and `ChartLegendContent`.
136
- * 3. Set height via `className="h-[300px]"` on `ChartContainer`, not on Recharts components.
137
- * 4. Do NOT add another `<ResponsiveContainer>` — it is already included inside.
138
- */
139
- function ChartContainer({
140
- id,
141
- className,
142
- children,
143
- config,
144
- ...props
145
- }: React.ComponentProps<"div"> & {
146
- config: ChartConfig;
147
- children: React.ComponentProps<
148
- typeof RechartsPrimitive.ResponsiveContainer
149
- >["children"];
150
- }) {
151
- const uniqueId = React.useId();
152
- const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
153
-
154
- return (
155
- <ChartContext.Provider value={{ config }}>
156
- <div
157
- data-slot="chart"
158
- data-chart={chartId}
159
- className={cn(
160
- "[&_.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",
161
- className,
162
- )}
163
- {...props}
164
- >
165
- <ChartStyle id={chartId} config={config} />
166
- <RechartsPrimitive.ResponsiveContainer width="100%" height="100%">
167
- {children}
168
- </RechartsPrimitive.ResponsiveContainer>
169
- </div>
170
- </ChartContext.Provider>
171
- );
172
- }
173
-
174
- const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
175
- const colorConfig = Object.entries(config).filter(
176
- ([, config]) => config.theme || config.color,
177
- );
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 =
193
- itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
194
- itemConfig.color;
195
- return color ? ` --color-${key}: ${color};` : null;
196
- })
197
- .join("\n")}
198
- }
199
- `,
200
- )
201
- .join("\n"),
202
- }}
203
- />
204
- );
205
- };
206
-
207
- const ChartTooltip = RechartsPrimitive.Tooltip;
208
-
209
- function ChartTooltipContent({
210
- active,
211
- payload,
212
- className,
213
- indicator = "dot",
214
- hideLabel = false,
215
- hideIndicator = false,
216
- label,
217
- labelFormatter,
218
- labelClassName,
219
- formatter,
220
- color,
221
- nameKey,
222
- labelKey,
223
- }: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
224
- React.ComponentProps<"div"> & {
225
- hideLabel?: boolean;
226
- hideIndicator?: boolean;
227
- indicator?: "line" | "dot" | "dashed";
228
- nameKey?: string;
229
- labelKey?: string;
230
- }) {
231
- const { config } = useChart();
232
-
233
- const tooltipLabel = React.useMemo(() => {
234
- if (hideLabel || !payload?.length) {
235
- return null;
236
- }
237
-
238
- const [item] = payload;
239
- const key = `${labelKey || item?.dataKey || item?.name || "value"}`;
240
- const itemConfig = getPayloadConfigFromPayload(config, item, key);
241
- const value =
242
- !labelKey && typeof label === "string"
243
- ? config[label as keyof typeof config]?.label || label
244
- : itemConfig?.label;
245
-
246
- if (labelFormatter) {
247
- return (
248
- <div className={cn("font-medium", labelClassName)}>
249
- {labelFormatter(value, payload)}
250
- </div>
251
- );
252
- }
253
-
254
- if (!value) {
255
- return null;
256
- }
257
-
258
- return <div className={cn("font-medium", labelClassName)}>{value}</div>;
259
- }, [
260
- label,
261
- labelFormatter,
262
- payload,
263
- hideLabel,
264
- labelClassName,
265
- config,
266
- labelKey,
267
- ]);
268
-
269
- if (!active || !payload?.length) {
270
- return null;
271
- }
272
-
273
- const nestLabel = payload.length === 1 && indicator !== "dot";
274
-
275
- return (
276
- <div
277
- className={cn(
278
- "border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
279
- className,
280
- )}
281
- >
282
- {!nestLabel ? tooltipLabel : null}
283
- <div className="grid gap-1.5">
284
- {payload.map((item, index) => {
285
- const key = `${nameKey || item.name || item.dataKey || "value"}`;
286
- const itemConfig = getPayloadConfigFromPayload(config, item, key);
287
- const indicatorColor = color || item.payload?.fill || item.color;
288
-
289
- return (
290
- <div
291
- key={item.dataKey}
292
- className={cn(
293
- "[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
294
- indicator === "dot" && "items-center",
295
- )}
296
- >
297
- {formatter && item?.value !== undefined && item.name ? (
298
- formatter(item.value, item.name, item, index, item.payload)
299
- ) : (
300
- <>
301
- {itemConfig?.icon ? (
302
- <itemConfig.icon />
303
- ) : (
304
- !hideIndicator && (
305
- <div
306
- className={cn(
307
- "shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
308
- {
309
- "h-2.5 w-2.5": indicator === "dot",
310
- "w-1": indicator === "line",
311
- "w-0 border-[1.5px] border-dashed bg-transparent":
312
- indicator === "dashed",
313
- "my-0.5": nestLabel && indicator === "dashed",
314
- },
315
- )}
316
- style={
317
- {
318
- "--color-bg": indicatorColor,
319
- "--color-border": indicatorColor,
320
- } as React.CSSProperties
321
- }
322
- />
323
- )
324
- )}
325
- <div
326
- className={cn(
327
- "flex flex-1 justify-between leading-none",
328
- nestLabel ? "items-end" : "items-center",
329
- )}
330
- >
331
- <div className="grid gap-1.5">
332
- {nestLabel ? tooltipLabel : null}
333
- <span className="text-muted-foreground">
334
- {itemConfig?.label || item.name}
335
- </span>
336
- </div>
337
- {item.value && (
338
- <span className="text-foreground font-mono font-medium tabular-nums">
339
- {item.value.toLocaleString()}
340
- </span>
341
- )}
342
- </div>
343
- </>
344
- )}
345
- </div>
346
- );
347
- })}
348
- </div>
349
- </div>
350
- );
351
- }
352
-
353
- const ChartLegend = RechartsPrimitive.Legend;
354
-
355
- function ChartLegendContent({
356
- className,
357
- hideIcon = false,
358
- payload,
359
- verticalAlign = "bottom",
360
- nameKey,
361
- }: React.ComponentProps<"div"> &
362
- Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
363
- hideIcon?: boolean;
364
- nameKey?: string;
365
- }) {
366
- const { config } = useChart();
367
-
368
- if (!payload?.length) {
369
- return null;
370
- }
371
-
372
- return (
373
- <div
374
- className={cn(
375
- "flex items-center justify-center gap-4",
376
- verticalAlign === "top" ? "pb-3" : "pt-3",
377
- className,
378
- )}
379
- >
380
- {payload.map((item) => {
381
- const key = `${nameKey ? item.value : item.dataKey || item.value || "value"}`;
382
- const itemConfig = getPayloadConfigFromPayload(config, item, key);
383
-
384
- return (
385
- <div
386
- key={item.value}
387
- className={cn(
388
- "[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3",
389
- )}
390
- >
391
- {itemConfig?.icon && !hideIcon ? (
392
- <itemConfig.icon />
393
- ) : (
394
- <div
395
- className="h-2 w-2 shrink-0 rounded-[2px] bg-(--color-bg)"
396
- style={{
397
- "--color-bg": item.color || `var(--color-${key})`,
398
- } as React.CSSProperties}
399
- />
400
- )}
401
- <span>{itemConfig?.label || item.value}</span>
402
- </div>
403
- );
404
- })}
405
- </div>
406
- );
407
- }
408
-
409
- // Helper to extract item config from a payload.
410
- function getPayloadConfigFromPayload(
411
- config: ChartConfig,
412
- payload: unknown,
413
- key: string,
414
- ) {
415
- if (typeof payload !== "object" || payload === null) {
416
- return undefined;
417
- }
418
-
419
- const payloadPayload =
420
- "payload" in payload &&
421
- typeof payload.payload === "object" &&
422
- payload.payload !== null
423
- ? payload.payload
424
- : undefined;
425
-
426
- let configLabelKey: string = key;
427
-
428
- if (
429
- key in payload &&
430
- typeof payload[key as keyof typeof payload] === "string"
431
- ) {
432
- configLabelKey = payload[key as keyof typeof payload] as string;
433
- } else if (
434
- payloadPayload &&
435
- key in payloadPayload &&
436
- typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
437
- ) {
438
- configLabelKey = payloadPayload[
439
- key as keyof typeof payloadPayload
440
- ] as string;
441
- }
442
-
443
- return configLabelKey in config
444
- ? config[configLabelKey]
445
- : config[key as keyof typeof config];
446
- }
447
-
448
- function getChartSeries(
449
- config: ChartConfig,
450
- series?: DashboardChartSeries[],
451
- ): DashboardChartSeries[] {
452
- if (series?.length) {
453
- return series;
454
- }
455
-
456
- return Object.entries(config).map(([key, item]) => ({
457
- key,
458
- label: item.label,
459
- }));
460
- }
461
-
462
- function getSeriesColor(
463
- key: string,
464
- index: number,
465
- colors?: DashboardChartColors,
466
- ) {
467
- if (Array.isArray(colors)) {
468
- return colors[index] || defaultChartColors[index % defaultChartColors.length];
469
- }
470
-
471
- return colors?.[key] || defaultChartColors[index % defaultChartColors.length];
472
- }
473
-
474
- function getChartConfigWithColors(
475
- config: ChartConfig,
476
- keys: string[],
477
- colors?: DashboardChartColors,
478
- ): ChartConfig {
479
- return keys.reduce<ChartConfig>((nextConfig, key, index) => {
480
- const item = config[key];
481
-
482
- if (item?.theme && !colors) {
483
- nextConfig[key] = item;
484
- return nextConfig;
485
- }
486
-
487
- nextConfig[key] = {
488
- label: item?.label || key,
489
- icon: item?.icon,
490
- color: colors ? getSeriesColor(key, index, colors) : item?.color || getSeriesColor(key, index),
491
- };
492
-
493
- return nextConfig;
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(
536
- data: DashboardChartDatum[],
537
- series: DashboardChartSeries[],
538
- ) {
539
- if (!data.length || !series.length) {
540
- return false;
541
- }
542
-
543
- return data.some((item) =>
544
- series.some((serie) => {
545
- const value = item[serie.key];
546
- return value !== null && value !== undefined && value !== "";
547
- }),
548
- );
549
- }
550
-
551
- function hasPieData(
552
- data: DashboardChartDatum[],
553
- nameKey: string,
554
- valueKey: string,
555
- ) {
556
- return data.some((item) => {
557
- const name = item[nameKey];
558
- const value = item[valueKey];
559
- return (
560
- name !== null &&
561
- name !== undefined &&
562
- name !== "" &&
563
- value !== null &&
564
- value !== undefined &&
565
- value !== ""
566
- );
567
- });
568
- }
569
-
570
- function ChartState({
571
- type,
572
- className,
573
- error,
574
- onRetry,
575
- retryLabel = "Try again",
576
- emptyTitle = "No data available",
577
- emptyDescription = "There is no data available for this chart yet.",
578
- errorTitle = "Connection error",
579
- errorDescription,
580
- loadingLabel = "Loading chart data",
581
- }: ChartStateProps & {
582
- type: "empty" | "error" | "loading";
583
- className?: string;
584
- }) {
585
- if (type === "loading") {
586
- return (
587
- <div
588
- className={cn(
589
- "flex min-h-[240px] flex-col justify-end gap-3 rounded-[var(--radius-card)] border border-border p-6",
590
- className,
591
- )}
592
- aria-label={typeof loadingLabel === "string" ? loadingLabel : undefined}
593
- >
594
- <Skeleton className="h-8 w-2/5" />
595
- <Skeleton className="h-14 w-3/5" />
596
- <Skeleton className="h-24 w-4/5" />
597
- <Skeleton className="h-36 w-full" />
598
- </div>
599
- );
600
- }
601
-
602
- if (type === "error") {
603
- return (
604
- <div
605
- className={cn(
606
- "flex min-h-[240px] items-center justify-center rounded-[var(--radius-card)] border border-border p-6",
607
- className,
608
- )}
609
- >
610
- <Alert variant="destructive" className="max-w-xl">
611
- <AlertTitle>{errorTitle}</AlertTitle>
612
- <AlertDescription>
613
- {errorDescription ||
614
- getErrorDescription(error) ||
615
- "Unable to load chart data. Check your connection and try again."}
616
- </AlertDescription>
617
- {onRetry ? (
618
- <div className="mt-3">
619
- <Button size="sm" variant="outline" onClick={onRetry}>
620
- <RefreshCw className="size-4" />
621
- {retryLabel}
622
- </Button>
623
- </div>
624
- ) : null}
625
- </Alert>
626
- </div>
627
- );
628
- }
629
-
630
- return (
631
- <Empty className={cn("min-h-[240px]", className)}>
632
- <EmptyIcon>
633
- <BarChart3 className="size-10 text-muted-foreground" />
634
- </EmptyIcon>
635
- <EmptyTitle>{emptyTitle}</EmptyTitle>
636
- <EmptyDescription>{emptyDescription}</EmptyDescription>
637
- {onRetry ? (
638
- <EmptyAction>
639
- <Button size="sm" variant="outline" onClick={onRetry}>
640
- <WifiOff className="size-4" />
641
- {retryLabel}
642
- </Button>
643
- </EmptyAction>
644
- ) : null}
645
- </Empty>
646
- );
647
- }
648
-
649
- function getChartState(
650
- state: ChartStateProps,
651
- hasData: boolean,
652
- className?: string,
653
- ) {
654
- if (state.isLoading) {
655
- return <ChartState {...state} type="loading" className={className} />;
656
- }
657
-
658
- if (state.error) {
659
- return <ChartState {...state} type="error" className={className} />;
660
- }
661
-
662
- if (!hasData) {
663
- return <ChartState {...state} type="empty" className={className} />;
664
- }
665
-
666
- return null;
667
- }
668
-
669
- export interface ChartCardProps
670
- extends Omit<React.ComponentProps<typeof Card>, "title"> {
671
- title: React.ReactNode;
672
- description?: React.ReactNode;
673
- action?: React.ReactNode;
674
- footer?: React.ReactNode;
675
- contentClassName?: string;
676
- }
677
-
678
- function ChartCard({
679
- title,
680
- description,
681
- action,
682
- footer,
683
- children,
684
- className,
685
- contentClassName,
686
- ...props
687
- }: ChartCardProps) {
688
- return (
689
- <Card className={cn("w-full min-w-0 overflow-hidden", className)} {...props}>
690
- <CardHeader>
691
- <CardTitle>{title}</CardTitle>
692
- {description ? (
693
- <CardDescription>{description}</CardDescription>
694
- ) : null}
695
- {action ? <CardAction>{action}</CardAction> : null}
696
- </CardHeader>
697
- <CardContent className={contentClassName}>{children}</CardContent>
698
- {footer ? <CardFooter>{footer}</CardFooter> : null}
699
- </Card>
700
- );
701
- }
702
-
703
- export interface DashboardBarChartProps
704
- extends Omit<React.ComponentProps<typeof ChartContainer>, "children">,
705
- ChartStateProps {
706
- data: DashboardChartDatum[];
707
- indexKey?: string;
708
- series?: DashboardChartSeries[];
709
- colors?: DashboardChartColors;
710
- barSize?: ChartBarSize;
711
- stacked?: boolean;
712
- showGrid?: boolean;
713
- showLegend?: boolean;
714
- valueFormatter?: ChartValueFormatter;
715
- }
716
-
717
- function DashboardBarChart({
718
- data,
719
- config,
720
- indexKey = "name",
721
- series,
722
- colors,
723
- barSize = "md",
724
- stacked = false,
725
- showGrid = true,
726
- showLegend = true,
727
- valueFormatter = formatTick,
728
- isLoading,
729
- error,
730
- onRetry,
731
- retryLabel,
732
- emptyTitle,
733
- emptyDescription,
734
- errorTitle,
735
- errorDescription,
736
- loadingLabel,
737
- stateClassName,
738
- className,
739
- ...props
740
- }: DashboardBarChartProps) {
741
- const chartSeries = getChartSeries(config, series);
742
- const chartConfig = getChartConfigWithColors(
743
- config,
744
- chartSeries.map((item) => item.key),
745
- colors,
746
- );
747
- const chartState = getChartState(
748
- {
749
- isLoading,
750
- error,
751
- onRetry,
752
- retryLabel,
753
- emptyTitle,
754
- emptyDescription,
755
- errorTitle,
756
- errorDescription,
757
- loadingLabel,
758
- },
759
- hasChartData(data, chartSeries),
760
- cn("h-[320px] w-full", stateClassName),
761
- );
762
-
763
- if (chartState) {
764
- return chartState;
765
- }
766
-
767
- return (
768
- <ChartContainer
769
- config={chartConfig}
770
- className={cn("h-[320px] w-full", className)}
771
- {...props}
772
- >
773
- <RechartsPrimitive.BarChart data={data} accessibilityLayer>
774
- {showGrid ? (
775
- <RechartsPrimitive.CartesianGrid vertical={false} />
776
- ) : null}
777
- <RechartsPrimitive.XAxis
778
- dataKey={indexKey}
779
- tickLine={false}
780
- axisLine={false}
781
- tickMargin={8}
782
- />
783
- <RechartsPrimitive.YAxis
784
- tickLine={false}
785
- axisLine={false}
786
- tickMargin={8}
787
- tickFormatter={valueFormatter}
788
- />
789
- <ChartTooltip content={<ChartTooltipContent />} />
790
- {showLegend ? (
791
- <ChartLegend content={<ChartLegendContent />} />
792
- ) : null}
793
- {(() => {
794
- // When stacked, only the last bar in each stackId group gets top radius.
795
- const topOfStack = new Set<string>();
796
- if (stacked) {
797
- const lastByStack = new Map<string, string>();
798
- chartSeries.forEach((s) => lastByStack.set(s.stackId || "total", s.key));
799
- lastByStack.forEach((key) => topOfStack.add(key));
800
- }
801
- return chartSeries.map((item) => {
802
- const isTop = !stacked || topOfStack.has(item.key);
803
- return (
804
- <RechartsPrimitive.Bar
805
- key={item.key}
806
- dataKey={item.key}
807
- fill={`var(--color-${item.key})`}
808
- radius={isTop ? [4, 4, 0, 0] : [0, 0, 0, 0]}
809
- barSize={getBarSize(barSize)}
810
- stackId={stacked ? item.stackId || "total" : item.stackId}
811
- />
812
- );
813
- });
814
- })()}
815
- </RechartsPrimitive.BarChart>
816
- </ChartContainer>
817
- );
818
- }
819
-
820
- export interface DashboardLineChartProps
821
- extends Omit<React.ComponentProps<typeof ChartContainer>, "children">,
822
- ChartStateProps {
823
- data: DashboardChartDatum[];
824
- indexKey?: string;
825
- series?: DashboardChartSeries[];
826
- colors?: DashboardChartColors;
827
- showDots?: boolean;
828
- showGrid?: boolean;
829
- showLegend?: boolean;
830
- valueFormatter?: ChartValueFormatter;
831
- }
832
-
833
- function DashboardLineChart({
834
- data,
835
- config,
836
- indexKey = "name",
837
- series,
838
- colors,
839
- showDots = false,
840
- showGrid = true,
841
- showLegend = true,
842
- valueFormatter = formatTick,
843
- isLoading,
844
- error,
845
- onRetry,
846
- retryLabel,
847
- emptyTitle,
848
- emptyDescription,
849
- errorTitle,
850
- errorDescription,
851
- loadingLabel,
852
- stateClassName,
853
- className,
854
- ...props
855
- }: DashboardLineChartProps) {
856
- const chartSeries = getChartSeries(config, series);
857
- const chartConfig = getChartConfigWithColors(
858
- config,
859
- chartSeries.map((item) => item.key),
860
- colors,
861
- );
862
- const chartState = getChartState(
863
- {
864
- isLoading,
865
- error,
866
- onRetry,
867
- retryLabel,
868
- emptyTitle,
869
- emptyDescription,
870
- errorTitle,
871
- errorDescription,
872
- loadingLabel,
873
- },
874
- hasChartData(data, chartSeries),
875
- cn("h-[320px] w-full", stateClassName),
876
- );
877
-
878
- if (chartState) {
879
- return chartState;
880
- }
881
-
882
- return (
883
- <ChartContainer
884
- config={chartConfig}
885
- className={cn("h-[320px] w-full", className)}
886
- {...props}
887
- >
888
- <RechartsPrimitive.LineChart data={data} accessibilityLayer>
889
- {showGrid ? (
890
- <RechartsPrimitive.CartesianGrid vertical={false} />
891
- ) : null}
892
- <RechartsPrimitive.XAxis
893
- dataKey={indexKey}
894
- tickLine={false}
895
- axisLine={false}
896
- tickMargin={8}
897
- />
898
- <RechartsPrimitive.YAxis
899
- tickLine={false}
900
- axisLine={false}
901
- tickMargin={8}
902
- tickFormatter={valueFormatter}
903
- />
904
- <ChartTooltip content={<ChartTooltipContent indicator="line" />} />
905
- {showLegend ? (
906
- <ChartLegend content={<ChartLegendContent />} />
907
- ) : null}
908
- {chartSeries.map((item) => (
909
- <RechartsPrimitive.Line
910
- key={item.key}
911
- type="monotone"
912
- dataKey={item.key}
913
- stroke={`var(--color-${item.key})`}
914
- strokeWidth={2}
915
- dot={showDots ? { r: 3, fill: `var(--color-${item.key})` } : false}
916
- activeDot={{ r: 5 }}
917
- yAxisId={item.yAxisId}
918
- />
919
- ))}
920
- </RechartsPrimitive.LineChart>
921
- </ChartContainer>
922
- );
923
- }
924
-
925
- export interface HorizontalBarChartProps
926
- extends Omit<React.ComponentProps<typeof ChartContainer>, "children">,
927
- ChartStateProps {
928
- data: DashboardChartDatum[];
929
- indexKey?: string;
930
- series?: DashboardChartSeries[];
931
- colors?: DashboardChartColors;
932
- barSize?: ChartBarSize;
933
- stacked?: boolean;
934
- categoryWidth?: number;
935
- showGrid?: boolean;
936
- showLegend?: boolean;
937
- valueFormatter?: ChartValueFormatter;
938
- }
939
-
940
- function HorizontalBarChart({
941
- data,
942
- config,
943
- indexKey = "name",
944
- series,
945
- colors,
946
- barSize = "md",
947
- stacked = false,
948
- categoryWidth = 96,
949
- showGrid = true,
950
- showLegend = true,
951
- valueFormatter = formatTick,
952
- isLoading,
953
- error,
954
- onRetry,
955
- retryLabel,
956
- emptyTitle,
957
- emptyDescription,
958
- errorTitle,
959
- errorDescription,
960
- loadingLabel,
961
- stateClassName,
962
- className,
963
- ...props
964
- }: HorizontalBarChartProps) {
965
- const chartSeries = getChartSeries(config, series);
966
- const chartConfig = getChartConfigWithColors(
967
- config,
968
- chartSeries.map((item) => item.key),
969
- colors,
970
- );
971
- const chartState = getChartState(
972
- {
973
- isLoading,
974
- error,
975
- onRetry,
976
- retryLabel,
977
- emptyTitle,
978
- emptyDescription,
979
- errorTitle,
980
- errorDescription,
981
- loadingLabel,
982
- },
983
- hasChartData(data, chartSeries),
984
- cn("h-[320px] w-full", stateClassName),
985
- );
986
-
987
- if (chartState) {
988
- return chartState;
989
- }
990
-
991
- return (
992
- <ChartContainer
993
- config={chartConfig}
994
- className={cn("h-[320px] w-full", className)}
995
- {...props}
996
- >
997
- <RechartsPrimitive.BarChart
998
- data={data}
999
- layout="vertical"
1000
- accessibilityLayer
1001
- margin={{ left: 8, right: 16 }}
1002
- >
1003
- {showGrid ? (
1004
- <RechartsPrimitive.CartesianGrid horizontal={false} />
1005
- ) : null}
1006
- <RechartsPrimitive.XAxis
1007
- type="number"
1008
- tickLine={false}
1009
- axisLine={false}
1010
- tickMargin={8}
1011
- tickFormatter={valueFormatter}
1012
- />
1013
- <RechartsPrimitive.YAxis
1014
- dataKey={indexKey}
1015
- type="category"
1016
- tickLine={false}
1017
- axisLine={false}
1018
- tickMargin={8}
1019
- width={categoryWidth}
1020
- />
1021
- <ChartTooltip content={<ChartTooltipContent />} />
1022
- {showLegend ? (
1023
- <ChartLegend content={<ChartLegendContent />} />
1024
- ) : null}
1025
- {chartSeries.map((item) => (
1026
- <RechartsPrimitive.Bar
1027
- key={item.key}
1028
- dataKey={item.key}
1029
- fill={`var(--color-${item.key})`}
1030
- radius={[0, 4, 4, 0]}
1031
- barSize={getBarSize(barSize)}
1032
- stackId={stacked ? item.stackId || "total" : item.stackId}
1033
- />
1034
- ))}
1035
- </RechartsPrimitive.BarChart>
1036
- </ChartContainer>
1037
- );
1038
- }
1039
-
1040
- export interface InteractiveTimeSeriesChartProps
1041
- extends Omit<React.ComponentProps<typeof ChartContainer>, "children">,
1042
- ChartStateProps {
1043
- data: DashboardChartDatum[];
1044
- indexKey?: string;
1045
- series?: DashboardChartSeries[];
1046
- colors?: DashboardChartColors;
1047
- periods?: ChartPeriod[];
1048
- defaultPeriod?: string;
1049
- defaultSeries?: string;
1050
- filterData?: (
1051
- data: DashboardChartDatum[],
1052
- period: string,
1053
- ) => DashboardChartDatum[];
1054
- showGrid?: boolean;
1055
- showLegend?: boolean;
1056
- valueFormatter?: ChartValueFormatter;
1057
- }
1058
-
1059
- function InteractiveTimeSeriesChart({
1060
- data,
1061
- config,
1062
- indexKey = "date",
1063
- series,
1064
- colors,
1065
- periods = defaultPeriods,
1066
- defaultPeriod = periods[1]?.value || periods[0]?.value || "30d",
1067
- defaultSeries,
1068
- filterData = defaultFilterData,
1069
- showGrid = true,
1070
- showLegend = false,
1071
- valueFormatter = formatTick,
1072
- isLoading,
1073
- error,
1074
- onRetry,
1075
- retryLabel,
1076
- emptyTitle,
1077
- emptyDescription,
1078
- errorTitle,
1079
- errorDescription,
1080
- loadingLabel,
1081
- stateClassName,
1082
- className,
1083
- ...props
1084
- }: InteractiveTimeSeriesChartProps) {
1085
- const chartSeries = getChartSeries(config, series);
1086
- const chartConfig = getChartConfigWithColors(
1087
- config,
1088
- chartSeries.map((item) => item.key),
1089
- colors,
1090
- );
1091
- const [period, setPeriod] = React.useState(defaultPeriod);
1092
- const [activeSeries, setActiveSeries] = React.useState(
1093
- defaultSeries || chartSeries[0]?.key || "",
1094
- );
1095
- const filteredData = React.useMemo(
1096
- () => filterData(data, period),
1097
- [data, filterData, period],
1098
- );
1099
- const visibleSeries =
1100
- chartSeries.length > 1
1101
- ? chartSeries.filter((item) => item.key === activeSeries)
1102
- : chartSeries;
1103
- const chartState = getChartState(
1104
- {
1105
- isLoading,
1106
- error,
1107
- onRetry,
1108
- retryLabel,
1109
- emptyTitle,
1110
- emptyDescription,
1111
- errorTitle,
1112
- errorDescription,
1113
- loadingLabel,
1114
- },
1115
- hasChartData(filteredData, visibleSeries),
1116
- cn("h-[320px] w-full", stateClassName),
1117
- );
1118
-
1119
- return (
1120
- <div className="w-full space-y-4">
1121
- <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
1122
- {chartSeries.length > 1 ? (
1123
- <div
1124
- className="inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground"
1125
- role="group"
1126
- aria-label="Selecionar metrica do grafico"
1127
- >
1128
- {chartSeries.map((item) => {
1129
- const label = item.label || config[item.key]?.label || item.key;
1130
-
1131
- return (
1132
- <Button
1133
- key={item.key}
1134
- type="button"
1135
- variant="ghost"
1136
- size="sm"
1137
- aria-pressed={activeSeries === item.key}
1138
- onClick={() => setActiveSeries(item.key)}
1139
- className={cn(
1140
- "h-8 px-3 text-sm font-medium",
1141
- activeSeries === item.key
1142
- ? "bg-background text-foreground shadow-sm"
1143
- : "text-muted-foreground hover:text-foreground",
1144
- )}
1145
- >
1146
- {label}
1147
- </Button>
1148
- );
1149
- })}
1150
- </div>
1151
- ) : (
1152
- <div />
1153
- )}
1154
- <Select value={period} onValueChange={setPeriod}>
1155
- <SelectTrigger className="w-full sm:w-[160px]" size="sm" aria-label="Selecionar periodo do grafico">
1156
- <SelectValue placeholder="Period" />
1157
- </SelectTrigger>
1158
- <SelectContent>
1159
- {periods.map((item) => (
1160
- <SelectItem key={item.value} value={item.value}>
1161
- {item.label}
1162
- </SelectItem>
1163
- ))}
1164
- </SelectContent>
1165
- </Select>
1166
- </div>
1167
- {chartState || (
1168
- <ChartContainer
1169
- config={chartConfig}
1170
- className={cn("h-[320px] w-full", className)}
1171
- {...props}
1172
- >
1173
- <RechartsPrimitive.AreaChart data={filteredData} accessibilityLayer>
1174
- {showGrid ? (
1175
- <RechartsPrimitive.CartesianGrid vertical={false} />
1176
- ) : null}
1177
- <RechartsPrimitive.XAxis
1178
- dataKey={indexKey}
1179
- tickLine={false}
1180
- axisLine={false}
1181
- tickMargin={8}
1182
- />
1183
- <RechartsPrimitive.YAxis
1184
- tickLine={false}
1185
- axisLine={false}
1186
- tickMargin={8}
1187
- tickFormatter={valueFormatter}
1188
- />
1189
- <ChartTooltip content={<ChartTooltipContent indicator="line" />} />
1190
- {showLegend ? (
1191
- <ChartLegend content={<ChartLegendContent />} />
1192
- ) : null}
1193
- {visibleSeries.map((item) => (
1194
- <RechartsPrimitive.Area
1195
- key={item.key}
1196
- type="monotone"
1197
- dataKey={item.key}
1198
- stroke={`var(--color-${item.key})`}
1199
- fill={`var(--color-${item.key})`}
1200
- fillOpacity={0.16}
1201
- strokeWidth={2}
1202
- dot={false}
1203
- />
1204
- ))}
1205
- </RechartsPrimitive.AreaChart>
1206
- </ChartContainer>
1207
- )}
1208
- </div>
1209
- );
1210
- }
1211
-
1212
- export interface ComboMetricChartProps
1213
- extends Omit<React.ComponentProps<typeof ChartContainer>, "children">,
1214
- ChartStateProps {
1215
- data: DashboardChartDatum[];
1216
- indexKey?: string;
1217
- series?: DashboardChartSeries[];
1218
- colors?: DashboardChartColors;
1219
- barSize?: ChartBarSize;
1220
- showGrid?: boolean;
1221
- showLegend?: boolean;
1222
- valueFormatter?: ChartValueFormatter;
1223
- }
1224
-
1225
- function ComboMetricChart({
1226
- data,
1227
- config,
1228
- indexKey = "name",
1229
- series,
1230
- colors,
1231
- barSize = "md",
1232
- showGrid = true,
1233
- showLegend = true,
1234
- valueFormatter = formatTick,
1235
- isLoading,
1236
- error,
1237
- onRetry,
1238
- retryLabel,
1239
- emptyTitle,
1240
- emptyDescription,
1241
- errorTitle,
1242
- errorDescription,
1243
- loadingLabel,
1244
- stateClassName,
1245
- className,
1246
- ...props
1247
- }: ComboMetricChartProps) {
1248
- const chartSeries = getChartSeries(config, series);
1249
- const chartConfig = getChartConfigWithColors(
1250
- config,
1251
- chartSeries.map((item) => item.key),
1252
- colors,
1253
- );
1254
- const chartState = getChartState(
1255
- {
1256
- isLoading,
1257
- error,
1258
- onRetry,
1259
- retryLabel,
1260
- emptyTitle,
1261
- emptyDescription,
1262
- errorTitle,
1263
- errorDescription,
1264
- loadingLabel,
1265
- },
1266
- hasChartData(data, chartSeries),
1267
- cn("h-[340px] w-full", stateClassName),
1268
- );
1269
-
1270
- if (chartState) {
1271
- return chartState;
1272
- }
1273
-
1274
- return (
1275
- <ChartContainer
1276
- config={chartConfig}
1277
- className={cn("h-[340px] w-full", className)}
1278
- {...props}
1279
- >
1280
- <RechartsPrimitive.ComposedChart data={data} accessibilityLayer>
1281
- {showGrid ? (
1282
- <RechartsPrimitive.CartesianGrid vertical={false} />
1283
- ) : null}
1284
- <RechartsPrimitive.XAxis
1285
- dataKey={indexKey}
1286
- tickLine={false}
1287
- axisLine={false}
1288
- tickMargin={8}
1289
- />
1290
- <RechartsPrimitive.YAxis
1291
- tickLine={false}
1292
- axisLine={false}
1293
- tickMargin={8}
1294
- tickFormatter={valueFormatter}
1295
- />
1296
- <ChartTooltip content={<ChartTooltipContent />} />
1297
- {showLegend ? (
1298
- <ChartLegend content={<ChartLegendContent />} />
1299
- ) : null}
1300
- {chartSeries.map((item) =>
1301
- item.type === "line" ? (
1302
- <RechartsPrimitive.Line
1303
- key={item.key}
1304
- type="monotone"
1305
- dataKey={item.key}
1306
- stroke={`var(--color-${item.key})`}
1307
- strokeWidth={2}
1308
- dot={false}
1309
- yAxisId={item.yAxisId}
1310
- />
1311
- ) : item.type === "area" ? (
1312
- <RechartsPrimitive.Area
1313
- key={item.key}
1314
- type="monotone"
1315
- dataKey={item.key}
1316
- stroke={`var(--color-${item.key})`}
1317
- fill={`var(--color-${item.key})`}
1318
- fillOpacity={0.12}
1319
- strokeWidth={2}
1320
- dot={false}
1321
- yAxisId={item.yAxisId}
1322
- />
1323
- ) : (
1324
- <RechartsPrimitive.Bar
1325
- key={item.key}
1326
- dataKey={item.key}
1327
- fill={`var(--color-${item.key})`}
1328
- radius={[4, 4, 0, 0]}
1329
- barSize={getBarSize(barSize)}
1330
- yAxisId={item.yAxisId}
1331
- />
1332
- ),
1333
- )}
1334
- </RechartsPrimitive.ComposedChart>
1335
- </ChartContainer>
1336
- );
1337
- }
1338
-
1339
- export interface DonutBreakdownChartProps
1340
- extends Omit<React.ComponentProps<typeof ChartContainer>, "children">,
1341
- ChartStateProps {
1342
- data: DashboardChartDatum[];
1343
- nameKey?: string;
1344
- valueKey?: string;
1345
- colors?: DashboardChartColors;
1346
- centerLabel?: React.ReactNode;
1347
- centerValue?: React.ReactNode;
1348
- showLegend?: boolean;
1349
- }
1350
-
1351
- function DonutBreakdownChart({
1352
- data,
1353
- config,
1354
- nameKey = "name",
1355
- valueKey = "value",
1356
- colors,
1357
- centerLabel,
1358
- centerValue,
1359
- showLegend = true,
1360
- isLoading,
1361
- error,
1362
- onRetry,
1363
- retryLabel,
1364
- emptyTitle,
1365
- emptyDescription,
1366
- errorTitle,
1367
- errorDescription,
1368
- loadingLabel,
1369
- stateClassName,
1370
- className,
1371
- ...props
1372
- }: DonutBreakdownChartProps) {
1373
- const [activeIndex, setActiveIndex] = React.useState(0);
1374
- const chartKeys = data.map((entry, index) =>
1375
- String(entry[nameKey] || `segment-${index}`),
1376
- );
1377
- const chartConfig = getChartConfigWithColors(config, chartKeys, colors);
1378
- const chartState = getChartState(
1379
- {
1380
- isLoading,
1381
- error,
1382
- onRetry,
1383
- retryLabel,
1384
- emptyTitle,
1385
- emptyDescription,
1386
- errorTitle,
1387
- errorDescription,
1388
- loadingLabel,
1389
- },
1390
- hasPieData(data, nameKey, valueKey),
1391
- cn("h-[320px] w-full", stateClassName),
1392
- );
1393
-
1394
- if (chartState) {
1395
- return chartState;
1396
- }
1397
-
1398
- return (
1399
- <ChartContainer
1400
- config={chartConfig}
1401
- className={cn("h-[320px] w-full", className)}
1402
- {...props}
1403
- >
1404
- <RechartsPrimitive.PieChart accessibilityLayer>
1405
- <ChartTooltip
1406
- cursor={false}
1407
- content={<ChartTooltipContent hideLabel nameKey={nameKey} />}
1408
- />
1409
- {showLegend ? (
1410
- <ChartLegend
1411
- content={<ChartLegendContent nameKey={nameKey} />}
1412
- verticalAlign="bottom"
1413
- />
1414
- ) : null}
1415
- <RechartsPrimitive.Pie
1416
- data={data}
1417
- dataKey={valueKey}
1418
- nameKey={nameKey}
1419
- innerRadius="58%"
1420
- outerRadius="82%"
1421
- paddingAngle={2}
1422
- activeIndex={activeIndex}
1423
- onMouseEnter={(_, index) => setActiveIndex(index)}
1424
- >
1425
- {data.map((entry, index) => {
1426
- const key = String(entry[nameKey] || `segment-${index}`);
1427
-
1428
- return (
1429
- <RechartsPrimitive.Cell
1430
- key={key}
1431
- fill={`var(--color-${key})`}
1432
- opacity={index === activeIndex ? 1 : 0.72}
1433
- />
1434
- );
1435
- })}
1436
- {centerValue || centerLabel ? (
1437
- <RechartsPrimitive.Label
1438
- position="center"
1439
- content={({ viewBox }) => {
1440
- if (!viewBox || !("cx" in viewBox) || !("cy" in viewBox)) {
1441
- return null;
1442
- }
1443
-
1444
- return (
1445
- <text
1446
- x={viewBox.cx}
1447
- y={viewBox.cy}
1448
- textAnchor="middle"
1449
- dominantBaseline="middle"
1450
- >
1451
- {centerValue ? (
1452
- <tspan
1453
- x={viewBox.cx}
1454
- y={viewBox.cy}
1455
- className="fill-foreground text-2xl font-semibold"
1456
- >
1457
- {centerValue}
1458
- </tspan>
1459
- ) : null}
1460
- {centerLabel ? (
1461
- <tspan
1462
- x={viewBox.cx}
1463
- y={Number(viewBox.cy) + 22}
1464
- className="fill-muted-foreground text-xs"
1465
- >
1466
- {centerLabel}
1467
- </tspan>
1468
- ) : null}
1469
- </text>
1470
- );
1471
- }}
1472
- />
1473
- ) : null}
1474
- </RechartsPrimitive.Pie>
1475
- </RechartsPrimitive.PieChart>
1476
- </ChartContainer>
1477
- );
1478
- }
1479
-
1480
- export {
1481
- ChartContainer,
1482
- ChartTooltip,
1483
- ChartTooltipContent,
1484
- ChartLegend,
1485
- ChartLegendContent,
1486
- ChartStyle,
1487
- ChartCard,
1488
- DashboardBarChart,
1489
- DashboardLineChart,
1490
- HorizontalBarChart,
1491
- InteractiveTimeSeriesChart,
1492
- ComboMetricChart,
1493
- DonutBreakdownChart,
1494
- };
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as RechartsPrimitive from "recharts";
5
+
6
+ import { cn } from "../../shared/utils";
7
+ import { Alert, AlertDescription, AlertTitle } from "../alert";
8
+ import { Button } from "../button";
9
+ import {
10
+ Card,
11
+ CardAction,
12
+ CardContent,
13
+ CardDescription,
14
+ CardFooter,
15
+ CardHeader,
16
+ CardTitle,
17
+ } from "../card";
18
+ import {
19
+ Empty,
20
+ EmptyAction,
21
+ EmptyDescription,
22
+ EmptyIcon,
23
+ EmptyTitle,
24
+ } from "../empty";
25
+ import {
26
+ Select,
27
+ SelectContent,
28
+ SelectItem,
29
+ SelectTrigger,
30
+ SelectValue,
31
+ } from "../select";
32
+ import { Skeleton } from "../skeleton";
33
+ import { BarChart3, RefreshCw, WifiOff } from "lucide-react";
34
+
35
+ // Format: { THEME_NAME: CSS_SELECTOR }
36
+ const THEMES = { light: "", dark: ".dark" } as const;
37
+
38
+ export type ChartConfig = {
39
+ [k in string]: {
40
+ label?: React.ReactNode;
41
+ icon?: React.ComponentType;
42
+ } & (
43
+ | { color?: string; theme?: never }
44
+ | { color?: never; theme: Record<keyof typeof THEMES, string> }
45
+ );
46
+ };
47
+
48
+ type ChartContextProps = {
49
+ config: ChartConfig;
50
+ };
51
+
52
+ export type DashboardChartDatum = Record<
53
+ string,
54
+ string | number | null | undefined
55
+ >;
56
+
57
+ export type DashboardChartSeries = {
58
+ key: string;
59
+ label?: React.ReactNode;
60
+ type?: "bar" | "line" | "area";
61
+ stackId?: string;
62
+ yAxisId?: string;
63
+ };
64
+
65
+ export type DashboardChartColors = string[] | Record<string, string>;
66
+
67
+ export type ChartPeriod = {
68
+ value: string;
69
+ label: string;
70
+ };
71
+
72
+ export type ChartBarSize = "sm" | "md" | "lg" | "xl" | number;
73
+
74
+ /** Curve interpolation type for line and area charts */
75
+ export type ChartCurveType =
76
+ | "monotone"
77
+ | "linear"
78
+ | "step"
79
+ | "stepBefore"
80
+ | "stepAfter"
81
+ | "natural"
82
+ | "basis";
83
+
84
+ type ChartValueFormatter = (value: number | string) => string;
85
+
86
+ export type ChartErrorState = boolean | string | Error | React.ReactNode;
87
+
88
+ export type ChartStateProps = {
89
+ isLoading?: boolean;
90
+ error?: ChartErrorState;
91
+ onRetry?: () => void;
92
+ retryLabel?: React.ReactNode;
93
+ emptyTitle?: React.ReactNode;
94
+ emptyDescription?: React.ReactNode;
95
+ errorTitle?: React.ReactNode;
96
+ errorDescription?: React.ReactNode;
97
+ loadingLabel?: React.ReactNode;
98
+ stateClassName?: string;
99
+ };
100
+
101
+ const defaultPeriods: ChartPeriod[] = [
102
+ { value: "7d", label: "7 days" },
103
+ { value: "30d", label: "30 days" },
104
+ { value: "90d", label: "90 days" },
105
+ ];
106
+
107
+ const defaultChartColors = [
108
+ "var(--chart-1)",
109
+ "var(--chart-2)",
110
+ "var(--chart-3)",
111
+ "var(--chart-4)",
112
+ "var(--chart-5)",
113
+ "var(--chart-6)",
114
+ "var(--chart-7)",
115
+ "var(--chart-8)",
116
+ ];
117
+
118
+ const chartBarSizes: Record<Exclude<ChartBarSize, number>, number> = {
119
+ sm: 8,
120
+ md: 14,
121
+ lg: 22,
122
+ xl: 32,
123
+ };
124
+
125
+ const ChartContext = React.createContext<ChartContextProps | null>(null);
126
+
127
+ export function useChart() {
128
+ const context = React.useContext(ChartContext);
129
+
130
+ if (!context) {
131
+ throw new Error("useChart must be used within a <ChartContainer />");
132
+ }
133
+
134
+ return context;
135
+ }
136
+
137
+ /**
138
+ * Root container for Recharts-based charts with theme-aware color injection.
139
+ *
140
+ * @description
141
+ * Wraps Recharts' `ResponsiveContainer` and injects CSS custom properties
142
+ * (`--color-*`) from a `ChartConfig` object, enabling full dark-mode
143
+ * support without hard-coded hex values in chart elements.
144
+ *
145
+ * @ai-rules
146
+ * 1. NEVER pass hex colors directly to Recharts elements (e.g., `fill="#4F46E5"`).
147
+ * Always use `fill="var(--color-keyName)"` referencing the injected CSS variables.
148
+ * 2. This wrapper is REQUIRED to use `ChartTooltipContent` and `ChartLegendContent`.
149
+ * 3. Set height via `className="h-[300px]"` on `ChartContainer`, not on Recharts components.
150
+ * 4. Do NOT add another `<ResponsiveContainer>` — it is already included inside.
151
+ */
152
+ function ChartContainer({
153
+ id,
154
+ className,
155
+ children,
156
+ config,
157
+ ...props
158
+ }: React.ComponentProps<"div"> & {
159
+ config: ChartConfig;
160
+ children: React.ComponentProps<
161
+ typeof RechartsPrimitive.ResponsiveContainer
162
+ >["children"];
163
+ }) {
164
+ const uniqueId = React.useId();
165
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
166
+
167
+ return (
168
+ <ChartContext.Provider value={{ config }}>
169
+ <div
170
+ data-slot="chart"
171
+ data-chart={chartId}
172
+ className={cn(
173
+ "[&_.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",
174
+ className,
175
+ )}
176
+ {...props}
177
+ >
178
+ <ChartStyle id={chartId} config={config} />
179
+ <RechartsPrimitive.ResponsiveContainer width="100%" height="100%">
180
+ {children}
181
+ </RechartsPrimitive.ResponsiveContainer>
182
+ </div>
183
+ </ChartContext.Provider>
184
+ );
185
+ }
186
+
187
+ const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
188
+ const colorConfig = Object.entries(config).filter(
189
+ ([, config]) => config.theme || config.color,
190
+ );
191
+
192
+ if (!colorConfig.length) {
193
+ return null;
194
+ }
195
+
196
+ return (
197
+ <style
198
+ dangerouslySetInnerHTML={{
199
+ __html: Object.entries(THEMES)
200
+ .map(
201
+ ([theme, prefix]) => `
202
+ ${prefix} [data-chart=${id}] {
203
+ ${colorConfig
204
+ .map(([key, itemConfig]) => {
205
+ const color =
206
+ itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
207
+ itemConfig.color;
208
+ return color ? ` --color-${key}: ${color};` : null;
209
+ })
210
+ .join("\n")}
211
+ }
212
+ `,
213
+ )
214
+ .join("\n"),
215
+ }}
216
+ />
217
+ );
218
+ };
219
+
220
+ const ChartTooltip = RechartsPrimitive.Tooltip;
221
+
222
+ function ChartTooltipContent({
223
+ active,
224
+ payload,
225
+ className,
226
+ indicator = "dot",
227
+ hideLabel = false,
228
+ hideIndicator = false,
229
+ label,
230
+ labelFormatter,
231
+ labelClassName,
232
+ formatter,
233
+ color,
234
+ nameKey,
235
+ labelKey,
236
+ }: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
237
+ React.ComponentProps<"div"> & {
238
+ hideLabel?: boolean;
239
+ hideIndicator?: boolean;
240
+ indicator?: "line" | "dot" | "dashed";
241
+ nameKey?: string;
242
+ labelKey?: string;
243
+ }) {
244
+ const { config } = useChart();
245
+
246
+ const tooltipLabel = React.useMemo(() => {
247
+ if (hideLabel || !payload?.length) {
248
+ return null;
249
+ }
250
+
251
+ const [item] = payload;
252
+ const key = `${labelKey || item?.dataKey || item?.name || "value"}`;
253
+ const itemConfig = getPayloadConfigFromPayload(config, item, key);
254
+ const value =
255
+ !labelKey && typeof label === "string"
256
+ ? config[label as keyof typeof config]?.label || label
257
+ : itemConfig?.label;
258
+
259
+ if (labelFormatter) {
260
+ return (
261
+ <div className={cn("font-medium", labelClassName)}>
262
+ {labelFormatter(value, payload)}
263
+ </div>
264
+ );
265
+ }
266
+
267
+ if (!value) {
268
+ return null;
269
+ }
270
+
271
+ return <div className={cn("font-medium", labelClassName)}>{value}</div>;
272
+ }, [
273
+ label,
274
+ labelFormatter,
275
+ payload,
276
+ hideLabel,
277
+ labelClassName,
278
+ config,
279
+ labelKey,
280
+ ]);
281
+
282
+ if (!active || !payload?.length) {
283
+ return null;
284
+ }
285
+
286
+ const nestLabel = payload.length === 1 && indicator !== "dot";
287
+
288
+ return (
289
+ <div
290
+ className={cn(
291
+ "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",
292
+ className,
293
+ )}
294
+ >
295
+ {!nestLabel ? tooltipLabel : null}
296
+ <div className="grid gap-1.5">
297
+ {payload.map((item, index) => {
298
+ const key = `${nameKey || item.name || item.dataKey || "value"}`;
299
+ const itemConfig = getPayloadConfigFromPayload(config, item, key);
300
+ const indicatorColor = color || item.payload?.fill || item.color;
301
+
302
+ return (
303
+ <div
304
+ key={item.dataKey}
305
+ className={cn(
306
+ "[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
307
+ indicator === "dot" && "items-center",
308
+ )}
309
+ >
310
+ {formatter && item?.value !== undefined && item.name ? (
311
+ formatter(item.value, item.name, item, index, item.payload)
312
+ ) : (
313
+ <>
314
+ {itemConfig?.icon ? (
315
+ <itemConfig.icon />
316
+ ) : (
317
+ !hideIndicator && (
318
+ <div
319
+ className={cn(
320
+ "shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
321
+ {
322
+ "h-2.5 w-2.5": indicator === "dot",
323
+ "w-1": indicator === "line",
324
+ "w-0 border-[1.5px] border-dashed bg-transparent":
325
+ indicator === "dashed",
326
+ "my-0.5": nestLabel && indicator === "dashed",
327
+ },
328
+ )}
329
+ style={
330
+ {
331
+ "--color-bg": indicatorColor,
332
+ "--color-border": indicatorColor,
333
+ } as React.CSSProperties
334
+ }
335
+ />
336
+ )
337
+ )}
338
+ <div
339
+ className={cn(
340
+ "flex flex-1 justify-between leading-none",
341
+ nestLabel ? "items-end" : "items-center",
342
+ )}
343
+ >
344
+ <div className="grid gap-1.5">
345
+ {nestLabel ? tooltipLabel : null}
346
+ <span className="text-muted-foreground">
347
+ {itemConfig?.label || item.name}
348
+ </span>
349
+ </div>
350
+ {item.value && (
351
+ <span className="text-foreground font-mono font-semibold tabular-nums">
352
+ {item.value.toLocaleString()}
353
+ </span>
354
+ )}
355
+ </div>
356
+ </>
357
+ )}
358
+ </div>
359
+ );
360
+ })}
361
+ </div>
362
+ </div>
363
+ );
364
+ }
365
+
366
+ const ChartLegend = RechartsPrimitive.Legend;
367
+
368
+ function ChartLegendContent({
369
+ className,
370
+ hideIcon = false,
371
+ payload,
372
+ verticalAlign = "bottom",
373
+ nameKey,
374
+ }: React.ComponentProps<"div"> &
375
+ Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
376
+ hideIcon?: boolean;
377
+ nameKey?: string;
378
+ }) {
379
+ const { config } = useChart();
380
+
381
+ if (!payload?.length) {
382
+ return null;
383
+ }
384
+
385
+ return (
386
+ <div
387
+ className={cn(
388
+ "flex items-center justify-center gap-4",
389
+ verticalAlign === "top" ? "pb-3" : "pt-3",
390
+ className,
391
+ )}
392
+ >
393
+ {payload.map((item) => {
394
+ const key = `${nameKey ? item.value : item.dataKey || item.value || "value"}`;
395
+ const itemConfig = getPayloadConfigFromPayload(config, item, key);
396
+
397
+ return (
398
+ <div
399
+ key={item.value}
400
+ className={cn(
401
+ "[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3",
402
+ )}
403
+ >
404
+ {itemConfig?.icon && !hideIcon ? (
405
+ <itemConfig.icon />
406
+ ) : (
407
+ <div
408
+ className="h-2 w-2 shrink-0 rounded-full bg-(--color-bg)"
409
+ style={{
410
+ "--color-bg": item.color || `var(--color-${key})`,
411
+ } as React.CSSProperties}
412
+ />
413
+ )}
414
+ <span className="text-muted-foreground text-xs">{itemConfig?.label || item.value}</span>
415
+ </div>
416
+ );
417
+ })}
418
+ </div>
419
+ );
420
+ }
421
+
422
+ // Helper to extract item config from a payload.
423
+ function getPayloadConfigFromPayload(
424
+ config: ChartConfig,
425
+ payload: unknown,
426
+ key: string,
427
+ ) {
428
+ if (typeof payload !== "object" || payload === null) {
429
+ return undefined;
430
+ }
431
+
432
+ const payloadPayload =
433
+ "payload" in payload &&
434
+ typeof payload.payload === "object" &&
435
+ payload.payload !== null
436
+ ? payload.payload
437
+ : undefined;
438
+
439
+ let configLabelKey: string = key;
440
+
441
+ if (
442
+ key in payload &&
443
+ typeof payload[key as keyof typeof payload] === "string"
444
+ ) {
445
+ configLabelKey = payload[key as keyof typeof payload] as string;
446
+ } else if (
447
+ payloadPayload &&
448
+ key in payloadPayload &&
449
+ typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
450
+ ) {
451
+ configLabelKey = payloadPayload[
452
+ key as keyof typeof payloadPayload
453
+ ] as string;
454
+ }
455
+
456
+ return configLabelKey in config
457
+ ? config[configLabelKey]
458
+ : config[key as keyof typeof config];
459
+ }
460
+
461
+ function getChartSeries(
462
+ config: ChartConfig,
463
+ series?: DashboardChartSeries[],
464
+ ): DashboardChartSeries[] {
465
+ if (series?.length) {
466
+ return series;
467
+ }
468
+
469
+ return Object.entries(config).map(([key, item]) => ({
470
+ key,
471
+ label: item.label,
472
+ }));
473
+ }
474
+
475
+ function getSeriesColor(
476
+ key: string,
477
+ index: number,
478
+ colors?: DashboardChartColors,
479
+ ) {
480
+ if (Array.isArray(colors)) {
481
+ return colors[index] || defaultChartColors[index % defaultChartColors.length];
482
+ }
483
+
484
+ return colors?.[key] || defaultChartColors[index % defaultChartColors.length];
485
+ }
486
+
487
+ function getChartConfigWithColors(
488
+ config: ChartConfig,
489
+ keys: string[],
490
+ colors?: DashboardChartColors,
491
+ ): ChartConfig {
492
+ return keys.reduce<ChartConfig>((nextConfig, key, index) => {
493
+ const item = config[key];
494
+
495
+ if (item?.theme && !colors) {
496
+ nextConfig[key] = item;
497
+ return nextConfig;
498
+ }
499
+
500
+ nextConfig[key] = {
501
+ label: item?.label || key,
502
+ icon: item?.icon,
503
+ color: colors ? getSeriesColor(key, index, colors) : item?.color || getSeriesColor(key, index),
504
+ };
505
+
506
+ return nextConfig;
507
+ }, {});
508
+ }
509
+
510
+ /** Build a ChartConfig from a list of keys (no pre-existing config needed). */
511
+ function buildChartConfig(
512
+ keys: string[],
513
+ colors?: DashboardChartColors,
514
+ ): ChartConfig {
515
+ return keys.reduce<ChartConfig>((cfg, key, index) => {
516
+ cfg[key] = {
517
+ label: key,
518
+ color: getSeriesColor(key, index, colors),
519
+ };
520
+ return cfg;
521
+ }, {});
522
+ }
523
+
524
+ function formatTick(value: number | string) {
525
+ if (typeof value !== "number") {
526
+ return value;
527
+ }
528
+
529
+ return Intl.NumberFormat("en", {
530
+ notation: "compact",
531
+ maximumFractionDigits: 1,
532
+ }).format(value);
533
+ }
534
+
535
+ function defaultFilterData(data: DashboardChartDatum[], period: string) {
536
+ const match = period.match(/^(\d+)/);
537
+
538
+ if (!match) {
539
+ return data;
540
+ }
541
+
542
+ const limit = Number(match[1]);
543
+ return data.slice(Math.max(data.length - limit, 0));
544
+ }
545
+
546
+ function getErrorDescription(error: ChartErrorState) {
547
+ if (typeof error === "string") {
548
+ return error;
549
+ }
550
+
551
+ if (error instanceof Error) {
552
+ return error.message;
553
+ }
554
+
555
+ return undefined;
556
+ }
557
+
558
+ function getBarSize(barSize: ChartBarSize = "md") {
559
+ return typeof barSize === "number" ? barSize : chartBarSizes[barSize];
560
+ }
561
+
562
+ function hasChartData(
563
+ data: DashboardChartDatum[],
564
+ series: DashboardChartSeries[],
565
+ ) {
566
+ if (!data.length || !series.length) {
567
+ return false;
568
+ }
569
+
570
+ return data.some((item) =>
571
+ series.some((serie) => {
572
+ const value = item[serie.key];
573
+ return value !== null && value !== undefined && value !== "";
574
+ }),
575
+ );
576
+ }
577
+
578
+ function hasPieData(
579
+ data: DashboardChartDatum[],
580
+ nameKey: string,
581
+ valueKey: string,
582
+ ) {
583
+ return data.some((item) => {
584
+ const name = item[nameKey];
585
+ const value = item[valueKey];
586
+ return (
587
+ name !== null &&
588
+ name !== undefined &&
589
+ name !== "" &&
590
+ value !== null &&
591
+ value !== undefined &&
592
+ value !== ""
593
+ );
594
+ });
595
+ }
596
+
597
+ function ChartState({
598
+ type,
599
+ className,
600
+ error,
601
+ onRetry,
602
+ retryLabel = "Try again",
603
+ emptyTitle = "No data available",
604
+ emptyDescription = "There is no data available for this chart yet.",
605
+ errorTitle = "Connection error",
606
+ errorDescription,
607
+ loadingLabel = "Loading chart data",
608
+ }: ChartStateProps & {
609
+ type: "empty" | "error" | "loading";
610
+ className?: string;
611
+ }) {
612
+ if (type === "loading") {
613
+ return (
614
+ <div
615
+ className={cn(
616
+ "flex min-h-[240px] flex-col justify-end gap-3 rounded-[var(--radius-card)] border border-border p-6",
617
+ className,
618
+ )}
619
+ aria-label={typeof loadingLabel === "string" ? loadingLabel : undefined}
620
+ >
621
+ <Skeleton className="h-8 w-2/5" />
622
+ <Skeleton className="h-14 w-3/5" />
623
+ <Skeleton className="h-24 w-4/5" />
624
+ <Skeleton className="h-36 w-full" />
625
+ </div>
626
+ );
627
+ }
628
+
629
+ if (type === "error") {
630
+ return (
631
+ <div
632
+ className={cn(
633
+ "flex min-h-[240px] items-center justify-center rounded-[var(--radius-card)] border border-border p-6",
634
+ className,
635
+ )}
636
+ >
637
+ <Alert variant="destructive" className="max-w-xl">
638
+ <AlertTitle>{errorTitle}</AlertTitle>
639
+ <AlertDescription>
640
+ {errorDescription ||
641
+ getErrorDescription(error) ||
642
+ "Unable to load chart data. Check your connection and try again."}
643
+ </AlertDescription>
644
+ {onRetry ? (
645
+ <div className="mt-3">
646
+ <Button size="sm" variant="outline" onClick={onRetry}>
647
+ <RefreshCw className="size-4" />
648
+ {retryLabel}
649
+ </Button>
650
+ </div>
651
+ ) : null}
652
+ </Alert>
653
+ </div>
654
+ );
655
+ }
656
+
657
+ return (
658
+ <Empty className={cn("min-h-[240px]", className)}>
659
+ <EmptyIcon>
660
+ <BarChart3 className="size-10 text-muted-foreground" />
661
+ </EmptyIcon>
662
+ <EmptyTitle>{emptyTitle}</EmptyTitle>
663
+ <EmptyDescription>{emptyDescription}</EmptyDescription>
664
+ {onRetry ? (
665
+ <EmptyAction>
666
+ <Button size="sm" variant="outline" onClick={onRetry}>
667
+ <WifiOff className="size-4" />
668
+ {retryLabel}
669
+ </Button>
670
+ </EmptyAction>
671
+ ) : null}
672
+ </Empty>
673
+ );
674
+ }
675
+
676
+ function getChartState(
677
+ state: ChartStateProps,
678
+ hasData: boolean,
679
+ className?: string,
680
+ ) {
681
+ if (state.isLoading) {
682
+ return <ChartState {...state} type="loading" className={className} />;
683
+ }
684
+
685
+ if (state.error) {
686
+ return <ChartState {...state} type="error" className={className} />;
687
+ }
688
+
689
+ if (!hasData) {
690
+ return <ChartState {...state} type="empty" className={className} />;
691
+ }
692
+
693
+ return null;
694
+ }
695
+
696
+ export interface ChartCardProps
697
+ extends Omit<React.ComponentProps<typeof Card>, "title"> {
698
+ title: React.ReactNode;
699
+ description?: React.ReactNode;
700
+ action?: React.ReactNode;
701
+ footer?: React.ReactNode;
702
+ contentClassName?: string;
703
+ }
704
+
705
+ function ChartCard({
706
+ title,
707
+ description,
708
+ action,
709
+ footer,
710
+ children,
711
+ className,
712
+ contentClassName,
713
+ ...props
714
+ }: ChartCardProps) {
715
+ return (
716
+ <Card className={cn("w-full min-w-0 overflow-hidden", className)} {...props}>
717
+ <CardHeader>
718
+ <CardTitle>{title}</CardTitle>
719
+ {description ? (
720
+ <CardDescription>{description}</CardDescription>
721
+ ) : null}
722
+ {action ? <CardAction>{action}</CardAction> : null}
723
+ </CardHeader>
724
+ <CardContent className={contentClassName}>{children}</CardContent>
725
+ {footer ? <CardFooter>{footer}</CardFooter> : null}
726
+ </Card>
727
+ );
728
+ }
729
+
730
+ // ─── Gradient defs helper ────────────────────────────────────────────────────
731
+
732
+ /**
733
+ * Renders SVG `<defs>` with linear gradients for each series key.
734
+ * Used internally by area charts when `gradientFill` is enabled.
735
+ */
736
+ function AreaGradientDefs({
737
+ seriesKeys,
738
+ opacity = 0.3,
739
+ }: {
740
+ seriesKeys: string[];
741
+ opacity?: number;
742
+ }) {
743
+ return (
744
+ <defs>
745
+ {seriesKeys.map((key) => (
746
+ <linearGradient
747
+ key={key}
748
+ id={`gradient-${key}`}
749
+ x1="0"
750
+ y1="0"
751
+ x2="0"
752
+ y2="1"
753
+ >
754
+ <stop
755
+ offset="5%"
756
+ stopColor={`var(--color-${key})`}
757
+ stopOpacity={opacity}
758
+ />
759
+ <stop
760
+ offset="95%"
761
+ stopColor={`var(--color-${key})`}
762
+ stopOpacity={0}
763
+ />
764
+ </linearGradient>
765
+ ))}
766
+ </defs>
767
+ );
768
+ }
769
+
770
+ // ─── DashboardBarChart ────────────────────────────────────────────────────────
771
+
772
+ export interface DashboardBarChartProps
773
+ extends Omit<React.ComponentProps<typeof ChartContainer>, "children">,
774
+ ChartStateProps {
775
+ data: DashboardChartDatum[];
776
+ indexKey?: string;
777
+ series?: DashboardChartSeries[];
778
+ colors?: DashboardChartColors;
779
+ barSize?: ChartBarSize;
780
+ stacked?: boolean;
781
+ showGrid?: boolean;
782
+ showLegend?: boolean;
783
+ valueFormatter?: ChartValueFormatter;
784
+ }
785
+
786
+ function DashboardBarChart({
787
+ data,
788
+ config,
789
+ indexKey = "name",
790
+ series,
791
+ colors,
792
+ barSize = "md",
793
+ stacked = false,
794
+ showGrid = true,
795
+ showLegend = true,
796
+ valueFormatter = formatTick,
797
+ isLoading,
798
+ error,
799
+ onRetry,
800
+ retryLabel,
801
+ emptyTitle,
802
+ emptyDescription,
803
+ errorTitle,
804
+ errorDescription,
805
+ loadingLabel,
806
+ stateClassName,
807
+ className,
808
+ ...props
809
+ }: DashboardBarChartProps) {
810
+ const chartSeries = getChartSeries(config, series);
811
+ const chartConfig = getChartConfigWithColors(
812
+ config,
813
+ chartSeries.map((item) => item.key),
814
+ colors,
815
+ );
816
+ const chartState = getChartState(
817
+ {
818
+ isLoading,
819
+ error,
820
+ onRetry,
821
+ retryLabel,
822
+ emptyTitle,
823
+ emptyDescription,
824
+ errorTitle,
825
+ errorDescription,
826
+ loadingLabel,
827
+ },
828
+ hasChartData(data, chartSeries),
829
+ cn("h-[320px] w-full", stateClassName),
830
+ );
831
+
832
+ if (chartState) {
833
+ return chartState;
834
+ }
835
+
836
+ return (
837
+ <ChartContainer
838
+ config={chartConfig}
839
+ className={cn("h-[320px] w-full", className)}
840
+ {...props}
841
+ >
842
+ <RechartsPrimitive.BarChart data={data} accessibilityLayer barGap={4}>
843
+ {showGrid ? (
844
+ <RechartsPrimitive.CartesianGrid
845
+ vertical={false}
846
+ strokeDasharray="3 3"
847
+ stroke="var(--border)"
848
+ strokeOpacity={0.5}
849
+ />
850
+ ) : null}
851
+ <RechartsPrimitive.XAxis
852
+ dataKey={indexKey}
853
+ tickLine={false}
854
+ axisLine={false}
855
+ tickMargin={8}
856
+ />
857
+ <RechartsPrimitive.YAxis
858
+ tickLine={false}
859
+ axisLine={false}
860
+ tickMargin={8}
861
+ tickFormatter={valueFormatter}
862
+ />
863
+ <ChartTooltip
864
+ cursor={{ fill: "var(--muted)", opacity: 0.4, radius: 4 }}
865
+ content={<ChartTooltipContent />}
866
+ />
867
+ {showLegend ? (
868
+ <ChartLegend content={<ChartLegendContent />} />
869
+ ) : null}
870
+ {(() => {
871
+ // When stacked, only the last bar in each stackId group gets top radius.
872
+ const topOfStack = new Set<string>();
873
+ if (stacked) {
874
+ const lastByStack = new Map<string, string>();
875
+ chartSeries.forEach((s) => lastByStack.set(s.stackId || "total", s.key));
876
+ lastByStack.forEach((key) => topOfStack.add(key));
877
+ }
878
+ return chartSeries.map((item) => {
879
+ const isTop = !stacked || topOfStack.has(item.key);
880
+ return (
881
+ <RechartsPrimitive.Bar
882
+ key={item.key}
883
+ dataKey={item.key}
884
+ fill={`var(--color-${item.key})`}
885
+ radius={isTop ? [4, 4, 0, 0] : [0, 0, 0, 0]}
886
+ barSize={getBarSize(barSize)}
887
+ stackId={stacked ? item.stackId || "total" : item.stackId}
888
+ isAnimationActive
889
+ animationDuration={600}
890
+ animationEasing="ease-out"
891
+ />
892
+ );
893
+ });
894
+ })()}
895
+ </RechartsPrimitive.BarChart>
896
+ </ChartContainer>
897
+ );
898
+ }
899
+
900
+ // ─── DashboardLineChart ───────────────────────────────────────────────────────
901
+
902
+ export interface DashboardLineChartProps
903
+ extends Omit<React.ComponentProps<typeof ChartContainer>, "children">,
904
+ ChartStateProps {
905
+ data: DashboardChartDatum[];
906
+ indexKey?: string;
907
+ series?: DashboardChartSeries[];
908
+ colors?: DashboardChartColors;
909
+ showDots?: boolean;
910
+ showGrid?: boolean;
911
+ showLegend?: boolean;
912
+ curveType?: ChartCurveType;
913
+ strokeWidth?: number;
914
+ valueFormatter?: ChartValueFormatter;
915
+ }
916
+
917
+ function DashboardLineChart({
918
+ data,
919
+ config,
920
+ indexKey = "name",
921
+ series,
922
+ colors,
923
+ showDots = false,
924
+ showGrid = true,
925
+ showLegend = true,
926
+ curveType = "monotone",
927
+ strokeWidth = 2,
928
+ valueFormatter = formatTick,
929
+ isLoading,
930
+ error,
931
+ onRetry,
932
+ retryLabel,
933
+ emptyTitle,
934
+ emptyDescription,
935
+ errorTitle,
936
+ errorDescription,
937
+ loadingLabel,
938
+ stateClassName,
939
+ className,
940
+ ...props
941
+ }: DashboardLineChartProps) {
942
+ const chartSeries = getChartSeries(config, series);
943
+ const chartConfig = getChartConfigWithColors(
944
+ config,
945
+ chartSeries.map((item) => item.key),
946
+ colors,
947
+ );
948
+ const chartState = getChartState(
949
+ {
950
+ isLoading,
951
+ error,
952
+ onRetry,
953
+ retryLabel,
954
+ emptyTitle,
955
+ emptyDescription,
956
+ errorTitle,
957
+ errorDescription,
958
+ loadingLabel,
959
+ },
960
+ hasChartData(data, chartSeries),
961
+ cn("h-[320px] w-full", stateClassName),
962
+ );
963
+
964
+ if (chartState) {
965
+ return chartState;
966
+ }
967
+
968
+ return (
969
+ <ChartContainer
970
+ config={chartConfig}
971
+ className={cn("h-[320px] w-full", className)}
972
+ {...props}
973
+ >
974
+ <RechartsPrimitive.LineChart data={data} accessibilityLayer>
975
+ {showGrid ? (
976
+ <RechartsPrimitive.CartesianGrid
977
+ vertical={false}
978
+ strokeDasharray="3 3"
979
+ stroke="var(--border)"
980
+ strokeOpacity={0.5}
981
+ />
982
+ ) : null}
983
+ <RechartsPrimitive.XAxis
984
+ dataKey={indexKey}
985
+ tickLine={false}
986
+ axisLine={false}
987
+ tickMargin={8}
988
+ />
989
+ <RechartsPrimitive.YAxis
990
+ tickLine={false}
991
+ axisLine={false}
992
+ tickMargin={8}
993
+ tickFormatter={valueFormatter}
994
+ />
995
+ <ChartTooltip content={<ChartTooltipContent indicator="line" />} />
996
+ {showLegend ? (
997
+ <ChartLegend content={<ChartLegendContent />} />
998
+ ) : null}
999
+ {chartSeries.map((item) => (
1000
+ <RechartsPrimitive.Line
1001
+ key={item.key}
1002
+ type={curveType}
1003
+ dataKey={item.key}
1004
+ stroke={`var(--color-${item.key})`}
1005
+ strokeWidth={strokeWidth}
1006
+ dot={showDots ? { r: 3, fill: `var(--color-${item.key})`, strokeWidth: 0 } : false}
1007
+ activeDot={{ r: 5, strokeWidth: 0 }}
1008
+ yAxisId={item.yAxisId}
1009
+ isAnimationActive
1010
+ animationDuration={600}
1011
+ animationEasing="ease-out"
1012
+ />
1013
+ ))}
1014
+ </RechartsPrimitive.LineChart>
1015
+ </ChartContainer>
1016
+ );
1017
+ }
1018
+
1019
+ // ─── HorizontalBarChart ───────────────────────────────────────────────────────
1020
+
1021
+ export interface HorizontalBarChartProps
1022
+ extends Omit<React.ComponentProps<typeof ChartContainer>, "children">,
1023
+ ChartStateProps {
1024
+ data: DashboardChartDatum[];
1025
+ indexKey?: string;
1026
+ series?: DashboardChartSeries[];
1027
+ colors?: DashboardChartColors;
1028
+ barSize?: ChartBarSize;
1029
+ stacked?: boolean;
1030
+ categoryWidth?: number;
1031
+ showGrid?: boolean;
1032
+ showLegend?: boolean;
1033
+ valueFormatter?: ChartValueFormatter;
1034
+ }
1035
+
1036
+ function HorizontalBarChart({
1037
+ data,
1038
+ config,
1039
+ indexKey = "name",
1040
+ series,
1041
+ colors,
1042
+ barSize = "md",
1043
+ stacked = false,
1044
+ categoryWidth = 96,
1045
+ showGrid = true,
1046
+ showLegend = true,
1047
+ valueFormatter = formatTick,
1048
+ isLoading,
1049
+ error,
1050
+ onRetry,
1051
+ retryLabel,
1052
+ emptyTitle,
1053
+ emptyDescription,
1054
+ errorTitle,
1055
+ errorDescription,
1056
+ loadingLabel,
1057
+ stateClassName,
1058
+ className,
1059
+ ...props
1060
+ }: HorizontalBarChartProps) {
1061
+ const chartSeries = getChartSeries(config, series);
1062
+ const chartConfig = getChartConfigWithColors(
1063
+ config,
1064
+ chartSeries.map((item) => item.key),
1065
+ colors,
1066
+ );
1067
+ const chartState = getChartState(
1068
+ {
1069
+ isLoading,
1070
+ error,
1071
+ onRetry,
1072
+ retryLabel,
1073
+ emptyTitle,
1074
+ emptyDescription,
1075
+ errorTitle,
1076
+ errorDescription,
1077
+ loadingLabel,
1078
+ },
1079
+ hasChartData(data, chartSeries),
1080
+ cn("h-[320px] w-full", stateClassName),
1081
+ );
1082
+
1083
+ if (chartState) {
1084
+ return chartState;
1085
+ }
1086
+
1087
+ return (
1088
+ <ChartContainer
1089
+ config={chartConfig}
1090
+ className={cn("h-[320px] w-full", className)}
1091
+ {...props}
1092
+ >
1093
+ <RechartsPrimitive.BarChart
1094
+ data={data}
1095
+ layout="vertical"
1096
+ accessibilityLayer
1097
+ margin={{ left: 8, right: 16 }}
1098
+ >
1099
+ {showGrid ? (
1100
+ <RechartsPrimitive.CartesianGrid
1101
+ horizontal={false}
1102
+ strokeDasharray="3 3"
1103
+ stroke="var(--border)"
1104
+ strokeOpacity={0.5}
1105
+ />
1106
+ ) : null}
1107
+ <RechartsPrimitive.XAxis
1108
+ type="number"
1109
+ tickLine={false}
1110
+ axisLine={false}
1111
+ tickMargin={8}
1112
+ tickFormatter={valueFormatter}
1113
+ />
1114
+ <RechartsPrimitive.YAxis
1115
+ dataKey={indexKey}
1116
+ type="category"
1117
+ tickLine={false}
1118
+ axisLine={false}
1119
+ tickMargin={8}
1120
+ width={categoryWidth}
1121
+ />
1122
+ <ChartTooltip content={<ChartTooltipContent />} />
1123
+ {showLegend ? (
1124
+ <ChartLegend content={<ChartLegendContent />} />
1125
+ ) : null}
1126
+ {chartSeries.map((item) => (
1127
+ <RechartsPrimitive.Bar
1128
+ key={item.key}
1129
+ dataKey={item.key}
1130
+ fill={`var(--color-${item.key})`}
1131
+ radius={[0, 4, 4, 0]}
1132
+ barSize={getBarSize(barSize)}
1133
+ stackId={stacked ? item.stackId || "total" : item.stackId}
1134
+ isAnimationActive
1135
+ animationDuration={600}
1136
+ animationEasing="ease-out"
1137
+ />
1138
+ ))}
1139
+ </RechartsPrimitive.BarChart>
1140
+ </ChartContainer>
1141
+ );
1142
+ }
1143
+
1144
+ // ─── InteractiveTimeSeriesChart ───────────────────────────────────────────────
1145
+
1146
+ export interface InteractiveTimeSeriesChartProps
1147
+ extends Omit<React.ComponentProps<typeof ChartContainer>, "children">,
1148
+ ChartStateProps {
1149
+ data: DashboardChartDatum[];
1150
+ indexKey?: string;
1151
+ series?: DashboardChartSeries[];
1152
+ colors?: DashboardChartColors;
1153
+ periods?: ChartPeriod[];
1154
+ defaultPeriod?: string;
1155
+ defaultSeries?: string;
1156
+ filterData?: (
1157
+ data: DashboardChartDatum[],
1158
+ period: string,
1159
+ ) => DashboardChartDatum[];
1160
+ showGrid?: boolean;
1161
+ showLegend?: boolean;
1162
+ showDots?: boolean;
1163
+ gradientFill?: boolean;
1164
+ curveType?: ChartCurveType;
1165
+ strokeWidth?: number;
1166
+ valueFormatter?: ChartValueFormatter;
1167
+ }
1168
+
1169
+ function InteractiveTimeSeriesChart({
1170
+ data,
1171
+ config,
1172
+ indexKey = "date",
1173
+ series,
1174
+ colors,
1175
+ periods = defaultPeriods,
1176
+ defaultPeriod = periods[1]?.value || periods[0]?.value || "30d",
1177
+ defaultSeries,
1178
+ filterData = defaultFilterData,
1179
+ showGrid = true,
1180
+ showLegend = false,
1181
+ showDots = false,
1182
+ gradientFill = true,
1183
+ curveType = "monotone",
1184
+ strokeWidth = 2,
1185
+ valueFormatter = formatTick,
1186
+ isLoading,
1187
+ error,
1188
+ onRetry,
1189
+ retryLabel,
1190
+ emptyTitle,
1191
+ emptyDescription,
1192
+ errorTitle,
1193
+ errorDescription,
1194
+ loadingLabel,
1195
+ stateClassName,
1196
+ className,
1197
+ ...props
1198
+ }: InteractiveTimeSeriesChartProps) {
1199
+ const chartSeries = getChartSeries(config, series);
1200
+ const chartConfig = getChartConfigWithColors(
1201
+ config,
1202
+ chartSeries.map((item) => item.key),
1203
+ colors,
1204
+ );
1205
+ const [period, setPeriod] = React.useState(defaultPeriod);
1206
+ const [activeSeries, setActiveSeries] = React.useState(
1207
+ defaultSeries || chartSeries[0]?.key || "",
1208
+ );
1209
+ const filteredData = React.useMemo(
1210
+ () => filterData(data, period),
1211
+ [data, filterData, period],
1212
+ );
1213
+ const visibleSeries =
1214
+ chartSeries.length > 1
1215
+ ? chartSeries.filter((item) => item.key === activeSeries)
1216
+ : chartSeries;
1217
+ const chartState = getChartState(
1218
+ {
1219
+ isLoading,
1220
+ error,
1221
+ onRetry,
1222
+ retryLabel,
1223
+ emptyTitle,
1224
+ emptyDescription,
1225
+ errorTitle,
1226
+ errorDescription,
1227
+ loadingLabel,
1228
+ },
1229
+ hasChartData(filteredData, visibleSeries),
1230
+ cn("h-[320px] w-full", stateClassName),
1231
+ );
1232
+
1233
+ return (
1234
+ <div className="w-full space-y-4">
1235
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
1236
+ {chartSeries.length > 1 ? (
1237
+ <div
1238
+ className="inline-flex h-10 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground"
1239
+ role="group"
1240
+ aria-label="Selecionar metrica do grafico"
1241
+ >
1242
+ {chartSeries.map((item) => {
1243
+ const label = item.label || config[item.key]?.label || item.key;
1244
+
1245
+ return (
1246
+ <Button
1247
+ key={item.key}
1248
+ type="button"
1249
+ variant="ghost"
1250
+ size="sm"
1251
+ aria-pressed={activeSeries === item.key}
1252
+ onClick={() => setActiveSeries(item.key)}
1253
+ className={cn(
1254
+ "h-8 px-3 text-sm font-medium transition-all",
1255
+ activeSeries === item.key
1256
+ ? "bg-background text-foreground shadow-sm"
1257
+ : "text-muted-foreground hover:text-foreground",
1258
+ )}
1259
+ >
1260
+ {label}
1261
+ </Button>
1262
+ );
1263
+ })}
1264
+ </div>
1265
+ ) : (
1266
+ <div />
1267
+ )}
1268
+ <Select value={period} onValueChange={setPeriod}>
1269
+ <SelectTrigger className="w-full sm:w-[160px]" size="sm" aria-label="Selecionar periodo do grafico">
1270
+ <SelectValue placeholder="Period" />
1271
+ </SelectTrigger>
1272
+ <SelectContent>
1273
+ {periods.map((item) => (
1274
+ <SelectItem key={item.value} value={item.value}>
1275
+ {item.label}
1276
+ </SelectItem>
1277
+ ))}
1278
+ </SelectContent>
1279
+ </Select>
1280
+ </div>
1281
+ {chartState || (
1282
+ <ChartContainer
1283
+ config={chartConfig}
1284
+ className={cn("h-[320px] w-full", className)}
1285
+ {...props}
1286
+ >
1287
+ <RechartsPrimitive.AreaChart data={filteredData} accessibilityLayer>
1288
+ {gradientFill && (
1289
+ <AreaGradientDefs
1290
+ seriesKeys={visibleSeries.map((s) => s.key)}
1291
+ opacity={0.4}
1292
+ />
1293
+ )}
1294
+ {showGrid ? (
1295
+ <RechartsPrimitive.CartesianGrid
1296
+ vertical={false}
1297
+ strokeDasharray="3 3"
1298
+ stroke="var(--border)"
1299
+ strokeOpacity={0.5}
1300
+ />
1301
+ ) : null}
1302
+ <RechartsPrimitive.XAxis
1303
+ dataKey={indexKey}
1304
+ tickLine={false}
1305
+ axisLine={false}
1306
+ tickMargin={8}
1307
+ />
1308
+ <RechartsPrimitive.YAxis
1309
+ tickLine={false}
1310
+ axisLine={false}
1311
+ tickMargin={8}
1312
+ tickFormatter={valueFormatter}
1313
+ />
1314
+ <ChartTooltip content={<ChartTooltipContent indicator="line" />} />
1315
+ {showLegend ? (
1316
+ <ChartLegend content={<ChartLegendContent />} />
1317
+ ) : null}
1318
+ {visibleSeries.map((item) => (
1319
+ <RechartsPrimitive.Area
1320
+ key={item.key}
1321
+ type={curveType}
1322
+ dataKey={item.key}
1323
+ stroke={`var(--color-${item.key})`}
1324
+ fill={
1325
+ gradientFill
1326
+ ? `url(#gradient-${item.key})`
1327
+ : `var(--color-${item.key})`
1328
+ }
1329
+ fillOpacity={gradientFill ? 1 : 0.16}
1330
+ strokeWidth={strokeWidth}
1331
+ dot={showDots ? { r: 3, fill: `var(--color-${item.key})`, strokeWidth: 0 } : false}
1332
+ activeDot={{ r: 5, strokeWidth: 0 }}
1333
+ isAnimationActive
1334
+ animationDuration={600}
1335
+ animationEasing="ease-out"
1336
+ />
1337
+ ))}
1338
+ </RechartsPrimitive.AreaChart>
1339
+ </ChartContainer>
1340
+ )}
1341
+ </div>
1342
+ );
1343
+ }
1344
+
1345
+ // ─── ComboMetricChart ─────────────────────────────────────────────────────────
1346
+
1347
+ export interface ComboMetricChartProps
1348
+ extends Omit<React.ComponentProps<typeof ChartContainer>, "children">,
1349
+ ChartStateProps {
1350
+ data: DashboardChartDatum[];
1351
+ indexKey?: string;
1352
+ series?: DashboardChartSeries[];
1353
+ colors?: DashboardChartColors;
1354
+ barSize?: ChartBarSize;
1355
+ showGrid?: boolean;
1356
+ showLegend?: boolean;
1357
+ gradientFill?: boolean;
1358
+ curveType?: ChartCurveType;
1359
+ valueFormatter?: ChartValueFormatter;
1360
+ }
1361
+
1362
+ function ComboMetricChart({
1363
+ data,
1364
+ config,
1365
+ indexKey = "name",
1366
+ series,
1367
+ colors,
1368
+ barSize = "md",
1369
+ showGrid = true,
1370
+ showLegend = true,
1371
+ gradientFill = false,
1372
+ curveType = "monotone",
1373
+ valueFormatter = formatTick,
1374
+ isLoading,
1375
+ error,
1376
+ onRetry,
1377
+ retryLabel,
1378
+ emptyTitle,
1379
+ emptyDescription,
1380
+ errorTitle,
1381
+ errorDescription,
1382
+ loadingLabel,
1383
+ stateClassName,
1384
+ className,
1385
+ ...props
1386
+ }: ComboMetricChartProps) {
1387
+ const chartSeries = getChartSeries(config, series);
1388
+ const chartConfig = getChartConfigWithColors(
1389
+ config,
1390
+ chartSeries.map((item) => item.key),
1391
+ colors,
1392
+ );
1393
+ const chartState = getChartState(
1394
+ {
1395
+ isLoading,
1396
+ error,
1397
+ onRetry,
1398
+ retryLabel,
1399
+ emptyTitle,
1400
+ emptyDescription,
1401
+ errorTitle,
1402
+ errorDescription,
1403
+ loadingLabel,
1404
+ },
1405
+ hasChartData(data, chartSeries),
1406
+ cn("h-[340px] w-full", stateClassName),
1407
+ );
1408
+
1409
+ if (chartState) {
1410
+ return chartState;
1411
+ }
1412
+
1413
+ const areaSeries = gradientFill
1414
+ ? chartSeries.filter((s) => s.type === "area").map((s) => s.key)
1415
+ : [];
1416
+
1417
+ return (
1418
+ <ChartContainer
1419
+ config={chartConfig}
1420
+ className={cn("h-[340px] w-full", className)}
1421
+ {...props}
1422
+ >
1423
+ <RechartsPrimitive.ComposedChart data={data} accessibilityLayer>
1424
+ {gradientFill && areaSeries.length > 0 && (
1425
+ <AreaGradientDefs seriesKeys={areaSeries} opacity={0.35} />
1426
+ )}
1427
+ {showGrid ? (
1428
+ <RechartsPrimitive.CartesianGrid
1429
+ vertical={false}
1430
+ strokeDasharray="3 3"
1431
+ stroke="var(--border)"
1432
+ strokeOpacity={0.5}
1433
+ />
1434
+ ) : null}
1435
+ <RechartsPrimitive.XAxis
1436
+ dataKey={indexKey}
1437
+ tickLine={false}
1438
+ axisLine={false}
1439
+ tickMargin={8}
1440
+ />
1441
+ <RechartsPrimitive.YAxis
1442
+ tickLine={false}
1443
+ axisLine={false}
1444
+ tickMargin={8}
1445
+ tickFormatter={valueFormatter}
1446
+ />
1447
+ <ChartTooltip content={<ChartTooltipContent />} />
1448
+ {showLegend ? (
1449
+ <ChartLegend content={<ChartLegendContent />} />
1450
+ ) : null}
1451
+ {chartSeries.map((item) =>
1452
+ item.type === "line" ? (
1453
+ <RechartsPrimitive.Line
1454
+ key={item.key}
1455
+ type={curveType}
1456
+ dataKey={item.key}
1457
+ stroke={`var(--color-${item.key})`}
1458
+ strokeWidth={2}
1459
+ dot={false}
1460
+ activeDot={{ r: 5, strokeWidth: 0 }}
1461
+ yAxisId={item.yAxisId}
1462
+ isAnimationActive
1463
+ animationDuration={600}
1464
+ animationEasing="ease-out"
1465
+ />
1466
+ ) : item.type === "area" ? (
1467
+ <RechartsPrimitive.Area
1468
+ key={item.key}
1469
+ type={curveType}
1470
+ dataKey={item.key}
1471
+ stroke={`var(--color-${item.key})`}
1472
+ fill={
1473
+ gradientFill
1474
+ ? `url(#gradient-${item.key})`
1475
+ : `var(--color-${item.key})`
1476
+ }
1477
+ fillOpacity={gradientFill ? 1 : 0.12}
1478
+ strokeWidth={2}
1479
+ dot={false}
1480
+ activeDot={{ r: 5, strokeWidth: 0 }}
1481
+ yAxisId={item.yAxisId}
1482
+ isAnimationActive
1483
+ animationDuration={600}
1484
+ animationEasing="ease-out"
1485
+ />
1486
+ ) : (
1487
+ <RechartsPrimitive.Bar
1488
+ key={item.key}
1489
+ dataKey={item.key}
1490
+ fill={`var(--color-${item.key})`}
1491
+ radius={[4, 4, 0, 0]}
1492
+ barSize={getBarSize(barSize)}
1493
+ yAxisId={item.yAxisId}
1494
+ isAnimationActive
1495
+ animationDuration={600}
1496
+ animationEasing="ease-out"
1497
+ />
1498
+ ),
1499
+ )}
1500
+ </RechartsPrimitive.ComposedChart>
1501
+ </ChartContainer>
1502
+ );
1503
+ }
1504
+
1505
+ // ─── DonutBreakdownChart ──────────────────────────────────────────────────────
1506
+
1507
+ export interface DonutBreakdownChartProps
1508
+ extends Omit<React.ComponentProps<typeof ChartContainer>, "children">,
1509
+ ChartStateProps {
1510
+ data: DashboardChartDatum[];
1511
+ nameKey?: string;
1512
+ valueKey?: string;
1513
+ colors?: DashboardChartColors;
1514
+ centerLabel?: React.ReactNode;
1515
+ centerValue?: React.ReactNode;
1516
+ showLegend?: boolean;
1517
+ /** Inner radius as percentage string (e.g. "58%") or number */
1518
+ innerRadius?: string | number;
1519
+ /** Outer radius as percentage string (e.g. "82%") or number */
1520
+ outerRadius?: string | number;
1521
+ }
1522
+
1523
+ function DonutBreakdownChart({
1524
+ data,
1525
+ config,
1526
+ nameKey = "name",
1527
+ valueKey = "value",
1528
+ colors,
1529
+ centerLabel,
1530
+ centerValue,
1531
+ showLegend = true,
1532
+ innerRadius = "58%",
1533
+ outerRadius = "82%",
1534
+ isLoading,
1535
+ error,
1536
+ onRetry,
1537
+ retryLabel,
1538
+ emptyTitle,
1539
+ emptyDescription,
1540
+ errorTitle,
1541
+ errorDescription,
1542
+ loadingLabel,
1543
+ stateClassName,
1544
+ className,
1545
+ ...props
1546
+ }: DonutBreakdownChartProps) {
1547
+ const [activeIndex, setActiveIndex] = React.useState(0);
1548
+ const chartKeys = data.map((entry, index) =>
1549
+ String(entry[nameKey] || `segment-${index}`),
1550
+ );
1551
+ const chartConfig = getChartConfigWithColors(config, chartKeys, colors);
1552
+ const chartState = getChartState(
1553
+ {
1554
+ isLoading,
1555
+ error,
1556
+ onRetry,
1557
+ retryLabel,
1558
+ emptyTitle,
1559
+ emptyDescription,
1560
+ errorTitle,
1561
+ errorDescription,
1562
+ loadingLabel,
1563
+ },
1564
+ hasPieData(data, nameKey, valueKey),
1565
+ cn("h-[320px] w-full", stateClassName),
1566
+ );
1567
+
1568
+ if (chartState) {
1569
+ return chartState;
1570
+ }
1571
+
1572
+ return (
1573
+ <ChartContainer
1574
+ config={chartConfig}
1575
+ className={cn("h-[320px] w-full", className)}
1576
+ {...props}
1577
+ >
1578
+ <RechartsPrimitive.PieChart accessibilityLayer>
1579
+ <ChartTooltip
1580
+ cursor={false}
1581
+ content={<ChartTooltipContent hideLabel nameKey={nameKey} />}
1582
+ />
1583
+ {showLegend ? (
1584
+ <ChartLegend
1585
+ content={<ChartLegendContent nameKey={nameKey} />}
1586
+ verticalAlign="bottom"
1587
+ />
1588
+ ) : null}
1589
+ <RechartsPrimitive.Pie
1590
+ data={data}
1591
+ dataKey={valueKey}
1592
+ nameKey={nameKey}
1593
+ innerRadius={innerRadius}
1594
+ outerRadius={outerRadius}
1595
+ paddingAngle={3}
1596
+ activeIndex={activeIndex}
1597
+ onMouseEnter={(_, index) => setActiveIndex(index)}
1598
+ isAnimationActive
1599
+ animationDuration={600}
1600
+ animationEasing="ease-out"
1601
+ >
1602
+ {data.map((entry, index) => {
1603
+ const key = String(entry[nameKey] || `segment-${index}`);
1604
+
1605
+ return (
1606
+ <RechartsPrimitive.Cell
1607
+ key={key}
1608
+ fill={`var(--color-${key})`}
1609
+ opacity={index === activeIndex ? 1 : 0.7}
1610
+ stroke="transparent"
1611
+ />
1612
+ );
1613
+ })}
1614
+ {centerValue || centerLabel ? (
1615
+ <RechartsPrimitive.Label
1616
+ position="center"
1617
+ content={({ viewBox }) => {
1618
+ if (!viewBox || !("cx" in viewBox) || !("cy" in viewBox)) {
1619
+ return null;
1620
+ }
1621
+
1622
+ return (
1623
+ <text
1624
+ x={viewBox.cx}
1625
+ y={viewBox.cy}
1626
+ textAnchor="middle"
1627
+ dominantBaseline="middle"
1628
+ >
1629
+ {centerValue ? (
1630
+ <tspan
1631
+ x={viewBox.cx}
1632
+ y={viewBox.cy}
1633
+ className="fill-foreground text-2xl font-semibold"
1634
+ >
1635
+ {centerValue}
1636
+ </tspan>
1637
+ ) : null}
1638
+ {centerLabel ? (
1639
+ <tspan
1640
+ x={viewBox.cx}
1641
+ y={Number(viewBox.cy) + 22}
1642
+ className="fill-muted-foreground text-xs"
1643
+ >
1644
+ {centerLabel}
1645
+ </tspan>
1646
+ ) : null}
1647
+ </text>
1648
+ );
1649
+ }}
1650
+ />
1651
+ ) : null}
1652
+ </RechartsPrimitive.Pie>
1653
+ </RechartsPrimitive.PieChart>
1654
+ </ChartContainer>
1655
+ );
1656
+ }
1657
+
1658
+ // ─── SparklineChart ───────────────────────────────────────────────────────────
1659
+
1660
+ export interface SparklineChartProps {
1661
+ /** Data array — each item must have the `dataKey` field */
1662
+ data: DashboardChartDatum[];
1663
+ /** Key to plot on the Y axis */
1664
+ dataKey: string;
1665
+ /** Color for the line/area — defaults to `var(--chart-1)` */
1666
+ color?: string;
1667
+ /** Show filled area under the line */
1668
+ filled?: boolean;
1669
+ /** Show the last data point as a dot */
1670
+ showEndDot?: boolean;
1671
+ /** Curve interpolation type */
1672
+ curveType?: ChartCurveType;
1673
+ /** Stroke width */
1674
+ strokeWidth?: number;
1675
+ className?: string;
1676
+ }
1677
+
1678
+ /**
1679
+ * Minimal inline sparkline chart — no axes, no grid, no tooltip.
1680
+ * Ideal for embedding inside stat cards or table cells.
1681
+ *
1682
+ * @ai-rules
1683
+ * - Use `filled` for area-style sparklines.
1684
+ * - Keep `className` height small (e.g. `h-12` or `h-16`).
1685
+ * - Do NOT wrap in `ChartCard` — it is designed to be inline.
1686
+ */
1687
+ function SparklineChart({
1688
+ data,
1689
+ dataKey,
1690
+ color = "var(--chart-1)",
1691
+ filled = true,
1692
+ showEndDot = true,
1693
+ curveType = "monotone",
1694
+ strokeWidth = 2,
1695
+ className,
1696
+ }: SparklineChartProps) {
1697
+ const sparkId = React.useId().replace(/:/g, "");
1698
+ const lastPoint = data[data.length - 1];
1699
+ const lastValue = lastPoint ? lastPoint[dataKey] : undefined;
1700
+
1701
+ return (
1702
+ <div className={cn("relative h-12 w-full min-w-0", className)}>
1703
+ <RechartsPrimitive.ResponsiveContainer width="100%" height="100%">
1704
+ <RechartsPrimitive.AreaChart
1705
+ data={data}
1706
+ margin={{ top: 4, right: showEndDot ? 8 : 0, bottom: 0, left: 0 }}
1707
+ >
1708
+ {filled && (
1709
+ <defs>
1710
+ <linearGradient
1711
+ id={`spark-gradient-${sparkId}`}
1712
+ x1="0"
1713
+ y1="0"
1714
+ x2="0"
1715
+ y2="1"
1716
+ >
1717
+ <stop offset="5%" stopColor={color} stopOpacity={0.3} />
1718
+ <stop offset="95%" stopColor={color} stopOpacity={0} />
1719
+ </linearGradient>
1720
+ </defs>
1721
+ )}
1722
+ <RechartsPrimitive.Area
1723
+ type={curveType}
1724
+ dataKey={dataKey}
1725
+ stroke={color}
1726
+ strokeWidth={strokeWidth}
1727
+ fill={filled ? `url(#spark-gradient-${sparkId})` : "none"}
1728
+ fillOpacity={1}
1729
+ dot={false}
1730
+ activeDot={false}
1731
+ isAnimationActive
1732
+ animationDuration={600}
1733
+ animationEasing="ease-out"
1734
+ />
1735
+ </RechartsPrimitive.AreaChart>
1736
+ </RechartsPrimitive.ResponsiveContainer>
1737
+ {showEndDot && lastValue !== undefined && lastValue !== null && (
1738
+ <div
1739
+ 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"
1740
+ style={{ backgroundColor: color }}
1741
+ />
1742
+ )}
1743
+ </div>
1744
+ );
1745
+ }
1746
+
1747
+ // ─── RadarChart ───────────────────────────────────────────────────────────────
1748
+
1749
+ export interface RadarMetricChartProps extends ChartStateProps {
1750
+ /** Data array — each item is one axis point on the radar */
1751
+ data: DashboardChartDatum[];
1752
+ /** Key in each datum used as the axis label */
1753
+ labelKey: string;
1754
+ /**
1755
+ * Series to render. Each entry maps to one `<Radar>` element.
1756
+ * Use a single entry for a simple radar, multiple for comparison.
1757
+ */
1758
+ series: DashboardChartSeries[];
1759
+ /** Override colors per series key or as an ordered array */
1760
+ colors?: DashboardChartColors;
1761
+ /** Fill the radar polygon (default: true) */
1762
+ filled?: boolean;
1763
+ /** Fill opacity when `filled` is true (default: 0.25) */
1764
+ fillOpacity?: number;
1765
+ /** Show dots on each axis point (default: false) */
1766
+ showDots?: boolean;
1767
+ /** Show the polar grid lines (default: true) */
1768
+ showGrid?: boolean;
1769
+ /** Show the legend (default: true when multiple series) */
1770
+ showLegend?: boolean;
1771
+ /** Format axis tick values */
1772
+ valueFormatter?: ChartValueFormatter;
1773
+ className?: string;
1774
+ }
1775
+
1776
+ function RadarMetricChart({
1777
+ data,
1778
+ labelKey,
1779
+ series,
1780
+ colors,
1781
+ filled = true,
1782
+ fillOpacity = 0.25,
1783
+ showDots = false,
1784
+ showGrid = true,
1785
+ showLegend,
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
+ }: RadarMetricChartProps) {
1799
+ const chartConfig = React.useMemo(
1800
+ () => buildChartConfig(series.map((s) => s.key), colors),
1801
+ [series, colors],
1802
+ );
1803
+
1804
+ const hasData = data.length > 0 && series.length > 0;
1805
+ const chartState = getChartState(
1806
+ { isLoading, error, emptyTitle, emptyDescription, errorTitle, errorDescription, loadingLabel, stateClassName, onRetry, retryLabel },
1807
+ hasData,
1808
+ stateClassName,
1809
+ );
1810
+
1811
+ const displayLegend = showLegend ?? series.length > 1;
1812
+
1813
+ return (
1814
+ <ChartContainer config={chartConfig} className={cn("h-[300px] w-full", className)}>
1815
+ {chartState ?? (
1816
+ <RechartsPrimitive.RadarChart data={data} accessibilityLayer>
1817
+ {showGrid && (
1818
+ <RechartsPrimitive.PolarGrid
1819
+ stroke="var(--border)"
1820
+ strokeOpacity={0.6}
1821
+ />
1822
+ )}
1823
+ <RechartsPrimitive.PolarAngleAxis
1824
+ dataKey={labelKey}
1825
+ tick={{ fill: "var(--muted-foreground)", fontSize: 12 }}
1826
+ />
1827
+ <RechartsPrimitive.PolarRadiusAxis
1828
+ tick={false}
1829
+ axisLine={false}
1830
+ tickFormatter={valueFormatter}
1831
+ />
1832
+ <ChartTooltip
1833
+ content={
1834
+ <ChartTooltipContent
1835
+ formatter={valueFormatter ? (v) => valueFormatter(v as number) : undefined}
1836
+ />
1837
+ }
1838
+ />
1839
+ {displayLegend && (
1840
+ <ChartLegend content={<ChartLegendContent />} />
1841
+ )}
1842
+ {series.map((s) => (
1843
+ <RechartsPrimitive.Radar
1844
+ key={s.key}
1845
+ name={s.label as string ?? s.key}
1846
+ dataKey={s.key}
1847
+ stroke={`var(--color-${s.key})`}
1848
+ fill={filled ? `var(--color-${s.key})` : "none"}
1849
+ fillOpacity={filled ? fillOpacity : 0}
1850
+ dot={showDots ? { r: 3, fill: `var(--color-${s.key})` } : false}
1851
+ isAnimationActive
1852
+ animationDuration={600}
1853
+ animationEasing="ease-out"
1854
+ />
1855
+ ))}
1856
+ </RechartsPrimitive.RadarChart>
1857
+ )}
1858
+ </ChartContainer>
1859
+ );
1860
+ }
1861
+
1862
+ // ─── PieMetricChart ───────────────────────────────────────────────────────────
1863
+
1864
+ export interface PieMetricChartProps extends ChartStateProps {
1865
+ /** Data array — each item is one slice */
1866
+ data: DashboardChartDatum[];
1867
+ /** Key in each datum used as the slice name/label */
1868
+ nameKey: string;
1869
+ /** Key in each datum used as the slice value */
1870
+ valueKey: string;
1871
+ /** Override colors as an ordered array or per-name map */
1872
+ colors?: DashboardChartColors;
1873
+ /** Outer radius of the pie (default: "80%") */
1874
+ outerRadius?: number | string;
1875
+ /** Inner radius — set > 0 to make a donut (default: 0) */
1876
+ innerRadius?: number | string;
1877
+ /** Show percentage labels inside/outside each slice (default: false) */
1878
+ showLabels?: boolean;
1879
+ /** Show the legend (default: true) */
1880
+ showLegend?: boolean;
1881
+ /**
1882
+ * Index of the slice to "explode" (offset outward).
1883
+ * Pass -1 or undefined to disable.
1884
+ */
1885
+ explodeIndex?: number;
1886
+ /** Offset distance for the exploded slice in px (default: 12) */
1887
+ explodeOffset?: number;
1888
+ /** Format tooltip values */
1889
+ valueFormatter?: ChartValueFormatter;
1890
+ className?: string;
1891
+ }
1892
+
1893
+ function PieMetricChart({
1894
+ data,
1895
+ nameKey,
1896
+ valueKey,
1897
+ colors,
1898
+ outerRadius = "80%",
1899
+ innerRadius = 0,
1900
+ showLabels = false,
1901
+ showLegend = true,
1902
+ explodeIndex,
1903
+ explodeOffset = 12,
1904
+ valueFormatter,
1905
+ isLoading,
1906
+ error,
1907
+ onRetry,
1908
+ retryLabel,
1909
+ emptyTitle,
1910
+ emptyDescription,
1911
+ errorTitle,
1912
+ errorDescription,
1913
+ loadingLabel,
1914
+ stateClassName,
1915
+ className,
1916
+ }: PieMetricChartProps) {
1917
+ // Build config from unique name values
1918
+ const names = React.useMemo(
1919
+ () => data.map((d) => String(d[nameKey] ?? "")),
1920
+ [data, nameKey],
1921
+ );
1922
+
1923
+ const chartConfig = React.useMemo(
1924
+ () => buildChartConfig(names, colors),
1925
+ [names, colors],
1926
+ );
1927
+
1928
+ const hasData = hasPieData(data, nameKey, valueKey);
1929
+ const chartState = getChartState(
1930
+ { isLoading, error, emptyTitle, emptyDescription, errorTitle, errorDescription, loadingLabel, stateClassName, onRetry, retryLabel },
1931
+ hasData,
1932
+ stateClassName,
1933
+ );
1934
+
1935
+ return (
1936
+ <ChartContainer config={chartConfig} className={cn("h-[300px] w-full", className)}>
1937
+ {chartState ?? (
1938
+ <RechartsPrimitive.PieChart accessibilityLayer>
1939
+ <ChartTooltip
1940
+ content={
1941
+ <ChartTooltipContent
1942
+ nameKey={nameKey}
1943
+ formatter={valueFormatter ? (v) => valueFormatter(v as number) : undefined}
1944
+ />
1945
+ }
1946
+ />
1947
+ {showLegend && <ChartLegend content={<ChartLegendContent nameKey={nameKey} />} />}
1948
+ <RechartsPrimitive.Pie
1949
+ data={data}
1950
+ dataKey={valueKey}
1951
+ nameKey={nameKey}
1952
+ outerRadius={outerRadius}
1953
+ innerRadius={innerRadius}
1954
+ paddingAngle={2}
1955
+ label={
1956
+ showLabels
1957
+ ? ({ cx, cy, midAngle, innerRadius: ir, outerRadius: or, percent }) => {
1958
+ const RADIAN = Math.PI / 180;
1959
+ const radius = Number(ir) + (Number(or) - Number(ir)) * 1.35;
1960
+ const x = Number(cx) + radius * Math.cos(-midAngle * RADIAN);
1961
+ const y = Number(cy) + radius * Math.sin(-midAngle * RADIAN);
1962
+ return percent > 0.04 ? (
1963
+ <text
1964
+ x={x}
1965
+ y={y}
1966
+ fill="var(--foreground)"
1967
+ textAnchor={x > Number(cx) ? "start" : "end"}
1968
+ dominantBaseline="central"
1969
+ fontSize={12}
1970
+ fontWeight={500}
1971
+ >
1972
+ {`${(percent * 100).toFixed(0)}%`}
1973
+ </text>
1974
+ ) : null;
1975
+ }
1976
+ : false
1977
+ }
1978
+ labelLine={false}
1979
+ isAnimationActive
1980
+ animationDuration={600}
1981
+ animationEasing="ease-out"
1982
+ >
1983
+ {data.map((entry, index) => {
1984
+ const name = String(entry[nameKey] ?? "");
1985
+ const isExploded = index === explodeIndex;
1986
+ return (
1987
+ <RechartsPrimitive.Cell
1988
+ key={`cell-${index}`}
1989
+ fill={chartConfig[name]?.color ?? `var(--chart-${(index % 8) + 1})`}
1990
+ stroke="var(--background)"
1991
+ strokeWidth={2}
1992
+ style={
1993
+ isExploded
1994
+ ? { filter: `drop-shadow(0 4px 8px color-mix(in srgb, ${chartConfig[name]?.color ?? "var(--chart-1)"} 40%, transparent))` }
1995
+ : undefined
1996
+ }
1997
+ />
1998
+ );
1999
+ })}
2000
+ </RechartsPrimitive.Pie>
2001
+ </RechartsPrimitive.PieChart>
2002
+ )}
2003
+ </ChartContainer>
2004
+ );
2005
+ }
2006
+
2007
+ // ─── RadialBarMetricChart ─────────────────────────────────────────────────────
2008
+
2009
+ export interface RadialBarMetricChartProps extends ChartStateProps {
2010
+ /**
2011
+ * Data array. Each item should have a `name` field (for labels) and
2012
+ * the `dataKey` field (for values, 0–100 for percentage-based display).
2013
+ */
2014
+ data: DashboardChartDatum[];
2015
+ /** Key in each datum used as the bar value (default: "value") */
2016
+ dataKey?: string;
2017
+ /** Key in each datum used as the bar label (default: "name") */
2018
+ nameKey?: string;
2019
+ /** Override colors as an ordered array or per-name map */
2020
+ colors?: DashboardChartColors;
2021
+ /** Inner radius of the radial bar (default: "30%") */
2022
+ innerRadius?: number | string;
2023
+ /** Outer radius of the radial bar (default: "100%") */
2024
+ outerRadius?: number | string;
2025
+ /** Start angle in degrees (default: 90 — top) */
2026
+ startAngle?: number;
2027
+ /** End angle in degrees (default: -270 — full circle) */
2028
+ endAngle?: number;
2029
+ /** Show background track behind each bar (default: true) */
2030
+ showBackground?: boolean;
2031
+ /** Show the legend (default: true) */
2032
+ showLegend?: boolean;
2033
+ /** Format tooltip values */
2034
+ valueFormatter?: ChartValueFormatter;
2035
+ className?: string;
2036
+ }
2037
+
2038
+ function RadialBarMetricChart({
2039
+ data,
2040
+ dataKey = "value",
2041
+ nameKey = "name",
2042
+ colors,
2043
+ innerRadius = "30%",
2044
+ outerRadius = "100%",
2045
+ startAngle = 90,
2046
+ endAngle = -270,
2047
+ showBackground = true,
2048
+ showLegend = true,
2049
+ valueFormatter,
2050
+ isLoading,
2051
+ error,
2052
+ onRetry,
2053
+ retryLabel,
2054
+ emptyTitle,
2055
+ emptyDescription,
2056
+ errorTitle,
2057
+ errorDescription,
2058
+ loadingLabel,
2059
+ stateClassName,
2060
+ className,
2061
+ }: RadialBarMetricChartProps) {
2062
+ const names = React.useMemo(
2063
+ () => data.map((d) => String(d[nameKey] ?? "")),
2064
+ [data, nameKey],
2065
+ );
2066
+
2067
+ const chartConfig = React.useMemo(
2068
+ () => buildChartConfig(names, colors),
2069
+ [names, colors],
2070
+ );
2071
+
2072
+ const hasData = data.length > 0;
2073
+ const chartState = getChartState(
2074
+ { isLoading, error, emptyTitle, emptyDescription, errorTitle, errorDescription, loadingLabel, stateClassName, onRetry, retryLabel },
2075
+ hasData,
2076
+ stateClassName,
2077
+ );
2078
+
2079
+ // Inject per-item fill color
2080
+ const coloredData = React.useMemo(
2081
+ () =>
2082
+ data.map((item, index) => {
2083
+ const name = String(item[nameKey] ?? "");
2084
+ return {
2085
+ ...item,
2086
+ fill: chartConfig[name]?.color ?? `var(--chart-${(index % 8) + 1})`,
2087
+ };
2088
+ }),
2089
+ [data, nameKey, chartConfig],
2090
+ );
2091
+
2092
+ return (
2093
+ <div className={cn("flex w-full flex-col gap-3", className)}>
2094
+ <ChartContainer config={chartConfig} className="h-[260px] w-full">
2095
+ {chartState ?? (
2096
+ <RechartsPrimitive.RadialBarChart
2097
+ data={coloredData}
2098
+ innerRadius={innerRadius}
2099
+ outerRadius={outerRadius}
2100
+ startAngle={startAngle}
2101
+ endAngle={endAngle}
2102
+ accessibilityLayer
2103
+ >
2104
+ <ChartTooltip
2105
+ cursor={false}
2106
+ content={
2107
+ <ChartTooltipContent
2108
+ nameKey={nameKey}
2109
+ formatter={valueFormatter ? (v) => valueFormatter(v as number) : undefined}
2110
+ />
2111
+ }
2112
+ />
2113
+ <RechartsPrimitive.RadialBar
2114
+ dataKey={dataKey}
2115
+ background={showBackground ? { fill: "var(--muted)" } : false}
2116
+ cornerRadius={6}
2117
+ isAnimationActive
2118
+ animationDuration={700}
2119
+ animationEasing="ease-out"
2120
+ />
2121
+ </RechartsPrimitive.RadialBarChart>
2122
+ )}
2123
+ </ChartContainer>
2124
+ {showLegend && !chartState && (
2125
+ <div className="flex flex-wrap justify-center gap-x-4 gap-y-1.5 px-2">
2126
+ {coloredData.map((item, index) => {
2127
+ const d = item as DashboardChartDatum & { fill: string };
2128
+ const name = String(d[nameKey] ?? "");
2129
+ const fillColor = d.fill ?? `var(--chart-${(index % 8) + 1})`;
2130
+ const val = d[dataKey];
2131
+ return (
2132
+ <div key={name} className="flex items-center gap-1.5">
2133
+ <span
2134
+ className="inline-block h-2.5 w-2.5 shrink-0 rounded-full"
2135
+ style={{ backgroundColor: fillColor }}
2136
+ />
2137
+ <span className="text-xs text-muted-foreground">
2138
+ {name}
2139
+ {val !== undefined && val !== null && (
2140
+ <span className="ml-1 font-medium text-foreground">
2141
+ {valueFormatter ? valueFormatter(val as number) : String(val)}
2142
+ </span>
2143
+ )}
2144
+ </span>
2145
+ </div>
2146
+ );
2147
+ })}
2148
+ </div>
2149
+ )}
2150
+ </div>
2151
+ );
2152
+ }
2153
+
2154
+ // ─── GaugeChart ───────────────────────────────────────────────────────────────
2155
+
2156
+ export interface GaugeChartThreshold {
2157
+ /** Upper bound of this zone (0–100) */
2158
+ value: number;
2159
+ /** Color for this zone */
2160
+ color: string;
2161
+ /** Optional label for this zone */
2162
+ label?: string;
2163
+ }
2164
+
2165
+ export interface GaugeChartProps {
2166
+ /** Current value (must be within [min, max]) */
2167
+ value: number;
2168
+ /** Minimum value (default: 0) */
2169
+ min?: number;
2170
+ /** Maximum value (default: 100) */
2171
+ max?: number;
2172
+ /**
2173
+ * Color zones. Each threshold defines the upper bound of a zone.
2174
+ * Zones are evaluated in order; the first zone whose `value >= current %`
2175
+ * determines the active color.
2176
+ * If omitted, uses `--chart-1`.
2177
+ */
2178
+ thresholds?: GaugeChartThreshold[];
2179
+ /** Label shown below the value (e.g. "CPU Usage") */
2180
+ label?: React.ReactNode;
2181
+ /** Format the center value text (default: shows percentage) */
2182
+ valueFormatter?: (value: number, percent: number) => string;
2183
+ /** Show the needle indicator (default: true) */
2184
+ showNeedle?: boolean;
2185
+ className?: string;
2186
+ }
2187
+
2188
+ function GaugeChart({
2189
+ value,
2190
+ min = 0,
2191
+ max = 100,
2192
+ thresholds,
2193
+ label,
2194
+ valueFormatter,
2195
+ showNeedle = true,
2196
+ className,
2197
+ }: GaugeChartProps) {
2198
+ const percent = Math.min(1, Math.max(0, (value - min) / (max - min)));
2199
+ const percentInt = Math.round(percent * 100);
2200
+
2201
+ // Determine active color from thresholds
2202
+ const activeColor = React.useMemo(() => {
2203
+ if (!thresholds || thresholds.length === 0) return "var(--chart-1)";
2204
+ for (const t of thresholds) {
2205
+ if (percentInt <= t.value) return t.color;
2206
+ }
2207
+ return thresholds[thresholds.length - 1].color;
2208
+ }, [thresholds, percentInt]);
2209
+
2210
+ const displayText = valueFormatter
2211
+ ? valueFormatter(value, percentInt)
2212
+ : `${percentInt}%`;
2213
+
2214
+ // SVG gauge constants — viewBox is 200×110, arc center at (100, 100)
2215
+ const cx = 100;
2216
+ const cy = 100;
2217
+ const R = 80; // outer radius
2218
+ const r = 54; // inner radius (track width = 26)
2219
+
2220
+ // Helper: polar → cartesian (angle in degrees, 0° = right, CCW)
2221
+ const polar = (angleDeg: number, radius: number) => {
2222
+ const rad = (angleDeg * Math.PI) / 180;
2223
+ return {
2224
+ x: cx + radius * Math.cos(rad),
2225
+ y: cy - radius * Math.sin(rad),
2226
+ };
2227
+ };
2228
+
2229
+ // Semicircle: from 180° (left) to 0° (right)
2230
+ // Track background arc path
2231
+ const trackStart = polar(180, R);
2232
+ const trackEnd = polar(0, R);
2233
+ const trackStartI = polar(180, r);
2234
+ const trackEndI = polar(0, r);
2235
+ const trackPath = [
2236
+ `M ${trackStart.x} ${trackStart.y}`,
2237
+ `A ${R} ${R} 0 0 1 ${trackEnd.x} ${trackEnd.y}`,
2238
+ `L ${trackEndI.x} ${trackEndI.y}`,
2239
+ `A ${r} ${r} 0 0 0 ${trackStartI.x} ${trackStartI.y}`,
2240
+ "Z",
2241
+ ].join(" ");
2242
+
2243
+ // Value arc: from 180° (left) clockwise to valueAngle
2244
+ // The gauge is a semicircle (max 180°), so largeArc is always 0.
2245
+ // largeArc=1 would sweep the reflex arc (>180°) which creates the filled-blob bug.
2246
+ const valueAngle = 180 - percent * 180;
2247
+ const valueEnd = polar(valueAngle, R);
2248
+ const valueEndI = polar(valueAngle, r);
2249
+ const valuePath =
2250
+ percent <= 0
2251
+ ? ""
2252
+ : percent >= 1
2253
+ ? trackPath // full arc = same as track
2254
+ : [
2255
+ `M ${trackStart.x} ${trackStart.y}`,
2256
+ `A ${R} ${R} 0 0 1 ${valueEnd.x} ${valueEnd.y}`,
2257
+ `L ${valueEndI.x} ${valueEndI.y}`,
2258
+ `A ${r} ${r} 0 0 0 ${trackStartI.x} ${trackStartI.y}`,
2259
+ "Z",
2260
+ ].join(" ");
2261
+
2262
+ // Needle: rotates from -90° (left, 0%) to +90° (right, 100%)
2263
+ // In our coordinate system: 180° at 0%, 0° at 100%
2264
+ const needleAngle = 180 - percent * 180;
2265
+ const needleTip = polar(needleAngle, r - 6);
2266
+ const needleBase1 = polar(needleAngle + 90, 5);
2267
+ const needleBase2 = polar(needleAngle - 90, 5);
2268
+
2269
+ return (
2270
+ <div className={cn("flex flex-col items-center gap-2", className)}>
2271
+ <div className="relative w-full max-w-[260px]">
2272
+ <svg
2273
+ viewBox="0 0 200 130"
2274
+ className="w-full"
2275
+ aria-label={`Gauge: ${displayText}`}
2276
+ >
2277
+ {/* Background track */}
2278
+ <path d={trackPath} fill="var(--muted)" />
2279
+
2280
+ {/* Value arc */}
2281
+ {valuePath && (
2282
+ <path d={valuePath} fill={activeColor} />
2283
+ )}
2284
+
2285
+ {/* Needle */}
2286
+ {showNeedle && (
2287
+ <g>
2288
+ <polygon
2289
+ points={`${needleTip.x},${needleTip.y} ${needleBase1.x},${needleBase1.y} ${needleBase2.x},${needleBase2.y}`}
2290
+ fill="var(--foreground)"
2291
+ opacity={0.85}
2292
+ />
2293
+ <circle cx={cx} cy={cy} r={5} fill="var(--foreground)" />
2294
+ </g>
2295
+ )}
2296
+
2297
+ {/* Value text — placed below the arc baseline (cy=100) to avoid needle overlap */}
2298
+ <text
2299
+ x={cx}
2300
+ y={cy + 18}
2301
+ textAnchor="middle"
2302
+ dominantBaseline="middle"
2303
+ fontSize={22}
2304
+ fontWeight={700}
2305
+ fill="currentColor"
2306
+ className="fill-foreground"
2307
+ >
2308
+ {displayText}
2309
+ </text>
2310
+
2311
+ {/* Label below value */}
2312
+ {label && (
2313
+ <text
2314
+ x={cx}
2315
+ y={cy + 36}
2316
+ textAnchor="middle"
2317
+ dominantBaseline="middle"
2318
+ fontSize={10}
2319
+ fill="currentColor"
2320
+ className="fill-muted-foreground"
2321
+ >
2322
+ {label}
2323
+ </text>
2324
+ )}
2325
+ </svg>
2326
+ </div>
2327
+
2328
+ {/* Threshold legend */}
2329
+ {thresholds && thresholds.length > 0 && (
2330
+ <div className="flex flex-wrap justify-center gap-x-3 gap-y-1">
2331
+ {thresholds.map((t, i) => (
2332
+ <div key={i} className="flex items-center gap-1.5">
2333
+ <span
2334
+ className="inline-block h-2 w-2 shrink-0 rounded-full"
2335
+ style={{ backgroundColor: t.color }}
2336
+ />
2337
+ <span className="text-xs text-muted-foreground">{t.label}</span>
2338
+ </div>
2339
+ ))}
2340
+ </div>
2341
+ )}
2342
+ </div>
2343
+ );
2344
+ }
2345
+
2346
+ // ─── Exports ──────────────────────────────────────────────────────────────────
2347
+
2348
+ export {
2349
+ ChartContainer,
2350
+ ChartTooltip,
2351
+ ChartTooltipContent,
2352
+ ChartLegend,
2353
+ ChartLegendContent,
2354
+ ChartStyle,
2355
+ ChartCard,
2356
+ DashboardBarChart,
2357
+ DashboardLineChart,
2358
+ HorizontalBarChart,
2359
+ InteractiveTimeSeriesChart,
2360
+ ComboMetricChart,
2361
+ DonutBreakdownChart,
2362
+ SparklineChart,
2363
+ RadarMetricChart,
2364
+ PieMetricChart,
2365
+ RadialBarMetricChart,
2366
+ GaugeChart,
2367
+ };