@alpic-ai/ui 0.0.0-dev.g20c64d3 → 0.0.0-dev.g20fd91c

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 (116) hide show
  1. package/dist/components/accordion-card.d.mts +5 -6
  2. package/dist/components/accordion.d.mts +5 -6
  3. package/dist/components/alert.d.mts +9 -11
  4. package/dist/components/area-chart.d.mts +64 -0
  5. package/dist/components/area-chart.mjs +275 -0
  6. package/dist/components/attachment-tile.d.mts +1 -3
  7. package/dist/components/avatar.d.mts +8 -10
  8. package/dist/components/badge.d.mts +2 -4
  9. package/dist/components/bar-chart.d.mts +50 -0
  10. package/dist/components/bar-chart.mjs +262 -0
  11. package/dist/components/bar-list.d.mts +31 -0
  12. package/dist/components/bar-list.mjs +111 -0
  13. package/dist/components/breadcrumb.d.mts +10 -11
  14. package/dist/components/button.d.mts +6 -8
  15. package/dist/components/card.d.mts +9 -10
  16. package/dist/components/chart-card.d.mts +25 -0
  17. package/dist/components/chart-card.mjs +48 -0
  18. package/dist/components/chart-container.d.mts +20 -0
  19. package/dist/components/chart-container.mjs +37 -0
  20. package/dist/components/chart-legend.d.mts +21 -0
  21. package/dist/components/chart-legend.mjs +35 -0
  22. package/dist/components/chart-tooltip.d.mts +33 -0
  23. package/dist/components/chart-tooltip.mjs +52 -0
  24. package/dist/components/checkbox.d.mts +2 -3
  25. package/dist/components/collapsible.d.mts +4 -5
  26. package/dist/components/combobox.d.mts +12 -11
  27. package/dist/components/combobox.mjs +7 -4
  28. package/dist/components/command.d.mts +9 -10
  29. package/dist/components/copyable.d.mts +2 -3
  30. package/dist/components/description-list.d.mts +5 -6
  31. package/dist/components/dialog.d.mts +15 -17
  32. package/dist/components/donut-chart.d.mts +46 -0
  33. package/dist/components/donut-chart.mjs +189 -0
  34. package/dist/components/dropdown-menu.d.mts +18 -20
  35. package/dist/components/form.d.mts +38 -21
  36. package/dist/components/form.mjs +7 -7
  37. package/dist/components/github-button.d.mts +1 -2
  38. package/dist/components/heatmap-chart.d.mts +48 -0
  39. package/dist/components/heatmap-chart.mjs +229 -0
  40. package/dist/components/input-group.d.mts +5 -7
  41. package/dist/components/input.d.mts +4 -5
  42. package/dist/components/input.mjs +2 -2
  43. package/dist/components/label.d.mts +2 -3
  44. package/dist/components/line-chart.d.mts +57 -0
  45. package/dist/components/line-chart.mjs +218 -0
  46. package/dist/components/page-loader.d.mts +1 -3
  47. package/dist/components/pagination.d.mts +3 -4
  48. package/dist/components/popover.d.mts +5 -6
  49. package/dist/components/radio-group.d.mts +3 -4
  50. package/dist/components/scroll-area.d.mts +3 -4
  51. package/dist/components/select-trigger-variants.d.mts +1 -3
  52. package/dist/components/select.d.mts +9 -10
  53. package/dist/components/separator.d.mts +2 -3
  54. package/dist/components/sheet.d.mts +11 -12
  55. package/dist/components/shimmer-text.d.mts +2 -2
  56. package/dist/components/sidebar.d.mts +34 -36
  57. package/dist/components/sidebar.mjs +10 -10
  58. package/dist/components/skeleton.d.mts +2 -4
  59. package/dist/components/sonner.d.mts +5 -6
  60. package/dist/components/spinner.d.mts +3 -5
  61. package/dist/components/stat.d.mts +32 -0
  62. package/dist/components/stat.mjs +117 -0
  63. package/dist/components/status-dot.d.mts +2 -4
  64. package/dist/components/switch.d.mts +2 -3
  65. package/dist/components/table.d.mts +10 -11
  66. package/dist/components/tabs.d.mts +12 -14
  67. package/dist/components/tag.d.mts +3 -5
  68. package/dist/components/task-progress.d.mts +1 -3
  69. package/dist/components/textarea.d.mts +3 -4
  70. package/dist/components/textarea.mjs +3 -3
  71. package/dist/components/toggle-group.d.mts +4 -6
  72. package/dist/components/toggle-group.mjs +3 -3
  73. package/dist/components/tooltip-icon-button.d.mts +1 -2
  74. package/dist/components/tooltip.d.mts +5 -6
  75. package/dist/components/typography.d.mts +4 -5
  76. package/dist/components/wizard.d.mts +4 -23
  77. package/dist/components/wizard.mjs +1 -19
  78. package/dist/hooks/use-chart-theme.d.mts +18 -0
  79. package/dist/hooks/use-chart-theme.mjs +57 -0
  80. package/dist/hooks/use-mobile.mjs +3 -3
  81. package/dist/hooks/use-reduced-motion.d.mts +4 -0
  82. package/dist/hooks/use-reduced-motion.mjs +16 -0
  83. package/dist/lib/chart-palette.d.mts +4 -0
  84. package/dist/lib/chart-palette.mjs +95 -0
  85. package/dist/lib/chart.d.mts +14 -0
  86. package/dist/lib/chart.mjs +42 -0
  87. package/package.json +30 -29
  88. package/src/components/area-chart.tsx +347 -0
  89. package/src/components/bar-chart.tsx +317 -0
  90. package/src/components/bar-list.tsx +166 -0
  91. package/src/components/chart-card.tsx +65 -0
  92. package/src/components/chart-container.tsx +51 -0
  93. package/src/components/chart-legend.tsx +49 -0
  94. package/src/components/chart-tooltip.tsx +93 -0
  95. package/src/components/combobox.tsx +9 -2
  96. package/src/components/donut-chart.tsx +217 -0
  97. package/src/components/form.tsx +1 -1
  98. package/src/components/heatmap-chart.tsx +331 -0
  99. package/src/components/line-chart.tsx +277 -0
  100. package/src/components/stat.tsx +113 -0
  101. package/src/components/textarea.tsx +1 -1
  102. package/src/components/wizard.tsx +1 -35
  103. package/src/hooks/use-chart-theme.ts +75 -0
  104. package/src/hooks/use-reduced-motion.ts +17 -0
  105. package/src/lib/chart-palette.ts +110 -0
  106. package/src/lib/chart.ts +90 -0
  107. package/src/stories/area-chart.stories.tsx +198 -0
  108. package/src/stories/bar-chart.stories.tsx +167 -0
  109. package/src/stories/bar-list.stories.tsx +83 -0
  110. package/src/stories/donut-chart.stories.tsx +110 -0
  111. package/src/stories/heatmap-chart.stories.tsx +105 -0
  112. package/src/stories/line-chart.stories.tsx +144 -0
  113. package/src/stories/stat.stories.tsx +64 -0
  114. package/src/stories/textarea.stories.tsx +7 -0
  115. package/src/stories/wizard.stories.tsx +23 -5
  116. package/src/styles/tokens.css +18 -0
