@alpic-ai/ui 0.0.0-dev.g19fc228 → 0.0.0-dev.g1a5d5ed
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.d.mts +2 -0
- package/dist/components/area-chart.mjs +9 -3
- package/dist/components/bar-chart.d.mts +2 -0
- package/dist/components/bar-chart.mjs +9 -3
- package/dist/components/bar-list.d.mts +3 -0
- package/dist/components/bar-list.mjs +19 -7
- package/dist/components/chart-card.d.mts +1 -1
- package/dist/components/chart-card.mjs +1 -1
- package/dist/components/chart-container.d.mts +1 -1
- package/dist/components/chart-legend.d.mts +5 -0
- package/dist/components/chart-legend.mjs +11 -2
- package/dist/components/donut-chart.mjs +4 -0
- package/dist/components/heatmap-chart.d.mts +8 -0
- package/dist/components/heatmap-chart.mjs +39 -8
- package/dist/components/line-chart.d.mts +2 -0
- package/dist/components/line-chart.mjs +10 -3
- package/dist/components/stat.d.mts +3 -1
- package/dist/components/stat.mjs +14 -4
- package/dist/components/textarea.mjs +1 -1
- package/dist/lib/chart.mjs +16 -1
- package/package.json +23 -23
- package/src/components/area-chart.tsx +12 -4
- package/src/components/bar-chart.tsx +12 -4
- package/src/components/bar-list.tsx +21 -6
- package/src/components/chart-card.tsx +8 -6
- package/src/components/chart-container.tsx +2 -0
- package/src/components/chart-legend.tsx +10 -2
- package/src/components/donut-chart.tsx +1 -1
- package/src/components/heatmap-chart.tsx +62 -18
- package/src/components/line-chart.tsx +18 -5
- package/src/components/stat.tsx +10 -6
- package/src/components/textarea.tsx +1 -1
- package/src/lib/chart.ts +34 -0
- package/src/stories/area-chart.stories.tsx +1 -1
- package/src/stories/bar-chart.stories.tsx +1 -1
- package/src/stories/bar-list.stories.tsx +1 -1
- package/src/stories/donut-chart.stories.tsx +1 -1
- package/src/stories/heatmap-chart.stories.tsx +1 -1
- package/src/stories/line-chart.stories.tsx +1 -1
- package/src/stories/textarea.stories.tsx +7 -0
- package/src/stories/wizard.stories.tsx +1 -1
- package/src/styles/tokens.css +0 -45
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
} from "recharts";
|
|
17
17
|
|
|
18
18
|
import { useReducedMotion } from "../hooks/use-reduced-motion";
|
|
19
|
-
import { type ChartSeries, orderByLuminance, resolveSeries } from "../lib/chart";
|
|
19
|
+
import { type ChartSeries, makeXAxisTick, orderByLuminance, resolveSeries } from "../lib/chart";
|
|
20
20
|
import type { ChartPaletteName } from "../lib/chart-palette";
|
|
21
21
|
import { cn } from "../lib/cn";
|
|
22
22
|
import { useChartContext } from "./chart-container";
|
|
@@ -39,6 +39,7 @@ export interface AreaChartProps {
|
|
|
39
39
|
variant?: "stacked" | "grouped" | "expand";
|
|
40
40
|
curve?: keyof typeof CURVE_TYPE;
|
|
41
41
|
legend?: boolean;
|
|
42
|
+
legendAlign?: "left" | "center" | "right";
|
|
42
43
|
valueFlags?: boolean;
|
|
43
44
|
height?: number;
|
|
44
45
|
yAxisWidth?: number;
|
|
@@ -60,6 +61,7 @@ function AreaChart({
|
|
|
60
61
|
variant = "stacked",
|
|
61
62
|
curve = "monotone",
|
|
62
63
|
legend = false,
|
|
64
|
+
legendAlign = "left",
|
|
63
65
|
valueFlags = false,
|
|
64
66
|
height = 200,
|
|
65
67
|
yAxisWidth = 48,
|
|
@@ -205,7 +207,7 @@ function AreaChart({
|
|
|
205
207
|
no data in range
|
|
206
208
|
</div>
|
|
207
209
|
) : (
|
|
208
|
-
<ResponsiveContainer width="100%" height="100%">
|
|
210
|
+
<ResponsiveContainer width="100%" height="100%" initialDimension={{ width: 0, height }}>
|
|
209
211
|
<RechartsAreaChart
|
|
210
212
|
data={data as Record<string, string | number>[]}
|
|
211
213
|
stackOffset={variant === "expand" ? "expand" : "none"}
|
|
@@ -234,7 +236,13 @@ function AreaChart({
|
|
|
234
236
|
</defs>
|
|
235
237
|
|
|
236
238
|
<CartesianGrid vertical={false} stroke={theme.grid} strokeDasharray="2 4" />
|
|
237
|
-
<XAxis
|
|
239
|
+
<XAxis
|
|
240
|
+
dataKey={index}
|
|
241
|
+
{...axis}
|
|
242
|
+
tick={makeXAxisTick(theme)}
|
|
243
|
+
interval="preserveStartEnd"
|
|
244
|
+
minTickGap={44}
|
|
245
|
+
/>
|
|
238
246
|
<YAxis
|
|
239
247
|
{...axis}
|
|
240
248
|
width={yAxisWidth}
|
|
@@ -331,7 +339,7 @@ function AreaChart({
|
|
|
331
339
|
)}
|
|
332
340
|
</div>
|
|
333
341
|
|
|
334
|
-
{legend && !isEmpty && <ChartLegend items={legendItems}
|
|
342
|
+
{legend && !isEmpty && <ChartLegend items={legendItems} align={legendAlign} insetLeft={yAxisWidth} />}
|
|
335
343
|
</div>
|
|
336
344
|
);
|
|
337
345
|
}
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
} from "recharts";
|
|
17
17
|
|
|
18
18
|
import { useReducedMotion } from "../hooks/use-reduced-motion";
|
|
19
|
-
import { type ChartSeries, orderByLuminance, resolveSeries } from "../lib/chart";
|
|
19
|
+
import { type ChartSeries, makeXAxisTick, orderByLuminance, resolveSeries } from "../lib/chart";
|
|
20
20
|
import type { ChartPaletteName } from "../lib/chart-palette";
|
|
21
21
|
import { cn } from "../lib/cn";
|
|
22
22
|
import type { ChartMarker } from "./area-chart";
|
|
@@ -33,6 +33,7 @@ export interface BarChartProps {
|
|
|
33
33
|
series: ChartSeries[];
|
|
34
34
|
variant?: "stacked" | "grouped" | "expand";
|
|
35
35
|
legend?: boolean;
|
|
36
|
+
legendAlign?: "left" | "center" | "right";
|
|
36
37
|
valueLabels?: boolean;
|
|
37
38
|
height?: number;
|
|
38
39
|
yAxisWidth?: number;
|
|
@@ -52,6 +53,7 @@ function BarChart({
|
|
|
52
53
|
series,
|
|
53
54
|
variant = "stacked",
|
|
54
55
|
legend = false,
|
|
56
|
+
legendAlign = "left",
|
|
55
57
|
valueLabels = false,
|
|
56
58
|
height = 200,
|
|
57
59
|
yAxisWidth = 48,
|
|
@@ -168,7 +170,7 @@ function BarChart({
|
|
|
168
170
|
no data in range
|
|
169
171
|
</div>
|
|
170
172
|
) : (
|
|
171
|
-
<ResponsiveContainer width="100%" height="100%">
|
|
173
|
+
<ResponsiveContainer width="100%" height="100%" initialDimension={{ width: 0, height }}>
|
|
172
174
|
<RechartsBarChart
|
|
173
175
|
data={data as Record<string, string | number>[]}
|
|
174
176
|
stackOffset={variant === "expand" ? "expand" : "none"}
|
|
@@ -198,7 +200,13 @@ function BarChart({
|
|
|
198
200
|
</defs>
|
|
199
201
|
|
|
200
202
|
<CartesianGrid vertical={false} stroke={theme.grid} strokeDasharray="2 4" />
|
|
201
|
-
<XAxis
|
|
203
|
+
<XAxis
|
|
204
|
+
dataKey={index}
|
|
205
|
+
{...axis}
|
|
206
|
+
tick={makeXAxisTick(theme)}
|
|
207
|
+
interval="preserveStartEnd"
|
|
208
|
+
minTickGap={44}
|
|
209
|
+
/>
|
|
202
210
|
<YAxis
|
|
203
211
|
{...axis}
|
|
204
212
|
width={yAxisWidth}
|
|
@@ -301,7 +309,7 @@ function BarChart({
|
|
|
301
309
|
)}
|
|
302
310
|
</div>
|
|
303
311
|
|
|
304
|
-
{legend && !isEmpty && <ChartLegend items={legendItems}
|
|
312
|
+
{legend && !isEmpty && <ChartLegend items={legendItems} align={legendAlign} insetLeft={yAxisWidth} />}
|
|
305
313
|
</div>
|
|
306
314
|
);
|
|
307
315
|
}
|
|
@@ -15,15 +15,22 @@ export interface BarListProps {
|
|
|
15
15
|
dataKey?: string;
|
|
16
16
|
maxItems?: number;
|
|
17
17
|
palette?: ChartPaletteName;
|
|
18
|
+
/** Renders bars in a single semantic hue (e.g. red for errors) rather than the palette ramp. */
|
|
19
|
+
semantic?: "error" | "warning" | "success";
|
|
18
20
|
loading?: boolean;
|
|
19
21
|
valueFormatter?: (value: number) => string;
|
|
20
22
|
labelFormatter?: (label: string | number) => string;
|
|
21
23
|
className?: string;
|
|
22
24
|
}
|
|
23
25
|
|
|
26
|
+
const SEMANTIC_KEY = { error: "destructive", warning: "warning", success: "success" } as const;
|
|
27
|
+
|
|
24
28
|
const PLACEHOLDER_HEIGHT = 168;
|
|
25
29
|
// Cap the ramp off its palest end so low-rank bars stay legible on a light card.
|
|
26
30
|
const RAMP_CEILING = 0.8;
|
|
31
|
+
// Semantic bars keep a single hue: the lightest bar still holds this much colour
|
|
32
|
+
// (mixed toward white) so a long error list never fades to near-invisible.
|
|
33
|
+
const SEMANTIC_FLOOR = 0.62;
|
|
27
34
|
const IN_FILL_SHADOW = "0 1px 2px rgb(0 0 0 / 0.28)";
|
|
28
35
|
|
|
29
36
|
function BarList({
|
|
@@ -32,12 +39,14 @@ function BarList({
|
|
|
32
39
|
dataKey = "value",
|
|
33
40
|
maxItems,
|
|
34
41
|
palette,
|
|
42
|
+
semantic,
|
|
35
43
|
loading = false,
|
|
36
44
|
valueFormatter = (value) => value.toLocaleString("en-US"),
|
|
37
45
|
labelFormatter,
|
|
38
46
|
className,
|
|
39
47
|
}: BarListProps) {
|
|
40
|
-
const { paletteName } = useChartContext(palette);
|
|
48
|
+
const { paletteName, theme } = useChartContext(palette);
|
|
49
|
+
const accent = semantic ? theme[SEMANTIC_KEY[semantic]] : null;
|
|
41
50
|
const reducedMotion = useReducedMotion();
|
|
42
51
|
const [mounted, setMounted] = React.useState(false);
|
|
43
52
|
const [active, setActive] = React.useState<number | null>(null);
|
|
@@ -55,11 +64,17 @@ function BarList({
|
|
|
55
64
|
}));
|
|
56
65
|
mapped.sort((lower, upper) => upper.value - lower.value);
|
|
57
66
|
const capped = maxItems ? mapped.slice(0, maxItems) : mapped;
|
|
58
|
-
return capped.map((row, rank) =>
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
67
|
+
return capped.map((row, rank) => {
|
|
68
|
+
const rankFraction = capped.length > 1 ? rank / (capped.length - 1) : 0;
|
|
69
|
+
const accentWeight = Math.round((1 - rankFraction * (1 - SEMANTIC_FLOOR)) * 100);
|
|
70
|
+
return {
|
|
71
|
+
...row,
|
|
72
|
+
color: accent
|
|
73
|
+
? `color-mix(in oklab, ${accent} ${accentWeight}%, white)`
|
|
74
|
+
: rampColor(paletteName, rankFraction * RAMP_CEILING),
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
}, [data, index, dataKey, maxItems, paletteName, accent]);
|
|
63
78
|
|
|
64
79
|
const maxValue = rows.reduce((max, row) => (row.value > max ? row.value : max), 0);
|
|
65
80
|
const isEmpty = rows.length === 0;
|
|
@@ -12,7 +12,7 @@ export interface ChartCardProps extends Omit<React.ComponentProps<"section">, "t
|
|
|
12
12
|
title?: React.ReactNode;
|
|
13
13
|
description?: React.ReactNode;
|
|
14
14
|
action?: React.ReactNode;
|
|
15
|
-
accent?: "top" | "left";
|
|
15
|
+
accent?: "top" | "left" | "none";
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
function ChartCard({
|
|
@@ -40,11 +40,13 @@ function ChartCard({
|
|
|
40
40
|
)}
|
|
41
41
|
{...props}
|
|
42
42
|
>
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
{accent !== "none" && (
|
|
44
|
+
<span
|
|
45
|
+
aria-hidden
|
|
46
|
+
className={cn("absolute", isLeft ? "inset-y-0 left-0 w-[3px]" : "inset-x-0 top-0 h-[3px]")}
|
|
47
|
+
style={{ background: lead }}
|
|
48
|
+
/>
|
|
49
|
+
)}
|
|
48
50
|
{(kicker || title || action) && (
|
|
49
51
|
<header className="mb-4 flex items-start justify-between gap-4">
|
|
50
52
|
<div className="flex flex-col gap-1">
|
|
@@ -6,6 +6,8 @@ import { type ChartTheme, useChartTheme } from "../hooks/use-chart-theme";
|
|
|
6
6
|
import { CHART_PALETTES, type ChartPaletteName } from "../lib/chart-palette";
|
|
7
7
|
import { cn } from "../lib/cn";
|
|
8
8
|
|
|
9
|
+
export type { ChartPaletteName } from "../lib/chart-palette";
|
|
10
|
+
|
|
9
11
|
interface ChartContextValue {
|
|
10
12
|
palette: readonly string[];
|
|
11
13
|
paletteName: ChartPaletteName;
|
|
@@ -10,8 +10,12 @@ export interface ChartLegendItem {
|
|
|
10
10
|
|
|
11
11
|
export interface ChartLegendProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
12
12
|
items: ChartLegendItem[];
|
|
13
|
+
align?: "left" | "center" | "right";
|
|
14
|
+
insetLeft?: number;
|
|
13
15
|
}
|
|
14
16
|
|
|
17
|
+
const ALIGN_CLASS = { left: "justify-start", center: "justify-center", right: "justify-end" } as const;
|
|
18
|
+
|
|
15
19
|
function Swatch({ color, dashed }: { color: string; dashed?: boolean }) {
|
|
16
20
|
return (
|
|
17
21
|
<span
|
|
@@ -22,9 +26,13 @@ function Swatch({ color, dashed }: { color: string; dashed?: boolean }) {
|
|
|
22
26
|
);
|
|
23
27
|
}
|
|
24
28
|
|
|
25
|
-
function ChartLegend({ items, className, ...props }: ChartLegendProps) {
|
|
29
|
+
function ChartLegend({ items, align = "left", insetLeft, className, style, ...props }: ChartLegendProps) {
|
|
26
30
|
return (
|
|
27
|
-
<div
|
|
31
|
+
<div
|
|
32
|
+
className={cn("flex flex-wrap gap-x-4 gap-y-1.5", ALIGN_CLASS[align], className)}
|
|
33
|
+
style={{ paddingLeft: align === "left" ? insetLeft : undefined, ...style }}
|
|
34
|
+
{...props}
|
|
35
|
+
>
|
|
28
36
|
{items.map((item) => (
|
|
29
37
|
<span
|
|
30
38
|
key={item.name}
|
|
@@ -99,7 +99,7 @@ function DonutChart({
|
|
|
99
99
|
) : (
|
|
100
100
|
<div className="flex flex-col items-center gap-5 @md:flex-row">
|
|
101
101
|
<div className="relative shrink-0" style={{ width: height, height }}>
|
|
102
|
-
<ResponsiveContainer width="100%" height="100%">
|
|
102
|
+
<ResponsiveContainer width="100%" height="100%" initialDimension={{ width: 0, height }}>
|
|
103
103
|
<RechartsPieChart>
|
|
104
104
|
<defs>
|
|
105
105
|
{slices.map((slice, slot) => (
|
|
@@ -17,21 +17,26 @@ export interface HeatmapChartProps {
|
|
|
17
17
|
palette?: ChartPaletteName;
|
|
18
18
|
xLabels?: readonly string[];
|
|
19
19
|
yLabels?: readonly string[];
|
|
20
|
+
showAllXLabels?: boolean;
|
|
20
21
|
highlightPeak?: boolean;
|
|
21
22
|
loading?: boolean;
|
|
22
23
|
valueFormatter?: (value: number) => string;
|
|
23
24
|
xTickFormatter?: (label: string) => string;
|
|
24
25
|
yTickFormatter?: (label: string) => string;
|
|
26
|
+
tooltipMetrics?: ReadonlyArray<{ key: string; label: string; format?: (value: number) => string }>;
|
|
25
27
|
ariaLabel?: string;
|
|
26
28
|
className?: string;
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
type CellRecord = Record<string, string | number | null | undefined>;
|
|
32
|
+
|
|
29
33
|
interface HoverState {
|
|
30
34
|
x: number;
|
|
31
35
|
y: number;
|
|
32
36
|
rowLabel: string;
|
|
33
37
|
colLabel: string;
|
|
34
38
|
value: number;
|
|
39
|
+
record: CellRecord | null;
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
const PLACEHOLDER_HEIGHT = 168;
|
|
@@ -71,11 +76,13 @@ function HeatmapChart({
|
|
|
71
76
|
palette,
|
|
72
77
|
xLabels,
|
|
73
78
|
yLabels,
|
|
79
|
+
showAllXLabels = false,
|
|
74
80
|
highlightPeak = true,
|
|
75
81
|
loading = false,
|
|
76
82
|
valueFormatter = (value) => value.toLocaleString("en-US"),
|
|
77
83
|
xTickFormatter,
|
|
78
84
|
yTickFormatter,
|
|
85
|
+
tooltipMetrics,
|
|
79
86
|
ariaLabel = "Activity heatmap",
|
|
80
87
|
className,
|
|
81
88
|
}: HeatmapChartProps) {
|
|
@@ -91,19 +98,23 @@ function HeatmapChart({
|
|
|
91
98
|
const columns = xLabels ? [...xLabels] : uniqueInOrder(data.map((row) => String(row[xKey] ?? "")));
|
|
92
99
|
const rows = yLabels ? [...yLabels] : uniqueInOrder(data.map((row) => String(row[yKey] ?? "")));
|
|
93
100
|
const valueAt = new Map<string, number>();
|
|
101
|
+
const recordAt = new Map<string, CellRecord>();
|
|
94
102
|
for (const row of data) {
|
|
95
|
-
|
|
103
|
+
const key = pairKey(String(row[yKey] ?? ""), String(row[xKey] ?? ""));
|
|
104
|
+
valueAt.set(key, Number(row[dataKey]) || 0);
|
|
105
|
+
recordAt.set(key, row);
|
|
96
106
|
}
|
|
97
107
|
let maxValue = 0;
|
|
98
108
|
let peakKey = "";
|
|
99
109
|
const cells = rows.flatMap((rowLabel, rowIndex) =>
|
|
100
110
|
columns.map((colLabel, colIndex) => {
|
|
101
|
-
const
|
|
111
|
+
const key = pairKey(rowLabel, colLabel);
|
|
112
|
+
const value = valueAt.get(key) ?? 0;
|
|
102
113
|
if (value > maxValue) {
|
|
103
114
|
maxValue = value;
|
|
104
|
-
peakKey =
|
|
115
|
+
peakKey = key;
|
|
105
116
|
}
|
|
106
|
-
return { rowLabel, colLabel, rowIndex, colIndex, value };
|
|
117
|
+
return { rowLabel, colLabel, rowIndex, colIndex, value, record: recordAt.get(key) ?? null };
|
|
107
118
|
}),
|
|
108
119
|
);
|
|
109
120
|
return { columns, rows, cells, maxValue, peakKey };
|
|
@@ -144,7 +155,7 @@ function HeatmapChart({
|
|
|
144
155
|
const ramp = heatRamp(paletteName, theme.isDark ? HEAT_EMPTY.dark : HEAT_EMPTY.light);
|
|
145
156
|
const totalWidth = PAD.left + columns.length * CELL + (columns.length - 1) * GAP + PAD.right;
|
|
146
157
|
const totalHeight = PAD.top + rows.length * CELL + (rows.length - 1) * GAP + PAD.bottom;
|
|
147
|
-
const xStride = Math.ceil(columns.length / MAX_X_TICKS);
|
|
158
|
+
const xStride = showAllXLabels ? 1 : Math.ceil(columns.length / MAX_X_TICKS);
|
|
148
159
|
const cellX = (col: number) => PAD.left + col * (CELL + GAP);
|
|
149
160
|
const cellY = (row: number) => PAD.top + row * (CELL + GAP);
|
|
150
161
|
const formatX = (label: string) => (xTickFormatter ? xTickFormatter(label) : label);
|
|
@@ -247,6 +258,7 @@ function HeatmapChart({
|
|
|
247
258
|
rowLabel: cell.rowLabel,
|
|
248
259
|
colLabel: cell.colLabel,
|
|
249
260
|
value: cell.value,
|
|
261
|
+
record: cell.record,
|
|
250
262
|
})
|
|
251
263
|
}
|
|
252
264
|
/>
|
|
@@ -264,19 +276,51 @@ function HeatmapChart({
|
|
|
264
276
|
<p className="mb-1.5 font-mono text-[10px] text-quaternary-foreground uppercase tracking-wider">
|
|
265
277
|
{formatY(hovered.rowLabel)} · {formatX(hovered.colLabel)}
|
|
266
278
|
</p>
|
|
267
|
-
|
|
268
|
-
<
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
279
|
+
{tooltipMetrics && tooltipMetrics.length > 0 ? (
|
|
280
|
+
<div className="flex flex-col gap-1">
|
|
281
|
+
{tooltipMetrics.map((metric) => {
|
|
282
|
+
const raw = hovered.record ? Number(hovered.record[metric.key]) || 0 : 0;
|
|
283
|
+
const isActive = metric.key === dataKey;
|
|
284
|
+
return (
|
|
285
|
+
<div key={metric.key} className="flex items-center justify-between gap-4 text-text-xs">
|
|
286
|
+
<span className="inline-flex items-center gap-2 text-muted-foreground">
|
|
287
|
+
<span
|
|
288
|
+
aria-hidden
|
|
289
|
+
className="size-2 shrink-0 rounded-[2px]"
|
|
290
|
+
style={{
|
|
291
|
+
background: isActive ? heatColor(ramp, hovered.value / maxValue) : theme.mutedForeground,
|
|
292
|
+
opacity: isActive ? 1 : 0.35,
|
|
293
|
+
}}
|
|
294
|
+
/>
|
|
295
|
+
{metric.label}
|
|
296
|
+
</span>
|
|
297
|
+
<span
|
|
298
|
+
className={cn(
|
|
299
|
+
"font-mono tabular-nums",
|
|
300
|
+
isActive ? "font-semibold text-foreground" : "text-muted-foreground",
|
|
301
|
+
)}
|
|
302
|
+
>
|
|
303
|
+
{(metric.format ?? valueFormatter)(raw)}
|
|
304
|
+
</span>
|
|
305
|
+
</div>
|
|
306
|
+
);
|
|
307
|
+
})}
|
|
308
|
+
</div>
|
|
309
|
+
) : (
|
|
310
|
+
<div className="flex items-center justify-between gap-4 text-text-xs">
|
|
311
|
+
<span className="inline-flex items-center gap-2 text-muted-foreground">
|
|
312
|
+
<span
|
|
313
|
+
aria-hidden
|
|
314
|
+
className="size-2 shrink-0 rounded-[2px]"
|
|
315
|
+
style={{ background: heatColor(ramp, hovered.value / maxValue) }}
|
|
316
|
+
/>
|
|
317
|
+
activity
|
|
318
|
+
</span>
|
|
319
|
+
<span className="font-mono font-semibold tabular-nums text-foreground">
|
|
320
|
+
{valueFormatter(hovered.value)}
|
|
321
|
+
</span>
|
|
322
|
+
</div>
|
|
323
|
+
)}
|
|
280
324
|
</div>,
|
|
281
325
|
document.body,
|
|
282
326
|
)}
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
} from "recharts";
|
|
17
17
|
|
|
18
18
|
import { useReducedMotion } from "../hooks/use-reduced-motion";
|
|
19
|
-
import { type ChartSeries, resolveSeries } from "../lib/chart";
|
|
19
|
+
import { type ChartSeries, makeXAxisTick, resolveSeries } from "../lib/chart";
|
|
20
20
|
import type { ChartPaletteName } from "../lib/chart-palette";
|
|
21
21
|
import { cn } from "../lib/cn";
|
|
22
22
|
import type { ChartMarker } from "./area-chart";
|
|
@@ -32,6 +32,7 @@ export interface LineChartProps {
|
|
|
32
32
|
series: ChartSeries[];
|
|
33
33
|
curve?: keyof typeof CURVE_TYPE;
|
|
34
34
|
legend?: boolean;
|
|
35
|
+
legendAlign?: "left" | "center" | "right";
|
|
35
36
|
valueFlags?: boolean;
|
|
36
37
|
dots?: boolean;
|
|
37
38
|
height?: number;
|
|
@@ -52,6 +53,7 @@ function LineChart({
|
|
|
52
53
|
series,
|
|
53
54
|
curve = "monotone",
|
|
54
55
|
legend = false,
|
|
56
|
+
legendAlign = "left",
|
|
55
57
|
valueFlags = false,
|
|
56
58
|
dots = false,
|
|
57
59
|
height = 200,
|
|
@@ -170,11 +172,22 @@ function LineChart({
|
|
|
170
172
|
no data in range
|
|
171
173
|
</div>
|
|
172
174
|
) : (
|
|
173
|
-
<ResponsiveContainer width="100%" height="100%">
|
|
175
|
+
<ResponsiveContainer width="100%" height="100%" initialDimension={{ width: 0, height }}>
|
|
174
176
|
<RechartsLineChart data={data as Record<string, string | number>[]} margin={margin}>
|
|
175
177
|
<CartesianGrid vertical={false} stroke={theme.grid} strokeDasharray="2 4" />
|
|
176
|
-
<XAxis
|
|
177
|
-
|
|
178
|
+
<XAxis
|
|
179
|
+
dataKey={index}
|
|
180
|
+
{...axis}
|
|
181
|
+
tick={makeXAxisTick(theme)}
|
|
182
|
+
interval="preserveStartEnd"
|
|
183
|
+
minTickGap={44}
|
|
184
|
+
/>
|
|
185
|
+
<YAxis
|
|
186
|
+
{...axis}
|
|
187
|
+
width={yAxisWidth}
|
|
188
|
+
domain={["auto", "auto"]}
|
|
189
|
+
tickFormatter={(value: number) => valueFormatter(value)}
|
|
190
|
+
/>
|
|
178
191
|
<Tooltip
|
|
179
192
|
offset={12}
|
|
180
193
|
allowEscapeViewBox={{ x: false, y: false }}
|
|
@@ -256,7 +269,7 @@ function LineChart({
|
|
|
256
269
|
)}
|
|
257
270
|
</div>
|
|
258
271
|
|
|
259
|
-
{legend && !isEmpty && <ChartLegend items={legendItems}
|
|
272
|
+
{legend && !isEmpty && <ChartLegend items={legendItems} align={legendAlign} insetLeft={yAxisWidth} />}
|
|
260
273
|
</div>
|
|
261
274
|
);
|
|
262
275
|
}
|
package/src/components/stat.tsx
CHANGED
|
@@ -31,8 +31,9 @@ export interface StatDelta {
|
|
|
31
31
|
export interface StatProps extends React.ComponentProps<"div"> {
|
|
32
32
|
value: React.ReactNode;
|
|
33
33
|
unit?: string;
|
|
34
|
-
delta?: StatDelta;
|
|
34
|
+
delta?: StatDelta | null;
|
|
35
35
|
sparkline?: number[] | Array<{ value: number }>;
|
|
36
|
+
semantic?: "error" | "warning" | "success";
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
const toSparkData = (sparkline: StatProps["sparkline"]) =>
|
|
@@ -40,12 +41,15 @@ const toSparkData = (sparkline: StatProps["sparkline"]) =>
|
|
|
40
41
|
typeof point === "number" ? { index, value: point } : { index, value: point.value },
|
|
41
42
|
);
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
const SEMANTIC_KEY = { error: "destructive", warning: "warning", success: "success" } as const;
|
|
45
|
+
|
|
46
|
+
function Stat({ value, unit, delta, sparkline, semantic, className, ...props }: StatProps) {
|
|
47
|
+
const { palette, theme } = useChartContext();
|
|
45
48
|
const gradientId = React.useId().replace(/:/g, "");
|
|
46
49
|
const sparkData = React.useMemo(() => toSparkData(sparkline), [sparkline]);
|
|
50
|
+
const hasSpark = sparkData.some((point) => point.value > 0);
|
|
47
51
|
// biome-ignore lint/style/noNonNullAssertion: palettes are never empty
|
|
48
|
-
const sparkColor = palette[0]!;
|
|
52
|
+
const sparkColor = semantic ? theme[SEMANTIC_KEY[semantic]] : palette[0]!;
|
|
49
53
|
|
|
50
54
|
const sentiment =
|
|
51
55
|
delta && (delta.invert ? delta.direction === "down" : delta.direction === "up") ? "positive" : "negative";
|
|
@@ -64,9 +68,9 @@ function Stat({ value, unit, delta, sparkline, className, ...props }: StatProps)
|
|
|
64
68
|
</DeltaPill>
|
|
65
69
|
)}
|
|
66
70
|
</div>
|
|
67
|
-
{
|
|
71
|
+
{hasSpark && (
|
|
68
72
|
<div className="h-9 w-full">
|
|
69
|
-
<ResponsiveContainer width="100%" height="100%">
|
|
73
|
+
<ResponsiveContainer width="100%" height="100%" initialDimension={{ width: 0, height: 36 }}>
|
|
70
74
|
<AreaChart data={sparkData} margin={{ top: 4, right: 2, bottom: 0, left: 2 }}>
|
|
71
75
|
<defs>
|
|
72
76
|
<linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
|
|
@@ -24,7 +24,7 @@ function Textarea({ className, id, label, required, hint, error, tooltip, ...pro
|
|
|
24
24
|
id={fieldId}
|
|
25
25
|
data-slot="textarea"
|
|
26
26
|
className={cn(
|
|
27
|
-
"block w-full min-h-[120px] resize-
|
|
27
|
+
"block w-full min-h-[120px] max-h-[480px] resize-y [field-sizing:content]",
|
|
28
28
|
"px-3.5 py-3",
|
|
29
29
|
"type-text-md text-foreground placeholder:text-placeholder",
|
|
30
30
|
"bg-background border border-border rounded-md",
|
package/src/lib/chart.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as React from "react";
|
|
1
2
|
import type { ChartTheme } from "../hooks/use-chart-theme";
|
|
2
3
|
import { luminance, paletteColor } from "./chart-palette";
|
|
3
4
|
|
|
@@ -54,3 +55,36 @@ export const resolveSeries = (
|
|
|
54
55
|
*/
|
|
55
56
|
export const orderByLuminance = (series: ResolvedSeries[]) =>
|
|
56
57
|
[...series].sort((lower, upper) => luminance(lower.color) - luminance(upper.color));
|
|
58
|
+
|
|
59
|
+
export const makeXAxisTick =
|
|
60
|
+
(theme: ChartTheme) =>
|
|
61
|
+
({
|
|
62
|
+
x,
|
|
63
|
+
y,
|
|
64
|
+
payload,
|
|
65
|
+
index,
|
|
66
|
+
visibleTicksCount,
|
|
67
|
+
}: {
|
|
68
|
+
x?: string | number;
|
|
69
|
+
y?: string | number;
|
|
70
|
+
payload?: { value?: string | number };
|
|
71
|
+
index?: number;
|
|
72
|
+
visibleTicksCount?: number;
|
|
73
|
+
}) => {
|
|
74
|
+
const isFirst = index === 0;
|
|
75
|
+
const isLast = visibleTicksCount != null && index === visibleTicksCount - 1;
|
|
76
|
+
const anchor = isFirst ? "start" : isLast ? "end" : "middle";
|
|
77
|
+
return React.createElement(
|
|
78
|
+
"text",
|
|
79
|
+
{
|
|
80
|
+
x: Number(x ?? 0),
|
|
81
|
+
y: Number(y ?? 0),
|
|
82
|
+
dy: 12,
|
|
83
|
+
textAnchor: anchor,
|
|
84
|
+
fill: theme.axisForeground,
|
|
85
|
+
fontFamily: theme.fontMono,
|
|
86
|
+
fontSize: 10,
|
|
87
|
+
},
|
|
88
|
+
String(payload?.value ?? ""),
|
|
89
|
+
);
|
|
90
|
+
};
|
|
@@ -89,7 +89,7 @@ const latencyPeak = latency.reduce((best, row) => (row.p95 > best.p95 ? row : be
|
|
|
89
89
|
const errorsPeak = errors.reduce((best, row) => (row.mcp + row.tool > best.mcp + best.tool ? row : best));
|
|
90
90
|
|
|
91
91
|
export const AllVariants: Story = () => (
|
|
92
|
-
<div className="
|
|
92
|
+
<div className="mx-auto max-w-[1600px] p-8">
|
|
93
93
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3">
|
|
94
94
|
<ChartCard
|
|
95
95
|
palette="magenta"
|
|
@@ -76,7 +76,7 @@ const sessionsSpark = stacked.map((row) => CLIENTS.reduce((acc, client) => acc +
|
|
|
76
76
|
const errorsPeak = errors.reduce((best, row) => (row.mcp + row.tool > best.mcp + best.tool ? row : best));
|
|
77
77
|
|
|
78
78
|
export const AllVariants: Story = () => (
|
|
79
|
-
<div className="
|
|
79
|
+
<div className="mx-auto max-w-[1600px] p-8">
|
|
80
80
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3">
|
|
81
81
|
<ChartCard
|
|
82
82
|
palette="magenta"
|
|
@@ -52,7 +52,7 @@ const fmtK = (value: number) => {
|
|
|
52
52
|
const toolCallsTotal = TOP_TOOLS.reduce((sum, row) => sum + row.calls, 0);
|
|
53
53
|
|
|
54
54
|
export const AllVariants: Story = () => (
|
|
55
|
-
<div className="
|
|
55
|
+
<div className="mx-auto max-w-[1600px] p-8">
|
|
56
56
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3">
|
|
57
57
|
<ChartCard palette="magenta" kicker="Last 7d" title="Top tools" description="Ranked · magenta ramp">
|
|
58
58
|
<Stat value={fmtK(toolCallsTotal)} unit="calls" delta={{ value: 12.4, direction: "up" }} />
|
|
@@ -72,7 +72,7 @@ const fmtK = (value: number) => {
|
|
|
72
72
|
const clientsTotal = clients.reduce((sum, row) => sum + row.sessions, 0);
|
|
73
73
|
|
|
74
74
|
export const AllVariants: Story = () => (
|
|
75
|
-
<div className="
|
|
75
|
+
<div className="mx-auto max-w-[1600px] p-8">
|
|
76
76
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3">
|
|
77
77
|
<ChartCard palette="magenta" kicker="Last 7d" title="Sessions by client" description="Donut · share readout">
|
|
78
78
|
<Stat value={fmtK(clientsTotal)} unit="sessions" delta={{ value: 6.2, direction: "up" }} />
|
|
@@ -39,7 +39,7 @@ const HOURS = Array.from({ length: 24 }, (_, hour) => String(hour).padStart(2, "
|
|
|
39
39
|
const nf = (value: number) => value.toLocaleString("en-US");
|
|
40
40
|
|
|
41
41
|
export const AllVariants: Story = () => (
|
|
42
|
-
<div className="
|
|
42
|
+
<div className="mx-auto max-w-[1600px] p-8">
|
|
43
43
|
<div className="grid grid-cols-1 gap-6 xl:grid-cols-2">
|
|
44
44
|
<ChartCard palette="magenta" accent="left" kicker="Last 7d" title="Activity" description="Square · hour × day">
|
|
45
45
|
<HeatmapChart
|
|
@@ -63,7 +63,7 @@ const tokensSpark = tokens.map((row) => row.v);
|
|
|
63
63
|
const latencyPeak = latency.reduce((best, row) => (row.p95 > best.p95 ? row : best));
|
|
64
64
|
|
|
65
65
|
export const AllVariants: Story = () => (
|
|
66
|
-
<div className="
|
|
66
|
+
<div className="mx-auto max-w-[1600px] p-8">
|
|
67
67
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3">
|
|
68
68
|
<ChartCard
|
|
69
69
|
palette="cyan"
|
|
@@ -55,6 +55,13 @@ export const AllVariants = () => {
|
|
|
55
55
|
hint="This is a hint text to help user."
|
|
56
56
|
disabled
|
|
57
57
|
/>
|
|
58
|
+
|
|
59
|
+
<span className={SECTION_HEADER}>Long pre-filled (auto-grows, no crop)</span>
|
|
60
|
+
<Textarea
|
|
61
|
+
id="long-prefilled"
|
|
62
|
+
label="Subtitle"
|
|
63
|
+
defaultValue="The fastest way to deploy, host, and scale remote MCP servers for your AI agents — with zero infrastructure to manage, built-in observability, and one-command rollbacks straight from your terminal."
|
|
64
|
+
/>
|
|
58
65
|
</div>
|
|
59
66
|
);
|
|
60
67
|
};
|
|
@@ -11,7 +11,7 @@ const steps = [
|
|
|
11
11
|
{ id: "branding", label: "Branding & metadata" },
|
|
12
12
|
{ id: "auth", label: "Authentication" },
|
|
13
13
|
{ id: "tools", label: "Tools & test cases" },
|
|
14
|
-
{ id: "review", label: "Review &
|
|
14
|
+
{ id: "review", label: "Review & export" },
|
|
15
15
|
];
|
|
16
16
|
|
|
17
17
|
/** The step rail is a vertical `TabsNav` composed with the consumer's selection state. */
|