@alpic-ai/ui 0.0.0-dev.g16d80c2 → 0.0.0-dev.g172fb9f

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 (60) hide show
  1. package/dist/components/area-chart.d.mts +62 -0
  2. package/dist/components/area-chart.mjs +269 -0
  3. package/dist/components/badge.d.mts +1 -1
  4. package/dist/components/bar-chart.d.mts +48 -0
  5. package/dist/components/bar-chart.mjs +256 -0
  6. package/dist/components/bar-list.d.mts +28 -0
  7. package/dist/components/bar-list.mjs +98 -0
  8. package/dist/components/button.d.mts +1 -1
  9. package/dist/components/chart-card.d.mts +25 -0
  10. package/dist/components/chart-card.mjs +48 -0
  11. package/dist/components/chart-container.d.mts +20 -0
  12. package/dist/components/chart-container.mjs +37 -0
  13. package/dist/components/chart-legend.d.mts +16 -0
  14. package/dist/components/chart-legend.mjs +26 -0
  15. package/dist/components/chart-tooltip.d.mts +33 -0
  16. package/dist/components/chart-tooltip.mjs +52 -0
  17. package/dist/components/donut-chart.d.mts +46 -0
  18. package/dist/components/donut-chart.mjs +185 -0
  19. package/dist/components/grid-fx.d.mts +13 -0
  20. package/dist/components/grid-fx.mjs +188 -0
  21. package/dist/components/heatmap-chart.d.mts +40 -0
  22. package/dist/components/heatmap-chart.mjs +198 -0
  23. package/dist/components/line-chart.d.mts +55 -0
  24. package/dist/components/line-chart.mjs +211 -0
  25. package/dist/components/spinner.d.mts +1 -1
  26. package/dist/components/stat.d.mts +30 -0
  27. package/dist/components/stat.mjs +107 -0
  28. package/dist/hooks/use-chart-theme.d.mts +18 -0
  29. package/dist/hooks/use-chart-theme.mjs +57 -0
  30. package/dist/hooks/use-reduced-motion.d.mts +4 -0
  31. package/dist/hooks/use-reduced-motion.mjs +16 -0
  32. package/dist/lib/chart-palette.d.mts +4 -0
  33. package/dist/lib/chart-palette.mjs +95 -0
  34. package/dist/lib/chart.d.mts +14 -0
  35. package/dist/lib/chart.mjs +27 -0
  36. package/package.json +2 -1
  37. package/src/components/area-chart.tsx +339 -0
  38. package/src/components/bar-chart.tsx +309 -0
  39. package/src/components/bar-list.tsx +150 -0
  40. package/src/components/chart-card.tsx +63 -0
  41. package/src/components/chart-container.tsx +49 -0
  42. package/src/components/chart-legend.tsx +41 -0
  43. package/src/components/chart-tooltip.tsx +93 -0
  44. package/src/components/donut-chart.tsx +217 -0
  45. package/src/components/grid-fx.tsx +238 -0
  46. package/src/components/heatmap-chart.tsx +287 -0
  47. package/src/components/line-chart.tsx +264 -0
  48. package/src/components/stat.tsx +109 -0
  49. package/src/hooks/use-chart-theme.ts +75 -0
  50. package/src/hooks/use-reduced-motion.ts +17 -0
  51. package/src/lib/chart-palette.ts +110 -0
  52. package/src/lib/chart.ts +56 -0
  53. package/src/stories/area-chart.stories.tsx +200 -0
  54. package/src/stories/bar-chart.stories.tsx +169 -0
  55. package/src/stories/bar-list.stories.tsx +85 -0
  56. package/src/stories/donut-chart.stories.tsx +112 -0
  57. package/src/stories/heatmap-chart.stories.tsx +107 -0
  58. package/src/stories/line-chart.stories.tsx +146 -0
  59. package/src/stories/stat.stories.tsx +64 -0
  60. package/src/styles/tokens.css +63 -0