@@ -0,0 +1,317 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import {
5
+ Bar,
6
+ CartesianGrid,
7
+ LabelList,
8
+ BarChart as RechartsBarChart,
9
+ ReferenceArea,
10
+ ReferenceDot,
11
+ ReferenceLine,
12
+ ResponsiveContainer,
13
+ Tooltip,
14
+ XAxis,
15
+ YAxis,
16
+ } from "recharts";
17
+
18
+ import { useReducedMotion } from "../hooks/use-reduced-motion";
19
+ import { type ChartSeries, makeXAxisTick, orderByLuminance, resolveSeries } from "../lib/chart";
20
+ import type { ChartPaletteName } from "../lib/chart-palette";
21
+ import { cn } from "../lib/cn";
22
+ import type { ChartMarker } from "./area-chart";
23
+ import { useChartContext } from "./chart-container";
24
+ import { ChartLegend } from "./chart-legend";
25
+ import { ChartTooltipContent } from "./chart-tooltip";
26
+
27
+ const BAR_RADIUS = 4;
28
+ const MAX_BAR_SIZE = 48;
29
+
30
+ export interface BarChartProps {
31
+ data: ReadonlyArray<Record<string, string | number | null | undefined>>;
32
+ index: string;
33
+ series: ChartSeries[];
34
+ variant?: "stacked" | "grouped" | "expand";
35
+ legend?: boolean;
36
+ legendAlign?: "left" | "center" | "right";
37
+ valueLabels?: boolean;
38
+ height?: number;
39
+ yAxisWidth?: number;
40
+ palette?: ChartPaletteName;
41
+ referenceLine?: { y: number; label?: string; band?: boolean };
42
+ markers?: ChartMarker[];
43
+ texture?: boolean;
44
+ loading?: boolean;
45
+ valueFormatter?: (value: number) => string;
46
+ labelFormatter?: (label: string | number) => string;
47
+ className?: string;
48
+ }
49
+
50
+ function BarChart({
51
+ data,
52
+ index,
53
+ series,
54
+ variant = "stacked",
55
+ legend = false,
56
+ legendAlign = "left",
57
+ valueLabels = false,
58
+ height = 200,
59
+ yAxisWidth = 48,
60
+ palette,
61
+ referenceLine,
62
+ markers,
63
+ texture = false,
64
+ loading = false,
65
+ valueFormatter = (value) => value.toLocaleString("en-US"),
66
+ labelFormatter,
67
+ className,
68
+ }: BarChartProps) {
69
+ const { palette: paletteColors, theme } = useChartContext(palette);
70
+ const reactId = React.useId().replace(/:/g, "");
71
+ const reducedMotion = useReducedMotion();
72
+ const animated = !reducedMotion;
73
+
74
+ const resolved = resolveSeries(series, paletteColors, theme);
75
+ const stacked = variant === "stacked" || variant === "expand";
76
+ const rendered = stacked ? orderByLuminance(resolved) : resolved;
77
+ const lead = resolved[0];
78
+ const withTotal = stacked && rendered.length > 1;
79
+
80
+ // Stacked bars reach the stack height (sum per x-point), not the tallest single
81
+ // series, so the explicit YAxis domain must not clip a tall stack short.
82
+ const numericMax = React.useMemo(() => {
83
+ let max = 0;
84
+ for (const row of data) {
85
+ let rowTotal = 0;
86
+ for (const entry of series) {
87
+ const value = Number(row[entry.key]);
88
+ if (Number.isFinite(value)) {
89
+ rowTotal += value;
90
+ if (!stacked && value > max) {
91
+ max = value;
92
+ }
93
+ }
94
+ }
95
+ if (stacked && rowTotal > max) {
96
+ max = rowTotal;
97
+ }
98
+ }
99
+ return max;
100
+ }, [data, series, stacked]);
101
+
102
+ const margin = { top: markers?.length || valueLabels ? 18 : 8, right: 8, bottom: 2, left: 0 };
103
+
104
+ const axis = {
105
+ stroke: theme.border,
106
+ tick: { fill: theme.axisForeground, fontSize: 10, fontFamily: theme.fontMono },
107
+ tickLine: false as const,
108
+ axisLine: { stroke: theme.border, strokeOpacity: 0.6 },
109
+ };
110
+
111
+ const legendItems = resolved.map((entry) => ({ name: entry.name, color: entry.color, dashed: entry.dashed }));
112
+
113
+ const radiusFor = (slot: number): [number, number, number, number] => {
114
+ if (!stacked || slot === rendered.length - 1) {
115
+ return [BAR_RADIUS, BAR_RADIUS, 0, 0];
116
+ }
117
+ return [0, 0, 0, 0];
118
+ };
119
+
120
+ const fillFor = (entry: (typeof rendered)[number], slot: number) => {
121
+ if (texture && lead && entry.key === lead.key) {
122
+ return `url(#bar-hatch-${reactId})`;
123
+ }
124
+ // Stacked segments read cleanest as flat solids — a per-segment gradient
125
+ // banding looks like a drop shadow between bands. Grouped bars keep the fade.
126
+ if (stacked) {
127
+ return entry.color;
128
+ }
129
+ return `url(#bar-${reactId}-${slot})`;
130
+ };
131
+
132
+ const renderValueLabel =
133
+ (color: string) =>
134
+ (props: {
135
+ x?: string | number;
136
+ y?: string | number;
137
+ width?: string | number;
138
+ value?: string | number | boolean | Array<string | number | boolean> | null;
139
+ }) => {
140
+ if (props.x == null || props.y == null) {
141
+ return null;
142
+ }
143
+ return (
144
+ <text
145
+ x={Number(props.x) + Number(props.width ?? 0) / 2}
146
+ y={Number(props.y) - 5}
147
+ textAnchor="middle"
148
+ fill={color}
149
+ fontFamily={theme.fontMono}
150
+ fontSize={10}
151
+ style={{ fontVariantNumeric: "tabular-nums" }}
152
+ >
153
+ {valueFormatter(Number(props.value ?? 0))}
154
+ </text>
155
+ );
156
+ };
157
+
158
+ const isEmpty = data.length === 0 || rendered.length === 0;
159
+
160
+ return (
161
+ <div data-slot="bar-chart" className={cn("flex w-full flex-col gap-3", className)}>
162
+ <div className="w-full" style={{ height }}>
163
+ {loading ? (
164
+ <div className="flex h-full items-center justify-center gap-2.5 font-mono text-quaternary-foreground text-text-xs">
165
+ <span className="size-4 animate-spin rounded-full border-2 border-border border-t-foreground" />
166
+ loading…
167
+ </div>
168
+ ) : isEmpty ? (
169
+ <div className="flex h-full items-center justify-center rounded-lg border border-border border-dashed font-mono text-quaternary-foreground text-text-xs">
170
+ no data in range
171
+ </div>
172
+ ) : (
173
+ <ResponsiveContainer width="100%" height="100%" initialDimension={{ width: 0, height }}>
174
+ <RechartsBarChart
175
+ data={data as Record<string, string | number>[]}
176
+ stackOffset={variant === "expand" ? "expand" : "none"}
177
+ margin={margin}
178
+ barCategoryGap={stacked ? "20%" : "16%"}
179
+ >
180
+ <defs>
181
+ {!stacked &&
182
+ rendered.map((entry, slot) => (
183
+ <linearGradient key={entry.key} id={`bar-${reactId}-${slot}`} x1="0" y1="0" x2="0" y2="1">
184
+ <stop offset="0%" stopColor={entry.color} stopOpacity={0.95} />
185
+ <stop offset="100%" stopColor={entry.color} stopOpacity={0.5} />
186
+ </linearGradient>
187
+ ))}
188
+ {texture && lead && (
189
+ <pattern
190
+ id={`bar-hatch-${reactId}`}
191
+ patternUnits="userSpaceOnUse"
192
+ width={6}
193
+ height={6}
194
+ patternTransform="rotate(45)"
195
+ >
196
+ <rect width={6} height={6} fill={lead.color} fillOpacity={0.22} />
197
+ <line x1={0} y1={0} x2={0} y2={6} stroke={lead.color} strokeWidth={1.2} strokeOpacity={0.65} />
198
+ </pattern>
199
+ )}
200
+ </defs>
201
+
202
+ <CartesianGrid vertical={false} stroke={theme.grid} strokeDasharray="2 4" />
203
+ <XAxis
204
+ dataKey={index}
205
+ {...axis}
206
+ tick={makeXAxisTick(theme)}
207
+ interval="preserveStartEnd"
208
+ minTickGap={44}
209
+ />
210
+ <YAxis
211
+ {...axis}
212
+ width={yAxisWidth}
213
+ domain={
214
+ referenceLine && variant !== "expand"
215
+ ? [0, Math.ceil(Math.max(numericMax, referenceLine.y) * 1.15)]
216
+ : undefined
217
+ }
218
+ tickFormatter={(value: number) =>
219
+ variant === "expand" ? `${Math.round(value * 100)}%` : valueFormatter(value)
220
+ }
221
+ />
222
+ <Tooltip
223
+ offset={12}
224
+ allowEscapeViewBox={{ x: false, y: false }}
225
+ cursor={{ fill: lead?.color ?? theme.mutedForeground, fillOpacity: theme.isDark ? 0.1 : 0.06 }}
226
+ content={
227
+ <ChartTooltipContent
228
+ valueFormatter={valueFormatter}
229
+ labelFormatter={labelFormatter}
230
+ showTotal={withTotal}
231
+ />
232
+ }
233
+ />
234
+ {referenceLine?.band && (
235
+ <ReferenceArea
236
+ y1={referenceLine.y}
237
+ y2={numericMax}
238
+ fill={theme.warning}
239
+ fillOpacity={0.06}
240
+ ifOverflow="extendDomain"
241
+ />
242
+ )}
243
+ {referenceLine && (
244
+ <ReferenceLine
245
+ y={referenceLine.y}
246
+ stroke={theme.warning}
247
+ strokeDasharray="4 4"
248
+ strokeOpacity={0.6}
249
+ label={
250
+ referenceLine.label
251
+ ? {
252
+ value: referenceLine.label,
253
+ fill: theme.warning,
254
+ fontSize: 9,
255
+ fontFamily: theme.fontMono,
256
+ position: "insideBottomRight",
257
+ }
258
+ : undefined
259
+ }
260
+ />
261
+ )}
262
+
263
+ {rendered.map((entry, slot) => (
264
+ <Bar
265
+ key={entry.key}
266
+ dataKey={entry.key}
267
+ name={entry.name}
268
+ stackId={stacked ? "stack" : undefined}
269
+ fill={fillFor(entry, slot)}
270
+ stroke={entry.color}
271
+ strokeWidth={0}
272
+ radius={radiusFor(slot)}
273
+ maxBarSize={MAX_BAR_SIZE}
274
+ activeBar={{ fillOpacity: 1, stroke: theme.card, strokeWidth: 1 }}
275
+ isAnimationActive={animated}
276
+ animationDuration={650}
277
+ animationEasing="ease-out"
278
+ >
279
+ {valueLabels && (!stacked || rendered.length === 1) && (
280
+ <LabelList dataKey={entry.key} content={renderValueLabel(entry.color)} />
281
+ )}
282
+ </Bar>
283
+ ))}
284
+
285
+ {markers?.map((marker) => (
286
+ <ReferenceDot
287
+ key={`${marker.x}-${marker.y}`}
288
+ x={marker.x}
289
+ y={marker.y}
290
+ r={3.5}
291
+ fill={marker.color ?? theme.foreground}
292
+ stroke={theme.card}
293
+ strokeWidth={2}
294
+ label={
295
+ marker.label
296
+ ? {
297
+ value: marker.label,
298
+ fill: marker.color ?? theme.foreground,
299
+ fontSize: 9,
300
+ fontFamily: theme.fontMono,
301
+ position: "top",
302
+ }
303
+ : undefined
304
+ }
305
+ />
306
+ ))}
307
+ </RechartsBarChart>
308
+ </ResponsiveContainer>
309
+ )}
310
+ </div>
311
+
312
+ {legend && !isEmpty && <ChartLegend items={legendItems} align={legendAlign} insetLeft={yAxisWidth} />}
313
+ </div>
314
+ );
315
+ }
316
+
317
+ export { BarChart };
@@ -0,0 +1,166 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+
5
+ import { useReducedMotion } from "../hooks/use-reduced-motion";
6
+ import { formatShare } from "../lib/chart";
7
+ import type { ChartPaletteName } from "../lib/chart-palette";
8
+ import { rampColor } from "../lib/chart-palette";
9
+ import { cn } from "../lib/cn";
10
+ import { useChartContext } from "./chart-container";
11
+
12
+ export interface BarListProps {
13
+ data: ReadonlyArray<Record<string, string | number | null | undefined>>;
14
+ index: string;
15
+ dataKey?: string;
16
+ maxItems?: number;
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";
20
+ loading?: boolean;
21
+ valueFormatter?: (value: number) => string;
22
+ labelFormatter?: (label: string | number) => string;
23
+ className?: string;
24
+ }
25
+
26
+ const SEMANTIC_KEY = { error: "destructive", warning: "warning", success: "success" } as const;
27
+
28
+ const PLACEHOLDER_HEIGHT = 168;
29
+ // Cap the ramp off its palest end so low-rank bars stay legible on a light card.
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;
34
+ const IN_FILL_SHADOW = "0 1px 2px rgb(0 0 0 / 0.28)";
35
+
36
+ function BarList({
37
+ data,
38
+ index,
39
+ dataKey = "value",
40
+ maxItems,
41
+ palette,
42
+ semantic,
43
+ loading = false,
44
+ valueFormatter = (value) => value.toLocaleString("en-US"),
45
+ labelFormatter,
46
+ className,
47
+ }: BarListProps) {
48
+ const { paletteName, theme } = useChartContext(palette);
49
+ const accent = semantic ? theme[SEMANTIC_KEY[semantic]] : null;
50
+ const reducedMotion = useReducedMotion();
51
+ const [mounted, setMounted] = React.useState(false);
52
+ const [active, setActive] = React.useState<number | null>(null);
53
+
54
+ React.useEffect(() => {
55
+ setMounted(true);
56
+ }, []);
57
+
58
+ const total = React.useMemo(() => data.reduce((sum, row) => sum + (Number(row[dataKey]) || 0), 0), [data, dataKey]);
59
+
60
+ const rows = React.useMemo(() => {
61
+ const mapped = data.map((row) => ({
62
+ name: String(row[index] ?? ""),
63
+ value: Number(row[dataKey]) || 0,
64
+ }));
65
+ mapped.sort((lower, upper) => upper.value - lower.value);
66
+ const capped = maxItems ? mapped.slice(0, maxItems) : mapped;
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]);
78
+
79
+ const maxValue = rows.reduce((max, row) => (row.value > max ? row.value : max), 0);
80
+ const isEmpty = rows.length === 0;
81
+ const formatName = (name: string) => (labelFormatter ? labelFormatter(name) : name);
82
+
83
+ if (loading) {
84
+ return (
85
+ <div
86
+ className={cn(
87
+ "flex items-center justify-center gap-2.5 font-mono text-quaternary-foreground text-text-xs",
88
+ className,
89
+ )}
90
+ style={{ minHeight: PLACEHOLDER_HEIGHT }}
91
+ >
92
+ <span className="size-4 animate-spin rounded-full border-2 border-border border-t-foreground" />
93
+ loading…
94
+ </div>
95
+ );
96
+ }
97
+
98
+ if (isEmpty) {
99
+ return (
100
+ <div
101
+ className={cn(
102
+ "flex items-center justify-center rounded-lg border border-border border-dashed font-mono text-quaternary-foreground text-text-xs",
103
+ className,
104
+ )}
105
+ style={{ minHeight: PLACEHOLDER_HEIGHT }}
106
+ >
107
+ no data in range
108
+ </div>
109
+ );
110
+ }
111
+
112
+ return (
113
+ <div data-slot="bar-list" className={cn("flex w-full flex-col", className)}>
114
+ {rows.map((row, slot) => {
115
+ const fraction = maxValue > 0 ? row.value / maxValue : 0;
116
+ const fillWidth = !reducedMotion && !mounted ? "0%" : `${fraction * 100}%`;
117
+ const dimmed = active !== null && active !== slot;
118
+ const isActive = active === slot;
119
+ return (
120
+ // biome-ignore lint/a11y/noStaticElementInteractions: decorative hover-sync; all values are visible without it
121
+ <div
122
+ key={row.name}
123
+ onMouseEnter={() => setActive(slot)}
124
+ onMouseLeave={() => setActive(null)}
125
+ className="flex items-center gap-3 py-1"
126
+ >
127
+ <div className="relative h-[30px] flex-1 overflow-hidden rounded-md bg-muted">
128
+ <span className="pointer-events-none absolute inset-x-3 top-1/2 z-0 -translate-y-1/2 truncate type-text-xs font-medium text-foreground">
129
+ {formatName(row.name)}
130
+ </span>
131
+ <div
132
+ className="absolute inset-y-0 left-0 z-10 overflow-hidden rounded-md"
133
+ style={{
134
+ width: fillWidth,
135
+ background: `linear-gradient(90deg, ${row.color}, color-mix(in oklab, ${row.color} 82%, transparent))`,
136
+ boxShadow: `inset 0 0 0 1px ${row.color}`,
137
+ opacity: dimmed ? 0.45 : 1,
138
+ transition: reducedMotion ? undefined : "width 700ms ease-out",
139
+ }}
140
+ >
141
+ <span
142
+ className="pointer-events-none absolute top-1/2 -translate-y-1/2 truncate type-text-xs font-medium text-white"
143
+ style={{
144
+ left: 12,
145
+ width: `calc(${100 / Math.max(fraction, 0.0001)}% - 24px)`,
146
+ textShadow: IN_FILL_SHADOW,
147
+ }}
148
+ >
149
+ {formatName(row.name)}
150
+ </span>
151
+ </div>
152
+ </div>
153
+ <span
154
+ className="min-w-[4rem] shrink-0 whitespace-nowrap text-right font-mono text-text-xs tabular-nums motion-safe:transition-colors"
155
+ style={{ color: isActive ? row.color : undefined }}
156
+ >
157
+ {isActive ? formatShare(total > 0 ? row.value / total : 0) : valueFormatter(row.value)}
158
+ </span>
159
+ </div>
160
+ );
161
+ })}
162
+ </div>
163
+ );
164
+ }
165
+
166
+ export { BarList };
@@ -0,0 +1,65 @@
1
+ "use client";
2
+
3
+ import type * as React from "react";
4
+
5
+ import { CHART_PALETTES, type ChartPaletteName } from "../lib/chart-palette";
6
+ import { cn } from "../lib/cn";
7
+ import { ChartContainer } from "./chart-container";
8
+
9
+ export interface ChartCardProps extends Omit<React.ComponentProps<"section">, "title"> {
10
+ palette?: ChartPaletteName;
11
+ kicker?: React.ReactNode;
12
+ title?: React.ReactNode;
13
+ description?: React.ReactNode;
14
+ action?: React.ReactNode;
15
+ accent?: "top" | "left" | "none";
16
+ }
17
+
18
+ function ChartCard({
19
+ palette = "magenta",
20
+ kicker,
21
+ title,
22
+ description,
23
+ action,
24
+ accent = "top",
25
+ className,
26
+ children,
27
+ ...props
28
+ }: ChartCardProps) {
29
+ // biome-ignore lint/style/noNonNullAssertion: palettes are never empty
30
+ const lead = CHART_PALETTES[palette][0]!;
31
+ const isLeft = accent === "left";
32
+
33
+ return (
34
+ <section
35
+ data-slot="chart-card"
36
+ className={cn(
37
+ "chart-rise relative overflow-hidden rounded-xl border bg-card p-5 text-card-foreground shadow-shadow",
38
+ isLeft && "pl-6",
39
+ className,
40
+ )}
41
+ {...props}
42
+ >
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
+ )}
50
+ {(kicker || title || action) && (
51
+ <header className="mb-4 flex items-start justify-between gap-4">
52
+ <div className="flex flex-col gap-1">
53
+ {kicker && <span className="font-mono text-[10px] text-primary uppercase tracking-[0.18em]">{kicker}</span>}
54
+ {title && <h3 className="type-text-md font-semibold leading-none tracking-tight">{title}</h3>}
55
+ {description && <p className="type-text-xs text-muted-foreground">{description}</p>}
56
+ </div>
57
+ {action}
58
+ </header>
59
+ )}
60
+ <ChartContainer palette={palette}>{children}</ChartContainer>
61
+ </section>
62
+ );
63
+ }
64
+
65
+ export { ChartCard };
@@ -0,0 +1,51 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+
5
+ import { type ChartTheme, useChartTheme } from "../hooks/use-chart-theme";
6
+ import { CHART_PALETTES, type ChartPaletteName } from "../lib/chart-palette";
7
+ import { cn } from "../lib/cn";
8
+
9
+ export type { ChartPaletteName } from "../lib/chart-palette";
10
+
11
+ interface ChartContextValue {
12
+ palette: readonly string[];
13
+ paletteName: ChartPaletteName;
14
+ theme: ChartTheme;
15
+ }
16
+
17
+ const ChartContext = React.createContext<ChartContextValue | null>(null);
18
+
19
+ function ChartContainer({
20
+ palette = "magenta",
21
+ className,
22
+ ...props
23
+ }: React.ComponentProps<"div"> & { palette?: ChartPaletteName }) {
24
+ const theme = useChartTheme();
25
+ const value = React.useMemo<ChartContextValue>(
26
+ () => ({ palette: CHART_PALETTES[palette], paletteName: palette, theme }),
27
+ [palette, theme],
28
+ );
29
+
30
+ return (
31
+ <ChartContext.Provider value={value}>
32
+ <div data-slot="chart-container" className={cn("flex flex-col gap-4", className)} {...props} />
33
+ </ChartContext.Provider>
34
+ );
35
+ }
36
+
37
+ function useChartContext(paletteOverride?: ChartPaletteName): ChartContextValue {
38
+ const provided = React.useContext(ChartContext);
39
+ const fallbackTheme = useChartTheme();
40
+ if (provided && !paletteOverride) {
41
+ return provided;
42
+ }
43
+ const paletteName = paletteOverride ?? provided?.paletteName ?? "magenta";
44
+ return {
45
+ palette: CHART_PALETTES[paletteName],
46
+ paletteName,
47
+ theme: provided?.theme ?? fallbackTheme,
48
+ };
49
+ }
50
+
51
+ export { ChartContainer, type ChartContextValue, useChartContext };
@@ -0,0 +1,49 @@
1
+ "use client";
2
+
3
+ import { cn } from "../lib/cn";
4
+
5
+ export interface ChartLegendItem {
6
+ name: string;
7
+ color: string;
8
+ dashed?: boolean;
9
+ }
10
+
11
+ export interface ChartLegendProps extends React.HTMLAttributes<HTMLDivElement> {
12
+ items: ChartLegendItem[];
13
+ align?: "left" | "center" | "right";
14
+ insetLeft?: number;
15
+ }
16
+
17
+ const ALIGN_CLASS = { left: "justify-start", center: "justify-center", right: "justify-end" } as const;
18
+
19
+ function Swatch({ color, dashed }: { color: string; dashed?: boolean }) {
20
+ return (
21
+ <span
22
+ aria-hidden
23
+ className="size-2 shrink-0 rounded-full"
24
+ style={dashed ? { border: `1.5px solid ${color}` } : { background: color }}
25
+ />
26
+ );
27
+ }
28
+
29
+ function ChartLegend({ items, align = "left", insetLeft, className, style, ...props }: ChartLegendProps) {
30
+ return (
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
+ >
36
+ {items.map((item) => (
37
+ <span
38
+ key={item.name}
39
+ className="inline-flex items-center gap-1.5 font-mono text-[10px] text-muted-foreground uppercase tracking-[0.12em]"
40
+ >
41
+ <Swatch color={item.color} dashed={item.dashed} />
42
+ {item.name}
43
+ </span>
44
+ ))}
45
+ </div>
46
+ );
47
+ }
48
+
49
+ export { ChartLegend };