@alpic-ai/ui 0.0.0-staging.g89e05ec → 0.0.0-staging.g8a42d0b
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.
- package/dist/components/area-chart.mjs +4 -6
- package/dist/components/bar-chart.d.mts +2 -0
- package/dist/components/bar-chart.mjs +44 -16
- package/dist/components/chart-tooltip.mjs +52 -27
- package/dist/components/heatmap-chart.d.mts +6 -0
- package/dist/components/heatmap-chart.mjs +146 -116
- package/dist/components/line-chart.mjs +4 -6
- package/dist/components/sidebar.mjs +1 -1
- package/dist/components/skeleton.mjs +1 -1
- package/dist/components/stat.mjs +4 -4
- package/dist/lib/chart-palette.mjs +4 -1
- package/package.json +25 -25
- package/src/components/area-chart.tsx +11 -6
- package/src/components/bar-chart.tsx +48 -7
- package/src/components/chart-tooltip.tsx +17 -1
- package/src/components/heatmap-chart.tsx +49 -5
- package/src/components/line-chart.tsx +11 -6
- package/src/components/skeleton.tsx +1 -1
- package/src/components/stat.tsx +4 -4
- package/src/lib/chart-palette.ts +4 -1
|
@@ -4,6 +4,7 @@ import { makeXAxisTick, orderByLuminance, resolveSeries } from "../lib/chart.mjs
|
|
|
4
4
|
import { useChartContext } from "./chart-container.mjs";
|
|
5
5
|
import { ChartLegend } from "./chart-legend.mjs";
|
|
6
6
|
import { ChartTooltipContent } from "./chart-tooltip.mjs";
|
|
7
|
+
import { Skeleton } from "./skeleton.mjs";
|
|
7
8
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
9
|
import * as React$1 from "react";
|
|
9
10
|
import { Area, AreaChart as AreaChart$1, CartesianGrid, LabelList, ReferenceArea, ReferenceDot, ReferenceLine, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
|
|
@@ -113,14 +114,11 @@ function AreaChart({ data, index, series, variant = "stacked", curve = "monotone
|
|
|
113
114
|
const isEmpty = data.length === 0 || rendered.length === 0;
|
|
114
115
|
return /* @__PURE__ */ jsxs("div", {
|
|
115
116
|
"data-slot": "area-chart",
|
|
116
|
-
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),
|
|
117
118
|
children: [/* @__PURE__ */ jsx("div", {
|
|
118
119
|
className: "w-full",
|
|
119
120
|
style: { height },
|
|
120
|
-
children: loading ? /* @__PURE__ */
|
|
121
|
-
className: "flex h-full items-center justify-center gap-2.5 font-mono text-quaternary-foreground text-text-xs",
|
|
122
|
-
children: [/* @__PURE__ */ jsx("span", { className: "size-4 animate-spin rounded-full border-2 border-border border-t-foreground" }), "loading…"]
|
|
123
|
-
}) : isEmpty ? /* @__PURE__ */ jsx("div", {
|
|
121
|
+
children: loading ? /* @__PURE__ */ jsx(Skeleton, { className: "h-full w-full rounded-lg" }) : isEmpty ? /* @__PURE__ */ jsx("div", {
|
|
124
122
|
className: "flex h-full items-center justify-center rounded-lg border border-border border-dashed font-mono text-quaternary-foreground text-text-xs",
|
|
125
123
|
children: "no data in range"
|
|
126
124
|
}) : /* @__PURE__ */ jsx(ResponsiveContainer, {
|
|
@@ -181,7 +179,7 @@ function AreaChart({ data, index, series, variant = "stacked", curve = "monotone
|
|
|
181
179
|
...axis,
|
|
182
180
|
tick: makeXAxisTick(theme),
|
|
183
181
|
interval: "preserveStartEnd",
|
|
184
|
-
minTickGap:
|
|
182
|
+
minTickGap: 56
|
|
185
183
|
}),
|
|
186
184
|
/* @__PURE__ */ jsx(YAxis, {
|
|
187
185
|
...axis,
|
|
@@ -22,6 +22,7 @@ interface BarChartProps {
|
|
|
22
22
|
};
|
|
23
23
|
markers?: ChartMarker[];
|
|
24
24
|
texture?: boolean;
|
|
25
|
+
partialLastBar?: boolean;
|
|
25
26
|
loading?: boolean;
|
|
26
27
|
valueFormatter?: (value: number) => string;
|
|
27
28
|
labelFormatter?: (label: string | number) => string;
|
|
@@ -41,6 +42,7 @@ declare function BarChart({
|
|
|
41
42
|
referenceLine,
|
|
42
43
|
markers,
|
|
43
44
|
texture,
|
|
45
|
+
partialLastBar,
|
|
44
46
|
loading,
|
|
45
47
|
valueFormatter,
|
|
46
48
|
labelFormatter,
|
|
@@ -4,13 +4,14 @@ import { makeXAxisTick, orderByLuminance, resolveSeries } from "../lib/chart.mjs
|
|
|
4
4
|
import { useChartContext } from "./chart-container.mjs";
|
|
5
5
|
import { ChartLegend } from "./chart-legend.mjs";
|
|
6
6
|
import { ChartTooltipContent } from "./chart-tooltip.mjs";
|
|
7
|
+
import { Skeleton } from "./skeleton.mjs";
|
|
7
8
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
9
|
import * as React$1 from "react";
|
|
9
|
-
import { Bar, BarChart as BarChart$1, CartesianGrid, LabelList, ReferenceArea, ReferenceDot, ReferenceLine, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
|
|
10
|
+
import { Bar, BarChart as BarChart$1, CartesianGrid, Cell, LabelList, ReferenceArea, ReferenceDot, ReferenceLine, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
|
|
10
11
|
//#region src/components/bar-chart.tsx
|
|
11
12
|
const BAR_RADIUS = 4;
|
|
12
13
|
const MAX_BAR_SIZE = 48;
|
|
13
|
-
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 }) {
|
|
14
|
+
function BarChart({ data, index, series, variant = "stacked", legend = false, legendAlign = "left", valueLabels = false, height = 200, yAxisWidth = 48, palette, referenceLine, markers, texture = false, partialLastBar = false, loading = false, valueFormatter = (value) => value.toLocaleString("en-US"), labelFormatter, className }) {
|
|
14
15
|
const { palette: paletteColors, theme } = useChartContext(palette);
|
|
15
16
|
const reactId = React$1.useId().replace(/:/g, "");
|
|
16
17
|
const resolved = resolveSeries(series, paletteColors, theme);
|
|
@@ -37,6 +38,20 @@ function BarChart({ data, index, series, variant = "stacked", legend = false, le
|
|
|
37
38
|
series,
|
|
38
39
|
stacked
|
|
39
40
|
]);
|
|
41
|
+
const topSlotByRow = React$1.useMemo(() => {
|
|
42
|
+
if (!stacked) return [];
|
|
43
|
+
return data.map((row) => {
|
|
44
|
+
for (let slot = rendered.length - 1; slot >= 0; slot -= 1) {
|
|
45
|
+
const value = Number(row[rendered[slot]?.key ?? ""]);
|
|
46
|
+
if (Number.isFinite(value) && value > 0) return slot;
|
|
47
|
+
}
|
|
48
|
+
return -1;
|
|
49
|
+
});
|
|
50
|
+
}, [
|
|
51
|
+
data,
|
|
52
|
+
rendered,
|
|
53
|
+
stacked
|
|
54
|
+
]);
|
|
40
55
|
const margin = {
|
|
41
56
|
top: markers?.length || valueLabels ? 18 : 8,
|
|
42
57
|
right: 8,
|
|
@@ -96,14 +111,11 @@ function BarChart({ data, index, series, variant = "stacked", legend = false, le
|
|
|
96
111
|
const isEmpty = data.length === 0 || rendered.length === 0;
|
|
97
112
|
return /* @__PURE__ */ jsxs("div", {
|
|
98
113
|
"data-slot": "bar-chart",
|
|
99
|
-
className: cn("flex w-full flex-col gap-3", className),
|
|
114
|
+
className: cn("flex w-full flex-col gap-3", "[&_.recharts-surface]:outline-none [&_.recharts-wrapper]:outline-none", className),
|
|
100
115
|
children: [/* @__PURE__ */ jsx("div", {
|
|
101
116
|
className: "w-full",
|
|
102
117
|
style: { height },
|
|
103
|
-
children: loading ? /* @__PURE__ */
|
|
104
|
-
className: "flex h-full items-center justify-center gap-2.5 font-mono text-quaternary-foreground text-text-xs",
|
|
105
|
-
children: [/* @__PURE__ */ jsx("span", { className: "size-4 animate-spin rounded-full border-2 border-border border-t-foreground" }), "loading…"]
|
|
106
|
-
}) : isEmpty ? /* @__PURE__ */ jsx("div", {
|
|
118
|
+
children: loading ? /* @__PURE__ */ jsx(Skeleton, { className: "h-full w-full rounded-lg" }) : isEmpty ? /* @__PURE__ */ jsx("div", {
|
|
107
119
|
className: "flex h-full items-center justify-center rounded-lg border border-border border-dashed font-mono text-quaternary-foreground text-text-xs",
|
|
108
120
|
children: "no data in range"
|
|
109
121
|
}) : /* @__PURE__ */ jsx(ResponsiveContainer, {
|
|
@@ -165,7 +177,7 @@ function BarChart({ data, index, series, variant = "stacked", legend = false, le
|
|
|
165
177
|
...axis,
|
|
166
178
|
tick: makeXAxisTick(theme),
|
|
167
179
|
interval: "preserveStartEnd",
|
|
168
|
-
minTickGap:
|
|
180
|
+
minTickGap: 56
|
|
169
181
|
}),
|
|
170
182
|
/* @__PURE__ */ jsx(YAxis, {
|
|
171
183
|
...axis,
|
|
@@ -209,7 +221,7 @@ function BarChart({ data, index, series, variant = "stacked", legend = false, le
|
|
|
209
221
|
position: "insideBottomRight"
|
|
210
222
|
} : void 0
|
|
211
223
|
}),
|
|
212
|
-
rendered.map((entry, slot) => /* @__PURE__ */
|
|
224
|
+
rendered.map((entry, slot) => /* @__PURE__ */ jsxs(Bar, {
|
|
213
225
|
dataKey: entry.key,
|
|
214
226
|
name: entry.name,
|
|
215
227
|
stackId: stacked ? "stack" : void 0,
|
|
@@ -218,18 +230,34 @@ function BarChart({ data, index, series, variant = "stacked", legend = false, le
|
|
|
218
230
|
strokeWidth: 0,
|
|
219
231
|
radius: radiusFor(slot),
|
|
220
232
|
maxBarSize: MAX_BAR_SIZE,
|
|
221
|
-
activeBar:
|
|
222
|
-
fillOpacity: 1,
|
|
223
|
-
stroke: theme.card,
|
|
224
|
-
strokeWidth: 1
|
|
225
|
-
},
|
|
233
|
+
activeBar: false,
|
|
226
234
|
isAnimationActive: false,
|
|
227
235
|
animationDuration: 650,
|
|
228
236
|
animationEasing: "ease-out",
|
|
229
|
-
children:
|
|
237
|
+
children: [(stacked || partialLastBar) && data.map((row, rowIndex) => {
|
|
238
|
+
const roundedTop = stacked ? topSlotByRow[rowIndex] === slot : true;
|
|
239
|
+
const isPartial = partialLastBar && rowIndex === data.length - 1;
|
|
240
|
+
return /* @__PURE__ */ jsx(Cell, {
|
|
241
|
+
radius: roundedTop ? [
|
|
242
|
+
BAR_RADIUS,
|
|
243
|
+
BAR_RADIUS,
|
|
244
|
+
0,
|
|
245
|
+
0
|
|
246
|
+
] : [
|
|
247
|
+
0,
|
|
248
|
+
0,
|
|
249
|
+
0,
|
|
250
|
+
0
|
|
251
|
+
],
|
|
252
|
+
fillOpacity: isPartial ? .15 : 1,
|
|
253
|
+
stroke: isPartial ? entry.color : void 0,
|
|
254
|
+
strokeWidth: isPartial ? 1.25 : 0,
|
|
255
|
+
strokeDasharray: isPartial ? "3 2" : void 0
|
|
256
|
+
}, `${entry.key}-${row[index] ?? rowIndex}`);
|
|
257
|
+
}), valueLabels && (!stacked || rendered.length === 1) && /* @__PURE__ */ jsx(LabelList, {
|
|
230
258
|
dataKey: entry.key,
|
|
231
259
|
content: renderValueLabel(entry.color)
|
|
232
|
-
})
|
|
260
|
+
})]
|
|
233
261
|
}, entry.key)),
|
|
234
262
|
markers?.map((marker) => /* @__PURE__ */ jsx(ReferenceDot, {
|
|
235
263
|
x: marker.x,
|
|
@@ -3,12 +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
10
|
const toNumber = (value) => typeof value === "number" ? value : Number(value ?? 0);
|
|
10
11
|
const total = payload.reduce((sum, item) => sum + toNumber(item.value), 0);
|
|
11
|
-
const
|
|
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);
|
|
12
16
|
return /* @__PURE__ */ jsxs("div", {
|
|
13
17
|
className: cn("min-w-[130px] rounded-lg border px-3 py-2.5 shadow-lg", "border-border bg-popover text-popover-foreground", className),
|
|
14
18
|
children: [!hideLabel && label !== void 0 && /* @__PURE__ */ jsx("p", {
|
|
@@ -16,37 +20,58 @@ function ChartTooltipContent({ active, payload, label, valueFormatter = (value)
|
|
|
16
20
|
children: labelFormatter ? labelFormatter(label) : label
|
|
17
21
|
}), /* @__PURE__ */ jsxs("div", {
|
|
18
22
|
className: "flex flex-col gap-1",
|
|
19
|
-
children: [
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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", {
|
|
27
47
|
className: "flex items-center justify-between gap-4 text-text-xs",
|
|
28
48
|
children: [/* @__PURE__ */ jsxs("span", {
|
|
29
|
-
className: "inline-flex items-center gap-2 text-
|
|
30
|
-
children: [
|
|
31
|
-
"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
35
69
|
}), /* @__PURE__ */ jsx("span", {
|
|
36
70
|
className: "font-mono font-semibold tabular-nums text-foreground",
|
|
37
|
-
children: valueFormatter(
|
|
71
|
+
children: valueFormatter(total)
|
|
38
72
|
})]
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
className: "mt-1 flex items-center justify-between gap-4 border-border border-t pt-1.5 text-text-xs",
|
|
42
|
-
children: [/* @__PURE__ */ jsx("span", {
|
|
43
|
-
className: "font-mono text-quaternary-foreground uppercase tracking-wider text-[10px]",
|
|
44
|
-
children: totalLabel
|
|
45
|
-
}), /* @__PURE__ */ jsx("span", {
|
|
46
|
-
className: "font-mono font-semibold tabular-nums text-foreground",
|
|
47
|
-
children: valueFormatter(total)
|
|
48
|
-
})]
|
|
49
|
-
})]
|
|
73
|
+
})
|
|
74
|
+
]
|
|
50
75
|
})]
|
|
51
76
|
});
|
|
52
77
|
}
|
|
@@ -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: [
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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:
|
|
151
|
-
height:
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
})
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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: "
|
|
218
|
-
style: { background:
|
|
219
|
-
}),
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
254
|
+
}), document.body)
|
|
255
|
+
]
|
|
226
256
|
});
|
|
227
257
|
}
|
|
228
258
|
//#endregion
|
|
@@ -4,6 +4,7 @@ import { makeXAxisTick, resolveSeries } from "../lib/chart.mjs";
|
|
|
4
4
|
import { useChartContext } from "./chart-container.mjs";
|
|
5
5
|
import { ChartLegend } from "./chart-legend.mjs";
|
|
6
6
|
import { ChartTooltipContent } from "./chart-tooltip.mjs";
|
|
7
|
+
import { Skeleton } from "./skeleton.mjs";
|
|
7
8
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
9
|
import * as React$1 from "react";
|
|
9
10
|
import { CartesianGrid, LabelList, Line, LineChart as LineChart$1, ReferenceArea, ReferenceDot, ReferenceLine, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
|
|
@@ -91,14 +92,11 @@ function LineChart({ data, index, series, curve = "monotone", legend = false, le
|
|
|
91
92
|
const isEmpty = data.length === 0 || resolved.length === 0;
|
|
92
93
|
return /* @__PURE__ */ jsxs("div", {
|
|
93
94
|
"data-slot": "line-chart",
|
|
94
|
-
className: cn("flex w-full flex-col gap-3", className),
|
|
95
|
+
className: cn("flex w-full flex-col gap-3", "[&_.recharts-surface]:outline-none [&_.recharts-wrapper]:outline-none", className),
|
|
95
96
|
children: [/* @__PURE__ */ jsx("div", {
|
|
96
97
|
className: "w-full",
|
|
97
98
|
style: { height },
|
|
98
|
-
children: loading ? /* @__PURE__ */
|
|
99
|
-
className: "flex h-full items-center justify-center gap-2.5 font-mono text-quaternary-foreground text-text-xs",
|
|
100
|
-
children: [/* @__PURE__ */ jsx("span", { className: "size-4 animate-spin rounded-full border-2 border-border border-t-foreground" }), "loading…"]
|
|
101
|
-
}) : isEmpty ? /* @__PURE__ */ jsx("div", {
|
|
99
|
+
children: loading ? /* @__PURE__ */ jsx(Skeleton, { className: "h-full w-full rounded-lg" }) : isEmpty ? /* @__PURE__ */ jsx("div", {
|
|
102
100
|
className: "flex h-full items-center justify-center rounded-lg border border-border border-dashed font-mono text-quaternary-foreground text-text-xs",
|
|
103
101
|
children: "no data in range"
|
|
104
102
|
}) : /* @__PURE__ */ jsx(ResponsiveContainer, {
|
|
@@ -122,7 +120,7 @@ function LineChart({ data, index, series, curve = "monotone", legend = false, le
|
|
|
122
120
|
...axis,
|
|
123
121
|
tick: makeXAxisTick(theme),
|
|
124
122
|
interval: "preserveStartEnd",
|
|
125
|
-
minTickGap:
|
|
123
|
+
minTickGap: 56
|
|
126
124
|
}),
|
|
127
125
|
/* @__PURE__ */ jsx(YAxis, {
|
|
128
126
|
...axis,
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { cn } from "../lib/cn.mjs";
|
|
3
|
+
import { Skeleton } from "./skeleton.mjs";
|
|
3
4
|
import { Button } from "./button.mjs";
|
|
4
5
|
import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip.mjs";
|
|
5
6
|
import { Separator } from "./separator.mjs";
|
|
6
7
|
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "./sheet.mjs";
|
|
7
8
|
import { useIsMobile } from "../hooks/use-mobile.mjs";
|
|
8
|
-
import { Skeleton } from "./skeleton.mjs";
|
|
9
9
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
10
|
import { cva } from "class-variance-authority";
|
|
11
11
|
import * as React$1 from "react";
|
|
@@ -3,7 +3,7 @@ import { cn } from "../lib/cn.mjs";
|
|
|
3
3
|
import { jsx } from "react/jsx-runtime";
|
|
4
4
|
import { cva } from "class-variance-authority";
|
|
5
5
|
//#region src/components/skeleton.tsx
|
|
6
|
-
const skeletonVariants = cva("bg-
|
|
6
|
+
const skeletonVariants = cva("bg-foreground/10 motion-safe:animate-pulse", {
|
|
7
7
|
variants: { shape: {
|
|
8
8
|
rectangle: "rounded-md",
|
|
9
9
|
circle: "rounded-full"
|
package/dist/components/stat.mjs
CHANGED
|
@@ -35,13 +35,13 @@ function Stat({ value, unit, delta, sparkline, semantic, className, ...props })
|
|
|
35
35
|
const sentiment = delta && (delta.invert ? delta.direction === "down" : delta.direction === "up") ? "positive" : "negative";
|
|
36
36
|
return /* @__PURE__ */ jsxs("div", {
|
|
37
37
|
"data-slot": "stat",
|
|
38
|
-
className: cn("flex flex-col gap-2.5", className),
|
|
38
|
+
className: cn("flex min-w-0 flex-col gap-2.5", className),
|
|
39
39
|
...props,
|
|
40
40
|
children: [/* @__PURE__ */ jsxs("div", {
|
|
41
|
-
className: "flex items-baseline gap-
|
|
41
|
+
className: "flex min-w-0 items-baseline gap-x-2.5",
|
|
42
42
|
children: [
|
|
43
43
|
/* @__PURE__ */ jsx("span", {
|
|
44
|
-
className: "type-display-sm font-bold leading-none tracking-tight tabular-nums text-foreground",
|
|
44
|
+
className: "type-display-sm min-w-0 truncate font-bold leading-none tracking-tight tabular-nums text-foreground",
|
|
45
45
|
children: value
|
|
46
46
|
}),
|
|
47
47
|
unit && /* @__PURE__ */ jsx("span", {
|
|
@@ -55,7 +55,7 @@ function Stat({ value, unit, delta, sparkline, semantic, className, ...props })
|
|
|
55
55
|
})
|
|
56
56
|
]
|
|
57
57
|
}), hasSpark && /* @__PURE__ */ jsx("div", {
|
|
58
|
-
className: "h-9 w-full",
|
|
58
|
+
className: "pointer-events-none h-9 w-full",
|
|
59
59
|
children: /* @__PURE__ */ jsx(ResponsiveContainer, {
|
|
60
60
|
width: "100%",
|
|
61
61
|
height: "100%",
|
|
@@ -42,7 +42,10 @@ const HEAT_RAMPS = {
|
|
|
42
42
|
magenta: heatRampMagenta,
|
|
43
43
|
cyan: heatRampMint
|
|
44
44
|
};
|
|
45
|
-
const heatRamp = (palette, empty) =>
|
|
45
|
+
const heatRamp = (palette, empty, isDark = true) => {
|
|
46
|
+
const [, ...colored] = HEAT_RAMPS[palette](empty);
|
|
47
|
+
return [empty, ...isDark ? colored : colored.reverse()];
|
|
48
|
+
};
|
|
46
49
|
const hexToRgb = (hex) => {
|
|
47
50
|
const value = Number.parseInt(hex.slice(1), 16);
|
|
48
51
|
return [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alpic-ai/ui",
|
|
3
|
-
"version": "0.0.0-staging.
|
|
3
|
+
"version": "0.0.0-staging.g8a42d0b",
|
|
4
4
|
"description": "Alpic design system — shared UI components",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -23,48 +23,48 @@
|
|
|
23
23
|
"src"
|
|
24
24
|
],
|
|
25
25
|
"peerDependencies": {
|
|
26
|
-
"lucide-react": "^1.
|
|
26
|
+
"lucide-react": "^1.23.0",
|
|
27
27
|
"react": "^19.2.7",
|
|
28
28
|
"react-dom": "^19.2.7",
|
|
29
|
-
"react-hook-form": "^7.
|
|
29
|
+
"react-hook-form": "^7.81.0",
|
|
30
30
|
"sonner": "^2.0.7",
|
|
31
|
-
"tailwindcss": "^4.3.
|
|
31
|
+
"tailwindcss": "^4.3.2",
|
|
32
32
|
"tw-animate-css": "^1.4.0"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@radix-ui/react-accordion": "^1.2.
|
|
36
|
-
"@radix-ui/react-avatar": "^1.2.
|
|
37
|
-
"@radix-ui/react-checkbox": "^1.3.
|
|
38
|
-
"@radix-ui/react-collapsible": "^1.1.
|
|
39
|
-
"@radix-ui/react-dialog": "^1.1.
|
|
40
|
-
"@radix-ui/react-dropdown-menu": "^2.1.
|
|
41
|
-
"@radix-ui/react-label": "^2.1.
|
|
42
|
-
"@radix-ui/react-popover": "^1.1.
|
|
43
|
-
"@radix-ui/react-radio-group": "^1.4.
|
|
44
|
-
"@radix-ui/react-scroll-area": "^1.2.
|
|
45
|
-
"@radix-ui/react-select": "^2.3.
|
|
46
|
-
"@radix-ui/react-separator": "^1.1.
|
|
35
|
+
"@radix-ui/react-accordion": "^1.2.15",
|
|
36
|
+
"@radix-ui/react-avatar": "^1.2.1",
|
|
37
|
+
"@radix-ui/react-checkbox": "^1.3.6",
|
|
38
|
+
"@radix-ui/react-collapsible": "^1.1.15",
|
|
39
|
+
"@radix-ui/react-dialog": "^1.1.18",
|
|
40
|
+
"@radix-ui/react-dropdown-menu": "^2.1.19",
|
|
41
|
+
"@radix-ui/react-label": "^2.1.11",
|
|
42
|
+
"@radix-ui/react-popover": "^1.1.18",
|
|
43
|
+
"@radix-ui/react-radio-group": "^1.4.2",
|
|
44
|
+
"@radix-ui/react-scroll-area": "^1.2.13",
|
|
45
|
+
"@radix-ui/react-select": "^2.3.2",
|
|
46
|
+
"@radix-ui/react-separator": "^1.1.11",
|
|
47
47
|
"@radix-ui/react-slot": "^1.3.0",
|
|
48
|
-
"@radix-ui/react-switch": "^1.3.
|
|
49
|
-
"@radix-ui/react-tabs": "^1.1.
|
|
50
|
-
"@radix-ui/react-toggle-group": "^1.1.
|
|
51
|
-
"@radix-ui/react-tooltip": "^1.2.
|
|
48
|
+
"@radix-ui/react-switch": "^1.3.2",
|
|
49
|
+
"@radix-ui/react-tabs": "^1.1.16",
|
|
50
|
+
"@radix-ui/react-toggle-group": "^1.1.14",
|
|
51
|
+
"@radix-ui/react-tooltip": "^1.2.11",
|
|
52
52
|
"class-variance-authority": "^0.7.1",
|
|
53
53
|
"clsx": "^2.1.1",
|
|
54
54
|
"cmdk": "^1.1.1",
|
|
55
|
-
"recharts": "^3.
|
|
55
|
+
"recharts": "^3.9.2",
|
|
56
56
|
"tailwind-merge": "^3.6.0"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@ladle/react": "^5.1.1",
|
|
60
|
-
"@tailwindcss/postcss": "^4.3.
|
|
60
|
+
"@tailwindcss/postcss": "^4.3.2",
|
|
61
61
|
"@types/react": "19.2.17",
|
|
62
62
|
"@types/react-dom": "19.2.3",
|
|
63
|
-
"lucide-react": "^1.
|
|
64
|
-
"react-hook-form": "^7.
|
|
63
|
+
"lucide-react": "^1.23.0",
|
|
64
|
+
"react-hook-form": "^7.81.0",
|
|
65
65
|
"shx": "^0.4.0",
|
|
66
66
|
"sonner": "^2.0.7",
|
|
67
|
-
"tailwindcss": "^4.3.
|
|
67
|
+
"tailwindcss": "^4.3.2",
|
|
68
68
|
"tsdown": "^0.22.3",
|
|
69
69
|
"tw-animate-css": "^1.4.0",
|
|
70
70
|
"typescript": "^6.0.3"
|
|
@@ -21,6 +21,7 @@ import { cn } from "../lib/cn";
|
|
|
21
21
|
import { useChartContext } from "./chart-container";
|
|
22
22
|
import { ChartLegend } from "./chart-legend";
|
|
23
23
|
import { ChartTooltipContent } from "./chart-tooltip";
|
|
24
|
+
import { Skeleton } from "./skeleton";
|
|
24
25
|
|
|
25
26
|
const CURVE_TYPE = { monotone: "monotone", linear: "linear", step: "stepAfter" } as const;
|
|
26
27
|
|
|
@@ -192,13 +193,17 @@ function AreaChart({
|
|
|
192
193
|
const isEmpty = data.length === 0 || rendered.length === 0;
|
|
193
194
|
|
|
194
195
|
return (
|
|
195
|
-
<div
|
|
196
|
+
<div
|
|
197
|
+
data-slot="area-chart"
|
|
198
|
+
className={cn(
|
|
199
|
+
"flex w-full flex-col gap-3",
|
|
200
|
+
"[&_.recharts-surface]:outline-none [&_.recharts-wrapper]:outline-none",
|
|
201
|
+
className,
|
|
202
|
+
)}
|
|
203
|
+
>
|
|
196
204
|
<div className="w-full" style={{ height }}>
|
|
197
205
|
{loading ? (
|
|
198
|
-
<
|
|
199
|
-
<span className="size-4 animate-spin rounded-full border-2 border-border border-t-foreground" />
|
|
200
|
-
loading…
|
|
201
|
-
</div>
|
|
206
|
+
<Skeleton className="h-full w-full rounded-lg" />
|
|
202
207
|
) : isEmpty ? (
|
|
203
208
|
<div className="flex h-full items-center justify-center rounded-lg border border-border border-dashed font-mono text-quaternary-foreground text-text-xs">
|
|
204
209
|
no data in range
|
|
@@ -238,7 +243,7 @@ function AreaChart({
|
|
|
238
243
|
{...axis}
|
|
239
244
|
tick={makeXAxisTick(theme)}
|
|
240
245
|
interval="preserveStartEnd"
|
|
241
|
-
minTickGap={
|
|
246
|
+
minTickGap={56}
|
|
242
247
|
/>
|
|
243
248
|
<YAxis
|
|
244
249
|
{...axis}
|
|
@@ -4,6 +4,7 @@ import * as React from "react";
|
|
|
4
4
|
import {
|
|
5
5
|
Bar,
|
|
6
6
|
CartesianGrid,
|
|
7
|
+
Cell,
|
|
7
8
|
LabelList,
|
|
8
9
|
BarChart as RechartsBarChart,
|
|
9
10
|
ReferenceArea,
|
|
@@ -22,6 +23,7 @@ import type { ChartMarker } from "./area-chart";
|
|
|
22
23
|
import { useChartContext } from "./chart-container";
|
|
23
24
|
import { ChartLegend } from "./chart-legend";
|
|
24
25
|
import { ChartTooltipContent } from "./chart-tooltip";
|
|
26
|
+
import { Skeleton } from "./skeleton";
|
|
25
27
|
|
|
26
28
|
const BAR_RADIUS = 4;
|
|
27
29
|
const MAX_BAR_SIZE = 48;
|
|
@@ -40,6 +42,7 @@ export interface BarChartProps {
|
|
|
40
42
|
referenceLine?: { y: number; label?: string; band?: boolean };
|
|
41
43
|
markers?: ChartMarker[];
|
|
42
44
|
texture?: boolean;
|
|
45
|
+
partialLastBar?: boolean;
|
|
43
46
|
loading?: boolean;
|
|
44
47
|
valueFormatter?: (value: number) => string;
|
|
45
48
|
labelFormatter?: (label: string | number) => string;
|
|
@@ -60,6 +63,7 @@ function BarChart({
|
|
|
60
63
|
referenceLine,
|
|
61
64
|
markers,
|
|
62
65
|
texture = false,
|
|
66
|
+
partialLastBar = false,
|
|
63
67
|
loading = false,
|
|
64
68
|
valueFormatter = (value) => value.toLocaleString("en-US"),
|
|
65
69
|
labelFormatter,
|
|
@@ -96,6 +100,21 @@ function BarChart({
|
|
|
96
100
|
return max;
|
|
97
101
|
}, [data, series, stacked]);
|
|
98
102
|
|
|
103
|
+
const topSlotByRow = React.useMemo(() => {
|
|
104
|
+
if (!stacked) {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
return data.map((row) => {
|
|
108
|
+
for (let slot = rendered.length - 1; slot >= 0; slot -= 1) {
|
|
109
|
+
const value = Number(row[rendered[slot]?.key ?? ""]);
|
|
110
|
+
if (Number.isFinite(value) && value > 0) {
|
|
111
|
+
return slot;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return -1;
|
|
115
|
+
});
|
|
116
|
+
}, [data, rendered, stacked]);
|
|
117
|
+
|
|
99
118
|
const margin = { top: markers?.length || valueLabels ? 18 : 8, right: 8, bottom: 2, left: 0 };
|
|
100
119
|
|
|
101
120
|
const axis = {
|
|
@@ -155,13 +174,17 @@ function BarChart({
|
|
|
155
174
|
const isEmpty = data.length === 0 || rendered.length === 0;
|
|
156
175
|
|
|
157
176
|
return (
|
|
158
|
-
<div
|
|
177
|
+
<div
|
|
178
|
+
data-slot="bar-chart"
|
|
179
|
+
className={cn(
|
|
180
|
+
"flex w-full flex-col gap-3",
|
|
181
|
+
"[&_.recharts-surface]:outline-none [&_.recharts-wrapper]:outline-none",
|
|
182
|
+
className,
|
|
183
|
+
)}
|
|
184
|
+
>
|
|
159
185
|
<div className="w-full" style={{ height }}>
|
|
160
186
|
{loading ? (
|
|
161
|
-
<
|
|
162
|
-
<span className="size-4 animate-spin rounded-full border-2 border-border border-t-foreground" />
|
|
163
|
-
loading…
|
|
164
|
-
</div>
|
|
187
|
+
<Skeleton className="h-full w-full rounded-lg" />
|
|
165
188
|
) : isEmpty ? (
|
|
166
189
|
<div className="flex h-full items-center justify-center rounded-lg border border-border border-dashed font-mono text-quaternary-foreground text-text-xs">
|
|
167
190
|
no data in range
|
|
@@ -202,7 +225,7 @@ function BarChart({
|
|
|
202
225
|
{...axis}
|
|
203
226
|
tick={makeXAxisTick(theme)}
|
|
204
227
|
interval="preserveStartEnd"
|
|
205
|
-
minTickGap={
|
|
228
|
+
minTickGap={56}
|
|
206
229
|
/>
|
|
207
230
|
<YAxis
|
|
208
231
|
{...axis}
|
|
@@ -268,11 +291,29 @@ function BarChart({
|
|
|
268
291
|
strokeWidth={0}
|
|
269
292
|
radius={radiusFor(slot)}
|
|
270
293
|
maxBarSize={MAX_BAR_SIZE}
|
|
271
|
-
activeBar={
|
|
294
|
+
activeBar={false}
|
|
272
295
|
isAnimationActive={false}
|
|
273
296
|
animationDuration={650}
|
|
274
297
|
animationEasing="ease-out"
|
|
275
298
|
>
|
|
299
|
+
{(stacked || partialLastBar) &&
|
|
300
|
+
data.map((row, rowIndex) => {
|
|
301
|
+
const roundedTop = stacked ? topSlotByRow[rowIndex] === slot : true;
|
|
302
|
+
const isPartial = partialLastBar && rowIndex === data.length - 1;
|
|
303
|
+
const cellRadius = (roundedTop
|
|
304
|
+
? [BAR_RADIUS, BAR_RADIUS, 0, 0]
|
|
305
|
+
: [0, 0, 0, 0]) as unknown as number;
|
|
306
|
+
return (
|
|
307
|
+
<Cell
|
|
308
|
+
key={`${entry.key}-${row[index] ?? rowIndex}`}
|
|
309
|
+
radius={cellRadius}
|
|
310
|
+
fillOpacity={isPartial ? 0.15 : 1}
|
|
311
|
+
stroke={isPartial ? entry.color : undefined}
|
|
312
|
+
strokeWidth={isPartial ? 1.25 : 0}
|
|
313
|
+
strokeDasharray={isPartial ? "3 2" : undefined}
|
|
314
|
+
/>
|
|
315
|
+
);
|
|
316
|
+
})}
|
|
276
317
|
{valueLabels && (!stacked || rendered.length === 1) && (
|
|
277
318
|
<LabelList dataKey={entry.key} content={renderValueLabel(entry.color)} />
|
|
278
319
|
)}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import { cn } from "../lib/cn";
|
|
4
4
|
import { useChartContext } from "./chart-container";
|
|
5
5
|
|
|
6
|
+
const MAX_TOOLTIP_ROWS = 8;
|
|
7
|
+
|
|
6
8
|
export interface ChartTooltipItem {
|
|
7
9
|
name?: string | number;
|
|
8
10
|
value?: number | string;
|
|
@@ -44,7 +46,11 @@ function ChartTooltipContent({
|
|
|
44
46
|
const toNumber = (value: ChartTooltipItem["value"]) => (typeof value === "number" ? value : Number(value ?? 0));
|
|
45
47
|
const total = payload.reduce((sum, item) => sum + toNumber(item.value), 0);
|
|
46
48
|
|
|
47
|
-
const
|
|
49
|
+
const sorted = [...payload].sort((lower, upper) => toNumber(upper.value) - toNumber(lower.value));
|
|
50
|
+
|
|
51
|
+
const rows = sorted.slice(0, MAX_TOOLTIP_ROWS);
|
|
52
|
+
const overflow = sorted.slice(MAX_TOOLTIP_ROWS);
|
|
53
|
+
const overflowTotal = overflow.reduce((sum, item) => sum + toNumber(item.value), 0);
|
|
48
54
|
|
|
49
55
|
return (
|
|
50
56
|
<div
|
|
@@ -77,6 +83,16 @@ function ChartTooltipContent({
|
|
|
77
83
|
</div>
|
|
78
84
|
);
|
|
79
85
|
})}
|
|
86
|
+
{overflow.length > 0 && (
|
|
87
|
+
<div className="flex items-center justify-between gap-4 text-text-xs">
|
|
88
|
+
<span className="inline-flex items-center gap-2 text-quaternary-foreground">
|
|
89
|
+
<span aria-hidden className="size-2 shrink-0" />+{overflow.length} more
|
|
90
|
+
</span>
|
|
91
|
+
<span className="font-mono font-semibold tabular-nums text-quaternary-foreground">
|
|
92
|
+
{valueFormatter(overflowTotal)}
|
|
93
|
+
</span>
|
|
94
|
+
</div>
|
|
95
|
+
)}
|
|
80
96
|
{showTotal && (
|
|
81
97
|
<div className="mt-1 flex items-center justify-between gap-4 border-border border-t pt-1.5 text-text-xs">
|
|
82
98
|
<span className="font-mono text-quaternary-foreground uppercase tracking-wider text-[10px]">
|
|
@@ -24,6 +24,9 @@ export interface HeatmapChartProps {
|
|
|
24
24
|
xTickFormatter?: (label: string) => string;
|
|
25
25
|
yTickFormatter?: (label: string) => string;
|
|
26
26
|
tooltipMetrics?: ReadonlyArray<{ key: string; label: string; format?: (value: number) => string }>;
|
|
27
|
+
tooltipTitleKey?: string;
|
|
28
|
+
colorScale?: "linear" | "rank";
|
|
29
|
+
showLegend?: boolean;
|
|
27
30
|
ariaLabel?: string;
|
|
28
31
|
className?: string;
|
|
29
32
|
}
|
|
@@ -83,6 +86,9 @@ function HeatmapChart({
|
|
|
83
86
|
xTickFormatter,
|
|
84
87
|
yTickFormatter,
|
|
85
88
|
tooltipMetrics,
|
|
89
|
+
tooltipTitleKey,
|
|
90
|
+
colorScale = "linear",
|
|
91
|
+
showLegend = false,
|
|
86
92
|
ariaLabel = "Activity heatmap",
|
|
87
93
|
className,
|
|
88
94
|
}: HeatmapChartProps) {
|
|
@@ -123,6 +129,28 @@ function HeatmapChart({
|
|
|
123
129
|
const { columns, rows, cells, maxValue, peakKey } = layout;
|
|
124
130
|
const isEmpty = columns.length === 0 || rows.length === 0 || maxValue <= 0;
|
|
125
131
|
|
|
132
|
+
// "rank" spreads clustered values across the whole ramp by colouring each cell
|
|
133
|
+
// by its position among the distinct non-zero values rather than its raw ratio
|
|
134
|
+
// to the max — the right read for skewed activity counts (à la GitHub).
|
|
135
|
+
const colorFractionOf = React.useMemo(() => {
|
|
136
|
+
if (colorScale === "rank") {
|
|
137
|
+
const sorted = [...new Set(cells.map((cell) => cell.value).filter((value) => value > 0))].sort(
|
|
138
|
+
(lower, upper) => lower - upper,
|
|
139
|
+
);
|
|
140
|
+
// Floor non-zero ranks above the empty colour so the quietest active day
|
|
141
|
+
// still reads as coloured, never mistaken for no data.
|
|
142
|
+
const RANK_FLOOR = 0.18;
|
|
143
|
+
const fractionByValue = new Map(
|
|
144
|
+
sorted.map((value, index) => [
|
|
145
|
+
value,
|
|
146
|
+
sorted.length > 1 ? RANK_FLOOR + (1 - RANK_FLOOR) * (index / (sorted.length - 1)) : 1,
|
|
147
|
+
]),
|
|
148
|
+
);
|
|
149
|
+
return (value: number) => (value > 0 ? (fractionByValue.get(value) ?? 0) : 0);
|
|
150
|
+
}
|
|
151
|
+
return (value: number) => (maxValue > 0 ? value / maxValue : 0);
|
|
152
|
+
}, [cells, colorScale, maxValue]);
|
|
153
|
+
|
|
126
154
|
if (loading) {
|
|
127
155
|
return (
|
|
128
156
|
<div
|
|
@@ -152,7 +180,7 @@ function HeatmapChart({
|
|
|
152
180
|
);
|
|
153
181
|
}
|
|
154
182
|
|
|
155
|
-
const ramp = heatRamp(paletteName, theme.isDark ? HEAT_EMPTY.dark : HEAT_EMPTY.light);
|
|
183
|
+
const ramp = heatRamp(paletteName, theme.isDark ? HEAT_EMPTY.dark : HEAT_EMPTY.light, theme.isDark);
|
|
156
184
|
const totalWidth = PAD.left + columns.length * CELL + (columns.length - 1) * GAP + PAD.right;
|
|
157
185
|
const totalHeight = PAD.top + rows.length * CELL + (rows.length - 1) * GAP + PAD.bottom;
|
|
158
186
|
const xStride = showAllXLabels ? 1 : Math.ceil(columns.length / MAX_X_TICKS);
|
|
@@ -209,7 +237,7 @@ function HeatmapChart({
|
|
|
209
237
|
</text>
|
|
210
238
|
))}
|
|
211
239
|
{cells.map((cell) => {
|
|
212
|
-
const fraction = cell.value
|
|
240
|
+
const fraction = colorFractionOf(cell.value);
|
|
213
241
|
const fill = heatColor(ramp, fraction);
|
|
214
242
|
const key = pairKey(cell.rowLabel, cell.colLabel);
|
|
215
243
|
const isPeak = highlightPeak && key === peakKey;
|
|
@@ -266,6 +294,18 @@ function HeatmapChart({
|
|
|
266
294
|
</svg>
|
|
267
295
|
</div>
|
|
268
296
|
|
|
297
|
+
{showLegend && (
|
|
298
|
+
<div className="mt-3 flex items-center justify-end gap-2">
|
|
299
|
+
<span className="font-mono text-[9px] text-quaternary-foreground uppercase tracking-[0.14em]">Less</span>
|
|
300
|
+
<span
|
|
301
|
+
aria-hidden
|
|
302
|
+
className="h-2 w-24 rounded-full border border-border"
|
|
303
|
+
style={{ background: `linear-gradient(90deg, ${ramp.join(", ")})` }}
|
|
304
|
+
/>
|
|
305
|
+
<span className="font-mono text-[9px] text-quaternary-foreground uppercase tracking-[0.14em]">More</span>
|
|
306
|
+
</div>
|
|
307
|
+
)}
|
|
308
|
+
|
|
269
309
|
{mounted &&
|
|
270
310
|
hovered &&
|
|
271
311
|
createPortal(
|
|
@@ -274,7 +314,9 @@ function HeatmapChart({
|
|
|
274
314
|
style={{ left: tooltipLeft, top: tooltipTop }}
|
|
275
315
|
>
|
|
276
316
|
<p className="mb-1.5 font-mono text-[10px] text-quaternary-foreground uppercase tracking-wider">
|
|
277
|
-
{
|
|
317
|
+
{tooltipTitleKey && typeof hovered.record?.[tooltipTitleKey] === "string"
|
|
318
|
+
? hovered.record[tooltipTitleKey]
|
|
319
|
+
: `${formatY(hovered.rowLabel)} · ${formatX(hovered.colLabel)}`}
|
|
278
320
|
</p>
|
|
279
321
|
{tooltipMetrics && tooltipMetrics.length > 0 ? (
|
|
280
322
|
<div className="flex flex-col gap-1">
|
|
@@ -288,7 +330,9 @@ function HeatmapChart({
|
|
|
288
330
|
aria-hidden
|
|
289
331
|
className="size-2 shrink-0 rounded-[2px]"
|
|
290
332
|
style={{
|
|
291
|
-
background: isActive
|
|
333
|
+
background: isActive
|
|
334
|
+
? heatColor(ramp, colorFractionOf(hovered.value))
|
|
335
|
+
: theme.mutedForeground,
|
|
292
336
|
opacity: isActive ? 1 : 0.35,
|
|
293
337
|
}}
|
|
294
338
|
/>
|
|
@@ -312,7 +356,7 @@ function HeatmapChart({
|
|
|
312
356
|
<span
|
|
313
357
|
aria-hidden
|
|
314
358
|
className="size-2 shrink-0 rounded-[2px]"
|
|
315
|
-
style={{ background: heatColor(ramp, hovered.value
|
|
359
|
+
style={{ background: heatColor(ramp, colorFractionOf(hovered.value)) }}
|
|
316
360
|
/>
|
|
317
361
|
activity
|
|
318
362
|
</span>
|
|
@@ -22,6 +22,7 @@ import type { ChartMarker } from "./area-chart";
|
|
|
22
22
|
import { useChartContext } from "./chart-container";
|
|
23
23
|
import { ChartLegend } from "./chart-legend";
|
|
24
24
|
import { ChartTooltipContent } from "./chart-tooltip";
|
|
25
|
+
import { Skeleton } from "./skeleton";
|
|
25
26
|
|
|
26
27
|
const CURVE_TYPE = { monotone: "monotone", linear: "linear", step: "stepAfter" } as const;
|
|
27
28
|
|
|
@@ -157,13 +158,17 @@ function LineChart({
|
|
|
157
158
|
const isEmpty = data.length === 0 || resolved.length === 0;
|
|
158
159
|
|
|
159
160
|
return (
|
|
160
|
-
<div
|
|
161
|
+
<div
|
|
162
|
+
data-slot="line-chart"
|
|
163
|
+
className={cn(
|
|
164
|
+
"flex w-full flex-col gap-3",
|
|
165
|
+
"[&_.recharts-surface]:outline-none [&_.recharts-wrapper]:outline-none",
|
|
166
|
+
className,
|
|
167
|
+
)}
|
|
168
|
+
>
|
|
161
169
|
<div className="w-full" style={{ height }}>
|
|
162
170
|
{loading ? (
|
|
163
|
-
<
|
|
164
|
-
<span className="size-4 animate-spin rounded-full border-2 border-border border-t-foreground" />
|
|
165
|
-
loading…
|
|
166
|
-
</div>
|
|
171
|
+
<Skeleton className="h-full w-full rounded-lg" />
|
|
167
172
|
) : isEmpty ? (
|
|
168
173
|
<div className="flex h-full items-center justify-center rounded-lg border border-border border-dashed font-mono text-quaternary-foreground text-text-xs">
|
|
169
174
|
no data in range
|
|
@@ -177,7 +182,7 @@ function LineChart({
|
|
|
177
182
|
{...axis}
|
|
178
183
|
tick={makeXAxisTick(theme)}
|
|
179
184
|
interval="preserveStartEnd"
|
|
180
|
-
minTickGap={
|
|
185
|
+
minTickGap={56}
|
|
181
186
|
/>
|
|
182
187
|
<YAxis
|
|
183
188
|
{...axis}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { cn } from "@alpic-ai/ui/lib/cn";
|
|
4
4
|
import { cva, type VariantProps } from "class-variance-authority";
|
|
5
5
|
|
|
6
|
-
const skeletonVariants = cva("bg-
|
|
6
|
+
const skeletonVariants = cva("bg-foreground/10 motion-safe:animate-pulse", {
|
|
7
7
|
variants: {
|
|
8
8
|
shape: {
|
|
9
9
|
rectangle: "rounded-md",
|
package/src/components/stat.tsx
CHANGED
|
@@ -55,9 +55,9 @@ function Stat({ value, unit, delta, sparkline, semantic, className, ...props }:
|
|
|
55
55
|
delta && (delta.invert ? delta.direction === "down" : delta.direction === "up") ? "positive" : "negative";
|
|
56
56
|
|
|
57
57
|
return (
|
|
58
|
-
<div data-slot="stat" className={cn("flex flex-col gap-2.5", className)} {...props}>
|
|
59
|
-
<div className="flex items-baseline gap-
|
|
60
|
-
<span className="type-display-sm font-bold leading-none tracking-tight tabular-nums text-foreground">
|
|
58
|
+
<div data-slot="stat" className={cn("flex min-w-0 flex-col gap-2.5", className)} {...props}>
|
|
59
|
+
<div className="flex min-w-0 items-baseline gap-x-2.5">
|
|
60
|
+
<span className="type-display-sm min-w-0 truncate font-bold leading-none tracking-tight tabular-nums text-foreground">
|
|
61
61
|
{value}
|
|
62
62
|
</span>
|
|
63
63
|
{unit && <span className="font-mono text-[11px] leading-none text-quaternary-foreground mb-0.5">{unit}</span>}
|
|
@@ -69,7 +69,7 @@ function Stat({ value, unit, delta, sparkline, semantic, className, ...props }:
|
|
|
69
69
|
)}
|
|
70
70
|
</div>
|
|
71
71
|
{hasSpark && (
|
|
72
|
-
<div className="h-9 w-full">
|
|
72
|
+
<div className="pointer-events-none h-9 w-full">
|
|
73
73
|
<ResponsiveContainer width="100%" height="100%" initialDimension={{ width: 0, height: 36 }}>
|
|
74
74
|
<AreaChart data={sparkData} margin={{ top: 4, right: 2, bottom: 0, left: 2 }}>
|
|
75
75
|
<defs>
|
package/src/lib/chart-palette.ts
CHANGED
|
@@ -55,7 +55,10 @@ const HEAT_RAMPS: Record<ChartPaletteName, (empty: string) => string[]> = {
|
|
|
55
55
|
cyan: heatRampMint,
|
|
56
56
|
};
|
|
57
57
|
|
|
58
|
-
export const heatRamp = (palette: ChartPaletteName, empty: string) =>
|
|
58
|
+
export const heatRamp = (palette: ChartPaletteName, empty: string, isDark = true) => {
|
|
59
|
+
const [, ...colored] = HEAT_RAMPS[palette](empty);
|
|
60
|
+
return [empty, ...(isDark ? colored : colored.reverse())];
|
|
61
|
+
};
|
|
59
62
|
|
|
60
63
|
const hexToRgb = (hex: string) => {
|
|
61
64
|
const value = Number.parseInt(hex.slice(1), 16);
|