@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.
Files changed (42) hide show
  1. package/dist/components/area-chart.d.mts +2 -0
  2. package/dist/components/area-chart.mjs +9 -3
  3. package/dist/components/bar-chart.d.mts +2 -0
  4. package/dist/components/bar-chart.mjs +9 -3
  5. package/dist/components/bar-list.d.mts +3 -0
  6. package/dist/components/bar-list.mjs +19 -7
  7. package/dist/components/chart-card.d.mts +1 -1
  8. package/dist/components/chart-card.mjs +1 -1
  9. package/dist/components/chart-container.d.mts +1 -1
  10. package/dist/components/chart-legend.d.mts +5 -0
  11. package/dist/components/chart-legend.mjs +11 -2
  12. package/dist/components/donut-chart.mjs +4 -0
  13. package/dist/components/heatmap-chart.d.mts +8 -0
  14. package/dist/components/heatmap-chart.mjs +39 -8
  15. package/dist/components/line-chart.d.mts +2 -0
  16. package/dist/components/line-chart.mjs +10 -3
  17. package/dist/components/stat.d.mts +3 -1
  18. package/dist/components/stat.mjs +14 -4
  19. package/dist/components/textarea.mjs +1 -1
  20. package/dist/lib/chart.mjs +16 -1
  21. package/package.json +23 -23
  22. package/src/components/area-chart.tsx +12 -4
  23. package/src/components/bar-chart.tsx +12 -4
  24. package/src/components/bar-list.tsx +21 -6
  25. package/src/components/chart-card.tsx +8 -6
  26. package/src/components/chart-container.tsx +2 -0
  27. package/src/components/chart-legend.tsx +10 -2
  28. package/src/components/donut-chart.tsx +1 -1
  29. package/src/components/heatmap-chart.tsx +62 -18
  30. package/src/components/line-chart.tsx +18 -5
  31. package/src/components/stat.tsx +10 -6
  32. package/src/components/textarea.tsx +1 -1
  33. package/src/lib/chart.ts +34 -0
  34. package/src/stories/area-chart.stories.tsx +1 -1
  35. package/src/stories/bar-chart.stories.tsx +1 -1
  36. package/src/stories/bar-list.stories.tsx +1 -1
  37. package/src/stories/donut-chart.stories.tsx +1 -1
  38. package/src/stories/heatmap-chart.stories.tsx +1 -1
  39. package/src/stories/line-chart.stories.tsx +1 -1
  40. package/src/stories/textarea.stories.tsx +7 -0
  41. package/src/stories/wizard.stories.tsx +1 -1
  42. 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 dataKey={index} {...axis} interval="preserveStartEnd" minTickGap={44} />
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} style={{ paddingLeft: yAxisWidth }} />}
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 dataKey={index} {...axis} interval="preserveStartEnd" minTickGap={44} />
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} style={{ paddingLeft: yAxisWidth }} />}
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
- ...row,
60
- color: rampColor(paletteName, (capped.length > 1 ? rank / (capped.length - 1) : 0) * RAMP_CEILING),
61
- }));
62
- }, [data, index, dataKey, maxItems, paletteName]);
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
- <span
44
- aria-hidden
45
- className={cn("absolute", isLeft ? "inset-y-0 left-0 w-[3px]" : "inset-x-0 top-0 h-[3px]")}
46
- style={{ background: lead }}
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 className={cn("flex flex-wrap gap-x-4 gap-y-1.5", className)} {...props}>
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
- valueAt.set(pairKey(String(row[yKey] ?? ""), String(row[xKey] ?? "")), Number(row[dataKey]) || 0);
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 value = valueAt.get(pairKey(rowLabel, colLabel)) ?? 0;
111
+ const key = pairKey(rowLabel, colLabel);
112
+ const value = valueAt.get(key) ?? 0;
102
113
  if (value > maxValue) {
103
114
  maxValue = value;
104
- peakKey = pairKey(rowLabel, colLabel);
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
- <div className="flex items-center justify-between gap-4 text-text-xs">
268
- <span className="inline-flex items-center gap-2 text-muted-foreground">
269
- <span
270
- aria-hidden
271
- className="size-2 shrink-0 rounded-[2px]"
272
- style={{ background: heatColor(ramp, hovered.value / maxValue) }}
273
- />
274
- activity
275
- </span>
276
- <span className="font-mono font-semibold tabular-nums text-foreground">
277
- {valueFormatter(hovered.value)}
278
- </span>
279
- </div>
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 dataKey={index} {...axis} interval="preserveStartEnd" minTickGap={44} />
177
- <YAxis {...axis} width={yAxisWidth} tickFormatter={(value: number) => valueFormatter(value)} />
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} style={{ paddingLeft: yAxisWidth }} />}
272
+ {legend && !isEmpty && <ChartLegend items={legendItems} align={legendAlign} insetLeft={yAxisWidth} />}
260
273
  </div>
261
274
  );
262
275
  }
@@ -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
- function Stat({ value, unit, delta, sparkline, className, ...props }: StatProps) {
44
- const { palette } = useChartContext();
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
- {sparkData.length > 0 && (
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-none",
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="chart-canvas mx-auto max-w-[1600px] p-8">
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="chart-canvas mx-auto max-w-[1600px] p-8">
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="chart-canvas mx-auto max-w-[1600px] p-8">
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="chart-canvas mx-auto max-w-[1600px] p-8">
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="chart-canvas mx-auto max-w-[1600px] p-8">
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="chart-canvas mx-auto max-w-[1600px] p-8">
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 & submit" },
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. */