@@ -0,0 +1,98 @@
1
+ "use client";
2
+ import { cn } from "../lib/cn.mjs";
3
+ import { useReducedMotion } from "../hooks/use-reduced-motion.mjs";
4
+ import { rampColor } from "../lib/chart-palette.mjs";
5
+ import { formatShare } from "../lib/chart.mjs";
6
+ import { useChartContext } from "./chart-container.mjs";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ import * as React$1 from "react";
9
+ //#region src/components/bar-list.tsx
10
+ const PLACEHOLDER_HEIGHT = 168;
11
+ const RAMP_CEILING = .8;
12
+ const IN_FILL_SHADOW = "0 1px 2px rgb(0 0 0 / 0.28)";
13
+ function BarList({ data, index, dataKey = "value", maxItems, palette, loading = false, valueFormatter = (value) => value.toLocaleString("en-US"), labelFormatter, className }) {
14
+ const { paletteName } = useChartContext(palette);
15
+ const reducedMotion = useReducedMotion();
16
+ const [mounted, setMounted] = React$1.useState(false);
17
+ const [active, setActive] = React$1.useState(null);
18
+ React$1.useEffect(() => {
19
+ setMounted(true);
20
+ }, []);
21
+ const total = React$1.useMemo(() => data.reduce((sum, row) => sum + (Number(row[dataKey]) || 0), 0), [data, dataKey]);
22
+ const rows = React$1.useMemo(() => {
23
+ const mapped = data.map((row) => ({
24
+ name: String(row[index] ?? ""),
25
+ value: Number(row[dataKey]) || 0
26
+ }));
27
+ mapped.sort((lower, upper) => upper.value - lower.value);
28
+ const capped = maxItems ? mapped.slice(0, maxItems) : mapped;
29
+ return capped.map((row, rank) => ({
30
+ ...row,
31
+ color: rampColor(paletteName, (capped.length > 1 ? rank / (capped.length - 1) : 0) * RAMP_CEILING)
32
+ }));
33
+ }, [
34
+ data,
35
+ index,
36
+ dataKey,
37
+ maxItems,
38
+ paletteName
39
+ ]);
40
+ const maxValue = rows.reduce((max, row) => row.value > max ? row.value : max, 0);
41
+ const isEmpty = rows.length === 0;
42
+ const formatName = (name) => labelFormatter ? labelFormatter(name) : name;
43
+ if (loading) return /* @__PURE__ */ jsxs("div", {
44
+ className: cn("flex items-center justify-center gap-2.5 font-mono text-quaternary-foreground text-text-xs", className),
45
+ style: { minHeight: PLACEHOLDER_HEIGHT },
46
+ children: [/* @__PURE__ */ jsx("span", { className: "size-4 animate-spin rounded-full border-2 border-border border-t-foreground" }), "loading…"]
47
+ });
48
+ if (isEmpty) return /* @__PURE__ */ jsx("div", {
49
+ className: cn("flex items-center justify-center rounded-lg border border-border border-dashed font-mono text-quaternary-foreground text-text-xs", className),
50
+ style: { minHeight: PLACEHOLDER_HEIGHT },
51
+ children: "no data in range"
52
+ });
53
+ return /* @__PURE__ */ jsx("div", {
54
+ "data-slot": "bar-list",
55
+ className: cn("flex w-full flex-col gap-2", className),
56
+ children: rows.map((row, slot) => {
57
+ const fraction = maxValue > 0 ? row.value / maxValue : 0;
58
+ const fillWidth = !reducedMotion && !mounted ? "0%" : `${fraction * 100}%`;
59
+ const dimmed = active !== null && active !== slot;
60
+ const isActive = active === slot;
61
+ return /* @__PURE__ */ jsxs("div", {
62
+ onMouseEnter: () => setActive(slot),
63
+ onMouseLeave: () => setActive(null),
64
+ className: "flex items-center gap-3",
65
+ children: [/* @__PURE__ */ jsxs("div", {
66
+ className: "relative h-[30px] flex-1 overflow-hidden rounded-md bg-muted",
67
+ children: [/* @__PURE__ */ jsx("span", {
68
+ className: "pointer-events-none absolute inset-x-3 top-1/2 z-0 -translate-y-1/2 truncate type-text-xs font-medium text-foreground",
69
+ children: formatName(row.name)
70
+ }), /* @__PURE__ */ jsx("div", {
71
+ className: "absolute inset-y-0 left-0 z-10 overflow-hidden rounded-md motion-safe:transition-[width,opacity] motion-safe:duration-700 motion-safe:ease-out",
72
+ style: {
73
+ width: fillWidth,
74
+ background: `linear-gradient(90deg, ${row.color}, color-mix(in oklab, ${row.color} 82%, transparent))`,
75
+ boxShadow: `inset 0 0 0 1px ${row.color}`,
76
+ opacity: dimmed ? .45 : 1
77
+ },
78
+ children: /* @__PURE__ */ jsx("span", {
79
+ className: "pointer-events-none absolute top-1/2 -translate-y-1/2 truncate type-text-xs font-medium text-white",
80
+ style: {
81
+ left: 12,
82
+ width: `calc(${100 / Math.max(fraction, 1e-4)}% - 24px)`,
83
+ textShadow: IN_FILL_SHADOW
84
+ },
85
+ children: formatName(row.name)
86
+ })
87
+ })]
88
+ }), /* @__PURE__ */ jsx("span", {
89
+ className: "w-16 shrink-0 text-right font-mono text-text-xs tabular-nums motion-safe:transition-colors",
90
+ style: { color: isActive ? row.color : void 0 },
91
+ children: isActive ? formatShare(total > 0 ? row.value / total : 0) : valueFormatter(row.value)
92
+ })]
93
+ }, row.name);
94
+ })
95
+ });
96
+ }
97
+ //#endregion
98
+ export { BarList };
@@ -3,7 +3,7 @@ import * as React$1 from "react";
3
3
 
