@alpic-ai/ui 0.0.0-dev.g0999450 → 0.0.0-dev.g0a2a154

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 (109) hide show
  1. package/dist/components/accordion-card.d.mts +5 -5
  2. package/dist/components/accordion.d.mts +5 -5
  3. package/dist/components/alert.d.mts +8 -8
  4. package/dist/components/area-chart.d.mts +62 -0
  5. package/dist/components/area-chart.mjs +269 -0
  6. package/dist/components/attachment-tile.d.mts +1 -1
  7. package/dist/components/avatar.d.mts +7 -7
  8. package/dist/components/badge.d.mts +1 -1
  9. package/dist/components/bar-chart.d.mts +48 -0
  10. package/dist/components/bar-chart.mjs +256 -0
  11. package/dist/components/bar-list.d.mts +28 -0
  12. package/dist/components/bar-list.mjs +98 -0
  13. package/dist/components/breadcrumb.d.mts +10 -10
  14. package/dist/components/button.d.mts +5 -5
  15. package/dist/components/card.d.mts +9 -9
  16. package/dist/components/chart-card.d.mts +25 -0
  17. package/dist/components/chart-card.mjs +48 -0
  18. package/dist/components/chart-container.d.mts +20 -0
  19. package/dist/components/chart-container.mjs +37 -0
  20. package/dist/components/chart-legend.d.mts +16 -0
  21. package/dist/components/chart-legend.mjs +26 -0
  22. package/dist/components/chart-tooltip.d.mts +33 -0
  23. package/dist/components/chart-tooltip.mjs +52 -0
  24. package/dist/components/checkbox.d.mts +2 -2
  25. package/dist/components/collapsible.d.mts +4 -4
  26. package/dist/components/combobox.d.mts +10 -10
  27. package/dist/components/command.d.mts +9 -9
  28. package/dist/components/copyable.d.mts +2 -2
  29. package/dist/components/description-list.d.mts +5 -5
  30. package/dist/components/dialog.d.mts +13 -13
  31. package/dist/components/donut-chart.d.mts +46 -0
  32. package/dist/components/donut-chart.mjs +185 -0
  33. package/dist/components/dropdown-menu.d.mts +17 -17
  34. package/dist/components/form.d.mts +18 -18
  35. package/dist/components/form.mjs +6 -6
  36. package/dist/components/github-button.d.mts +1 -1
  37. package/dist/components/heatmap-chart.d.mts +40 -0
  38. package/dist/components/heatmap-chart.mjs +198 -0
  39. package/dist/components/input-group.d.mts +4 -4
  40. package/dist/components/input.d.mts +4 -4
  41. package/dist/components/input.mjs +2 -2
  42. package/dist/components/label.d.mts +2 -2
  43. package/dist/components/line-chart.d.mts +55 -0
  44. package/dist/components/line-chart.mjs +211 -0
  45. package/dist/components/page-loader.d.mts +1 -1
  46. package/dist/components/pagination.d.mts +3 -3
  47. package/dist/components/popover.d.mts +5 -5
  48. package/dist/components/radio-group.d.mts +3 -3
  49. package/dist/components/scroll-area.d.mts +3 -3
  50. package/dist/components/select.d.mts +9 -9
  51. package/dist/components/separator.d.mts +2 -2
  52. package/dist/components/sheet.d.mts +11 -11
  53. package/dist/components/shimmer-text.d.mts +3 -1
  54. package/dist/components/sidebar.d.mts +33 -33
  55. package/dist/components/sidebar.mjs +10 -10
  56. package/dist/components/skeleton.d.mts +1 -1
  57. package/dist/components/sonner.d.mts +5 -5
  58. package/dist/components/spinner.d.mts +2 -2
  59. package/dist/components/stat.d.mts +30 -0
  60. package/dist/components/stat.mjs +107 -0
  61. package/dist/components/status-dot.d.mts +1 -1
  62. package/dist/components/switch.d.mts +2 -2
  63. package/dist/components/table.d.mts +10 -10
  64. package/dist/components/tabs.d.mts +10 -10
  65. package/dist/components/tag.d.mts +3 -3
  66. package/dist/components/task-progress.d.mts +1 -1
  67. package/dist/components/textarea.d.mts +3 -3
  68. package/dist/components/textarea.mjs +2 -2
  69. package/dist/components/toggle-group.d.mts +3 -3
  70. package/dist/components/toggle-group.mjs +3 -3
  71. package/dist/components/tooltip.d.mts +5 -5
  72. package/dist/components/typography.d.mts +4 -4
  73. package/dist/components/wizard.d.mts +4 -22
  74. package/dist/components/wizard.mjs +1 -19
  75. package/dist/hooks/use-chart-theme.d.mts +18 -0
  76. package/dist/hooks/use-chart-theme.mjs +57 -0
  77. package/dist/hooks/use-mobile.mjs +3 -3
  78. package/dist/hooks/use-reduced-motion.d.mts +4 -0
  79. package/dist/hooks/use-reduced-motion.mjs +16 -0
  80. package/dist/lib/chart-palette.d.mts +4 -0
  81. package/dist/lib/chart-palette.mjs +95 -0
  82. package/dist/lib/chart.d.mts +14 -0
  83. package/dist/lib/chart.mjs +27 -0
  84. package/package.json +30 -29
  85. package/src/components/area-chart.tsx +339 -0
  86. package/src/components/bar-chart.tsx +309 -0
  87. package/src/components/bar-list.tsx +150 -0
  88. package/src/components/chart-card.tsx +63 -0
  89. package/src/components/chart-container.tsx +49 -0
  90. package/src/components/chart-legend.tsx +41 -0
  91. package/src/components/chart-tooltip.tsx +93 -0
  92. package/src/components/donut-chart.tsx +217 -0
  93. package/src/components/heatmap-chart.tsx +287 -0
  94. package/src/components/line-chart.tsx +264 -0
  95. package/src/components/stat.tsx +109 -0
  96. package/src/components/wizard.tsx +1 -35
  97. package/src/hooks/use-chart-theme.ts +75 -0
  98. package/src/hooks/use-reduced-motion.ts +17 -0
  99. package/src/lib/chart-palette.ts +110 -0
  100. package/src/lib/chart.ts +56 -0
  101. package/src/stories/area-chart.stories.tsx +198 -0
  102. package/src/stories/bar-chart.stories.tsx +167 -0
  103. package/src/stories/bar-list.stories.tsx +83 -0
  104. package/src/stories/donut-chart.stories.tsx +110 -0
  105. package/src/stories/heatmap-chart.stories.tsx +105 -0
  106. package/src/stories/line-chart.stories.tsx +144 -0
  107. package/src/stories/stat.stories.tsx +64 -0
  108. package/src/stories/wizard.stories.tsx +22 -4
  109. package/src/styles/tokens.css +63 -0
