xertica-ui 2.1.2 → 2.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +1 -1
  3. package/bin/cli.ts +1 -1
  4. package/bin/generate-tokens.ts +13 -7
  5. package/components/assistant/xertica-assistant/index.ts +2 -0
  6. package/components/assistant/xertica-assistant/parts/AssistantCollapsedView.tsx +97 -0
  7. package/components/assistant/xertica-assistant/parts/AssistantConversationList.tsx +104 -0
  8. package/components/assistant/xertica-assistant/parts/AssistantDocumentEditor.tsx +81 -0
  9. package/components/assistant/xertica-assistant/parts/AssistantFeedbackDialog.tsx +86 -0
  10. package/components/assistant/xertica-assistant/parts/AssistantHeader.tsx +77 -0
  11. package/components/assistant/xertica-assistant/parts/AssistantMessageBubble.tsx +573 -0
  12. package/components/assistant/xertica-assistant/parts/AssistantTabBar.tsx +65 -0
  13. package/components/assistant/xertica-assistant/parts/AssistantTypingIndicator.tsx +41 -0
  14. package/components/assistant/xertica-assistant/parts/AssistantWelcomeScreen.tsx +98 -0
  15. package/components/assistant/xertica-assistant/parts/index.ts +16 -0
  16. package/components/assistant/xertica-assistant/types.ts +139 -0
  17. package/components/assistant/xertica-assistant/use-assistant.ts +559 -0
  18. package/components/assistant/xertica-assistant/xertica-assistant.stories.tsx +200 -0
  19. package/components/assistant/xertica-assistant/xertica-assistant.tsx +198 -1460
  20. package/components/brand/theme-toggle/ThemeToggle.tsx +8 -27
  21. package/components/hooks/index.ts +3 -0
  22. package/components/hooks/use-layout-shortcuts.ts +46 -0
  23. package/components/layout/sidebar/index.ts +2 -0
  24. package/components/layout/sidebar/sidebar.stories.tsx +160 -8
  25. package/components/layout/sidebar/sidebar.tsx +606 -497
  26. package/components/layout/sidebar/use-sidebar.ts +104 -0
  27. package/components/media/audio-player/AudioPlayer.tsx +131 -206
  28. package/components/media/audio-player/use-audio-player.ts +298 -0
  29. package/components/pages/home-page/HomePage.tsx +1 -1
  30. package/components/pages/template-content/TemplateContent.tsx +5 -5
  31. package/components/pages/template-page/TemplatePage.tsx +5 -5
  32. package/components/shared/CustomTooltipContent.tsx +52 -0
  33. package/components/shared/layout-constants.ts +1 -1
  34. package/components/ui/chart/chart.stories.tsx +966 -7
  35. package/components/ui/chart/chart.tsx +918 -45
  36. package/components/ui/file-upload/file-upload.stories.tsx +100 -0
  37. package/components/ui/file-upload/file-upload.tsx +14 -74
  38. package/components/ui/file-upload/index.ts +1 -0
  39. package/components/ui/file-upload/use-file-upload.ts +181 -0
  40. package/components/ui/pagination/index.ts +2 -0
  41. package/components/ui/pagination/pagination.stories.tsx +94 -0
  42. package/components/ui/pagination/use-pagination.ts +194 -0
  43. package/components/ui/rich-text-editor/index.ts +2 -0
  44. package/components/ui/rich-text-editor/rich-text-editor.stories.tsx +129 -1
  45. package/components/ui/rich-text-editor/rich-text-editor.tsx +86 -305
  46. package/components/ui/rich-text-editor/use-rich-text-editor.ts +439 -0
  47. package/components/ui/stepper/index.ts +3 -1
  48. package/components/ui/stepper/stepper.stories.tsx +116 -0
  49. package/components/ui/stepper/stepper.tsx +4 -4
  50. package/components/ui/stepper/use-stepper.ts +137 -0
  51. package/components/ui/tree-view/index.ts +4 -1
  52. package/components/ui/tree-view/tree-view.stories.tsx +110 -4
  53. package/components/ui/tree-view/tree-view.tsx +17 -125
  54. package/components/ui/tree-view/use-tree-view.ts +229 -0
  55. package/contexts/AssistenteContext.tsx +17 -54
  56. package/contexts/BrandColorsContext.tsx +6 -17
  57. package/contexts/LayoutContext.tsx +5 -31
  58. package/dist/AssistantChart-BAudAfne.cjs +3591 -0
  59. package/dist/AssistantChart-BP8upjMk.js +3565 -0
  60. package/dist/AudioPlayer-1ypwE2Wh.cjs +936 -0
  61. package/dist/AudioPlayer-DuKXrCfy.js +937 -0
  62. package/dist/CustomTooltipContent-DHjkY0ww.js +40 -0
  63. package/dist/CustomTooltipContent-c_K-DWRr.cjs +56 -0
  64. package/dist/LanguageContext-BwhwC3G2.js +657 -0
  65. package/dist/LanguageContext-DvUt5jBg.cjs +656 -0
  66. package/dist/LayoutContext-BDmcZfMH.cjs +84 -0
  67. package/dist/LayoutContext-dbQvdC4O.js +85 -0
  68. package/dist/ThemeContext-RTy1m2Uq.js +82 -0
  69. package/dist/ThemeContext-bSzuOit2.cjs +81 -0
  70. package/dist/VerifyEmailPage-C_ihbcth.js +2828 -0
  71. package/dist/VerifyEmailPage-Dt7zgA4w.cjs +2827 -0
  72. package/dist/XerticaProvider-CW9hpCdF.cjs +39 -0
  73. package/dist/XerticaProvider-siSt9uG2.js +40 -0
  74. package/dist/XerticaXLogo-D8jf0SNv.cjs +214 -0
  75. package/dist/XerticaXLogo-fAJMy3H4.js +215 -0
  76. package/dist/assistant.cjs.js +2 -1
  77. package/dist/assistant.es.js +3 -2
  78. package/dist/brand.cjs.js +2 -2
  79. package/dist/brand.es.js +2 -2
  80. package/dist/cli.js +14 -8
  81. package/dist/components/assistant/xertica-assistant/index.d.ts +2 -0
  82. package/dist/components/assistant/xertica-assistant/parts/AssistantCollapsedView.d.ts +13 -0
  83. package/dist/components/assistant/xertica-assistant/parts/AssistantConversationList.d.ts +16 -0
  84. package/dist/components/assistant/xertica-assistant/parts/AssistantDocumentEditor.d.ts +17 -0
  85. package/dist/components/assistant/xertica-assistant/parts/AssistantFeedbackDialog.d.ts +19 -0
  86. package/dist/components/assistant/xertica-assistant/parts/AssistantHeader.d.ts +11 -0
  87. package/dist/components/assistant/xertica-assistant/parts/AssistantMessageBubble.d.ts +29 -0
  88. package/dist/components/assistant/xertica-assistant/parts/AssistantTabBar.d.ts +13 -0
  89. package/dist/components/assistant/xertica-assistant/parts/AssistantTypingIndicator.d.ts +4 -0
  90. package/dist/components/assistant/xertica-assistant/parts/AssistantWelcomeScreen.d.ts +17 -0
  91. package/dist/components/assistant/xertica-assistant/parts/index.d.ts +16 -0
  92. package/dist/components/assistant/xertica-assistant/types.d.ts +106 -0
  93. package/dist/components/assistant/xertica-assistant/use-assistant.d.ts +125 -0
  94. package/dist/components/assistant/xertica-assistant/xertica-assistant.d.ts +8 -97
  95. package/dist/components/hooks/index.d.ts +3 -0
  96. package/dist/components/hooks/use-layout-shortcuts.d.ts +22 -0
  97. package/dist/components/layout/sidebar/index.d.ts +2 -0
  98. package/dist/components/layout/sidebar/sidebar.d.ts +80 -0
  99. package/dist/components/layout/sidebar/use-sidebar.d.ts +22 -0
  100. package/dist/components/media/audio-player/AudioPlayer.d.ts +4 -1
  101. package/dist/components/media/audio-player/use-audio-player.d.ts +72 -0
  102. package/dist/components/shared/CustomTooltipContent.d.ts +20 -0
  103. package/dist/components/shared/layout-constants.d.ts +1 -1
  104. package/dist/components/ui/alert/alert.d.ts +1 -1
  105. package/dist/components/ui/badge/badge.d.ts +1 -1
  106. package/dist/components/ui/button/button.d.ts +2 -2
  107. package/dist/components/ui/chart/chart.d.ts +162 -5
  108. package/dist/components/ui/file-upload/file-upload.d.ts +2 -0
  109. package/dist/components/ui/file-upload/index.d.ts +1 -0
  110. package/dist/components/ui/file-upload/use-file-upload.d.ts +49 -0
  111. package/dist/components/ui/pagination/index.d.ts +2 -0
  112. package/dist/components/ui/pagination/use-pagination.d.ts +78 -0
  113. package/dist/components/ui/rich-text-editor/index.d.ts +2 -0
  114. package/dist/components/ui/rich-text-editor/use-rich-text-editor.d.ts +107 -0
  115. package/dist/components/ui/stepper/index.d.ts +3 -1
  116. package/dist/components/ui/stepper/stepper.d.ts +2 -2
  117. package/dist/components/ui/stepper/use-stepper.d.ts +60 -0
  118. package/dist/components/ui/tree-view/index.d.ts +4 -1
  119. package/dist/components/ui/tree-view/tree-view.d.ts +4 -6
  120. package/dist/components/ui/tree-view/use-tree-view.d.ts +60 -0
  121. package/dist/contexts/AssistenteContext.d.ts +10 -49
  122. package/dist/hooks.cjs.js +30 -10
  123. package/dist/hooks.es.js +25 -4
  124. package/dist/index.cjs.js +20 -9
  125. package/dist/index.es.js +38 -27
  126. package/dist/layout.cjs.js +82 -1
  127. package/dist/layout.es.js +83 -2
  128. package/dist/media.cjs.js +1 -1
  129. package/dist/media.es.js +1 -1
  130. package/dist/pages.cjs.js +1 -1
  131. package/dist/pages.es.js +1 -1
  132. package/dist/rich-text-editor-BmsjY03B.js +2949 -0
  133. package/dist/rich-text-editor-GS2kpTAK.cjs +2966 -0
  134. package/dist/sidebar-CVUGHOS_.cjs +756 -0
  135. package/dist/sidebar-CmvwjnVb.js +757 -0
  136. package/dist/ui.cjs.js +12 -2
  137. package/dist/ui.es.js +24 -14
  138. package/dist/use-audio-player-Bkh23vQ3.js +177 -0
  139. package/dist/use-audio-player-Dn1NR9xN.cjs +176 -0
  140. package/dist/utils/color-utils.d.ts +51 -0
  141. package/dist/xertica-assistant-BMqdyRVi.js +2082 -0
  142. package/dist/xertica-assistant-Bj3vBCq_.cjs +2081 -0
  143. package/dist/xertica-ui.css +1 -1
  144. package/docs/ai-usage.md +28 -10
  145. package/docs/architecture-improvements.md +463 -0
  146. package/docs/architecture.md +77 -1
  147. package/docs/components/assistant-chart.md +1 -1
  148. package/docs/components/assistant.md +159 -0
  149. package/docs/components/audio-player.md +46 -0
  150. package/docs/components/branding.md +251 -0
  151. package/docs/components/chart.md +354 -39
  152. package/docs/components/code-block.md +108 -0
  153. package/docs/components/file-upload.md +119 -2
  154. package/docs/components/formatted-document.md +113 -0
  155. package/docs/components/hooks.md +430 -0
  156. package/docs/components/image-with-fallback.md +106 -0
  157. package/docs/components/map-layers.md +140 -0
  158. package/docs/components/modern-chat-input.md +163 -0
  159. package/docs/components/pages.md +351 -0
  160. package/docs/components/pagination.md +187 -0
  161. package/docs/components/rich-text-editor.md +164 -0
  162. package/docs/components/sidebar.md +153 -4
  163. package/docs/components/stepper.md +157 -12
  164. package/docs/components/tree-view.md +164 -6
  165. package/docs/doc-audit.md +223 -0
  166. package/docs/getting-started.md +155 -1
  167. package/docs/guidelines.md +14 -8
  168. package/docs/layout.md +2 -2
  169. package/docs/llms.md +29 -9
  170. package/docs/patterns/detail-page.md +276 -0
  171. package/docs/patterns/settings.md +346 -0
  172. package/docs/patterns/wizard.md +217 -0
  173. package/guidelines/Guidelines.md +5 -3
  174. package/llms.txt +1 -1
  175. package/package.json +10 -10
  176. package/styles/xertica/tokens.css +41 -12
  177. package/templates/CLAUDE.md +16 -6
  178. package/templates/guidelines/Guidelines.md +16 -4
  179. package/templates/package.json +3 -3
  180. package/templates/src/styles/xertica/tokens.css +39 -10
  181. package/utils/color-utils.ts +72 -0