4
4
  //#region src/components/button.d.ts
5
5
  declare const buttonVariants: (props?: ({
6
- variant?: "destructive" | "secondary" | "primary" | "tertiary" | "link" | "link-muted" | "cta" | null | undefined;
6
+ variant?: "destructive" | "primary" | "secondary" | "tertiary" | "link" | "link-muted" | "cta" | null | undefined;
7
7
  size?: "default" | "icon" | "icon-rounded" | "pill" | null | undefined;
8
8
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
9
9
  interface ButtonProps extends React$1.ComponentProps<"button">, VariantProps<typeof buttonVariants> {
@@ -0,0 +1,25 @@
1
+ import { ChartPaletteName } from "../lib/chart-palette.mjs";
2
+ import * as React$1 from "react";
3
+
4
+ //#region src/components/chart-card.d.ts
5
+ interface ChartCardProps extends Omit<React$1.ComponentProps<"section">, "title"> {
6
+ palette?: ChartPaletteName;
7
+ kicker?: React$1.ReactNode;
8
+ title?: React$1.ReactNode;
9
+ description?: React$1.ReactNode;
10
+ action?: React$1.ReactNode;
11
+ accent?: "top" | "left";
12
+ }
13
+ declare function ChartCard({
14
+ palette,
15
+ kicker,
16
+ title,
17
+ description,
18
+ action,
19
+ accent,
20
+ className,
21
+ children,
22
+ ...props
23
+ }: ChartCardProps): React$1.JSX.Element;
24
+ //#endregion
25
+ export { ChartCard, ChartCardProps };
@@ -0,0 +1,48 @@
1
+ "use client";
2
+ import { cn } from "../lib/cn.mjs";
3
+ import { CHART_PALETTES } from "../lib/chart-palette.mjs";
4
+ import { ChartContainer } from "./chart-container.mjs";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ //#region src/components/chart-card.tsx
7
+ function ChartCard({ palette = "magenta", kicker, title, description, action, accent = "top", className, children, ...props }) {
8
+ const lead = CHART_PALETTES[palette][0];
9
+ const isLeft = accent === "left";
10
+ return /* @__PURE__ */ jsxs("section", {
11
+ "data-slot": "chart-card",
12
+ className: cn("chart-rise relative overflow-hidden rounded-xl border bg-card p-5 text-card-foreground shadow-shadow", isLeft && "pl-6", className),
13
+ ...props,
14
+ children: [
15
+ /* @__PURE__ */ jsx("span", {
16
+ "aria-hidden": true,
17
+ className: cn("absolute", isLeft ? "inset-y-0 left-0 w-[3px]" : "inset-x-0 top-0 h-[3px]"),
18
+ style: { background: lead }
19
+ }),
20
+ (kicker || title || action) && /* @__PURE__ */ jsxs("header", {
21
+ className: "mb-4 flex items-start justify-between gap-4",
22
+ children: [/* @__PURE__ */ jsxs("div", {
23
+ className: "flex flex-col gap-1",
24
+ children: [
25
+ kicker && /* @__PURE__ */ jsx("span", {
26
+ className: "font-mono text-[10px] text-primary uppercase tracking-[0.18em]",
27
+ children: kicker
28
+ }),
29
+ title && /* @__PURE__ */ jsx("h3", {
30
+ className: "type-text-md font-semibold leading-none tracking-tight",
31
+ children: title
32
+ }),
33
+ description && /* @__PURE__ */ jsx("p", {
34
+ className: "type-text-xs text-muted-foreground",
35
+ children: description
36
+ })
37
+ ]
38
+ }), action]
39
+ }),
40
+ /* @__PURE__ */ jsx(ChartContainer, {
41
+ palette,
42
+ children
43
+ })
44
+ ]
45
+ });
46
+ }
47
+ //#endregion
48
+ export { ChartCard };
@@ -0,0 +1,20 @@
1
+ import { ChartTheme } from "../hooks/use-chart-theme.mjs";
2
+ import { ChartPaletteName } from "../lib/chart-palette.mjs";
3
+ import * as React$1 from "react";
4
+
5
+ //#region src/components/chart-container.d.ts
6
+ interface ChartContextValue {
7
+ palette: readonly string[];
8
+ paletteName: ChartPaletteName;
9
+ theme: ChartTheme;
10
+ }
11
+ declare function ChartContainer({
12
+ palette,
13
+ className,
14
+ ...props
15
+ }: React$1.ComponentProps<"div"> & {
16
+ palette?: ChartPaletteName;
17
+ }): React$1.JSX.Element;
18
+ declare function useChartContext(paletteOverride?: ChartPaletteName): ChartContextValue;
19
+ //#endregion
20
+ export { ChartContainer, type ChartContextValue, useChartContext };
@@ -0,0 +1,37 @@
1
+ "use client";
2
+ import { cn } from "../lib/cn.mjs";
3
+ import { CHART_PALETTES } from "../lib/chart-palette.mjs";
4
+ import { useChartTheme } from "../hooks/use-chart-theme.mjs";
5
+ import { jsx } from "react/jsx-runtime";
6
+ import * as React$1 from "react";
7
+ //#region src/components/chart-container.tsx
8
+ const ChartContext = React$1.createContext(null);
9
+ function ChartContainer({ palette = "magenta", className, ...props }) {
10
+ const theme = useChartTheme();
11
+ const value = React$1.useMemo(() => ({
12
+ palette: CHART_PALETTES[palette],
13
+ paletteName: palette,
14
+ theme
15
+ }), [palette, theme]);
16
+ return /* @__PURE__ */ jsx(ChartContext.Provider, {
17
+ value,
18
+ children: /* @__PURE__ */ jsx("div", {
19
+ "data-slot": "chart-container",
20
+ className: cn("flex flex-col gap-4", className),
21
+ ...props
22
+ })
23
+ });
24
+ }
25
+ function useChartContext(paletteOverride) {
26
+ const provided = React$1.useContext(ChartContext);
27
+ const fallbackTheme = useChartTheme();
28
+ if (provided && !paletteOverride) return provided;
29
+ const paletteName = paletteOverride ?? provided?.paletteName ?? "magenta";
30
+ return {
31
+ palette: CHART_PALETTES[paletteName],
32
+ paletteName,
33
+ theme: provided?.theme ?? fallbackTheme
34
+ };
35
+ }
36
+ //#endregion
37
+ export { ChartContainer, useChartContext };
@@ -0,0 +1,16 @@
1
+ //#region src/components/chart-legend.d.ts
2
+ interface ChartLegendItem {
3
+ name: string;
4
+ color: string;
5
+ dashed?: boolean;
6
+ }
7
+ interface ChartLegendProps extends React.HTMLAttributes<HTMLDivElement> {
8
+ items: ChartLegendItem[];
9
+ }
10
+ declare function ChartLegend({
11
+ items,
12
+ className,
13
+ ...props
14
+ }: ChartLegendProps): import("react").JSX.Element;
15
+ //#endregion
16
+ export { ChartLegend, ChartLegendItem, ChartLegendProps };
@@ -0,0 +1,26 @@
1
+ "use client";
2
+ import { cn } from "../lib/cn.mjs";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ //#region src/components/chart-legend.tsx
5
+ function Swatch({ color, dashed }) {
6
+ return /* @__PURE__ */ jsx("span", {
7
+ "aria-hidden": true,
8
+ className: "size-2 shrink-0 rounded-full",
9
+ style: dashed ? { border: `1.5px solid ${color}` } : { background: color }
10
+ });
11
+ }
12
+ function ChartLegend({ items, className, ...props }) {
13
+ return /* @__PURE__ */ jsx("div", {
14
+ className: cn("flex flex-wrap gap-x-4 gap-y-1.5", className),
15
+ ...props,
16
+ children: items.map((item) => /* @__PURE__ */ jsxs("span", {
17
+ className: "inline-flex items-center gap-1.5 font-mono text-[10px] text-muted-foreground uppercase tracking-[0.12em]",
18
+ children: [/* @__PURE__ */ jsx(Swatch, {
19
+ color: item.color,
20
+ dashed: item.dashed
21
+ }), item.name]
22
+ }, item.name))
23
+ });
24
+ }
25
+ //#endregion
26
+ export { ChartLegend };
@@ -0,0 +1,33 @@
1
+ //#region src/components/chart-tooltip.d.ts
2
+ interface ChartTooltipItem {
3
+ name?: string | number;
4
+ value?: number | string;
5
+ color?: string;
6
+ stroke?: string;
7
+ fill?: string;
8
+ dataKey?: string | number;
9
+ }
10
+ interface ChartTooltipContentProps {
11
+ active?: boolean;
12
+ payload?: ChartTooltipItem[];
13
+ label?: string | number;
14
+ valueFormatter?: (value: number) => string;
15
+ labelFormatter?: (label: string | number) => string;
16
+ hideLabel?: boolean;
17
+ showTotal?: boolean;
18
+ totalLabel?: string;
19
+ className?: string;
20
+ }
21
+ declare function ChartTooltipContent({
22
+ active,
23
+ payload,
24
+ label,
25
+ valueFormatter,
26
+ labelFormatter,
27
+ hideLabel,
28
+ showTotal,
29
+ totalLabel,
30
+ className
31
+ }: ChartTooltipContentProps): import("react").JSX.Element | null;
32
+ //#endregion
33
+ export { ChartTooltipContent, ChartTooltipContentProps, ChartTooltipItem };
@@ -0,0 +1,52 @@
1
+ "use client";
2
+ import { cn } from "../lib/cn.mjs";
3
+ import { useChartContext } from "./chart-container.mjs";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ //#region src/components/chart-tooltip.tsx
6
+ function ChartTooltipContent({ active, payload, label, valueFormatter = (value) => value.toLocaleString("en-US"), labelFormatter, hideLabel, showTotal, totalLabel = "Total", className }) {
7
+ const { theme } = useChartContext();
8
+ if (!active || !payload?.length) return null;
9
+ const total = payload.reduce((sum, item) => sum + (typeof item.value === "number" ? item.value : Number(item.value ?? 0)), 0);
10
+ return /* @__PURE__ */ jsxs("div", {
11
+ className: cn("min-w-[130px] rounded-lg border px-3 py-2.5 shadow-lg", "border-border bg-popover text-popover-foreground", className),
12
+ children: [!hideLabel && label !== void 0 && /* @__PURE__ */ jsx("p", {
13
+ className: "font-mono text-[10px] uppercase tracking-wider text-quaternary-foreground mb-1.5",
14
+ children: labelFormatter ? labelFormatter(label) : label
15
+ }), /* @__PURE__ */ jsxs("div", {
16
+ className: "flex flex-col gap-1",
17
+ children: [payload.map((item, index) => {
18
+ const swatch = [
19
+ item.color,
20
+ item.stroke,
21
+ item.fill
22
+ ].find((candidate) => typeof candidate === "string" && candidate.length > 0 && !candidate.startsWith("url(")) ?? theme.mutedForeground;
23
+ const numeric = typeof item.value === "number" ? item.value : Number(item.value ?? 0);
24
+ return /* @__PURE__ */ jsxs("div", {
25
+ className: "flex items-center justify-between gap-4 text-text-xs",
26
+ children: [/* @__PURE__ */ jsxs("span", {
27
+ className: "inline-flex items-center gap-2 text-muted-foreground",
28
+ children: [/* @__PURE__ */ jsx("span", {
29
+ "aria-hidden": true,
30
+ className: "size-2 shrink-0 rounded-[2px]",
31
+ style: { background: swatch }
32
+ }), item.name]
33
+ }), /* @__PURE__ */ jsx("span", {
34
+ className: "font-mono font-semibold tabular-nums text-foreground",
35
+ children: valueFormatter(numeric)
36
+ })]
37
+ }, `${item.dataKey ?? index}`);
38
+ }), showTotal && /* @__PURE__ */ jsxs("div", {
39
+ className: "mt-1 flex items-center justify-between gap-4 border-border border-t pt-1.5 text-text-xs",
40
+ children: [/* @__PURE__ */ jsx("span", {
41
+ className: "font-mono text-quaternary-foreground uppercase tracking-wider text-[10px]",
42
+ children: totalLabel
43
+ }), /* @__PURE__ */ jsx("span", {
44
+ className: "font-mono font-semibold tabular-nums text-foreground",
45
+ children: valueFormatter(total)
46
+ })]
47
+ })]
48
+ })]
49
+ });
50
+ }
51
+ //#endregion
52
+ export { ChartTooltipContent };
@@ -0,0 +1,46 @@
1
+ import { ChartPaletteName } from "../lib/chart-palette.mjs";
2
+ import * as React$1 from "react";
3
+
4
+ //#region src/components/donut-chart.d.ts
5
+ declare const GEOMETRY: {
6
+ readonly donut: {
7
+ readonly inner: "64%";
8
+ readonly outer: "92%";
9
+ };
10
+ readonly ring: {
11
+ readonly inner: "78%";
12
+ readonly outer: "92%";
13
+ };
14
+ };
15
+ interface DonutChartProps {
16
+ data: ReadonlyArray<Record<string, string | number | null | undefined>>;
17
+ index: string;
18
+ dataKey?: string;
19
+ variant?: keyof typeof GEOMETRY;
20
+ legend?: boolean;
21
+ paddingAngle?: number;
22
+ height?: number;
23
+ palette?: ChartPaletteName;
24
+ centerLabel?: string;
25
+ loading?: boolean;
26
+ valueFormatter?: (value: number) => string;
27
+ labelFormatter?: (label: string | number) => string;
28
+ className?: string;
29
+ }
30
+ declare function DonutChart({
31
+ data,
32
+ index,
33
+ dataKey,
34
+ variant,
35
+ legend,
36
+ paddingAngle,
37
+ height,
38
+ palette,
39
+ centerLabel,
40
+ loading,
41
+ valueFormatter,
42
+ labelFormatter,
43
+ className
44
+ }: DonutChartProps): React$1.JSX.Element;
45
+ //#endregion
46
+ export { DonutChart, DonutChartProps };
@@ -0,0 +1,185 @@
1
+ "use client";
2
+ import { cn } from "../lib/cn.mjs";
3
+ import { useReducedMotion } from "../hooks/use-reduced-motion.mjs";
4
+ import { paletteColor } from "../lib/chart-palette.mjs";
5
+ import { formatShare } from "../lib/chart.mjs";
6
+ import { useChartContext } from "./chart-container.mjs";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ import * as React$1 from "react";
9
+ import { Cell, Pie, PieChart, ResponsiveContainer } from "recharts";
10
+ //#region src/components/donut-chart.tsx
11
+ const GEOMETRY = {
12
+ donut: {
13
+ inner: "64%",
14
+ outer: "92%"
15
+ },
16
+ ring: {
17
+ inner: "78%",
18
+ outer: "92%"
19
+ }
20
+ };
21
+ function DonutChart({ data, index, dataKey = "value", variant = "donut", legend = false, paddingAngle = 1, height = 220, palette, centerLabel = "total", loading = false, valueFormatter = (value) => value.toLocaleString("en-US"), labelFormatter, className }) {
22
+ const { palette: paletteColors, theme } = useChartContext(palette);
23
+ const reactId = React$1.useId().replace(/:/g, "");
24
+ const animated = !useReducedMotion();
25
+ const [active, setActive] = React$1.useState(null);
26
+ const rowRefs = React$1.useRef([]);
27
+ const slices = React$1.useMemo(() => {
28
+ const mapped = data.map((row) => ({
29
+ name: String(row[index] ?? ""),
30
+ value: Number(row[dataKey]) || 0
31
+ }));
32
+ mapped.sort((lower, upper) => upper.value - lower.value);
33
+ return mapped.map((slice, rank) => ({
34
+ ...slice,
35
+ color: paletteColor(paletteColors, rank)
36
+ }));
37
+ }, [
38
+ data,
39
+ index,
40
+ dataKey,
41
+ paletteColors
42
+ ]);
43
+ const total = slices.reduce((sum, slice) => sum + slice.value, 0);
44
+ const maxValue = slices.reduce((max, slice) => slice.value > max ? slice.value : max, 0);
45
+ const geometry = GEOMETRY[variant] ?? GEOMETRY.donut;
46
+ const formatName = (name) => labelFormatter ? labelFormatter(name) : name;
47
+ const isEmpty = slices.length === 0 || total <= 0;
48
+ const activeSlice = active !== null ? slices[active] : void 0;
49
+ const centerTitle = activeSlice ? formatName(activeSlice.name) : centerLabel;
50
+ const centerValue = valueFormatter(activeSlice ? activeSlice.value : total);
51
+ React$1.useEffect(() => {
52
+ if (active !== null) rowRefs.current[active]?.scrollIntoView({ block: "nearest" });
53
+ }, [active]);
54
+ return /* @__PURE__ */ jsx("div", {
55
+ "data-slot": "donut-chart",
56
+ className: cn("@container flex w-full flex-col gap-3", className),
57
+ children: loading ? /* @__PURE__ */ jsxs("div", {
58
+ className: "flex items-center justify-center gap-2.5 font-mono text-quaternary-foreground text-text-xs",
59
+ style: { height },
60
+ children: [/* @__PURE__ */ jsx("span", { className: "size-4 animate-spin rounded-full border-2 border-border border-t-foreground" }), "loading…"]
61
+ }) : isEmpty ? /* @__PURE__ */ jsx("div", {
62
+ className: "flex items-center justify-center rounded-lg border border-border border-dashed font-mono text-quaternary-foreground text-text-xs",
63
+ style: { height },
64
+ children: "no data in range"
65
+ }) : /* @__PURE__ */ jsxs("div", {
66
+ className: "flex flex-col items-center gap-5 @md:flex-row",
67
+ children: [/* @__PURE__ */ jsxs("div", {
68
+ className: "relative shrink-0",
69
+ style: {
70
+ width: height,
71
+ height
72
+ },
73
+ children: [/* @__PURE__ */ jsx(ResponsiveContainer, {
74
+ width: "100%",
75
+ height: "100%",
76
+ children: /* @__PURE__ */ jsxs(PieChart, { children: [/* @__PURE__ */ jsx("defs", { children: slices.map((slice, slot) => /* @__PURE__ */ jsxs("linearGradient", {
77
+ id: `donut-${reactId}-${slot}`,
78
+ x1: "0",
79
+ y1: "0",
80
+ x2: "0",
81
+ y2: "1",
82
+ children: [/* @__PURE__ */ jsx("stop", {
83
+ offset: "0%",
84
+ stopColor: slice.color,
85
+ stopOpacity: 1
86
+ }), /* @__PURE__ */ jsx("stop", {
87
+ offset: "100%",
88
+ stopColor: slice.color,
89
+ stopOpacity: .7
90
+ })]
91
+ }, slice.name)) }), /* @__PURE__ */ jsx(Pie, {
92
+ data: slices,
93
+ dataKey: "value",
94
+ nameKey: "name",
95
+ innerRadius: geometry.inner,
96
+ outerRadius: geometry.outer,
97
+ paddingAngle,
98
+ startAngle: 90,
99
+ endAngle: -270,
100
+ cornerRadius: 2,
101
+ stroke: theme.card,
102
+ strokeWidth: 1.5,
103
+ isAnimationActive: animated,
104
+ animationDuration: 650,
105
+ animationEasing: "ease-out",
106
+ onMouseEnter: (_entry, sliceIndex) => setActive(sliceIndex),
107
+ onMouseLeave: () => setActive(null),
108
+ children: slices.map((slice, slot) => /* @__PURE__ */ jsx(Cell, {
109
+ className: "motion-safe:[transition:fill-opacity_180ms_ease-out]",
110
+ fill: `url(#donut-${reactId}-${slot})`,
111
+ fillOpacity: active === null || active === slot ? 1 : .55,
112
+ stroke: theme.card,
113
+ strokeWidth: 1.5
114
+ }, slice.name))
115
+ })] })
116
+ }), /* @__PURE__ */ jsxs("div", {
117
+ className: "pointer-events-none absolute inset-0 flex flex-col items-center justify-center gap-1 text-center",
118
+ children: [
119
+ /* @__PURE__ */ jsx("span", {
120
+ className: "max-w-[72%] truncate font-mono text-[10px] text-quaternary-foreground uppercase tracking-[0.18em]",
121
+ children: centerTitle
122
+ }),
123
+ /* @__PURE__ */ jsx("span", {
124
+ className: "font-mono font-semibold text-[28px] text-foreground leading-none tabular-nums",
125
+ children: centerValue
126
+ }),
127
+ activeSlice && /* @__PURE__ */ jsx("span", {
128
+ className: "font-mono font-medium text-[11px] tabular-nums",
129
+ style: { color: activeSlice.color },
130
+ children: formatShare(activeSlice.value / total)
131
+ })
132
+ ]
133
+ })]
134
+ }), legend && /* @__PURE__ */ jsx("div", {
135
+ "data-slot": "donut-readout",
136
+ className: "flex min-w-0 flex-1 flex-col overflow-y-auto pr-1",
137
+ style: { maxHeight: height },
138
+ children: slices.map((slice, slot) => /* @__PURE__ */ jsxs("div", {
139
+ ref: (node) => {
140
+ rowRefs.current[slot] = node;
141
+ },
142
+ onMouseEnter: () => setActive(slot),
143
+ onMouseLeave: () => setActive(null),
144
+ className: cn("flex flex-col gap-1.5 border-border/40 border-b px-2 py-2 text-text-xs last:border-b-0 motion-safe:transition-colors", active === slot ? "bg-muted/50" : "bg-transparent"),
145
+ children: [/* @__PURE__ */ jsxs("div", {
146
+ className: "flex items-center justify-between gap-3",
147
+ children: [/* @__PURE__ */ jsxs("span", {
148
+ className: "inline-flex min-w-0 items-center gap-2 text-muted-foreground",
149
+ children: [/* @__PURE__ */ jsx("span", {
150
+ "aria-hidden": true,
151
+ className: "h-2 w-2.5 shrink-0 rounded-[3px]",
152
+ style: { background: slice.color }
153
+ }), /* @__PURE__ */ jsx("span", {
154
+ className: "truncate",
155
+ children: formatName(slice.name)
156
+ })]
157
+ }), /* @__PURE__ */ jsxs("span", {
158
+ className: "flex shrink-0 items-center gap-3 font-mono tabular-nums",
159
+ children: [/* @__PURE__ */ jsx("span", {
160
+ className: "min-w-[3.5rem] text-right font-semibold text-foreground",
161
+ children: valueFormatter(slice.value)
162
+ }), /* @__PURE__ */ jsx("span", {
163
+ className: "w-10 text-right text-quaternary-foreground",
164
+ children: formatShare(slice.value / total)
165
+ })]
166
+ })]
167
+ }), /* @__PURE__ */ jsx("span", {
168
+ "aria-hidden": true,
169
+ className: "relative block h-[2px] w-full overflow-hidden rounded-full bg-border/40",
170
+ children: /* @__PURE__ */ jsx("span", {
171
+ className: "absolute inset-y-0 left-0 rounded-full motion-safe:transition-[width] motion-safe:duration-500",
172
+ style: {
173
+ width: `${maxValue > 0 ? slice.value / maxValue * 100 : 0}%`,
174
+ background: slice.color,
175
+ opacity: active === null || active === slot ? 1 : .45
176
+ }
177
+ })
178
+ })]
179
+ }, slice.name))
180
+ })]
181
+ })
182
+ });
183
+ }
184
+ //#endregion
185
+ export { DonutChart };
@@ -0,0 +1,13 @@
1
+ import * as React$1 from "react";
2
+
3
+ //#region src/components/grid-fx.d.ts
4
+ declare function GridFx({
5
+ className,
6
+ cellSize,
7
+ style,
8
+ ...props
9
+ }: React$1.ComponentProps<"canvas"> & {
10
+ cellSize?: number;
11
+ }): React$1.JSX.Element | null;
12
+ //#endregion
13
+ export { GridFx };