@@ -0,0 +1,18 @@
1
+ //#region src/hooks/use-chart-theme.d.ts
2
+ interface ChartTheme {
3
+ foreground: string;
4
+ mutedForeground: string;
5
+ axisForeground: string;
6
+ grid: string;
7
+ border: string;
8
+ card: string;
9
+ popover: string;
10
+ destructive: string;
11
+ warning: string;
12
+ success: string;
13
+ fontMono: string;
14
+ isDark: boolean;
15
+ }
16
+ declare function useChartTheme(): ChartTheme;
17
+ //#endregion
18
+ export { ChartTheme, useChartTheme };
@@ -0,0 +1,57 @@
1
+ "use client";
2
+ import * as React$1 from "react";
3
+ //#region src/hooks/use-chart-theme.ts
4
+ const TOKEN_MAP = {
5
+ foreground: "--color-foreground",
6
+ mutedForeground: "--color-muted-foreground",
7
+ axisForeground: "--color-quaternary-foreground",
8
+ grid: "--color-sidebar-border",
9
+ border: "--color-border",
10
+ card: "--color-card",
11
+ popover: "--color-popover",
12
+ destructive: "--color-destructive",
13
+ warning: "--color-warning",
14
+ success: "--color-success",
15
+ fontMono: "--font-mono"
16
+ };
17
+ const FALLBACK = {
18
+ foreground: "#121e1e",
19
+ mutedForeground: "#3a4848",
20
+ axisForeground: "#6f7f7f",
21
+ grid: "#e3eaea",
22
+ border: "#acb8b8",
23
+ card: "#f8fafa",
24
+ popover: "#ffffff",
25
+ destructive: "#d92d20",
26
+ warning: "#dc6803",
27
+ success: "#079455",
28
+ fontMono: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
29
+ isDark: false
30
+ };
31
+ const readTheme = () => {
32
+ if (typeof window === "undefined") return FALLBACK;
33
+ const styles = window.getComputedStyle(document.documentElement);
34
+ const entries = Object.entries(TOKEN_MAP).map(([key, token]) => {
35
+ return [key, styles.getPropertyValue(token).trim() || FALLBACK[key]];
36
+ });
37
+ return {
38
+ ...Object.fromEntries(entries),
39
+ isDark: document.documentElement.classList.contains("dark")
40
+ };
41
+ };
42
+ function useChartTheme() {
43
+ const [theme, setTheme] = React$1.useState(readTheme);
44
+ React$1.useEffect(() => {
45
+ const update = () => setTheme(readTheme());
46
+ update();
47
+ const observer = new MutationObserver(update);
48
+ observer.observe(document.documentElement, {
49
+ attributes: true,
50
+ attributeFilter: ["class"]
51
+ });
52
+ return () => observer.disconnect();
53
+ }, []);
54
+ return theme;
55
+ }
56
+ //#endregion
57
+ export { useChartTheme };
@@ -1,9 +1,9 @@
1
- import * as React from "react";
1
+ import * as React$1 from "react";
2
2
  //#region src/hooks/use-mobile.ts
