@alpic-ai/ui 0.0.0-dev.g05467b7 → 0.0.0-dev.g05c89ce
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 +25 -12
- 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 +9 -5
- package/dist/components/form.mjs +1 -1
- 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/wizard.d.mts +1 -19
- package/dist/components/wizard.mjs +1 -19
- 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 +26 -10
- 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 +6 -6
- package/src/components/form.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/wizard.tsx +1 -35
- package/src/lib/chart.ts +34 -0
- package/src/stories/area-chart.stories.tsx +1 -3
- package/src/stories/bar-chart.stories.tsx +1 -3
- package/src/stories/bar-list.stories.tsx +1 -3
- package/src/stories/donut-chart.stories.tsx +1 -3
- package/src/stories/heatmap-chart.stories.tsx +1 -3
- package/src/stories/line-chart.stories.tsx +1 -3
- package/src/stories/wizard.stories.tsx +23 -5
- package/src/styles/tokens.css +0 -45
- package/dist/components/grid-fx.d.mts +0 -13
- package/dist/components/grid-fx.mjs +0 -188
- package/src/components/grid-fx.tsx +0 -238
|
@@ -1,25 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { cn } from "../lib/cn.mjs";
|
|
3
|
-
import { TabsNav, TabsNavList, TabsNavTrigger } from "./tabs.mjs";
|
|
4
3
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
4
|
//#region src/components/wizard.tsx
|
|
6
|
-
function WizardSteps({ steps, activeIdx, onSelect, ariaLabel = "Wizard steps", className }) {
|
|
7
|
-
return /* @__PURE__ */ jsx(TabsNav, {
|
|
8
|
-
orientation: "vertical",
|
|
9
|
-
"aria-label": ariaLabel,
|
|
10
|
-
className,
|
|
11
|
-
children: /* @__PURE__ */ jsx(TabsNavList, { children: steps.map((step, idx) => /* @__PURE__ */ jsx(TabsNavTrigger, {
|
|
12
|
-
active: idx === activeIdx,
|
|
13
|
-
asChild: true,
|
|
14
|
-
children: /* @__PURE__ */ jsx("button", {
|
|
15
|
-
type: "button",
|
|
16
|
-
onClick: () => onSelect(idx),
|
|
17
|
-
className: "w-full justify-start text-left",
|
|
18
|
-
children: step.label
|
|
19
|
-
})
|
|
20
|
-
}, step.id)) })
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
5
|
function WizardProgress({ current, total, className, ...props }) {
|
|
24
6
|
const percent = total > 0 ? Math.round(current / total * 100) : 0;
|
|
25
7
|
return /* @__PURE__ */ jsxs("div", {
|
|
@@ -43,4 +25,4 @@ function WizardProgress({ current, total, className, ...props }) {
|
|
|
43
25
|
});
|
|
44
26
|
}
|
|
45
27
|
//#endregion
|
|
46
|
-
export { WizardProgress
|
|
28
|
+
export { WizardProgress };
|
package/dist/lib/chart.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { luminance, paletteColor } from "./chart-palette.mjs";
|
|
2
|
+
import * as React$1 from "react";
|
|
2
3
|
//#region src/lib/chart.ts
|
|
3
4
|
const formatShare = (fraction) => `${(fraction * 100).toFixed(fraction >= .1 ? 0 : 1)}%`;
|
|
4
5
|
const semanticColor = (theme, semantic) => {
|
|
@@ -23,5 +24,19 @@ const resolveSeries = (series, palette, theme) => series.map((entry, index) => (
|
|
|
23
24
|
* stack order in Recharts — first entry sits at the bottom.
|
|
24
25
|
*/
|
|
25
26
|
const orderByLuminance = (series) => [...series].sort((lower, upper) => luminance(lower.color) - luminance(upper.color));
|
|
27
|
+
const makeXAxisTick = (theme) => ({ x, y, payload, index, visibleTicksCount }) => {
|
|
28
|
+
const isFirst = index === 0;
|
|
29
|
+
const isLast = visibleTicksCount != null && index === visibleTicksCount - 1;
|
|
30
|
+
const anchor = isFirst ? "start" : isLast ? "end" : "middle";
|
|
31
|
+
return React$1.createElement("text", {
|
|
32
|
+
x: Number(x ?? 0),
|
|
33
|
+
y: Number(y ?? 0),
|
|
34
|
+
dy: 12,
|
|
35
|
+
textAnchor: anchor,
|
|
36
|
+
fill: theme.axisForeground,
|
|
37
|
+
fontFamily: theme.fontMono,
|
|
38
|
+
fontSize: 10
|
|
39
|
+
}, String(payload?.value ?? ""));
|
|
40
|
+
};
|
|
26
41
|
//#endregion
|
|
27
|
-
export { formatShare, orderByLuminance, resolveSeries };
|
|
42
|
+
export { formatShare, makeXAxisTick, orderByLuminance, resolveSeries };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alpic-ai/ui",
|
|
3
|
-
"version": "0.0.0-dev.
|
|
3
|
+
"version": "0.0.0-dev.g05c89ce",
|
|
4
4
|
"description": "Alpic design system — shared UI components",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -23,32 +23,32 @@
|
|
|
23
23
|
"src"
|
|
24
24
|
],
|
|
25
25
|
"peerDependencies": {
|
|
26
|
-
"lucide-react": "^1.
|
|
26
|
+
"lucide-react": "^1.21.0",
|
|
27
27
|
"react": "^19.2.7",
|
|
28
28
|
"react-dom": "^19.2.7",
|
|
29
|
-
"react-hook-form": "^7.
|
|
29
|
+
"react-hook-form": "^7.80.0",
|
|
30
30
|
"sonner": "^2.0.7",
|
|
31
31
|
"tailwindcss": "^4.3.1",
|
|
32
32
|
"tw-animate-css": "^1.4.0"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@radix-ui/react-accordion": "^1.2.
|
|
36
|
-
"@radix-ui/react-avatar": "^1.
|
|
37
|
-
"@radix-ui/react-checkbox": "^1.3.
|
|
38
|
-
"@radix-ui/react-collapsible": "^1.1.
|
|
39
|
-
"@radix-ui/react-dialog": "^1.1.
|
|
40
|
-
"@radix-ui/react-dropdown-menu": "^2.1.
|
|
41
|
-
"@radix-ui/react-label": "^2.1.
|
|
42
|
-
"@radix-ui/react-popover": "^1.1.
|
|
43
|
-
"@radix-ui/react-radio-group": "^1.4.
|
|
44
|
-
"@radix-ui/react-scroll-area": "^1.2.
|
|
45
|
-
"@radix-ui/react-select": "^2.3.
|
|
46
|
-
"@radix-ui/react-separator": "^1.1.
|
|
47
|
-
"@radix-ui/react-slot": "^1.
|
|
48
|
-
"@radix-ui/react-switch": "^1.3.
|
|
49
|
-
"@radix-ui/react-tabs": "^1.1.
|
|
50
|
-
"@radix-ui/react-toggle-group": "^1.1.
|
|
51
|
-
"@radix-ui/react-tooltip": "^1.2.
|
|
35
|
+
"@radix-ui/react-accordion": "^1.2.14",
|
|
36
|
+
"@radix-ui/react-avatar": "^1.2.0",
|
|
37
|
+
"@radix-ui/react-checkbox": "^1.3.5",
|
|
38
|
+
"@radix-ui/react-collapsible": "^1.1.14",
|
|
39
|
+
"@radix-ui/react-dialog": "^1.1.17",
|
|
40
|
+
"@radix-ui/react-dropdown-menu": "^2.1.18",
|
|
41
|
+
"@radix-ui/react-label": "^2.1.10",
|
|
42
|
+
"@radix-ui/react-popover": "^1.1.17",
|
|
43
|
+
"@radix-ui/react-radio-group": "^1.4.1",
|
|
44
|
+
"@radix-ui/react-scroll-area": "^1.2.12",
|
|
45
|
+
"@radix-ui/react-select": "^2.3.1",
|
|
46
|
+
"@radix-ui/react-separator": "^1.1.10",
|
|
47
|
+
"@radix-ui/react-slot": "^1.3.0",
|
|
48
|
+
"@radix-ui/react-switch": "^1.3.1",
|
|
49
|
+
"@radix-ui/react-tabs": "^1.1.15",
|
|
50
|
+
"@radix-ui/react-toggle-group": "^1.1.13",
|
|
51
|
+
"@radix-ui/react-tooltip": "^1.2.10",
|
|
52
52
|
"class-variance-authority": "^0.7.1",
|
|
53
53
|
"clsx": "^2.1.1",
|
|
54
54
|
"cmdk": "^1.1.1",
|
|
@@ -60,12 +60,12 @@
|
|
|
60
60
|
"@tailwindcss/postcss": "^4.3.1",
|
|
61
61
|
"@types/react": "19.2.17",
|
|
62
62
|
"@types/react-dom": "19.2.3",
|
|
63
|
-
"lucide-react": "^1.
|
|
64
|
-
"react-hook-form": "^7.
|
|
63
|
+
"lucide-react": "^1.21.0",
|
|
64
|
+
"react-hook-form": "^7.80.0",
|
|
65
65
|
"shx": "^0.4.0",
|
|
66
66
|
"sonner": "^2.0.7",
|
|
67
67
|
"tailwindcss": "^4.3.1",
|
|
68
|
-
"tsdown": "^0.22.
|
|
68
|
+
"tsdown": "^0.22.3",
|
|
69
69
|
"tw-animate-css": "^1.4.0",
|
|
70
70
|
"typescript": "^6.0.3"
|
|
71
71
|
},
|
|
@@ -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;
|
|
@@ -95,7 +110,7 @@ function BarList({
|
|
|
95
110
|
}
|
|
96
111
|
|
|
97
112
|
return (
|
|
98
|
-
<div data-slot="bar-list" className={cn("flex w-full flex-col
|
|
113
|
+
<div data-slot="bar-list" className={cn("flex w-full flex-col", className)}>
|
|
99
114
|
{rows.map((row, slot) => {
|
|
100
115
|
const fraction = maxValue > 0 ? row.value / maxValue : 0;
|
|
101
116
|
const fillWidth = !reducedMotion && !mounted ? "0%" : `${fraction * 100}%`;
|
|
@@ -107,19 +122,20 @@ function BarList({
|
|
|
107
122
|
key={row.name}
|
|
108
123
|
onMouseEnter={() => setActive(slot)}
|
|
109
124
|
onMouseLeave={() => setActive(null)}
|
|
110
|
-
className="flex items-center gap-3"
|
|
125
|
+
className="flex items-center gap-3 py-1"
|
|
111
126
|
>
|
|
112
127
|
<div className="relative h-[30px] flex-1 overflow-hidden rounded-md bg-muted">
|
|
113
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">
|
|
114
129
|
{formatName(row.name)}
|
|
115
130
|
</span>
|
|
116
131
|
<div
|
|
117
|
-
className="absolute inset-y-0 left-0 z-10 overflow-hidden rounded-md
|
|
132
|
+
className="absolute inset-y-0 left-0 z-10 overflow-hidden rounded-md"
|
|
118
133
|
style={{
|
|
119
134
|
width: fillWidth,
|
|
120
135
|
background: `linear-gradient(90deg, ${row.color}, color-mix(in oklab, ${row.color} 82%, transparent))`,
|
|
121
136
|
boxShadow: `inset 0 0 0 1px ${row.color}`,
|
|
122
137
|
opacity: dimmed ? 0.45 : 1,
|
|
138
|
+
transition: reducedMotion ? undefined : "width 700ms ease-out",
|
|
123
139
|
}}
|
|
124
140
|
>
|
|
125
141
|
<span
|
|
@@ -135,7 +151,7 @@ function BarList({
|
|
|
135
151
|
</div>
|
|
136
152
|
</div>
|
|
137
153
|
<span
|
|
138
|
-
className="w-
|
|
154
|
+
className="min-w-[4rem] shrink-0 whitespace-nowrap text-right font-mono text-text-xs tabular-nums motion-safe:transition-colors"
|
|
139
155
|
style={{ color: isActive ? row.color : undefined }}
|
|
140
156
|
>
|
|
141
157
|
{isActive ? formatShare(total > 0 ? row.value / total : 0) : valueFormatter(row.value)}
|
|
@@ -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) => (
|
|
@@ -142,7 +142,7 @@ function DonutChart({
|
|
|
142
142
|
</ResponsiveContainer>
|
|
143
143
|
|
|
144
144
|
<div className="pointer-events-none absolute inset-0 flex flex-col items-center justify-center gap-1 text-center">
|
|
145
|
-
<span className="max-w-[
|
|
145
|
+
<span className="max-w-[52%] truncate font-mono text-[10px] text-quaternary-foreground uppercase tracking-[0.18em]">
|
|
146
146
|
{centerTitle}
|
|
147
147
|
</span>
|
|
148
148
|
<span className="font-mono font-semibold text-[28px] text-foreground leading-none tabular-nums">
|
|
@@ -176,7 +176,7 @@ function DonutChart({
|
|
|
176
176
|
active === slot ? "bg-muted/50" : "bg-transparent",
|
|
177
177
|
)}
|
|
178
178
|
>
|
|
179
|
-
<div className="flex items-center justify-between gap-
|
|
179
|
+
<div className="flex items-center justify-between gap-2">
|
|
180
180
|
<span className="inline-flex min-w-0 items-center gap-2 text-muted-foreground">
|
|
181
181
|
<span
|
|
182
182
|
aria-hidden
|
|
@@ -185,11 +185,11 @@ function DonutChart({
|
|
|
185
185
|
/>
|
|
186
186
|
<span className="truncate">{formatName(slice.name)}</span>
|
|
187
187
|
</span>
|
|
188
|
-
<span className="flex shrink-0 items-center gap-
|
|
189
|
-
<span className="min-w-[
|
|
188
|
+
<span className="flex shrink-0 items-center gap-2 font-mono tabular-nums">
|
|
189
|
+
<span className="min-w-[2.25rem] text-right font-semibold text-foreground">
|
|
190
190
|
{valueFormatter(slice.value)}
|
|
191
191
|
</span>
|
|
192
|
-
<span className="w-
|
|
192
|
+
<span className="w-9 text-right text-quaternary-foreground">
|
|
193
193
|
{formatShare(slice.value / total)}
|
|
194
194
|
</span>
|
|
195
195
|
</span>
|
package/src/components/form.tsx
CHANGED
|
@@ -106,7 +106,7 @@ function FormLabel({ className, required, tooltip, children, ...props }: FormLab
|
|
|
106
106
|
{children}
|
|
107
107
|
</Label>
|
|
108
108
|
{required && (
|
|
109
|
-
<span aria-hidden className="type-text-sm font-medium text-required">
|
|
109
|
+
<span aria-hidden className="type-text-sm font-medium text-required leading-none">
|
|
110
110
|
*
|
|
111
111
|
</span>
|
|
112
112
|
)}
|
|
@@ -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
|
)}
|