xertica-ui 2.2.0 → 2.3.0

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