3
3
  const MOBILE_BREAKPOINT = 768;
4
4
  function useIsMobile() {
5
- const [isMobile, setIsMobile] = React.useState(void 0);
6
- React.useEffect(() => {
5
+ const [isMobile, setIsMobile] = React$1.useState(void 0);
6
+ React$1.useEffect(() => {
7
7
  const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
8
8
  const onChange = () => setIsMobile(mql.matches);
9
9
  mql.addEventListener("change", onChange);
@@ -0,0 +1,4 @@
1
+ //#region src/hooks/use-reduced-motion.d.ts
2
+ declare function useReducedMotion(): boolean;
3
+ //#endregion
4
+ export { useReducedMotion };
@@ -0,0 +1,16 @@
1
+ "use client";
2
+ import * as React$1 from "react";
3
+ //#region src/hooks/use-reduced-motion.ts
4
+ function useReducedMotion() {
5
+ const [reduced, setReduced] = React$1.useState(false);
6
+ React$1.useEffect(() => {
7
+ const query = window.matchMedia("(prefers-reduced-motion: reduce)");
8
+ const update = () => setReduced(query.matches);
9
+ update();
10
+ query.addEventListener("change", update);
11
+ return () => query.removeEventListener("change", update);
12
+ }, []);
13
+ return reduced;
14
+ }
15
+ //#endregion
16
+ export { useReducedMotion };
@@ -0,0 +1,4 @@
1
+ //#region src/lib/chart-palette.d.ts
2
+ type ChartPaletteName = "magenta" | "cyan";
3
+ //#endregion
4
+ export { ChartPaletteName };
@@ -0,0 +1,95 @@
1
+ const CHART_PALETTES = {
2
+ magenta: [
3
+ "#e90060",
4
+ "#ff7eb6",
5
+ "#9b5de5",
6
+ "#5b8def",
7
+ "#36c5f0",
8
+ "#6eece7",
9
+ "#d98a3d",
10
+ "#2bb6a3"
11
+ ],
12
+ cyan: [
13
+ "#17b8cf",
14
+ "#41ddc9",
15
+ "#3f8ff0",
16
+ "#8b6cf0",
17
+ "#c46bf0",
18
+ "#ff7eb6",
19
+ "#e90060",
20
+ "#c98be0"
21
+ ]
22
+ };
23
+ const heatRampMagenta = (empty) => [
24
+ empty,
25
+ "#5b0a31",
26
+ "#a3034a",
27
+ "#e90060",
28
+ "#ff7eb6"
29
+ ];
30
+ const heatRampMint = (empty) => [
31
+ empty,
32
+ "#0c4a52",
33
+ "#17b8cf",
34
+ "#41ddc9",
35
+ "#6eece7"
36
+ ];
37
+ const HEAT_EMPTY = {
38
+ light: "#eef3f3",
39
+ dark: "#102222"
40
+ };
41
+ const HEAT_RAMPS = {
42
+ magenta: heatRampMagenta,
43
+ cyan: heatRampMint
44
+ };
45
+ const heatRamp = (palette, empty) => HEAT_RAMPS[palette](empty);
46
+ const hexToRgb = (hex) => {
47
+ const value = Number.parseInt(hex.slice(1), 16);
48
+ return [
49
+ value >> 16 & 255,
50
+ value >> 8 & 255,
51
+ value & 255
52
+ ];
53
+ };
54
+ const luminance = (hex) => {
55
+ const linear = hexToRgb(hex).map((channel) => {
56
+ const normalized = channel / 255;
57
+ return normalized <= .03928 ? normalized / 12.92 : ((normalized + .055) / 1.055) ** 2.4;
58
+ });
59
+ return .2126 * linear[0] + .7152 * linear[1] + .0722 * linear[2];
60
+ };
61
+ const paletteColor = (palette, index) => {
62
+ return palette[(index % palette.length + palette.length) % palette.length];
63
+ };
64
+ const RAMP_ENDS = {
65
+ magenta: ["#e90060", "#ff7eb6"],
66
+ cyan: ["#17b8cf", "#6eece7"]
67
+ };
68
+ const mixHex = (from, to, fraction) => {
69
+ const [fromR, fromG, fromB] = hexToRgb(from);
70
+ const [toR, toG, toB] = hexToRgb(to);
71
+ const ratio = Math.min(1, Math.max(0, fraction));
72
+ const channel = (start, end) => Math.round(start + (end - start) * ratio);
73
+ return `#${[
74
+ channel(fromR, toR),
75
+ channel(fromG, toG),
76
+ channel(fromB, toB)
77
+ ].map((value) => value.toString(16).padStart(2, "0")).join("")}`;
78
+ };
79
+ const rampColor = (palette, fraction) => {
80
+ const [from, to] = RAMP_ENDS[palette];
81
+ return mixHex(from, to, fraction);
82
+ };
83
+ /**
84
+ * Interpolate continuously across a multi-stop heat ramp (e.g. `heatRampMagenta`),
85
+ * so a normalized 0..1 value reads as a smooth single-hue gradient rather than
86
+ * the five discrete bands.
87
+ */
88
+ const heatColor = (stops, fraction) => {
89
+ const clamped = Math.min(1, Math.max(0, fraction));
90
+ const segment = (stops.length - 1) * clamped;
91
+ const lowerIndex = Math.min(stops.length - 2, Math.floor(segment));
92
+ return mixHex(stops[lowerIndex], stops[lowerIndex + 1], segment - lowerIndex);
93
+ };
94
+ //#endregion
95
+ export { CHART_PALETTES, HEAT_EMPTY, heatColor, heatRamp, luminance, paletteColor, rampColor };
@@ -0,0 +1,14 @@
1
+ //#region src/lib/chart.d.ts
2
+ /**
3
+ * Declarative description of one plotted series. The same shape drives area,
4
+ * line and bar charts — what differs is how each chart renders it.
5
+ */
6
+ interface ChartSeries {
7
+ key: string;
8
+ name?: string;
9
+ color?: string;
10
+ semantic?: "error" | "warning" | "success";
11
+ dashed?: boolean;
12
+ }
13
+ //#endregion
14
+ export { ChartSeries };
@@ -0,0 +1,27 @@
1
+ import { luminance, paletteColor } from "./chart-palette.mjs";
2
+ //#region src/lib/chart.ts
3
+ const formatShare = (fraction) => `${(fraction * 100).toFixed(fraction >= .1 ? 0 : 1)}%`;
4
+ const semanticColor = (theme, semantic) => {
5
+ if (semantic === "error") return theme.destructive;
6
+ if (semantic === "warning") return theme.warning;
7
+ return theme.success;
8
+ };
9
+ /**
10
+ * Resolve each series to a concrete color and name. Color precedence:
11
+ * explicit `color` → `semantic` token → palette slot by declared index (so the
12
+ * lead series keeps the lead color even after the bands are reordered).
13
+ */
14
+ const resolveSeries = (series, palette, theme) => series.map((entry, index) => ({
15
+ ...entry,
16
+ name: entry.name ?? entry.key,
17
+ color: entry.color ?? (entry.semantic ? semanticColor(theme, entry.semantic) : paletteColor(palette, index))
18
+ }));
19
+ /**
20
+ * Stacked bands read cleanest as a vertical gradient: darkest at the baseline,
21
+ * lightest at the top edge. Sorting by luminance enforces that for any palette
22
+ * (this is what fixed the "muddy cyan" stack in the lab). Render order maps to
23
+ * stack order in Recharts — first entry sits at the bottom.
24
+ */
25
+ const orderByLuminance = (series) => [...series].sort((lower, upper) => luminance(lower.color) - luminance(upper.color));
26
+ //#endregion
27
+ export { formatShare, orderByLuminance, resolveSeries };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alpic-ai/ui",
3
- "version": "0.0.0-dev.g0999450",
3
+ "version": "0.0.0-dev.g0a2a154",
4
4
  "description": "Alpic design system — shared UI components",
5
5
  "type": "module",
6
6
  "exports": {
@@ -23,48 +23,49 @@
23
23
  "src"
24
24
  ],
25
25
  "peerDependencies": {
26
- "lucide-react": "^1.17.0",
27
- "react": "^19.2.6",
28
- "react-dom": "^19.2.6",
29
- "react-hook-form": "^7.77.0",
26
+ "lucide-react": "^1.18.0",
27
+ "react": "^19.2.7",
28
+ "react-dom": "^19.2.7",
29
+ "react-hook-form": "^7.79.0",
30
30
  "sonner": "^2.0.7",
31
- "tailwindcss": "^4.3.0",
31
+ "tailwindcss": "^4.3.1",
32
32
  "tw-animate-css": "^1.4.0"
33
33
  },
34
34
  "dependencies": {
35
- "@radix-ui/react-accordion": "^1.2.12",
36
- "@radix-ui/react-avatar": "^1.1.11",
37
- "@radix-ui/react-checkbox": "^1.3.3",
38
- "@radix-ui/react-collapsible": "^1.1.12",
39
- "@radix-ui/react-dialog": "^1.1.15",
40
- "@radix-ui/react-dropdown-menu": "^2.1.16",
41
- "@radix-ui/react-label": "^2.1.8",
42
- "@radix-ui/react-popover": "^1.1.15",
43
- "@radix-ui/react-radio-group": "^1.3.8",
44
- "@radix-ui/react-scroll-area": "^1.2.10",
45
- "@radix-ui/react-select": "^2.2.6",
46
- "@radix-ui/react-separator": "^1.1.8",
47
- "@radix-ui/react-slot": "^1.2.4",
48
- "@radix-ui/react-switch": "^1.2.6",
49
- "@radix-ui/react-tabs": "^1.1.13",
50
- "@radix-ui/react-toggle-group": "^1.1.11",
51
- "@radix-ui/react-tooltip": "^1.2.8",
35
+ "@radix-ui/react-accordion": "^1.2.13",
36
+ "@radix-ui/react-avatar": "^1.1.12",
37
+ "@radix-ui/react-checkbox": "^1.3.4",
38
+ "@radix-ui/react-collapsible": "^1.1.13",
39
+ "@radix-ui/react-dialog": "^1.1.16",
40
+ "@radix-ui/react-dropdown-menu": "^2.1.17",
41
+ "@radix-ui/react-label": "^2.1.9",
42
+ "@radix-ui/react-popover": "^1.1.16",
43
+ "@radix-ui/react-radio-group": "^1.4.0",
44
+ "@radix-ui/react-scroll-area": "^1.2.11",
45
+ "@radix-ui/react-select": "^2.3.0",
46
+ "@radix-ui/react-separator": "^1.1.9",
47
+ "@radix-ui/react-slot": "^1.2.5",
48
+ "@radix-ui/react-switch": "^1.3.0",
49
+ "@radix-ui/react-tabs": "^1.1.14",
50
+ "@radix-ui/react-toggle-group": "^1.1.12",
51
+ "@radix-ui/react-tooltip": "^1.2.9",
52
52
  "class-variance-authority": "^0.7.1",
53
53
  "clsx": "^2.1.1",
54
54
  "cmdk": "^1.1.1",
55
+ "recharts": "^3.8.1",
55
56
  "tailwind-merge": "^3.6.0"
56
57
  },
57
58
  "devDependencies": {
58
59
  "@ladle/react": "^5.1.1",
59
- "@tailwindcss/postcss": "^4.3.0",
60
- "@types/react": "19.2.15",
60
+ "@tailwindcss/postcss": "^4.3.1",
61
+ "@types/react": "19.2.17",
61
62
  "@types/react-dom": "19.2.3",
62
- "lucide-react": "^1.17.0",
63
- "react-hook-form": "^7.77.0",
63
+ "lucide-react": "^1.18.0",
64
+ "react-hook-form": "^7.79.0",
64
65
  "shx": "^0.4.0",
65
66
  "sonner": "^2.0.7",
66
- "tailwindcss": "^4.3.0",
67
- "tsdown": "^0.22.1",
67
+ "tailwindcss": "^4.3.1",
68
+ "tsdown": "^0.22.2",
68
69
  "tw-animate-css": "^1.4.0",
69
70
  "typescript": "^6.0.3"
70
71
  },
@@ -0,0 +1,339 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import {
5
+ Area,
6
+ CartesianGrid,
7
+ LabelList,
8
+ AreaChart as RechartsAreaChart,
9
+ ReferenceArea,
10
+ ReferenceDot,
11
+ ReferenceLine,
12
+ ResponsiveContainer,
13
+ Tooltip,
14
+ XAxis,
15
+ YAxis,
16
+ } from "recharts";
17
+
18
+ import { useReducedMotion } from "../hooks/use-reduced-motion";
19
+ import { type ChartSeries, orderByLuminance, resolveSeries } from "../lib/chart";
20
+ import type { ChartPaletteName } from "../lib/chart-palette";
21
+ import { cn } from "../lib/cn";
22
+ import { useChartContext } from "./chart-container";
23
+ import { ChartLegend } from "./chart-legend";
24
+ import { ChartTooltipContent } from "./chart-tooltip";
25
+
26
+ const CURVE_TYPE = { monotone: "monotone", linear: "linear", step: "stepAfter" } as const;
27
+
28
+ export interface ChartMarker {
29
+ x: string | number;
30
+ y: number;
31
+ label?: string;
32
+ color?: string;
33
+ }
34
+
35
+ export interface AreaChartProps {
36
+ data: ReadonlyArray<Record<string, string | number | null | undefined>>;
37
+ index: string;
38
+ series: ChartSeries[];
39
+ variant?: "stacked" | "grouped" | "expand";
40
+ curve?: keyof typeof CURVE_TYPE;
41
+ legend?: boolean;
42
+ valueFlags?: boolean;
43
+ height?: number;
44
+ yAxisWidth?: number;
45
+ palette?: ChartPaletteName;
46
+ referenceLine?: { y: number; label?: string; band?: boolean };
47
+ markers?: ChartMarker[];
48
+ lastValueLabel?: boolean;
49
+ texture?: boolean;
50
+ loading?: boolean;
51
+ valueFormatter?: (value: number) => string;
52
+ labelFormatter?: (label: string | number) => string;
53
+ className?: string;
54
+ }
55
+
56
+ function AreaChart({
57
+ data,
58
+ index,
59
+ series,
60
+ variant = "stacked",
61
+ curve = "monotone",
62
+ legend = false,
63
+ valueFlags = false,
64
+ height = 200,
65
+ yAxisWidth = 48,
66
+ palette,
67
+ referenceLine,
68
+ markers,
69
+ lastValueLabel = false,
70
+ texture = false,
71
+ loading = false,
72
+ valueFormatter = (value) => value.toLocaleString("en-US"),
73
+ labelFormatter,
74
+ className,
75
+ }: AreaChartProps) {
76
+ const { palette: paletteColors, theme } = useChartContext(palette);
77
+ const reactId = React.useId().replace(/:/g, "");
78
+ const reducedMotion = useReducedMotion();
79
+ const animated = !reducedMotion;
80
+
81
+ const resolved = resolveSeries(series, paletteColors, theme);
82
+ const stacked = variant === "stacked" || variant === "expand";
83
+ const rendered = stacked ? orderByLuminance(resolved) : resolved;
84
+
85
+ const strokeOnly = variant === "grouped" && rendered.length > 1;
86
+ const filled = !strokeOnly;
87
+ const lead = resolved[0];
88
+ const withTotal = stacked && rendered.length > 1;
89
+
90
+ // Stacked bands must reach the stack height (sum per x-point), not the tallest
91
+ // single series, or the reference band stops short of the chart top.
92
+ const numericMax = React.useMemo(() => {
93
+ let max = 0;
94
+ for (const row of data) {
95
+ let rowTotal = 0;
96
+ for (const entry of series) {
97
+ const value = Number(row[entry.key]);
98
+ if (Number.isFinite(value)) {
99
+ rowTotal += value;
100
+ if (!stacked && value > max) {
101
+ max = value;
102
+ }
103
+ }
104
+ }
105
+ if (stacked && rowTotal > max) {
106
+ max = rowTotal;
107
+ }
108
+ }
109
+ return max;
110
+ }, [data, series, stacked]);
111
+
112
+ const curveType = CURVE_TYPE[curve];
113
+ const margin = { top: markers?.length ? 18 : 8, right: lastValueLabel ? 56 : 8, bottom: 2, left: 0 };
114
+
115
+ const axis = {
116
+ stroke: theme.border,
117
+ tick: { fill: theme.axisForeground, fontSize: 10, fontFamily: theme.fontMono },
118
+ tickLine: false as const,
119
+ axisLine: { stroke: theme.border, strokeOpacity: 0.6 },
120
+ };
121
+
122
+ const legendItems = resolved.map((entry) => ({ name: entry.name, color: entry.color, dashed: entry.dashed }));
123
+
124
+ const activeDotFor = (entry: (typeof rendered)[number]) =>
125
+ valueFlags
126
+ ? (dotProps: { cx?: number; cy?: number; value?: number | string }) => {
127
+ if (dotProps.cx == null || dotProps.cy == null) {
128
+ return <g />;
129
+ }
130
+ return (
131
+ <g>
132
+ <circle
133
+ cx={dotProps.cx}
134
+ cy={dotProps.cy}
135
+ r={3.5}
136
+ fill={entry.color}
137
+ stroke={theme.card}
138
+ strokeWidth={2}
139
+ />
140
+ <text
141
+ x={dotProps.cx}
142
+ y={dotProps.cy - 8}
143
+ textAnchor="middle"
144
+ fill={entry.color}
145
+ fontFamily={theme.fontMono}
146
+ fontSize={10}
147
+ style={{ fontVariantNumeric: "tabular-nums" }}
148
+ >
149
+ {valueFormatter(Number(dotProps.value ?? 0))}
150
+ </text>
151
+ </g>
152
+ );
153
+ }
154
+ : { r: 3.5, fill: entry.color, stroke: theme.card, strokeWidth: 2 };
155
+
156
+ const fillFor = (entry: (typeof rendered)[number], slot: number) => {
157
+ if (!filled) {
158
+ return "none";
159
+ }
160
+ if (texture && lead && entry.key === lead.key) {
161
+ return `url(#hatch-${reactId})`;
162
+ }
163
+ return `url(#area-${reactId}-${slot})`;
164
+ };
165
+
166
+ const renderLastLabel =
167
+ (color: string) =>
168
+ (props: {
169
+ x?: string | number;
170
+ y?: string | number;
171
+ value?: string | number | boolean | Array<string | number | boolean> | null;
172
+ index?: number;
173
+ }) => {
174
+ if (props.index !== data.length - 1 || props.x == null || props.y == null) {
175
+ return null;
176
+ }
177
+ return (
178
+ <text
179
+ x={Number(props.x) + 6}
180
+ y={Number(props.y)}
181
+ dy={3}
182
+ fill={color}
183
+ fontFamily={theme.fontMono}
184
+ fontSize={10}
185
+ textAnchor="start"
186
+ style={{ fontVariantNumeric: "tabular-nums" }}
187
+ >
188
+ {valueFormatter(Number(props.value ?? 0))}
189
+ </text>
190
+ );
191
+ };
192
+
193
+ const isEmpty = data.length === 0 || rendered.length === 0;
194
+
195
+ return (
196
+ <div data-slot="area-chart" className={cn("flex w-full flex-col gap-3", className)}>
197
+ <div className="w-full" style={{ height }}>
198
+ {loading ? (
199
+ <div className="flex h-full items-center justify-center gap-2.5 font-mono text-quaternary-foreground text-text-xs">
200
+ <span className="size-4 animate-spin rounded-full border-2 border-border border-t-foreground" />
201
+ loading…
202
+ </div>
203
+ ) : isEmpty ? (
204
+ <div className="flex h-full items-center justify-center rounded-lg border border-border border-dashed font-mono text-quaternary-foreground text-text-xs">
205
+ no data in range
206
+ </div>
207
+ ) : (
208
+ <ResponsiveContainer width="100%" height="100%">
209
+ <RechartsAreaChart
210
+ data={data as Record<string, string | number>[]}
211
+ stackOffset={variant === "expand" ? "expand" : "none"}
212
+ margin={margin}
213
+ >
214
+ <defs>
215
+ {filled &&
216
+ rendered.map((entry, slot) => (
217
+ <linearGradient key={entry.key} id={`area-${reactId}-${slot}`} x1="0" y1="0" x2="0" y2="1">
218
+ <stop offset="0%" stopColor={entry.color} stopOpacity={stacked ? 0.78 : 0.5} />
219
+ <stop offset="100%" stopColor={entry.color} stopOpacity={stacked ? 0.32 : 0.04} />
220
+ </linearGradient>
221
+ ))}
222
+ {texture && filled && lead && (
223
+ <pattern
224
+ id={`hatch-${reactId}`}
225
+ patternUnits="userSpaceOnUse"
226
+ width={6}
227
+ height={6}
228
+ patternTransform="rotate(45)"
229
+ >
230
+ <rect width={6} height={6} fill={lead.color} fillOpacity={0.18} />
231
+ <line x1={0} y1={0} x2={0} y2={6} stroke={lead.color} strokeWidth={1.2} strokeOpacity={0.6} />
232
+ </pattern>
233
+ )}
234
+ </defs>
235
+
236
+ <CartesianGrid vertical={false} stroke={theme.grid} strokeDasharray="2 4" />
237
+ <XAxis dataKey={index} {...axis} interval="preserveStartEnd" minTickGap={44} />
238
+ <YAxis
239
+ {...axis}
240
+ width={yAxisWidth}
241
+ tickFormatter={(value: number) =>
242
+ variant === "expand" ? `${Math.round(value * 100)}%` : valueFormatter(value)
243
+ }
244
+ />
245
+ <Tooltip
246
+ offset={12}
247
+ allowEscapeViewBox={{ x: false, y: false }}
248
+ cursor={{ stroke: theme.axisForeground, strokeWidth: 1, strokeDasharray: "3 3" }}
249
+ content={
250
+ <ChartTooltipContent
251
+ valueFormatter={valueFormatter}
252
+ labelFormatter={labelFormatter}
253
+ showTotal={withTotal}
254
+ />
255
+ }
256
+ />
257
+ {referenceLine?.band && (
258
+ <ReferenceArea
259
+ y1={referenceLine.y}
260
+ y2={numericMax}
261
+ fill={theme.warning}
262
+ fillOpacity={0.06}
263
+ ifOverflow="extendDomain"
264
+ />
265
+ )}
266
+ {referenceLine && (
267
+ <ReferenceLine
268
+ y={referenceLine.y}
269
+ stroke={theme.warning}
270
+ strokeDasharray="4 4"
271
+ strokeOpacity={0.6}
272
+ label={
273
+ referenceLine.label
274
+ ? {
275
+ value: referenceLine.label,
276
+ fill: theme.warning,
277
+ fontSize: 9,
278
+ fontFamily: theme.fontMono,
279
+ position: "insideBottomRight",
280
+ }
281
+ : undefined
282
+ }
283
+ />
284
+ )}
285
+
286
+ {rendered.map((entry, slot) => (
287
+ <Area
288
+ key={entry.key}
289
+ type={curveType}
290
+ dataKey={entry.key}
291
+ name={entry.name}
292
+ stackId={stacked ? "stack" : undefined}
293
+ stroke={entry.color}
294
+ strokeWidth={entry.dashed ? 2 : 1.6}
295
+ strokeDasharray={entry.dashed ? "5 3" : undefined}
296
+ fill={fillFor(entry, slot)}
297
+ dot={false}
298
+ activeDot={activeDotFor(entry)}
299
+ isAnimationActive={animated}
300
+ animationDuration={650}
301
+ animationEasing="ease-out"
302
+ >
303
+ {lastValueLabel && <LabelList dataKey={entry.key} content={renderLastLabel(entry.color)} />}
304
+ </Area>
305
+ ))}
306
+
307
+ {markers?.map((marker) => (
308
+ <ReferenceDot
309
+ key={`${marker.x}-${marker.y}`}
310
+ x={marker.x}
311
+ y={marker.y}
312
+ r={3.5}
313
+ fill={marker.color ?? theme.foreground}
314
+ stroke={theme.card}
315
+ strokeWidth={2}
316
+ label={
317
+ marker.label
318
+ ? {
319
+ value: marker.label,
320
+ fill: marker.color ?? theme.foreground,
321
+ fontSize: 9,
322
+ fontFamily: theme.fontMono,
323
+ position: "top",
324
+ }
325
+ : undefined
326
+ }
327
+ />
328
+ ))}
329
+ </RechartsAreaChart>
330
+ </ResponsiveContainer>
331
+ )}
332
+ </div>
333
+
334
+ {legend && !isEmpty && <ChartLegend items={legendItems} style={{ paddingLeft: yAxisWidth }} />}
335
+ </div>
336
+ );
337
+ }
338
+
339
+ export { AreaChart };