@@ -71,6 +71,16 @@ export type ChartPeriod = {
71
71
 
72
72
  export type ChartBarSize = "sm" | "md" | "lg" | "xl" | number;
73
73
 
74
+ /** Curve interpolation type for line and area charts */
75
+ export type ChartCurveType =
76
+ | "monotone"
77
+ | "linear"
78
+ | "step"
79
+ | "stepBefore"
80
+ | "stepAfter"
81
+ | "natural"
82
+ | "basis";
83
+
74
84
  type ChartValueFormatter = (value: number | string) => string;
75
85
 
76
86
  export type ChartErrorState = boolean | string | Error | React.ReactNode;
@@ -100,6 +110,9 @@ const defaultChartColors = [
100
110
  "var(--chart-3)",
101
111
  "var(--chart-4)",
102
112
  "var(--chart-5)",
113
+ "var(--chart-6)",
114
+ "var(--chart-7)",
115
+ "var(--chart-8)",
103
116
  ];
104
117
 
105
118
  const chartBarSizes: Record<Exclude<ChartBarSize, number>, number> = {
@@ -188,13 +201,13 @@ const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
188
201
  ([theme, prefix]) => `
189
202
  ${prefix} [data-chart=${id}] {
190
203
  ${colorConfig
191
- .map(([key, itemConfig]) => {
192
- const color =
193
- itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
194
- itemConfig.color;
195
- return color ? ` --color-${key}: ${color};` : null;
196
- })
197
- .join("\n")}
204
+ .map(([key, itemConfig]) => {
205
+ const color =
206
+ itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
207
+ itemConfig.color;
208
+ return color ? ` --color-${key}: ${color};` : null;
209
+ })
210
+ .join("\n")}
198
211
  }
199
212
  `,
200
213
  )
@@ -275,7 +288,7 @@ function ChartTooltipContent({
275
288
  return (
276
289
  <div
277
290
  className={cn(
278
- "border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
291
+ "border-border/50 bg-background/95 backdrop-blur-sm grid min-w-[8rem] items-start gap-1.5 rounded-xl border px-3 py-2 text-xs shadow-xl",
279
292
  className,
280
293
  )}
281
294
  >
@@ -335,7 +348,7 @@ function ChartTooltipContent({
335
348
  </span>
336
349
  </div>
337
350
  {item.value && (
338
- <span className="text-foreground font-mono font-medium tabular-nums">
351
+ <span className="text-foreground font-mono font-semibold tabular-nums">
339
352
  {item.value.toLocaleString()}
340
353
  </span>
341
354
  )}
@@ -392,13 +405,13 @@ function ChartLegendContent({
392
405
  <itemConfig.icon />
393
406
  ) : (
394
407
  <div
395
- className="h-2 w-2 shrink-0 rounded-[2px] bg-(--color-bg)"
408
+ className="h-2 w-2 shrink-0 rounded-full bg-(--color-bg)"
396
409
  style={{
397
410
  "--color-bg": item.color || `var(--color-${key})`,
398
411
  } as React.CSSProperties}
399
412
  />
400
413
  )}
401
- <span>{itemConfig?.label || item.value}</span>
414
+ <span className="text-muted-foreground text-xs">{itemConfig?.label || item.value}</span>
402
415
  </div>
403
416
  );
404
417
  })}
@@ -418,8 +431,8 @@ function getPayloadConfigFromPayload(
418
431
 
419
432
  const payloadPayload =
420
433
  "payload" in payload &&
421
- typeof payload.payload === "object" &&
422
- payload.payload !== null
434
+ typeof payload.payload === "object" &&
435
+ payload.payload !== null
423
436
  ? payload.payload
424
437
  : undefined;
425
438
 
@@ -494,6 +507,20 @@ function getChartConfigWithColors(
494
507
  }, {});
495
508
  }
496
509
 
510
+ /** Build a ChartConfig from a list of keys (no pre-existing config needed). */
511
+ function buildChartConfig(
512
+ keys: string[],
513
+ colors?: DashboardChartColors,
514
+ ): ChartConfig {
515
+ return keys.reduce<ChartConfig>((cfg, key, index) => {
516
+ cfg[key] = {
517
+ label: key,
518
+ color: getSeriesColor(key, index, colors),
519
+ };
520
+ return cfg;
521
+ }, {});
522
+ }
523
+
497
524
  function formatTick(value: number | string) {
498
525
  if (typeof value !== "number") {
499
526
  return value;
@@ -700,9 +727,51 @@ function ChartCard({
700
727
  );
701
728
  }
702
729
 
730
+ // ─── Gradient defs helper ────────────────────────────────────────────────────
731
+
732
+ /**
733
+ * Renders SVG `<defs>` with linear gradients for each series key.
734
+ * Used internally by area charts when `gradientFill` is enabled.
735
+ */
736
+ function AreaGradientDefs({
737
+ seriesKeys,
738
+ opacity = 0.3,
739
+ }: {
740
+ seriesKeys: string[];
741
+ opacity?: number;
742
+ }) {
743
+ return (
744
+ <defs>
745
+ {seriesKeys.map((key) => (
746
+ <linearGradient
747
+ key={key}
748
+ id={`gradient-${key}`}
749
+ x1="0"
750
+ y1="0"
751
+ x2="0"
752
+ y2="1"
753
+ >
754
+ <stop
755
+ offset="5%"
756
+ stopColor={`var(--color-${key})`}
757
+ stopOpacity={opacity}
758
+ />
759
+ <stop
760
+ offset="95%"
761
+ stopColor={`var(--color-${key})`}
762
+ stopOpacity={0}
763
+ />
764
+ </linearGradient>
765
+ ))}
766
+ </defs>
767
+ );
768
+ }
769
+
770
+ // ─── DashboardBarChart ────────────────────────────────────────────────────────
771
+
703
772
  export interface DashboardBarChartProps
704
773
  extends Omit<React.ComponentProps<typeof ChartContainer>, "children">,
705
- ChartStateProps {
774
+ ChartStateProps {
706
775
  data: DashboardChartDatum[];
707
776
  indexKey?: string;
708
777
  series?: DashboardChartSeries[];
@@ -770,9 +839,14 @@ function DashboardBarChart({
770
839
  className={cn("h-[320px] w-full", className)}
771
840
  {...props}
772
841
  >
773
- <RechartsPrimitive.BarChart data={data} accessibilityLayer>
842
+ <RechartsPrimitive.BarChart data={data} accessibilityLayer barGap={4}>
774
843
  {showGrid ? (
775
- <RechartsPrimitive.CartesianGrid vertical={false} />
844
+ <RechartsPrimitive.CartesianGrid
845
+ vertical={false}
846
+ strokeDasharray="3 3"
847
+ stroke="var(--border)"
848
+ strokeOpacity={0.5}
849
+ />
776
850
  ) : null}
777
851
  <RechartsPrimitive.XAxis
778
852
  dataKey={indexKey}
@@ -786,7 +860,10 @@ function DashboardBarChart({
786
860
  tickMargin={8}
787
861
  tickFormatter={valueFormatter}
788
862
  />
789
- <ChartTooltip content={<ChartTooltipContent />} />
863
+ <ChartTooltip
864
+ cursor={{ fill: "var(--muted)", opacity: 0.4, radius: 4 }}
865
+ content={<ChartTooltipContent />}
866
+ />
790
867
  {showLegend ? (
791
868
  <ChartLegend content={<ChartLegendContent />} />
792
869
  ) : null}
@@ -808,6 +885,9 @@ function DashboardBarChart({
808
885
  radius={isTop ? [4, 4, 0, 0] : [0, 0, 0, 0]}
809
886
  barSize={getBarSize(barSize)}
810
887
  stackId={stacked ? item.stackId || "total" : item.stackId}
888
+ isAnimationActive
889
+ animationDuration={600}
890
+ animationEasing="ease-out"
811
891
  />
812
892
  );
813
893
  });
@@ -817,9 +897,11 @@ function DashboardBarChart({
817
897
  );
818
898
  }
819
899
 
900
+ // ─── DashboardLineChart ───────────────────────────────────────────────────────
901
+
820
902
  export interface DashboardLineChartProps
821
903
  extends Omit<React.ComponentProps<typeof ChartContainer>, "children">,
822
- ChartStateProps {
904
+ ChartStateProps {
823
905
  data: DashboardChartDatum[];
824
906
  indexKey?: string;
825
907
  series?: DashboardChartSeries[];
@@ -827,6 +909,8 @@ export interface DashboardLineChartProps
827
909
  showDots?: boolean;
828
910
  showGrid?: boolean;
829
911
  showLegend?: boolean;
912
+ curveType?: ChartCurveType;
913
+ strokeWidth?: number;
830
914
  valueFormatter?: ChartValueFormatter;
831
915
  }
832
916
 
@@ -839,6 +923,8 @@ function DashboardLineChart({
839
923
  showDots = false,
840
924
  showGrid = true,
841
925
  showLegend = true,
926
+ curveType = "monotone",
927
+ strokeWidth = 2,
842
928
  valueFormatter = formatTick,
843
929
  isLoading,
844
930
  error,
@@ -887,7 +973,12 @@ function DashboardLineChart({
887
973
  >
888
974
  <RechartsPrimitive.LineChart data={data} accessibilityLayer>
889
975
  {showGrid ? (
890
- <RechartsPrimitive.CartesianGrid vertical={false} />
976
+ <RechartsPrimitive.CartesianGrid
977
+ vertical={false}
978
+ strokeDasharray="3 3"
979
+ stroke="var(--border)"
980
+ strokeOpacity={0.5}
981
+ />
891
982
  ) : null}
892
983
  <RechartsPrimitive.XAxis
893
984
  dataKey={indexKey}
@@ -908,13 +999,16 @@ function DashboardLineChart({
908
999
  {chartSeries.map((item) => (
909
1000
  <RechartsPrimitive.Line
910
1001
  key={item.key}
911
- type="monotone"
1002
+ type={curveType}
912
1003
  dataKey={item.key}
913
1004
  stroke={`var(--color-${item.key})`}
914
- strokeWidth={2}
915
- dot={showDots ? { r: 3, fill: `var(--color-${item.key})` } : false}
916
- activeDot={{ r: 5 }}
1005
+ strokeWidth={strokeWidth}
1006
+ dot={showDots ? { r: 3, fill: `var(--color-${item.key})`, strokeWidth: 0 } : false}
1007
+ activeDot={{ r: 5, strokeWidth: 0 }}
917
1008
  yAxisId={item.yAxisId}
1009
+ isAnimationActive
1010
+ animationDuration={600}
1011
+ animationEasing="ease-out"
918
1012
  />
919
1013
  ))}
920
1014
  </RechartsPrimitive.LineChart>
@@ -922,9 +1016,11 @@ function DashboardLineChart({
922
1016
  );
923
1017
  }
924
1018
 
1019
+ // ─── HorizontalBarChart ───────────────────────────────────────────────────────
1020
+
925
1021
  export interface HorizontalBarChartProps
926
1022
  extends Omit<React.ComponentProps<typeof ChartContainer>, "children">,
927
- ChartStateProps {
1023
+ ChartStateProps {
928
1024
  data: DashboardChartDatum[];
929
1025
  indexKey?: string;
930
1026
  series?: DashboardChartSeries[];
@@ -1001,7 +1097,12 @@ function HorizontalBarChart({
1001
1097
  margin={{ left: 8, right: 16 }}
1002
1098
  >
1003
1099
  {showGrid ? (
1004
- <RechartsPrimitive.CartesianGrid horizontal={false} />
1100
+ <RechartsPrimitive.CartesianGrid
1101
+ horizontal={false}
1102
+ strokeDasharray="3 3"
1103
+ stroke="var(--border)"
1104
+ strokeOpacity={0.5}
1105
+ />
1005
1106
  ) : null}
1006
1107
  <RechartsPrimitive.XAxis
1007
1108
  type="number"
@@ -1030,6 +1131,9 @@ function HorizontalBarChart({
1030
1131
  radius={[0, 4, 4, 0]}
1031
1132
  barSize={getBarSize(barSize)}
1032
1133
  stackId={stacked ? item.stackId || "total" : item.stackId}
1134
+ isAnimationActive
1135
+ animationDuration={600}
1136
+ animationEasing="ease-out"
1033
1137
  />
1034
1138
  ))}
1035
1139
  </RechartsPrimitive.BarChart>
@@ -1037,9 +1141,11 @@ function HorizontalBarChart({
1037
1141
  );
1038
1142
  }
1039
1143
 
1144
+ // ─── InteractiveTimeSeriesChart ───────────────────────────────────────────────
1145
+
1040
1146
  export interface InteractiveTimeSeriesChartProps
1041
1147
  extends Omit<React.ComponentProps<typeof ChartContainer>, "children">,
1042
- ChartStateProps {
1148
+ ChartStateProps {
1043
1149
  data: DashboardChartDatum[];
1044
1150
  indexKey?: string;
1045
1151
  series?: DashboardChartSeries[];
@@ -1053,6 +1159,10 @@ export interface InteractiveTimeSeriesChartProps
1053
1159
  ) => DashboardChartDatum[];
1054
1160
  showGrid?: boolean;
1055
1161
  showLegend?: boolean;
1162
+ showDots?: boolean;
1163
+ gradientFill?: boolean;
1164
+ curveType?: ChartCurveType;
1165
+ strokeWidth?: number;
1056
1166
  valueFormatter?: ChartValueFormatter;
1057
1167
  }
1058
1168
 
@@ -1068,6 +1178,10 @@ function InteractiveTimeSeriesChart({
1068
1178
  filterData = defaultFilterData,
1069
1179
  showGrid = true,
1070
1180
  showLegend = false,
1181
+ showDots = false,
1182
+ gradientFill = true,
1183
+ curveType = "monotone",
1184
+ strokeWidth = 2,
1071
1185
  valueFormatter = formatTick,
1072
1186
  isLoading,
1073
1187
  error,
@@ -1121,7 +1235,7 @@ function InteractiveTimeSeriesChart({
1121
1235
  <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
1122
1236
  {chartSeries.length > 1 ? (
1123
1237
  <div
1124
- className="inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground"
1238
+ className="inline-flex h-10 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground"
1125
1239
  role="group"
1126
1240
  aria-label="Selecionar metrica do grafico"
1127
1241
  >
@@ -1137,7 +1251,7 @@ function InteractiveTimeSeriesChart({
1137
1251
  aria-pressed={activeSeries === item.key}
1138
1252
  onClick={() => setActiveSeries(item.key)}
1139
1253
  className={cn(
1140
- "h-8 px-3 text-sm font-medium",
1254
+ "h-8 px-3 text-sm font-medium transition-all",
1141
1255
  activeSeries === item.key
1142
1256
  ? "bg-background text-foreground shadow-sm"
1143
1257
  : "text-muted-foreground hover:text-foreground",
@@ -1171,8 +1285,19 @@ function InteractiveTimeSeriesChart({
1171
1285
  {...props}
1172
1286
  >
1173
1287
  <RechartsPrimitive.AreaChart data={filteredData} accessibilityLayer>
1288
+ {gradientFill && (
1289
+ <AreaGradientDefs
1290
+ seriesKeys={visibleSeries.map((s) => s.key)}
1291
+ opacity={0.4}
1292
+ />
1293
+ )}
1174
1294
  {showGrid ? (
1175
- <RechartsPrimitive.CartesianGrid vertical={false} />
1295
+ <RechartsPrimitive.CartesianGrid
1296
+ vertical={false}
1297
+ strokeDasharray="3 3"
1298
+ stroke="var(--border)"
1299
+ strokeOpacity={0.5}
1300
+ />
1176
1301
  ) : null}
1177
1302
  <RechartsPrimitive.XAxis
1178
1303
  dataKey={indexKey}
@@ -1193,13 +1318,21 @@ function InteractiveTimeSeriesChart({
1193
1318
  {visibleSeries.map((item) => (
1194
1319
  <RechartsPrimitive.Area
1195
1320
  key={item.key}
1196
- type="monotone"
1321
+ type={curveType}
1197
1322
  dataKey={item.key}
1198
1323
  stroke={`var(--color-${item.key})`}
1199
- fill={`var(--color-${item.key})`}
1200
- fillOpacity={0.16}
1201
- strokeWidth={2}
1202
- dot={false}
1324
+ fill={
1325
+ gradientFill
1326
+ ? `url(#gradient-${item.key})`
1327
+ : `var(--color-${item.key})`
1328
+ }
1329
+ fillOpacity={gradientFill ? 1 : 0.16}
1330
+ strokeWidth={strokeWidth}
1331
+ dot={showDots ? { r: 3, fill: `var(--color-${item.key})`, strokeWidth: 0 } : false}
1332
+ activeDot={{ r: 5, strokeWidth: 0 }}
1333
+ isAnimationActive
1334
+ animationDuration={600}
1335
+ animationEasing="ease-out"
1203
1336
  />
1204
1337
  ))}
1205
1338
  </RechartsPrimitive.AreaChart>
@@ -1209,9 +1342,11 @@ function InteractiveTimeSeriesChart({
1209
1342
  );
1210
1343
  }
1211
1344
 
1345
+ // ─── ComboMetricChart ─────────────────────────────────────────────────────────
1346
+
1212
1347
  export interface ComboMetricChartProps
1213
1348
  extends Omit<React.ComponentProps<typeof ChartContainer>, "children">,
1214
- ChartStateProps {
1349
+ ChartStateProps {
1215
1350
  data: DashboardChartDatum[];
1216
1351
  indexKey?: string;
1217
1352
  series?: DashboardChartSeries[];
@@ -1219,6 +1354,8 @@ export interface ComboMetricChartProps
1219
1354
  barSize?: ChartBarSize;
1220
1355
  showGrid?: boolean;
1221
1356
  showLegend?: boolean;
1357
+ gradientFill?: boolean;
1358
+ curveType?: ChartCurveType;
1222
1359
  valueFormatter?: ChartValueFormatter;
1223
1360
  }
1224
1361
 
@@ -1231,6 +1368,8 @@ function ComboMetricChart({
1231
1368
  barSize = "md",
1232
1369
  showGrid = true,
1233
1370
  showLegend = true,
1371
+ gradientFill = false,
1372
+ curveType = "monotone",
1234
1373
  valueFormatter = formatTick,
1235
1374
  isLoading,
1236
1375
  error,
@@ -1271,6 +1410,10 @@ function ComboMetricChart({
1271
1410
  return chartState;
1272
1411
  }
1273
1412
 
1413
+ const areaSeries = gradientFill
1414
+ ? chartSeries.filter((s) => s.type === "area").map((s) => s.key)
1415
+ : [];
1416
+
1274
1417
  return (
1275
1418
  <ChartContainer
1276
1419
  config={chartConfig}
@@ -1278,8 +1421,16 @@ function ComboMetricChart({
1278
1421
  {...props}
1279
1422
  >
1280
1423
  <RechartsPrimitive.ComposedChart data={data} accessibilityLayer>
1424
+ {gradientFill && areaSeries.length > 0 && (
1425
+ <AreaGradientDefs seriesKeys={areaSeries} opacity={0.35} />
1426
+ )}
1281
1427
  {showGrid ? (
1282
- <RechartsPrimitive.CartesianGrid vertical={false} />
1428
+ <RechartsPrimitive.CartesianGrid
1429
+ vertical={false}
1430
+ strokeDasharray="3 3"
1431
+ stroke="var(--border)"
1432
+ strokeOpacity={0.5}
1433
+ />
1283
1434
  ) : null}
1284
1435
  <RechartsPrimitive.XAxis
1285
1436
  dataKey={indexKey}
@@ -1301,24 +1452,36 @@ function ComboMetricChart({
1301
1452
  item.type === "line" ? (
1302
1453
  <RechartsPrimitive.Line
1303
1454
  key={item.key}
1304
- type="monotone"
1455
+ type={curveType}
1305
1456
  dataKey={item.key}
1306
1457
  stroke={`var(--color-${item.key})`}
1307
1458
  strokeWidth={2}
1308
1459
  dot={false}
1460
+ activeDot={{ r: 5, strokeWidth: 0 }}
1309
1461
  yAxisId={item.yAxisId}
1462
+ isAnimationActive
1463
+ animationDuration={600}
1464
+ animationEasing="ease-out"
1310
1465
  />
1311
1466
  ) : item.type === "area" ? (
1312
1467
  <RechartsPrimitive.Area
1313
1468
  key={item.key}
1314
- type="monotone"
1469
+ type={curveType}
1315
1470
  dataKey={item.key}
1316
1471
  stroke={`var(--color-${item.key})`}
1317
- fill={`var(--color-${item.key})`}
1318
- fillOpacity={0.12}
1472
+ fill={
1473
+ gradientFill
1474
+ ? `url(#gradient-${item.key})`
1475
+ : `var(--color-${item.key})`
1476
+ }
1477
+ fillOpacity={gradientFill ? 1 : 0.12}
1319
1478
  strokeWidth={2}
1320
1479
  dot={false}
1480
+ activeDot={{ r: 5, strokeWidth: 0 }}
1321
1481
  yAxisId={item.yAxisId}
1482
+ isAnimationActive
1483
+ animationDuration={600}
1484
+ animationEasing="ease-out"
1322
1485
  />
1323
1486
  ) : (
1324
1487
  <RechartsPrimitive.Bar
@@ -1328,6 +1491,9 @@ function ComboMetricChart({
1328
1491
  radius={[4, 4, 0, 0]}
1329
1492
  barSize={getBarSize(barSize)}
1330
1493
  yAxisId={item.yAxisId}
1494
+ isAnimationActive
1495
+ animationDuration={600}
1496
+ animationEasing="ease-out"
1331
1497
  />
1332
1498
  ),
1333
1499
  )}
@@ -1336,9 +1502,11 @@ function ComboMetricChart({
1336
1502
  );
1337
1503
  }
1338
1504
 
1505
+ // ─── DonutBreakdownChart ──────────────────────────────────────────────────────
1506
+
1339
1507
  export interface DonutBreakdownChartProps
1340
1508
  extends Omit<React.ComponentProps<typeof ChartContainer>, "children">,
1341
- ChartStateProps {
1509
+ ChartStateProps {
1342
1510
  data: DashboardChartDatum[];
1343
1511
  nameKey?: string;
1344
1512
  valueKey?: string;
@@ -1346,6 +1514,10 @@ export interface DonutBreakdownChartProps
1346
1514
  centerLabel?: React.ReactNode;
1347
1515
  centerValue?: React.ReactNode;
1348
1516
  showLegend?: boolean;
1517
+ /** Inner radius as percentage string (e.g. "58%") or number */
1518
+ innerRadius?: string | number;
1519
+ /** Outer radius as percentage string (e.g. "82%") or number */
1520
+ outerRadius?: string | number;
1349
1521
  }
1350
1522
 
1351
1523
  function DonutBreakdownChart({
@@ -1357,6 +1529,8 @@ function DonutBreakdownChart({
1357
1529
  centerLabel,
1358
1530
  centerValue,
1359
1531
  showLegend = true,
1532
+ innerRadius = "58%",
1533
+ outerRadius = "82%",
1360
1534
  isLoading,
1361
1535
  error,
1362
1536
  onRetry,
@@ -1416,11 +1590,14 @@ function DonutBreakdownChart({
1416
1590
  data={data}
1417
1591
  dataKey={valueKey}
1418
1592
  nameKey={nameKey}
1419
- innerRadius="58%"
1420
- outerRadius="82%"
1421
- paddingAngle={2}
1593
+ innerRadius={innerRadius}
1594
+ outerRadius={outerRadius}
1595
+ paddingAngle={3}
1422
1596
  activeIndex={activeIndex}
1423
1597
  onMouseEnter={(_, index) => setActiveIndex(index)}
1598
+ isAnimationActive
1599
+ animationDuration={600}
1600
+ animationEasing="ease-out"
1424
1601
  >
1425
1602
  {data.map((entry, index) => {
1426
1603
  const key = String(entry[nameKey] || `segment-${index}`);
@@ -1429,7 +1606,8 @@ function DonutBreakdownChart({
1429
1606
  <RechartsPrimitive.Cell
1430
1607
  key={key}
1431
1608
  fill={`var(--color-${key})`}
1432
- opacity={index === activeIndex ? 1 : 0.72}
1609
+ opacity={index === activeIndex ? 1 : 0.7}
1610
+ stroke="transparent"
1433
1611
  />
1434
1612
  );
1435
1613
  })}
@@ -1477,6 +1655,696 @@ function DonutBreakdownChart({
1477
1655
  );
1478
1656
  }
1479
1657
 
1658
+ // ─── SparklineChart ───────────────────────────────────────────────────────────
1659
+
1660
+ export interface SparklineChartProps {
1661
+ /** Data array — each item must have the `dataKey` field */
1662
+ data: DashboardChartDatum[];
1663
+ /** Key to plot on the Y axis */
1664
+ dataKey: string;
1665
+ /** Color for the line/area — defaults to `var(--chart-1)` */
1666
+ color?: string;
1667
+ /** Show filled area under the line */
1668
+ filled?: boolean;
1669
+ /** Show the last data point as a dot */
1670
+ showEndDot?: boolean;
1671
+ /** Curve interpolation type */
1672
+ curveType?: ChartCurveType;
1673
+ /** Stroke width */
1674
+ strokeWidth?: number;
1675
+ className?: string;
1676
+ }
1677
+
1678
+ /**
1679
+ * Minimal inline sparkline chart — no axes, no grid, no tooltip.
1680
+ * Ideal for embedding inside stat cards or table cells.
1681
+ *
1682
+ * @ai-rules
1683
+ * - Use `filled` for area-style sparklines.
1684
+ * - Keep `className` height small (e.g. `h-12` or `h-16`).
1685
+ * - Do NOT wrap in `ChartCard` — it is designed to be inline.
1686
+ */
1687
+ function SparklineChart({
1688
+ data,
1689
+ dataKey,
1690
+ color = "var(--chart-1)",
1691
+ filled = true,
1692
+ showEndDot = true,
1693
+ curveType = "monotone",
1694
+ strokeWidth = 2,
1695
+ className,
1696
+ }: SparklineChartProps) {
1697
+ const sparkId = React.useId().replace(/:/g, "");
1698
+ const lastPoint = data[data.length - 1];
1699
+ const lastValue = lastPoint ? lastPoint[dataKey] : undefined;
1700
+
1701
+ return (
1702
+ <div className={cn("relative h-12 w-full min-w-0", className)}>
1703
+ <RechartsPrimitive.ResponsiveContainer width="100%" height="100%">
1704
+ <RechartsPrimitive.AreaChart
1705
+ data={data}
1706
+ margin={{ top: 4, right: showEndDot ? 8 : 0, bottom: 0, left: 0 }}
1707
+ >
1708
+ {filled && (
1709
+ <defs>
1710
+ <linearGradient
1711
+ id={`spark-gradient-${sparkId}`}
1712
+ x1="0"
1713
+ y1="0"
1714
+ x2="0"
1715
+ y2="1"
1716
+ >
1717
+ <stop offset="5%" stopColor={color} stopOpacity={0.3} />
1718
+ <stop offset="95%" stopColor={color} stopOpacity={0} />
1719
+ </linearGradient>
1720
+ </defs>
1721
+ )}
1722
+ <RechartsPrimitive.Area
1723
+ type={curveType}
1724
+ dataKey={dataKey}
1725
+ stroke={color}
1726
+ strokeWidth={strokeWidth}
1727
+ fill={filled ? `url(#spark-gradient-${sparkId})` : "none"}
1728
+ fillOpacity={1}
1729
+ dot={false}
1730
+ activeDot={false}
1731
+ isAnimationActive
1732
+ animationDuration={600}
1733
+ animationEasing="ease-out"
1734
+ />
1735
+ </RechartsPrimitive.AreaChart>
1736
+ </RechartsPrimitive.ResponsiveContainer>
1737
+ {showEndDot && lastValue !== undefined && lastValue !== null && (
1738
+ <div
1739
+ className="pointer-events-none absolute right-0 top-1/2 h-2.5 w-2.5 -translate-y-1/2 rounded-full ring-2 ring-background"
1740
+ style={{ backgroundColor: color }}
1741
+ />
1742
+ )}
1743
+ </div>
1744
+ );
1745
+ }
1746
+
1747
+ // ─── RadarChart ───────────────────────────────────────────────────────────────
1748
+
1749
+ export interface RadarMetricChartProps extends ChartStateProps {
1750
+ /** Data array — each item is one axis point on the radar */
1751
+ data: DashboardChartDatum[];
1752
+ /** Key in each datum used as the axis label */
1753
+ labelKey: string;
1754
+ /**
1755
+ * Series to render. Each entry maps to one `<Radar>` element.
1756
+ * Use a single entry for a simple radar, multiple for comparison.
1757
+ */
1758
+ series: DashboardChartSeries[];
1759
+ /** Override colors per series key or as an ordered array */
1760
+ colors?: DashboardChartColors;
1761
+ /** Fill the radar polygon (default: true) */
1762
+ filled?: boolean;
1763
+ /** Fill opacity when `filled` is true (default: 0.25) */
1764
+ fillOpacity?: number;
1765
+ /** Show dots on each axis point (default: false) */
1766
+ showDots?: boolean;
1767
+ /** Show the polar grid lines (default: true) */
1768
+ showGrid?: boolean;
1769
+ /** Show the legend (default: true when multiple series) */
1770
+ showLegend?: boolean;
1771
+ /** Format axis tick values */
1772
+ valueFormatter?: ChartValueFormatter;
1773
+ className?: string;
1774
+ }
1775
+
1776
+ function RadarMetricChart({
1777
+ data,
1778
+ labelKey,
1779
+ series,
1780
+ colors,
1781
+ filled = true,
1782
+ fillOpacity = 0.25,
1783
+ showDots = false,
1784
+ showGrid = true,
1785
+ showLegend,
1786
+ valueFormatter,
1787
+ isLoading,
1788
+ error,
1789
+ onRetry,
1790
+ retryLabel,
1791
+ emptyTitle,
1792
+ emptyDescription,
1793
+ errorTitle,
1794
+ errorDescription,
1795
+ loadingLabel,
1796
+ stateClassName,
1797
+ className,
1798
+ }: RadarMetricChartProps) {
1799
+ const chartConfig = React.useMemo(
1800
+ () => buildChartConfig(series.map((s) => s.key), colors),
1801
+ [series, colors],
1802
+ );
1803
+
1804
+ const hasData = data.length > 0 && series.length > 0;
1805
+ const chartState = getChartState(
1806
+ { isLoading, error, emptyTitle, emptyDescription, errorTitle, errorDescription, loadingLabel, stateClassName, onRetry, retryLabel },
1807
+ hasData,
1808
+ stateClassName,
1809
+ );
1810
+
1811
+ const displayLegend = showLegend ?? series.length > 1;
1812
+
1813
+ return (
1814
+ <ChartContainer config={chartConfig} className={cn("h-[300px] w-full", className)}>
1815
+ {chartState ?? (
1816
+ <RechartsPrimitive.RadarChart data={data} accessibilityLayer>
1817
+ {showGrid && (
1818
+ <RechartsPrimitive.PolarGrid
1819
+ stroke="var(--border)"
1820
+ strokeOpacity={0.6}
1821
+ />
1822
+ )}
1823
+ <RechartsPrimitive.PolarAngleAxis
1824
+ dataKey={labelKey}
1825
+ tick={{ fill: "var(--muted-foreground)", fontSize: 12 }}
1826
+ />
1827
+ <RechartsPrimitive.PolarRadiusAxis
1828
+ tick={false}
1829
+ axisLine={false}
1830
+ tickFormatter={valueFormatter}
1831
+ />
1832
+ <ChartTooltip
1833
+ content={
1834
+ <ChartTooltipContent
1835
+ formatter={valueFormatter ? (v) => valueFormatter(v as number) : undefined}
1836
+ />
1837
+ }
1838
+ />
1839
+ {displayLegend && (
1840
+ <ChartLegend content={<ChartLegendContent />} />
1841
+ )}
1842
+ {series.map((s) => (
1843
+ <RechartsPrimitive.Radar
1844
+ key={s.key}
1845
+ name={s.label as string ?? s.key}
1846
+ dataKey={s.key}
1847
+ stroke={`var(--color-${s.key})`}
1848
+ fill={filled ? `var(--color-${s.key})` : "none"}
1849
+ fillOpacity={filled ? fillOpacity : 0}
1850
+ dot={showDots ? { r: 3, fill: `var(--color-${s.key})` } : false}
1851
+ isAnimationActive
1852
+ animationDuration={600}
1853
+ animationEasing="ease-out"
1854
+ />
1855
+ ))}
1856
+ </RechartsPrimitive.RadarChart>
1857
+ )}
1858
+ </ChartContainer>
1859
+ );
1860
+ }
1861
+
1862
+ // ─── PieMetricChart ───────────────────────────────────────────────────────────
1863
+
1864
+ export interface PieMetricChartProps extends ChartStateProps {
1865
+ /** Data array — each item is one slice */
1866
+ data: DashboardChartDatum[];
1867
+ /** Key in each datum used as the slice name/label */
1868
+ nameKey: string;
1869
+ /** Key in each datum used as the slice value */
1870
+ valueKey: string;
1871
+ /** Override colors as an ordered array or per-name map */
1872
+ colors?: DashboardChartColors;
1873
+ /** Outer radius of the pie (default: "80%") */
1874
+ outerRadius?: number | string;
1875
+ /** Inner radius — set > 0 to make a donut (default: 0) */
1876
+ innerRadius?: number | string;
1877
+ /** Show percentage labels inside/outside each slice (default: false) */
1878
+ showLabels?: boolean;
1879
+ /** Show the legend (default: true) */
1880
+ showLegend?: boolean;
1881
+ /**
1882
+ * Index of the slice to "explode" (offset outward).
1883
+ * Pass -1 or undefined to disable.
1884
+ */
1885
+ explodeIndex?: number;
1886
+ /** Offset distance for the exploded slice in px (default: 12) */
1887
+ explodeOffset?: number;
1888
+ /** Format tooltip values */
1889
+ valueFormatter?: ChartValueFormatter;
1890
+ className?: string;
1891
+ }
1892
+
1893
+ function PieMetricChart({
1894
+ data,
1895
+ nameKey,
1896
+ valueKey,
1897
+ colors,
1898
+ outerRadius = "80%",
1899
+ innerRadius = 0,
1900
+ showLabels = false,
1901
+ showLegend = true,
1902
+ explodeIndex,
1903
+ explodeOffset = 12,
1904
+ valueFormatter,
1905
+ isLoading,
1906
+ error,
1907
+ onRetry,
1908
+ retryLabel,
1909
+ emptyTitle,
1910
+ emptyDescription,
1911
+ errorTitle,
1912
+ errorDescription,
1913
+ loadingLabel,
1914
+ stateClassName,
1915
+ className,
1916
+ }: PieMetricChartProps) {
1917
+ // Build config from unique name values
1918
+ const names = React.useMemo(
1919
+ () => data.map((d) => String(d[nameKey] ?? "")),
1920
+ [data, nameKey],
1921
+ );
1922
+
1923
+ const chartConfig = React.useMemo(
1924
+ () => buildChartConfig(names, colors),
1925
+ [names, colors],
1926
+ );
1927
+
1928
+ const hasData = hasPieData(data, nameKey, valueKey);
1929
+ const chartState = getChartState(
1930
+ { isLoading, error, emptyTitle, emptyDescription, errorTitle, errorDescription, loadingLabel, stateClassName, onRetry, retryLabel },
1931
+ hasData,
1932
+ stateClassName,
1933
+ );
1934
+
1935
+ return (
1936
+ <ChartContainer config={chartConfig} className={cn("h-[300px] w-full", className)}>
1937
+ {chartState ?? (
1938
+ <RechartsPrimitive.PieChart accessibilityLayer>
1939
+ <ChartTooltip
1940
+ content={
1941
+ <ChartTooltipContent
1942
+ nameKey={nameKey}
1943
+ formatter={valueFormatter ? (v) => valueFormatter(v as number) : undefined}
1944
+ />
1945
+ }
1946
+ />
1947
+ {showLegend && <ChartLegend content={<ChartLegendContent nameKey={nameKey} />} />}
1948
+ <RechartsPrimitive.Pie
1949
+ data={data}
1950
+ dataKey={valueKey}
1951
+ nameKey={nameKey}
1952
+ outerRadius={outerRadius}
1953
+ innerRadius={innerRadius}
1954
+ paddingAngle={2}
1955
+ label={
1956
+ showLabels
1957
+ ? ({ cx, cy, midAngle, innerRadius: ir, outerRadius: or, percent }) => {
1958
+ const RADIAN = Math.PI / 180;
1959
+ const radius = Number(ir) + (Number(or) - Number(ir)) * 1.35;
1960
+ const x = Number(cx) + radius * Math.cos(-midAngle * RADIAN);
1961
+ const y = Number(cy) + radius * Math.sin(-midAngle * RADIAN);
1962
+ return percent > 0.04 ? (
1963
+ <text
1964
+ x={x}
1965
+ y={y}
1966
+ fill="var(--foreground)"
1967
+ textAnchor={x > Number(cx) ? "start" : "end"}
1968
+ dominantBaseline="central"
1969
+ fontSize={12}
1970
+ fontWeight={500}
1971
+ >
1972
+ {`${(percent * 100).toFixed(0)}%`}
1973
+ </text>
1974
+ ) : null;
1975
+ }
1976
+ : false
1977
+ }
1978
+ labelLine={false}
1979
+ isAnimationActive
1980
+ animationDuration={600}
1981
+ animationEasing="ease-out"
1982
+ >
1983
+ {data.map((entry, index) => {
1984
+ const name = String(entry[nameKey] ?? "");
1985
+ const isExploded = index === explodeIndex;
1986
+ return (
1987
+ <RechartsPrimitive.Cell
1988
+ key={`cell-${index}`}
1989
+ fill={chartConfig[name]?.color ?? `var(--chart-${(index % 8) + 1})`}
1990
+ stroke="var(--background)"
1991
+ strokeWidth={2}
1992
+ style={
1993
+ isExploded
1994
+ ? { filter: `drop-shadow(0 4px 8px color-mix(in srgb, ${chartConfig[name]?.color ?? "var(--chart-1)"} 40%, transparent))` }
1995
+ : undefined
1996
+ }
1997
+ />
1998
+ );
1999
+ })}
2000
+ </RechartsPrimitive.Pie>
2001
+ </RechartsPrimitive.PieChart>
2002
+ )}
2003
+ </ChartContainer>
2004
+ );
2005
+ }
2006
+
2007
+ // ─── RadialBarMetricChart ─────────────────────────────────────────────────────
2008
+
2009
+ export interface RadialBarMetricChartProps extends ChartStateProps {
2010
+ /**
2011
+ * Data array. Each item should have a `name` field (for labels) and
2012
+ * the `dataKey` field (for values, 0–100 for percentage-based display).
2013
+ */
2014
+ data: DashboardChartDatum[];
2015
+ /** Key in each datum used as the bar value (default: "value") */
2016
+ dataKey?: string;
2017
+ /** Key in each datum used as the bar label (default: "name") */
2018
+ nameKey?: string;
2019
+ /** Override colors as an ordered array or per-name map */
2020
+ colors?: DashboardChartColors;
2021
+ /** Inner radius of the radial bar (default: "30%") */
2022
+ innerRadius?: number | string;
2023
+ /** Outer radius of the radial bar (default: "100%") */
2024
+ outerRadius?: number | string;
2025
+ /** Start angle in degrees (default: 90 — top) */
2026
+ startAngle?: number;
2027
+ /** End angle in degrees (default: -270 — full circle) */
2028
+ endAngle?: number;
2029
+ /** Show background track behind each bar (default: true) */
2030
+ showBackground?: boolean;
2031
+ /** Show the legend (default: true) */
2032
+ showLegend?: boolean;
2033
+ /** Format tooltip values */
2034
+ valueFormatter?: ChartValueFormatter;
2035
+ className?: string;
2036
+ }
2037
+
2038
+ function RadialBarMetricChart({
2039
+ data,
2040
+ dataKey = "value",
2041
+ nameKey = "name",
2042
+ colors,
2043
+ innerRadius = "30%",
2044
+ outerRadius = "100%",
2045
+ startAngle = 90,
2046
+ endAngle = -270,
2047
+ showBackground = true,
2048
+ showLegend = true,
2049
+ valueFormatter,
2050
+ isLoading,
2051
+ error,
2052
+ onRetry,
2053
+ retryLabel,
2054
+ emptyTitle,
2055
+ emptyDescription,
2056
+ errorTitle,
2057
+ errorDescription,
2058
+ loadingLabel,
2059
+ stateClassName,
2060
+ className,
2061
+ }: RadialBarMetricChartProps) {
2062
+ const names = React.useMemo(
2063
+ () => data.map((d) => String(d[nameKey] ?? "")),
2064
+ [data, nameKey],
2065
+ );
2066
+
2067
+ const chartConfig = React.useMemo(
2068
+ () => buildChartConfig(names, colors),
2069
+ [names, colors],
2070
+ );
2071
+
2072
+ const hasData = data.length > 0;
2073
+ const chartState = getChartState(
2074
+ { isLoading, error, emptyTitle, emptyDescription, errorTitle, errorDescription, loadingLabel, stateClassName, onRetry, retryLabel },
2075
+ hasData,
2076
+ stateClassName,
2077
+ );
2078
+
2079
+ // Inject per-item fill color
2080
+ const coloredData = React.useMemo(
2081
+ () =>
2082
+ data.map((item, index) => {
2083
+ const name = String(item[nameKey] ?? "");
2084
+ return {
2085
+ ...item,
2086
+ fill: chartConfig[name]?.color ?? `var(--chart-${(index % 8) + 1})`,
2087
+ };
2088
+ }),
2089
+ [data, nameKey, chartConfig],
2090
+ );
2091
+
2092
+ return (
2093
+ <div className={cn("flex w-full flex-col gap-3", className)}>
2094
+ <ChartContainer config={chartConfig} className="h-[260px] w-full">
2095
+ {chartState ?? (
2096
+ <RechartsPrimitive.RadialBarChart
2097
+ data={coloredData}
2098
+ innerRadius={innerRadius}
2099
+ outerRadius={outerRadius}
2100
+ startAngle={startAngle}
2101
+ endAngle={endAngle}
2102
+ accessibilityLayer
2103
+ >
2104
+ <ChartTooltip
2105
+ cursor={false}
2106
+ content={
2107
+ <ChartTooltipContent
2108
+ nameKey={nameKey}
2109
+ formatter={valueFormatter ? (v) => valueFormatter(v as number) : undefined}
2110
+ />
2111
+ }
2112
+ />
2113
+ <RechartsPrimitive.RadialBar
2114
+ dataKey={dataKey}
2115
+ background={showBackground ? { fill: "var(--muted)" } : false}
2116
+ cornerRadius={6}
2117
+ isAnimationActive
2118
+ animationDuration={700}
2119
+ animationEasing="ease-out"
2120
+ />
2121
+ </RechartsPrimitive.RadialBarChart>
2122
+ )}
2123
+ </ChartContainer>
2124
+ {showLegend && !chartState && (
2125
+ <div className="flex flex-wrap justify-center gap-x-4 gap-y-1.5 px-2">
2126
+ {coloredData.map((item, index) => {
2127
+ const d = item as DashboardChartDatum & { fill: string };
2128
+ const name = String(d[nameKey] ?? "");
2129
+ const fillColor = d.fill ?? `var(--chart-${(index % 8) + 1})`;
2130
+ const val = d[dataKey];
2131
+ return (
2132
+ <div key={name} className="flex items-center gap-1.5">
2133
+ <span
2134
+ className="inline-block h-2.5 w-2.5 shrink-0 rounded-full"
2135
+ style={{ backgroundColor: fillColor }}
2136
+ />
2137
+ <span className="text-xs text-muted-foreground">
2138
+ {name}
2139
+ {val !== undefined && val !== null && (
2140
+ <span className="ml-1 font-medium text-foreground">
2141
+ {valueFormatter ? valueFormatter(val as number) : String(val)}
2142
+ </span>
2143
+ )}
2144
+ </span>
2145
+ </div>
2146
+ );
2147
+ })}
2148
+ </div>
2149
+ )}
2150
+ </div>
2151
+ );
2152
+ }
2153
+
2154
+ // ─── GaugeChart ───────────────────────────────────────────────────────────────
2155
+
2156
+ export interface GaugeChartThreshold {
2157
+ /** Upper bound of this zone (0–100) */
2158
+ value: number;
2159
+ /** Color for this zone */
2160
+ color: string;
2161
+ /** Optional label for this zone */
2162
+ label?: string;
2163
+ }
2164
+
2165
+ export interface GaugeChartProps {
2166
+ /** Current value (must be within [min, max]) */
2167
+ value: number;
2168
+ /** Minimum value (default: 0) */
2169
+ min?: number;
2170
+ /** Maximum value (default: 100) */
2171
+ max?: number;
2172
+ /**
2173
+ * Color zones. Each threshold defines the upper bound of a zone.
2174
+ * Zones are evaluated in order; the first zone whose `value >= current %`
2175
+ * determines the active color.
2176
+ * If omitted, uses `--chart-1`.
2177
+ */
2178
+ thresholds?: GaugeChartThreshold[];
2179
+ /** Label shown below the value (e.g. "CPU Usage") */
2180
+ label?: React.ReactNode;
2181
+ /** Format the center value text (default: shows percentage) */
2182
+ valueFormatter?: (value: number, percent: number) => string;
2183
+ /** Show the needle indicator (default: true) */
2184
+ showNeedle?: boolean;
2185
+ className?: string;
2186
+ }
2187
+
2188
+ function GaugeChart({
2189
+ value,
2190
+ min = 0,
2191
+ max = 100,
2192
+ thresholds,
2193
+ label,
2194
+ valueFormatter,
2195
+ showNeedle = true,
2196
+ className,
2197
+ }: GaugeChartProps) {
2198
+ const percent = Math.min(1, Math.max(0, (value - min) / (max - min)));
2199
+ const percentInt = Math.round(percent * 100);
2200
+
2201
+ // Determine active color from thresholds
2202
+ const activeColor = React.useMemo(() => {
2203
+ if (!thresholds || thresholds.length === 0) return "var(--chart-1)";
2204
+ for (const t of thresholds) {
2205
+ if (percentInt <= t.value) return t.color;
2206
+ }
2207
+ return thresholds[thresholds.length - 1].color;
2208
+ }, [thresholds, percentInt]);
2209
+
2210
+ const displayText = valueFormatter
2211
+ ? valueFormatter(value, percentInt)
2212
+ : `${percentInt}%`;
2213
+
2214
+ // SVG gauge constants — viewBox is 200×110, arc center at (100, 100)
2215
+ const cx = 100;
2216
+ const cy = 100;
2217
+ const R = 80; // outer radius
2218
+ const r = 54; // inner radius (track width = 26)
2219
+
2220
+ // Helper: polar → cartesian (angle in degrees, 0° = right, CCW)
2221
+ const polar = (angleDeg: number, radius: number) => {
2222
+ const rad = (angleDeg * Math.PI) / 180;
2223
+ return {
2224
+ x: cx + radius * Math.cos(rad),
2225
+ y: cy - radius * Math.sin(rad),
2226
+ };
2227
+ };
2228
+
2229
+ // Semicircle: from 180° (left) to 0° (right)
2230
+ // Track background arc path
2231
+ const trackStart = polar(180, R);
2232
+ const trackEnd = polar(0, R);
2233
+ const trackStartI = polar(180, r);
2234
+ const trackEndI = polar(0, r);
2235
+ const trackPath = [
2236
+ `M ${trackStart.x} ${trackStart.y}`,
2237
+ `A ${R} ${R} 0 0 1 ${trackEnd.x} ${trackEnd.y}`,
2238
+ `L ${trackEndI.x} ${trackEndI.y}`,
2239
+ `A ${r} ${r} 0 0 0 ${trackStartI.x} ${trackStartI.y}`,
2240
+ "Z",
2241
+ ].join(" ");
2242
+
2243
+ // Value arc: from 180° (left) clockwise to valueAngle
2244
+ // The gauge is a semicircle (max 180°), so largeArc is always 0.
2245
+ // largeArc=1 would sweep the reflex arc (>180°) which creates the filled-blob bug.
2246
+ const valueAngle = 180 - percent * 180;
2247
+ const valueEnd = polar(valueAngle, R);
2248
+ const valueEndI = polar(valueAngle, r);
2249
+ const valuePath =
2250
+ percent <= 0
2251
+ ? ""
2252
+ : percent >= 1
2253
+ ? trackPath // full arc = same as track
2254
+ : [
2255
+ `M ${trackStart.x} ${trackStart.y}`,
2256
+ `A ${R} ${R} 0 0 1 ${valueEnd.x} ${valueEnd.y}`,
2257
+ `L ${valueEndI.x} ${valueEndI.y}`,
2258
+ `A ${r} ${r} 0 0 0 ${trackStartI.x} ${trackStartI.y}`,
2259
+ "Z",
2260
+ ].join(" ");
2261
+
2262
+ // Needle: rotates from -90° (left, 0%) to +90° (right, 100%)
2263
+ // In our coordinate system: 180° at 0%, 0° at 100%
2264
+ const needleAngle = 180 - percent * 180;
2265
+ const needleTip = polar(needleAngle, r - 6);
2266
+ const needleBase1 = polar(needleAngle + 90, 5);
2267
+ const needleBase2 = polar(needleAngle - 90, 5);
2268
+
2269
+ return (
2270
+ <div className={cn("flex flex-col items-center gap-2", className)}>
2271
+ <div className="relative w-full max-w-[260px]">
2272
+ <svg
2273
+ viewBox="0 0 200 130"
2274
+ className="w-full"
2275
+ aria-label={`Gauge: ${displayText}`}
2276
+ >
2277
+ {/* Background track */}
2278
+ <path d={trackPath} fill="var(--muted)" />
2279
+
2280
+ {/* Value arc */}
2281
+ {valuePath && (
2282
+ <path d={valuePath} fill={activeColor} />
2283
+ )}
2284
+
2285
+ {/* Needle */}
2286
+ {showNeedle && (
2287
+ <g>
2288
+ <polygon
2289
+ points={`${needleTip.x},${needleTip.y} ${needleBase1.x},${needleBase1.y} ${needleBase2.x},${needleBase2.y}`}
2290
+ fill="var(--foreground)"
2291
+ opacity={0.85}
2292
+ />
2293
+ <circle cx={cx} cy={cy} r={5} fill="var(--foreground)" />
2294
+ </g>
2295
+ )}
2296
+
2297
+ {/* Value text — placed below the arc baseline (cy=100) to avoid needle overlap */}
2298
+ <text
2299
+ x={cx}
2300
+ y={cy + 18}
2301
+ textAnchor="middle"
2302
+ dominantBaseline="middle"
2303
+ fontSize={22}
2304
+ fontWeight={700}
2305
+ fill="currentColor"
2306
+ className="fill-foreground"
2307
+ >
2308
+ {displayText}
2309
+ </text>
2310
+
2311
+ {/* Label below value */}
2312
+ {label && (
2313
+ <text
2314
+ x={cx}
2315
+ y={cy + 36}
2316
+ textAnchor="middle"
2317
+ dominantBaseline="middle"
2318
+ fontSize={10}
2319
+ fill="currentColor"
2320
+ className="fill-muted-foreground"
2321
+ >
2322
+ {label}
2323
+ </text>
2324
+ )}
2325
+ </svg>
2326
+ </div>
2327
+
2328
+ {/* Threshold legend */}
2329
+ {thresholds && thresholds.length > 0 && (
2330
+ <div className="flex flex-wrap justify-center gap-x-3 gap-y-1">
2331
+ {thresholds.map((t, i) => (
2332
+ <div key={i} className="flex items-center gap-1.5">
2333
+ <span
2334
+ className="inline-block h-2 w-2 shrink-0 rounded-full"
2335
+ style={{ backgroundColor: t.color }}
2336
+ />
2337
+ <span className="text-xs text-muted-foreground">{t.label}</span>
2338
+ </div>
2339
+ ))}
2340
+ </div>
2341
+ )}
2342
+ </div>
2343
+ );
2344
+ }
2345
+
2346
+ // ─── Exports ──────────────────────────────────────────────────────────────────
2347
+
1480
2348
  export {
1481
2349
  ChartContainer,
1482
2350
  ChartTooltip,
@@ -1491,4 +2359,9 @@ export {
1491
2359
  InteractiveTimeSeriesChart,
1492
2360
  ComboMetricChart,
1493
2361
  DonutBreakdownChart,
2362
+ SparklineChart,
2363
+ RadarMetricChart,
2364
+ PieMetricChart,
2365
+ RadialBarMetricChart,
2366
+ GaugeChart,
1494
2367
  };