@alpic-ai/ui 1.143.0 → 1.145.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,10 @@
1
1
  "use client";
2
2
  import { cn } from "../lib/cn.mjs";
3
- import { useReducedMotion } from "../hooks/use-reduced-motion.mjs";
4
3
  import { makeXAxisTick, orderByLuminance, resolveSeries } from "../lib/chart.mjs";
5
4
  import { useChartContext } from "./chart-container.mjs";
6
5
  import { ChartLegend } from "./chart-legend.mjs";
7
6
  import { ChartTooltipContent } from "./chart-tooltip.mjs";
7
+ import { Skeleton } from "./skeleton.mjs";
8
8
  import { jsx, jsxs } from "react/jsx-runtime";
9
9
  import * as React$1 from "react";
10
10
  import { Area, AreaChart as AreaChart$1, CartesianGrid, LabelList, ReferenceArea, ReferenceDot, ReferenceLine, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
@@ -17,7 +17,6 @@ const CURVE_TYPE = {
17
17
  function AreaChart({ data, index, series, variant = "stacked", curve = "monotone", legend = false, legendAlign = "left", valueFlags = false, height = 200, yAxisWidth = 48, palette, referenceLine, markers, lastValueLabel = false, texture = false, loading = false, valueFormatter = (value) => value.toLocaleString("en-US"), labelFormatter, className }) {
18
18
  const { palette: paletteColors, theme } = useChartContext(palette);
19
19
  const reactId = React$1.useId().replace(/:/g, "");
20
- const animated = !useReducedMotion();
21
20
  const resolved = resolveSeries(series, paletteColors, theme);
22
21
  const stacked = variant === "stacked" || variant === "expand";
23
22
  const rendered = stacked ? orderByLuminance(resolved) : resolved;
@@ -115,14 +114,11 @@ function AreaChart({ data, index, series, variant = "stacked", curve = "monotone
115
114
  const isEmpty = data.length === 0 || rendered.length === 0;
116
115
  return /* @__PURE__ */ jsxs("div", {
117
116
  "data-slot": "area-chart",
118
- className: cn("flex w-full flex-col gap-3", className),
117
+ className: cn("flex w-full flex-col gap-3", "[&_.recharts-surface]:outline-none [&_.recharts-wrapper]:outline-none", className),
119
118
  children: [/* @__PURE__ */ jsx("div", {
120
119
  className: "w-full",
121
120
  style: { height },
122
- children: loading ? /* @__PURE__ */ jsxs("div", {
123
- className: "flex h-full items-center justify-center gap-2.5 font-mono text-quaternary-foreground text-text-xs",
124
- children: [/* @__PURE__ */ jsx("span", { className: "size-4 animate-spin rounded-full border-2 border-border border-t-foreground" }), "loading…"]
125
- }) : isEmpty ? /* @__PURE__ */ jsx("div", {
121
+ children: loading ? /* @__PURE__ */ jsx(Skeleton, { className: "h-full w-full rounded-lg" }) : isEmpty ? /* @__PURE__ */ jsx("div", {
126
122
  className: "flex h-full items-center justify-center rounded-lg border border-border border-dashed font-mono text-quaternary-foreground text-text-xs",
127
123
  children: "no data in range"
128
124
  }) : /* @__PURE__ */ jsx(ResponsiveContainer, {
@@ -183,7 +179,7 @@ function AreaChart({ data, index, series, variant = "stacked", curve = "monotone
183
179
  ...axis,
184
180
  tick: makeXAxisTick(theme),
185
181
  interval: "preserveStartEnd",
186
- minTickGap: 44
182
+ minTickGap: 56
187
183
  }),
188
184
  /* @__PURE__ */ jsx(YAxis, {
189
185
  ...axis,
@@ -238,7 +234,7 @@ function AreaChart({ data, index, series, variant = "stacked", curve = "monotone
238
234
  fill: fillFor(entry, slot),
239
235
  dot: false,
240
236
  activeDot: activeDotFor(entry),
241
- isAnimationActive: animated,
237
+ isAnimationActive: false,
242
238
  animationDuration: 650,
243
239
  animationEasing: "ease-out",
244
240
  children: lastValueLabel && /* @__PURE__ */ jsx(LabelList, {
@@ -1,10 +1,10 @@
1
1
  "use client";
2
2
  import { cn } from "../lib/cn.mjs";
3
- import { useReducedMotion } from "../hooks/use-reduced-motion.mjs";
4
3
  import { makeXAxisTick, orderByLuminance, resolveSeries } from "../lib/chart.mjs";
5
4
  import { useChartContext } from "./chart-container.mjs";
6
5
  import { ChartLegend } from "./chart-legend.mjs";
7
6
  import { ChartTooltipContent } from "./chart-tooltip.mjs";
7
+ import { Skeleton } from "./skeleton.mjs";
8
8
  import { jsx, jsxs } from "react/jsx-runtime";
9
9
  import * as React$1 from "react";
10
10
  import { Bar, BarChart as BarChart$1, CartesianGrid, LabelList, ReferenceArea, ReferenceDot, ReferenceLine, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
@@ -14,7 +14,6 @@ const MAX_BAR_SIZE = 48;
14
14
  function BarChart({ data, index, series, variant = "stacked", legend = false, legendAlign = "left", valueLabels = false, height = 200, yAxisWidth = 48, palette, referenceLine, markers, texture = false, loading = false, valueFormatter = (value) => value.toLocaleString("en-US"), labelFormatter, className }) {
15
15
  const { palette: paletteColors, theme } = useChartContext(palette);
16
16
  const reactId = React$1.useId().replace(/:/g, "");
17
- const animated = !useReducedMotion();
18
17
  const resolved = resolveSeries(series, paletteColors, theme);
19
18
  const stacked = variant === "stacked" || variant === "expand";
20
19
  const rendered = stacked ? orderByLuminance(resolved) : resolved;
@@ -98,14 +97,11 @@ function BarChart({ data, index, series, variant = "stacked", legend = false, le
98
97
  const isEmpty = data.length === 0 || rendered.length === 0;
99
98
  return /* @__PURE__ */ jsxs("div", {
100
99
  "data-slot": "bar-chart",
101
- className: cn("flex w-full flex-col gap-3", className),
100
+ className: cn("flex w-full flex-col gap-3", "[&_.recharts-surface]:outline-none [&_.recharts-wrapper]:outline-none", className),
102
101
  children: [/* @__PURE__ */ jsx("div", {
103
102
  className: "w-full",
104
103
  style: { height },
105
- children: loading ? /* @__PURE__ */ jsxs("div", {
106
- className: "flex h-full items-center justify-center gap-2.5 font-mono text-quaternary-foreground text-text-xs",
107
- children: [/* @__PURE__ */ jsx("span", { className: "size-4 animate-spin rounded-full border-2 border-border border-t-foreground" }), "loading…"]
108
- }) : isEmpty ? /* @__PURE__ */ jsx("div", {
104
+ children: loading ? /* @__PURE__ */ jsx(Skeleton, { className: "h-full w-full rounded-lg" }) : isEmpty ? /* @__PURE__ */ jsx("div", {
109
105
  className: "flex h-full items-center justify-center rounded-lg border border-border border-dashed font-mono text-quaternary-foreground text-text-xs",
110
106
  children: "no data in range"
111
107
  }) : /* @__PURE__ */ jsx(ResponsiveContainer, {
@@ -167,7 +163,7 @@ function BarChart({ data, index, series, variant = "stacked", legend = false, le
167
163
  ...axis,
168
164
  tick: makeXAxisTick(theme),
169
165
  interval: "preserveStartEnd",
170
- minTickGap: 44
166
+ minTickGap: 56
171
167
  }),
172
168
  /* @__PURE__ */ jsx(YAxis, {
173
169
  ...axis,
@@ -220,12 +216,8 @@ function BarChart({ data, index, series, variant = "stacked", legend = false, le
220
216
  strokeWidth: 0,
221
217
  radius: radiusFor(slot),
222
218
  maxBarSize: MAX_BAR_SIZE,
223
- activeBar: {
224
- fillOpacity: 1,
225
- stroke: theme.card,
226
- strokeWidth: 1
227
- },
228
- isAnimationActive: animated,
219
+ activeBar: false,
220
+ isAnimationActive: false,
229
221
  animationDuration: 650,
230
222
  animationEasing: "ease-out",
231
223
  children: valueLabels && (!stacked || rendered.length === 1) && /* @__PURE__ */ jsx(LabelList, {
@@ -1,6 +1,5 @@
1
1
  "use client";
2
2
  import { cn } from "../lib/cn.mjs";
3
- import { useReducedMotion } from "../hooks/use-reduced-motion.mjs";
4
3
  import { rampColor } from "../lib/chart-palette.mjs";
5
4
  import { formatShare } from "../lib/chart.mjs";
6
5
  import { useChartContext } from "./chart-container.mjs";
@@ -19,12 +18,7 @@ const IN_FILL_SHADOW = "0 1px 2px rgb(0 0 0 / 0.28)";
19
18
  function BarList({ data, index, dataKey = "value", maxItems, palette, semantic, loading = false, valueFormatter = (value) => value.toLocaleString("en-US"), labelFormatter, className }) {
20
19
  const { paletteName, theme } = useChartContext(palette);
21
20
  const accent = semantic ? theme[SEMANTIC_KEY[semantic]] : null;
22
- const reducedMotion = useReducedMotion();
23
- const [mounted, setMounted] = React$1.useState(false);
24
21
  const [active, setActive] = React$1.useState(null);
25
- React$1.useEffect(() => {
26
- setMounted(true);
27
- }, []);
28
22
  const total = React$1.useMemo(() => data.reduce((sum, row) => sum + (Number(row[dataKey]) || 0), 0), [data, dataKey]);
29
23
  const rows = React$1.useMemo(() => {
30
24
  const mapped = data.map((row) => ({
@@ -67,7 +61,7 @@ function BarList({ data, index, dataKey = "value", maxItems, palette, semantic,
67
61
  className: cn("flex w-full flex-col", className),
68
62
  children: rows.map((row, slot) => {
69
63
  const fraction = maxValue > 0 ? row.value / maxValue : 0;
70
- const fillWidth = !reducedMotion && !mounted ? "0%" : `${fraction * 100}%`;
64
+ const fillWidth = `${fraction * 100}%`;
71
65
  const dimmed = active !== null && active !== slot;
72
66
  const isActive = active === slot;
73
67
  return /* @__PURE__ */ jsxs("div", {
@@ -85,8 +79,7 @@ function BarList({ data, index, dataKey = "value", maxItems, palette, semantic,
85
79
  width: fillWidth,
86
80
  background: `linear-gradient(90deg, ${row.color}, color-mix(in oklab, ${row.color} 82%, transparent))`,
87
81
  boxShadow: `inset 0 0 0 1px ${row.color}`,
88
- opacity: dimmed ? .45 : 1,
89
- transition: reducedMotion ? void 0 : "width 700ms ease-out"
82
+ opacity: dimmed ? .45 : 1
90
83
  },
91
84
  children: /* @__PURE__ */ jsx("span", {
92
85
  className: "pointer-events-none absolute top-1/2 -translate-y-1/2 truncate type-text-xs font-medium text-white",
@@ -3,10 +3,16 @@ import { cn } from "../lib/cn.mjs";
3
3
  import { useChartContext } from "./chart-container.mjs";
4
4
  import { jsx, jsxs } from "react/jsx-runtime";
5
5
  //#region src/components/chart-tooltip.tsx
6
+ const MAX_TOOLTIP_ROWS = 8;
6
7
  function ChartTooltipContent({ active, payload, label, valueFormatter = (value) => value.toLocaleString("en-US"), labelFormatter, hideLabel, showTotal, totalLabel = "Total", className }) {
7
8
  const { theme } = useChartContext();
8
9
  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
+ const toNumber = (value) => typeof value === "number" ? value : Number(value ?? 0);
11
+ const total = payload.reduce((sum, item) => sum + toNumber(item.value), 0);
12
+ const sorted = [...payload].sort((lower, upper) => toNumber(upper.value) - toNumber(lower.value));
13
+ const rows = sorted.slice(0, MAX_TOOLTIP_ROWS);
14
+ const overflow = sorted.slice(MAX_TOOLTIP_ROWS);
15
+ const overflowTotal = overflow.reduce((sum, item) => sum + toNumber(item.value), 0);
10
16
  return /* @__PURE__ */ jsxs("div", {
11
17
  className: cn("min-w-[130px] rounded-lg border px-3 py-2.5 shadow-lg", "border-border bg-popover text-popover-foreground", className),
12
18
  children: [!hideLabel && label !== void 0 && /* @__PURE__ */ jsx("p", {
@@ -14,37 +20,58 @@ function ChartTooltipContent({ active, payload, label, valueFormatter = (value)
14
20
  children: labelFormatter ? labelFormatter(label) : label
15
21
  }), /* @__PURE__ */ jsxs("div", {
16
22
  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", {
23
+ children: [
24
+ rows.map((item, index) => {
25
+ const swatch = [
26
+ item.color,
27
+ item.stroke,
28
+ item.fill
29
+ ].find((candidate) => typeof candidate === "string" && candidate.length > 0 && !candidate.startsWith("url(")) ?? theme.mutedForeground;
30
+ const numeric = toNumber(item.value);
31
+ return /* @__PURE__ */ jsxs("div", {
32
+ className: "flex items-center justify-between gap-4 text-text-xs",
33
+ children: [/* @__PURE__ */ jsxs("span", {
34
+ className: "inline-flex items-center gap-2 text-muted-foreground",
35
+ children: [/* @__PURE__ */ jsx("span", {
36
+ "aria-hidden": true,
37
+ className: "size-2 shrink-0 rounded-[2px]",
38
+ style: { background: swatch }
39
+ }), item.name]
40
+ }), /* @__PURE__ */ jsx("span", {
41
+ className: "font-mono font-semibold tabular-nums text-foreground",
42
+ children: valueFormatter(numeric)
43
+ })]
44
+ }, `${item.dataKey ?? index}`);
45
+ }),
46
+ overflow.length > 0 && /* @__PURE__ */ jsxs("div", {
25
47
  className: "flex items-center justify-between gap-4 text-text-xs",
26
48
  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]
49
+ className: "inline-flex items-center gap-2 text-quaternary-foreground",
50
+ children: [
51
+ /* @__PURE__ */ jsx("span", {
52
+ "aria-hidden": true,
53
+ className: "size-2 shrink-0"
54
+ }),
55
+ "+",
56
+ overflow.length,
57
+ " more"
58
+ ]
59
+ }), /* @__PURE__ */ jsx("span", {
60
+ className: "font-mono font-semibold tabular-nums text-quaternary-foreground",
61
+ children: valueFormatter(overflowTotal)
62
+ })]
63
+ }),
64
+ showTotal && /* @__PURE__ */ jsxs("div", {
65
+ className: "mt-1 flex items-center justify-between gap-4 border-border border-t pt-1.5 text-text-xs",
66
+ children: [/* @__PURE__ */ jsx("span", {
67
+ className: "font-mono text-quaternary-foreground uppercase tracking-wider text-[10px]",
68
+ children: totalLabel
33
69
  }), /* @__PURE__ */ jsx("span", {
34
70
  className: "font-mono font-semibold tabular-nums text-foreground",
35
- children: valueFormatter(numeric)
71
+ children: valueFormatter(total)
36
72
  })]
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
- })]
73
+ })
74
+ ]
48
75
  })]
49
76
  });
50
77
  }
@@ -1,6 +1,5 @@
1
1
  "use client";
2
2
  import { cn } from "../lib/cn.mjs";
3
- import { useReducedMotion } from "../hooks/use-reduced-motion.mjs";
4
3
  import { paletteColor } from "../lib/chart-palette.mjs";
5
4
  import { formatShare } from "../lib/chart.mjs";
6
5
  import { useChartContext } from "./chart-container.mjs";
@@ -21,7 +20,6 @@ const GEOMETRY = {
21
20
  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
21
  const { palette: paletteColors, theme } = useChartContext(palette);
23
22
  const reactId = React$1.useId().replace(/:/g, "");
24
- const animated = !useReducedMotion();
25
23
  const [active, setActive] = React$1.useState(null);
26
24
  const rowRefs = React$1.useRef([]);
27
25
  const slices = React$1.useMemo(() => {
@@ -104,7 +102,7 @@ function DonutChart({ data, index, dataKey = "value", variant = "donut", legend
104
102
  cornerRadius: 2,
105
103
  stroke: theme.card,
106
104
  strokeWidth: 1.5,
107
- isAnimationActive: animated,
105
+ isAnimationActive: false,
108
106
  animationDuration: 650,
109
107
  animationEasing: "ease-out",
110
108
  onMouseEnter: (_entry, sliceIndex) => setActive(sliceIndex),
@@ -41,10 +41,12 @@ const useFormField = () => {
41
41
  const FormItemContext = React$1.createContext({});
42
42
  function FormItem({ className, ...props }) {
43
43
  const id = React$1.useId();
44
+ const { name } = React$1.useContext(FormFieldContext);
44
45
  return /* @__PURE__ */ jsx(FormItemContext.Provider, {
45
46
  value: { id },
46
47
  children: /* @__PURE__ */ jsx("div", {
47
48
  "data-slot": "form-item",
49
+ "data-field-name": name,
48
50
  className: cn("grid gap-2", className),
49
51
  ...props
50
52
  })
@@ -22,6 +22,9 @@ interface HeatmapChartProps {
22
22
  label: string;
23
23
  format?: (value: number) => string;
24
24
  }>;
25
+ tooltipTitleKey?: string;
26
+ colorScale?: "linear" | "rank";
27
+ showLegend?: boolean;
25
28
  ariaLabel?: string;
26
29
  className?: string;
27
30
  }
@@ -41,6 +44,9 @@ declare function HeatmapChart({
41
44
  xTickFormatter,
42
45
  yTickFormatter,
43
46
  tooltipMetrics,
47
+ tooltipTitleKey,
48
+ colorScale,
49
+ showLegend,
44
50
  ariaLabel,
45
51
  className
46
52
  }: HeatmapChartProps): React$1.JSX.Element;
@@ -31,7 +31,7 @@ const uniqueInOrder = (values) => {
31
31
  }
32
32
  return ordered;
33
33
  };
34
- function HeatmapChart({ data, xKey, yKey, dataKey = "value", variant = "square", palette, xLabels, yLabels, showAllXLabels = false, highlightPeak = true, loading = false, valueFormatter = (value) => value.toLocaleString("en-US"), xTickFormatter, yTickFormatter, tooltipMetrics, ariaLabel = "Activity heatmap", className }) {
34
+ function HeatmapChart({ data, xKey, yKey, dataKey = "value", variant = "square", palette, xLabels, yLabels, showAllXLabels = false, highlightPeak = true, loading = false, valueFormatter = (value) => value.toLocaleString("en-US"), xTickFormatter, yTickFormatter, tooltipMetrics, tooltipTitleKey, colorScale = "linear", showLegend = false, ariaLabel = "Activity heatmap", className }) {
35
35
  const { paletteName, theme } = useChartContext(palette);
36
36
  const [hovered, setHovered] = React$1.useState(null);
37
37
  const [mounted, setMounted] = React$1.useState(false);
@@ -81,6 +81,19 @@ function HeatmapChart({ data, xKey, yKey, dataKey = "value", variant = "square",
81
81
  yLabels
82
82
  ]);
83
83
  const isEmpty = columns.length === 0 || rows.length === 0 || maxValue <= 0;
84
+ const colorFractionOf = React$1.useMemo(() => {
85
+ if (colorScale === "rank") {
86
+ const sorted = [...new Set(cells.map((cell) => cell.value).filter((value) => value > 0))].sort((lower, upper) => lower - upper);
87
+ const RANK_FLOOR = .18;
88
+ const fractionByValue = new Map(sorted.map((value, index) => [value, sorted.length > 1 ? RANK_FLOOR + (1 - RANK_FLOOR) * (index / (sorted.length - 1)) : 1]));
89
+ return (value) => value > 0 ? fractionByValue.get(value) ?? 0 : 0;
90
+ }
91
+ return (value) => maxValue > 0 ? value / maxValue : 0;
92
+ }, [
93
+ cells,
94
+ colorScale,
95
+ maxValue
96
+ ]);
84
97
  if (loading) return /* @__PURE__ */ jsxs("div", {
85
98
  className: cn("flex items-center justify-center gap-2.5 font-mono text-quaternary-foreground text-text-xs", className),
86
99
  style: { minHeight: PLACEHOLDER_HEIGHT },
@@ -91,7 +104,7 @@ function HeatmapChart({ data, xKey, yKey, dataKey = "value", variant = "square",
91
104
  style: { minHeight: PLACEHOLDER_HEIGHT },
92
105
  children: "no data in range"
93
106
  });
94
- const ramp = heatRamp(paletteName, theme.isDark ? HEAT_EMPTY.dark : HEAT_EMPTY.light);
107
+ const ramp = heatRamp(paletteName, theme.isDark ? HEAT_EMPTY.dark : HEAT_EMPTY.light, theme.isDark);
95
108
  const totalWidth = PAD.left + columns.length * CELL + (columns.length - 1) * GAP + PAD.right;
96
109
  const totalHeight = PAD.top + rows.length * CELL + (rows.length - 1) * GAP + PAD.bottom;
97
110
  const xStride = showAllXLabels ? 1 : Math.ceil(columns.length / MAX_X_TICKS);
@@ -104,125 +117,142 @@ function HeatmapChart({ data, xKey, yKey, dataKey = "value", variant = "square",
104
117
  return /* @__PURE__ */ jsxs("div", {
105
118
  "data-slot": "heatmap-chart",
106
119
  className: cn("w-full", className),
107
- children: [/* @__PURE__ */ jsx("div", {
108
- className: "overflow-x-auto",
109
- children: /* @__PURE__ */ jsxs("svg", {
110
- viewBox: `0 0 ${totalWidth} ${totalHeight}`,
111
- className: "block min-w-[480px]",
112
- style: {
113
- width: "100%",
114
- height: "auto"
115
- },
116
- role: "img",
117
- "aria-label": ariaLabel,
118
- onMouseLeave: () => setHovered(null),
119
- children: [
120
- columns.map((label, col) => col % xStride === 0 ? /* @__PURE__ */ jsx("text", {
121
- x: cellX(col) + CELL / 2,
122
- y: PAD.top - 7,
123
- textAnchor: "middle",
124
- className: "fill-quaternary-foreground font-mono text-[8.5px]",
125
- children: formatX(label)
126
- }, `x-${label}`) : null),
127
- rows.map((label, row) => /* @__PURE__ */ jsx("text", {
128
- x: PAD.left - 8,
129
- y: cellY(row) + CELL / 2 + 3,
130
- textAnchor: "end",
131
- className: "fill-quaternary-foreground font-mono text-[8.5px]",
132
- children: formatY(label)
133
- }, `y-${label}`)),
134
- cells.map((cell) => {
135
- const fraction = cell.value / maxValue;
136
- const fill = heatColor(ramp, fraction);
137
- const key = pairKey(cell.rowLabel, cell.colLabel);
138
- const isPeak = highlightPeak && key === peakKey;
139
- if (variant === "dot") return /* @__PURE__ */ jsx("circle", {
140
- cx: cellX(cell.colIndex) + CELL / 2,
141
- cy: cellY(cell.rowIndex) + CELL / 2,
142
- r: CELL / 2 * (DOT_MIN + DOT_RANGE * fraction),
143
- fill,
144
- stroke: isPeak ? theme.foreground : void 0,
145
- strokeWidth: isPeak ? 1.4 : void 0
146
- }, key);
147
- return /* @__PURE__ */ jsx("rect", {
120
+ children: [
121
+ /* @__PURE__ */ jsx("div", {
122
+ className: "overflow-x-auto",
123
+ children: /* @__PURE__ */ jsxs("svg", {
124
+ viewBox: `0 0 ${totalWidth} ${totalHeight}`,
125
+ className: "block min-w-[480px]",
126
+ style: {
127
+ width: "100%",
128
+ height: "auto"
129
+ },
130
+ role: "img",
131
+ "aria-label": ariaLabel,
132
+ onMouseLeave: () => setHovered(null),
133
+ children: [
134
+ columns.map((label, col) => col % xStride === 0 ? /* @__PURE__ */ jsx("text", {
135
+ x: cellX(col) + CELL / 2,
136
+ y: PAD.top - 7,
137
+ textAnchor: "middle",
138
+ className: "fill-quaternary-foreground font-mono text-[8.5px]",
139
+ children: formatX(label)
140
+ }, `x-${label}`) : null),
141
+ rows.map((label, row) => /* @__PURE__ */ jsx("text", {
142
+ x: PAD.left - 8,
143
+ y: cellY(row) + CELL / 2 + 3,
144
+ textAnchor: "end",
145
+ className: "fill-quaternary-foreground font-mono text-[8.5px]",
146
+ children: formatY(label)
147
+ }, `y-${label}`)),
148
+ cells.map((cell) => {
149
+ const fraction = colorFractionOf(cell.value);
150
+ const fill = heatColor(ramp, fraction);
151
+ const key = pairKey(cell.rowLabel, cell.colLabel);
152
+ const isPeak = highlightPeak && key === peakKey;
153
+ if (variant === "dot") return /* @__PURE__ */ jsx("circle", {
154
+ cx: cellX(cell.colIndex) + CELL / 2,
155
+ cy: cellY(cell.rowIndex) + CELL / 2,
156
+ r: CELL / 2 * (DOT_MIN + DOT_RANGE * fraction),
157
+ fill,
158
+ stroke: isPeak ? theme.foreground : void 0,
159
+ strokeWidth: isPeak ? 1.4 : void 0
160
+ }, key);
161
+ return /* @__PURE__ */ jsx("rect", {
162
+ x: cellX(cell.colIndex),
163
+ y: cellY(cell.rowIndex),
164
+ width: CELL,
165
+ height: CELL,
166
+ rx: 2.5,
167
+ fill,
168
+ stroke: isPeak ? theme.foreground : void 0,
169
+ strokeWidth: isPeak ? 1.4 : void 0
170
+ }, key);
171
+ }),
172
+ cells.map((cell) => /* @__PURE__ */ jsx("rect", {
148
173
  x: cellX(cell.colIndex),
149
174
  y: cellY(cell.rowIndex),
150
- width: CELL,
151
- height: CELL,
152
- rx: 2.5,
153
- fill,
154
- stroke: isPeak ? theme.foreground : void 0,
155
- strokeWidth: isPeak ? 1.4 : void 0
156
- }, key);
157
- }),
158
- cells.map((cell) => /* @__PURE__ */ jsx("rect", {
159
- x: cellX(cell.colIndex),
160
- y: cellY(cell.rowIndex),
161
- width: 25,
162
- height: 25,
163
- fill: "transparent",
164
- onMouseMove: (event) => setHovered({
165
- x: event.clientX,
166
- y: event.clientY,
167
- rowLabel: cell.rowLabel,
168
- colLabel: cell.colLabel,
169
- value: cell.value,
170
- record: cell.record
171
- })
172
- }, `hit-${pairKey(cell.rowLabel, cell.colLabel)}`))
173
- ]
174
- })
175
- }), mounted && hovered && createPortal(/* @__PURE__ */ jsxs("div", {
176
- className: "pointer-events-none fixed z-50 min-w-[130px] rounded-lg border border-border bg-popover px-3 py-2.5 text-popover-foreground shadow-lg",
177
- style: {
178
- left: tooltipLeft,
179
- top: tooltipTop
180
- },
181
- children: [/* @__PURE__ */ jsxs("p", {
182
- className: "mb-1.5 font-mono text-[10px] text-quaternary-foreground uppercase tracking-wider",
183
- children: [
184
- formatY(hovered.rowLabel),
185
- " · ",
186
- formatX(hovered.colLabel)
187
- ]
188
- }), tooltipMetrics && tooltipMetrics.length > 0 ? /* @__PURE__ */ jsx("div", {
189
- className: "flex flex-col gap-1",
190
- children: tooltipMetrics.map((metric) => {
191
- const raw = hovered.record ? Number(hovered.record[metric.key]) || 0 : 0;
192
- const isActive = metric.key === dataKey;
193
- return /* @__PURE__ */ jsxs("div", {
194
- className: "flex items-center justify-between gap-4 text-text-xs",
195
- children: [/* @__PURE__ */ jsxs("span", {
196
- className: "inline-flex items-center gap-2 text-muted-foreground",
197
- children: [/* @__PURE__ */ jsx("span", {
198
- "aria-hidden": true,
199
- className: "size-2 shrink-0 rounded-[2px]",
200
- style: {
201
- background: isActive ? heatColor(ramp, hovered.value / maxValue) : theme.mutedForeground,
202
- opacity: isActive ? 1 : .35
203
- }
204
- }), metric.label]
205
- }), /* @__PURE__ */ jsx("span", {
206
- className: cn("font-mono tabular-nums", isActive ? "font-semibold text-foreground" : "text-muted-foreground"),
207
- children: (metric.format ?? valueFormatter)(raw)
208
- })]
209
- }, metric.key);
175
+ width: 25,
176
+ height: 25,
177
+ fill: "transparent",
178
+ onMouseMove: (event) => setHovered({
179
+ x: event.clientX,
180
+ y: event.clientY,
181
+ rowLabel: cell.rowLabel,
182
+ colLabel: cell.colLabel,
183
+ value: cell.value,
184
+ record: cell.record
185
+ })
186
+ }, `hit-${pairKey(cell.rowLabel, cell.colLabel)}`))
187
+ ]
210
188
  })
211
- }) : /* @__PURE__ */ jsxs("div", {
212
- className: "flex items-center justify-between gap-4 text-text-xs",
213
- children: [/* @__PURE__ */ jsxs("span", {
214
- className: "inline-flex items-center gap-2 text-muted-foreground",
215
- children: [/* @__PURE__ */ jsx("span", {
189
+ }),
190
+ showLegend && /* @__PURE__ */ jsxs("div", {
191
+ className: "mt-3 flex items-center justify-end gap-2",
192
+ children: [
193
+ /* @__PURE__ */ jsx("span", {
194
+ className: "font-mono text-[9px] text-quaternary-foreground uppercase tracking-[0.14em]",
195
+ children: "Less"
196
+ }),
197
+ /* @__PURE__ */ jsx("span", {
216
198
  "aria-hidden": true,
217
- className: "size-2 shrink-0 rounded-[2px]",
218
- style: { background: heatColor(ramp, hovered.value / maxValue) }
219
- }), "activity"]
220
- }), /* @__PURE__ */ jsx("span", {
221
- className: "font-mono font-semibold tabular-nums text-foreground",
222
- children: valueFormatter(hovered.value)
199
+ className: "h-2 w-24 rounded-full border border-border",
200
+ style: { background: `linear-gradient(90deg, ${ramp.join(", ")})` }
201
+ }),
202
+ /* @__PURE__ */ jsx("span", {
203
+ className: "font-mono text-[9px] text-quaternary-foreground uppercase tracking-[0.14em]",
204
+ children: "More"
205
+ })
206
+ ]
207
+ }),
208
+ mounted && hovered && createPortal(/* @__PURE__ */ jsxs("div", {
209
+ className: "pointer-events-none fixed z-50 min-w-[130px] rounded-lg border border-border bg-popover px-3 py-2.5 text-popover-foreground shadow-lg",
210
+ style: {
211
+ left: tooltipLeft,
212
+ top: tooltipTop
213
+ },
214
+ children: [/* @__PURE__ */ jsx("p", {
215
+ className: "mb-1.5 font-mono text-[10px] text-quaternary-foreground uppercase tracking-wider",
216
+ children: tooltipTitleKey && typeof hovered.record?.[tooltipTitleKey] === "string" ? hovered.record[tooltipTitleKey] : `${formatY(hovered.rowLabel)} · ${formatX(hovered.colLabel)}`
217
+ }), tooltipMetrics && tooltipMetrics.length > 0 ? /* @__PURE__ */ jsx("div", {
218
+ className: "flex flex-col gap-1",
219
+ children: tooltipMetrics.map((metric) => {
220
+ const raw = hovered.record ? Number(hovered.record[metric.key]) || 0 : 0;
221
+ const isActive = metric.key === dataKey;
222
+ return /* @__PURE__ */ jsxs("div", {
223
+ className: "flex items-center justify-between gap-4 text-text-xs",
224
+ children: [/* @__PURE__ */ jsxs("span", {
225
+ className: "inline-flex items-center gap-2 text-muted-foreground",
226
+ children: [/* @__PURE__ */ jsx("span", {
227
+ "aria-hidden": true,
228
+ className: "size-2 shrink-0 rounded-[2px]",
229
+ style: {
230
+ background: isActive ? heatColor(ramp, colorFractionOf(hovered.value)) : theme.mutedForeground,
231
+ opacity: isActive ? 1 : .35
232
+ }
233
+ }), metric.label]
234
+ }), /* @__PURE__ */ jsx("span", {
235
+ className: cn("font-mono tabular-nums", isActive ? "font-semibold text-foreground" : "text-muted-foreground"),
236
+ children: (metric.format ?? valueFormatter)(raw)
237
+ })]
238
+ }, metric.key);
239
+ })
240
+ }) : /* @__PURE__ */ jsxs("div", {
241
+ className: "flex items-center justify-between gap-4 text-text-xs",
242
+ children: [/* @__PURE__ */ jsxs("span", {
243
+ className: "inline-flex items-center gap-2 text-muted-foreground",
244
+ children: [/* @__PURE__ */ jsx("span", {
245
+ "aria-hidden": true,
246
+ className: "size-2 shrink-0 rounded-[2px]",
247
+ style: { background: heatColor(ramp, colorFractionOf(hovered.value)) }
248
+ }), "activity"]
249
+ }), /* @__PURE__ */ jsx("span", {
250
+ className: "font-mono font-semibold tabular-nums text-foreground",
251
+ children: valueFormatter(hovered.value)
252
+ })]
223
253
  })]
224
- })]
225
- }), document.body)]
254
+ }), document.body)
255
+ ]
226
256
  });
227
257
  }
228
258
  //#endregion