@alpic-ai/ui 1.144.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.
@@ -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__ */ jsxs("div", {
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: 44
182
+ minTickGap: 56
185
183
  }),
186
184
  /* @__PURE__ */ jsx(YAxis, {
187
185
  ...axis,
@@ -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 { Bar, BarChart as BarChart$1, CartesianGrid, LabelList, ReferenceArea, ReferenceDot, ReferenceLine, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
@@ -96,14 +97,11 @@ function BarChart({ data, index, series, variant = "stacked", legend = false, le
96
97
  const isEmpty = data.length === 0 || rendered.length === 0;
97
98
  return /* @__PURE__ */ jsxs("div", {
98
99
  "data-slot": "bar-chart",
99
- 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),
100
101
  children: [/* @__PURE__ */ jsx("div", {
101
102
  className: "w-full",
102
103
  style: { height },
103
- children: loading ? /* @__PURE__ */ jsxs("div", {
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", {
104
+ children: loading ? /* @__PURE__ */ jsx(Skeleton, { className: "h-full w-full rounded-lg" }) : isEmpty ? /* @__PURE__ */ jsx("div", {
107
105
  className: "flex h-full items-center justify-center rounded-lg border border-border border-dashed font-mono text-quaternary-foreground text-text-xs",
108
106
  children: "no data in range"
109
107
  }) : /* @__PURE__ */ jsx(ResponsiveContainer, {
@@ -165,7 +163,7 @@ function BarChart({ data, index, series, variant = "stacked", legend = false, le
165
163
  ...axis,
166
164
  tick: makeXAxisTick(theme),
167
165
  interval: "preserveStartEnd",
168
- minTickGap: 44
166
+ minTickGap: 56
169
167
  }),
170
168
  /* @__PURE__ */ jsx(YAxis, {
171
169
  ...axis,
@@ -218,11 +216,7 @@ function BarChart({ data, index, series, variant = "stacked", legend = false, le
218
216
  strokeWidth: 0,
219
217
  radius: radiusFor(slot),
220
218
  maxBarSize: MAX_BAR_SIZE,
221
- activeBar: {
222
- fillOpacity: 1,
223
- stroke: theme.card,
224
- strokeWidth: 1
225
- },
219
+ activeBar: false,
226
220
  isAnimationActive: false,
227
221
  animationDuration: 650,
228
222
  animationEasing: "ease-out",
@@ -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 rows = [...payload].sort((lower, upper) => toNumber(upper.value) - toNumber(lower.value));
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: [rows.map((item, index) => {
20
- const swatch = [
21
- item.color,
22
- item.stroke,
23
- item.fill
24
- ].find((candidate) => typeof candidate === "string" && candidate.length > 0 && !candidate.startsWith("url(")) ?? theme.mutedForeground;
25
- const numeric = toNumber(item.value);
26
- 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", {
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-muted-foreground",
30
- children: [/* @__PURE__ */ jsx("span", {
31
- "aria-hidden": true,
32
- className: "size-2 shrink-0 rounded-[2px]",
33
- style: { background: swatch }
34
- }), 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
35
69
  }), /* @__PURE__ */ jsx("span", {
36
70
  className: "font-mono font-semibold tabular-nums text-foreground",
37
- children: valueFormatter(numeric)
71
+ children: valueFormatter(total)
38
72
  })]
39
- }, `${item.dataKey ?? index}`);
40
- }), showTotal && /* @__PURE__ */ jsxs("div", {
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: [/* @__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
@@ -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__ */ jsxs("div", {
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: 44
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-accent motion-safe:animate-pulse", {
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"
@@ -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-3",
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) => HEAT_RAMPS[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": "1.144.0",
3
+ "version": "1.145.0",
4
4
  "description": "Alpic design system — shared UI components",
5
5
  "type": "module",
6
6
  "exports": {
@@ -52,7 +52,7 @@
52
52
  "class-variance-authority": "^0.7.1",
53
53
  "clsx": "^2.1.1",
54
54
  "cmdk": "^1.1.1",
55
- "recharts": "^3.8.1",
55
+ "recharts": "^3.9.0",
56
56
  "tailwind-merge": "^3.6.0"
57
57
  },
58
58
  "devDependencies": {
@@ -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 data-slot="area-chart" className={cn("flex w-full flex-col gap-3", className)}>
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
- <div className="flex h-full items-center justify-center gap-2.5 font-mono text-quaternary-foreground text-text-xs">
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={44}
246
+ minTickGap={56}
242
247
  />
243
248
  <YAxis
244
249
  {...axis}
@@ -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 BAR_RADIUS = 4;
27
28
  const MAX_BAR_SIZE = 48;
@@ -155,13 +156,17 @@ function BarChart({
155
156
  const isEmpty = data.length === 0 || rendered.length === 0;
156
157
 
157
158
  return (
158
- <div data-slot="bar-chart" className={cn("flex w-full flex-col gap-3", className)}>
159
+ <div
160
+ data-slot="bar-chart"
161
+ className={cn(
162
+ "flex w-full flex-col gap-3",
163
+ "[&_.recharts-surface]:outline-none [&_.recharts-wrapper]:outline-none",
164
+ className,
165
+ )}
166
+ >
159
167
  <div className="w-full" style={{ height }}>
160
168
  {loading ? (
161
- <div className="flex h-full items-center justify-center gap-2.5 font-mono text-quaternary-foreground text-text-xs">
162
- <span className="size-4 animate-spin rounded-full border-2 border-border border-t-foreground" />
163
- loading…
164
- </div>
169
+ <Skeleton className="h-full w-full rounded-lg" />
165
170
  ) : isEmpty ? (
166
171
  <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
172
  no data in range
@@ -202,7 +207,7 @@ function BarChart({
202
207
  {...axis}
203
208
  tick={makeXAxisTick(theme)}
204
209
  interval="preserveStartEnd"
205
- minTickGap={44}
210
+ minTickGap={56}
206
211
  />
207
212
  <YAxis
208
213
  {...axis}
@@ -268,7 +273,7 @@ function BarChart({
268
273
  strokeWidth={0}
269
274
  radius={radiusFor(slot)}
270
275
  maxBarSize={MAX_BAR_SIZE}
271
- activeBar={{ fillOpacity: 1, stroke: theme.card, strokeWidth: 1 }}
276
+ activeBar={false}
272
277
  isAnimationActive={false}
273
278
  animationDuration={650}
274
279
  animationEasing="ease-out"
@@ -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 rows = [...payload].sort((lower, upper) => toNumber(upper.value) - toNumber(lower.value));
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 / maxValue;
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
- {formatY(hovered.rowLabel)} · {formatX(hovered.colLabel)}
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 ? heatColor(ramp, hovered.value / maxValue) : theme.mutedForeground,
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 / maxValue) }}
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 data-slot="line-chart" className={cn("flex w-full flex-col gap-3", className)}>
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
- <div className="flex h-full items-center justify-center gap-2.5 font-mono text-quaternary-foreground text-text-xs">
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={44}
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-accent motion-safe:animate-pulse", {
6
+ const skeletonVariants = cva("bg-foreground/10 motion-safe:animate-pulse", {
7
7
  variants: {
8
8
  shape: {
9
9
  rectangle: "rounded-md",
@@ -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-3">
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>
@@ -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) => HEAT_RAMPS[palette](empty);
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);