@alpic-ai/ui 1.143.0 → 1.145.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/area-chart.mjs +5 -9
- package/dist/components/bar-chart.mjs +6 -14
- package/dist/components/bar-list.mjs +2 -9
- package/dist/components/chart-tooltip.mjs +54 -27
- package/dist/components/donut-chart.mjs +1 -3
- package/dist/components/form.mjs +2 -0
- package/dist/components/heatmap-chart.d.mts +6 -0
- package/dist/components/heatmap-chart.mjs +146 -116
- package/dist/components/line-chart.mjs +5 -9
- package/dist/components/sidebar.mjs +1 -1
- package/dist/components/skeleton.mjs +1 -1
- package/dist/components/stat.mjs +4 -4
- package/dist/lib/chart-palette.mjs +20 -17
- package/package.json +2 -2
- package/src/components/area-chart.tsx +12 -10
- package/src/components/bar-chart.tsx +13 -11
- package/src/components/bar-list.tsx +1 -9
- package/src/components/chart-tooltip.tsx +22 -6
- package/src/components/donut-chart.tsx +1 -4
- package/src/components/form.tsx +3 -1
- package/src/components/heatmap-chart.tsx +49 -5
- package/src/components/line-chart.tsx +12 -10
- package/src/components/skeleton.tsx +1 -1
- package/src/components/stat.tsx +4 -4
- package/src/lib/chart-palette.ts +20 -17
- package/dist/hooks/use-reduced-motion.d.mts +0 -4
- package/dist/hooks/use-reduced-motion.mjs +0 -16
- package/src/hooks/use-reduced-motion.ts +0 -17
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { cn } from "../lib/cn.mjs";
|
|
3
|
-
import { useReducedMotion } from "../hooks/use-reduced-motion.mjs";
|
|
4
3
|
import { makeXAxisTick, resolveSeries } from "../lib/chart.mjs";
|
|
5
4
|
import { useChartContext } from "./chart-container.mjs";
|
|
6
5
|
import { ChartLegend } from "./chart-legend.mjs";
|
|
7
6
|
import { ChartTooltipContent } from "./chart-tooltip.mjs";
|
|
7
|
+
import { Skeleton } from "./skeleton.mjs";
|
|
8
8
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
9
|
import * as React$1 from "react";
|
|
10
10
|
import { CartesianGrid, LabelList, Line, LineChart as LineChart$1, ReferenceArea, ReferenceDot, ReferenceLine, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
|
|
@@ -16,7 +16,6 @@ const CURVE_TYPE = {
|
|
|
16
16
|
};
|
|
17
17
|
function LineChart({ data, index, series, curve = "monotone", legend = false, legendAlign = "left", valueFlags = false, dots = false, height = 200, yAxisWidth = 48, palette, referenceLine, markers, lastValueLabel = false, loading = false, valueFormatter = (value) => value.toLocaleString("en-US"), labelFormatter, className }) {
|
|
18
18
|
const { palette: paletteColors, theme } = useChartContext(palette);
|
|
19
|
-
const animated = !useReducedMotion();
|
|
20
19
|
const resolved = resolveSeries(series, paletteColors, theme);
|
|
21
20
|
const numericMax = React$1.useMemo(() => {
|
|
22
21
|
let max = 0;
|
|
@@ -93,14 +92,11 @@ function LineChart({ data, index, series, curve = "monotone", legend = false, le
|
|
|
93
92
|
const isEmpty = data.length === 0 || resolved.length === 0;
|
|
94
93
|
return /* @__PURE__ */ jsxs("div", {
|
|
95
94
|
"data-slot": "line-chart",
|
|
96
|
-
className: cn("flex w-full flex-col gap-3", className),
|
|
95
|
+
className: cn("flex w-full flex-col gap-3", "[&_.recharts-surface]:outline-none [&_.recharts-wrapper]:outline-none", className),
|
|
97
96
|
children: [/* @__PURE__ */ jsx("div", {
|
|
98
97
|
className: "w-full",
|
|
99
98
|
style: { height },
|
|
100
|
-
children: loading ? /* @__PURE__ */
|
|
101
|
-
className: "flex h-full items-center justify-center gap-2.5 font-mono text-quaternary-foreground text-text-xs",
|
|
102
|
-
children: [/* @__PURE__ */ jsx("span", { className: "size-4 animate-spin rounded-full border-2 border-border border-t-foreground" }), "loading…"]
|
|
103
|
-
}) : isEmpty ? /* @__PURE__ */ jsx("div", {
|
|
99
|
+
children: loading ? /* @__PURE__ */ jsx(Skeleton, { className: "h-full w-full rounded-lg" }) : isEmpty ? /* @__PURE__ */ jsx("div", {
|
|
104
100
|
className: "flex h-full items-center justify-center rounded-lg border border-border border-dashed font-mono text-quaternary-foreground text-text-xs",
|
|
105
101
|
children: "no data in range"
|
|
106
102
|
}) : /* @__PURE__ */ jsx(ResponsiveContainer, {
|
|
@@ -124,7 +120,7 @@ function LineChart({ data, index, series, curve = "monotone", legend = false, le
|
|
|
124
120
|
...axis,
|
|
125
121
|
tick: makeXAxisTick(theme),
|
|
126
122
|
interval: "preserveStartEnd",
|
|
127
|
-
minTickGap:
|
|
123
|
+
minTickGap: 56
|
|
128
124
|
}),
|
|
129
125
|
/* @__PURE__ */ jsx(YAxis, {
|
|
130
126
|
...axis,
|
|
@@ -181,7 +177,7 @@ function LineChart({ data, index, series, curve = "monotone", legend = false, le
|
|
|
181
177
|
strokeWidth: 0
|
|
182
178
|
} : false,
|
|
183
179
|
activeDot: activeDotFor(entry),
|
|
184
|
-
isAnimationActive:
|
|
180
|
+
isAnimationActive: false,
|
|
185
181
|
animationDuration: 650,
|
|
186
182
|
animationEasing: "ease-out",
|
|
187
183
|
children: lastValueLabel && /* @__PURE__ */ jsx(LabelList, {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { cn } from "../lib/cn.mjs";
|
|
3
|
+
import { Skeleton } from "./skeleton.mjs";
|
|
3
4
|
import { Button } from "./button.mjs";
|
|
4
5
|
import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip.mjs";
|
|
5
6
|
import { Separator } from "./separator.mjs";
|
|
6
7
|
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "./sheet.mjs";
|
|
7
8
|
import { useIsMobile } from "../hooks/use-mobile.mjs";
|
|
8
|
-
import { Skeleton } from "./skeleton.mjs";
|
|
9
9
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
10
|
import { cva } from "class-variance-authority";
|
|
11
11
|
import * as React$1 from "react";
|
|
@@ -3,7 +3,7 @@ import { cn } from "../lib/cn.mjs";
|
|
|
3
3
|
import { jsx } from "react/jsx-runtime";
|
|
4
4
|
import { cva } from "class-variance-authority";
|
|
5
5
|
//#region src/components/skeleton.tsx
|
|
6
|
-
const skeletonVariants = cva("bg-
|
|
6
|
+
const skeletonVariants = cva("bg-foreground/10 motion-safe:animate-pulse", {
|
|
7
7
|
variants: { shape: {
|
|
8
8
|
rectangle: "rounded-md",
|
|
9
9
|
circle: "rounded-full"
|
package/dist/components/stat.mjs
CHANGED
|
@@ -35,13 +35,13 @@ function Stat({ value, unit, delta, sparkline, semantic, className, ...props })
|
|
|
35
35
|
const sentiment = delta && (delta.invert ? delta.direction === "down" : delta.direction === "up") ? "positive" : "negative";
|
|
36
36
|
return /* @__PURE__ */ jsxs("div", {
|
|
37
37
|
"data-slot": "stat",
|
|
38
|
-
className: cn("flex flex-col gap-2.5", className),
|
|
38
|
+
className: cn("flex min-w-0 flex-col gap-2.5", className),
|
|
39
39
|
...props,
|
|
40
40
|
children: [/* @__PURE__ */ jsxs("div", {
|
|
41
|
-
className: "flex items-baseline gap-
|
|
41
|
+
className: "flex min-w-0 items-baseline gap-x-2.5",
|
|
42
42
|
children: [
|
|
43
43
|
/* @__PURE__ */ jsx("span", {
|
|
44
|
-
className: "type-display-sm font-bold leading-none tracking-tight tabular-nums text-foreground",
|
|
44
|
+
className: "type-display-sm min-w-0 truncate font-bold leading-none tracking-tight tabular-nums text-foreground",
|
|
45
45
|
children: value
|
|
46
46
|
}),
|
|
47
47
|
unit && /* @__PURE__ */ jsx("span", {
|
|
@@ -55,7 +55,7 @@ function Stat({ value, unit, delta, sparkline, semantic, className, ...props })
|
|
|
55
55
|
})
|
|
56
56
|
]
|
|
57
57
|
}), hasSpark && /* @__PURE__ */ jsx("div", {
|
|
58
|
-
className: "h-9 w-full",
|
|
58
|
+
className: "pointer-events-none h-9 w-full",
|
|
59
59
|
children: /* @__PURE__ */ jsx(ResponsiveContainer, {
|
|
60
60
|
width: "100%",
|
|
61
61
|
height: "100%",
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
const CHART_PALETTES = {
|
|
2
2
|
magenta: [
|
|
3
|
-
"#
|
|
4
|
-
"#
|
|
5
|
-
"#
|
|
6
|
-
"#
|
|
7
|
-
"#
|
|
8
|
-
"#
|
|
9
|
-
"#
|
|
10
|
-
"#
|
|
3
|
+
"#da1b6a",
|
|
4
|
+
"#ec82b0",
|
|
5
|
+
"#9a67d7",
|
|
6
|
+
"#668fdf",
|
|
7
|
+
"#47bde0",
|
|
8
|
+
"#75ddd8",
|
|
9
|
+
"#cd8c4d",
|
|
10
|
+
"#3eb0a1"
|
|
11
11
|
],
|
|
12
12
|
cyan: [
|
|
13
|
-
"#
|
|
14
|
-
"#
|
|
15
|
-
"#
|
|
16
|
-
"#
|
|
17
|
-
"#
|
|
18
|
-
"#
|
|
19
|
-
"#
|
|
20
|
-
"#
|
|
13
|
+
"#2eb2c5",
|
|
14
|
+
"#50d0c0",
|
|
15
|
+
"#4f90e0",
|
|
16
|
+
"#8d74e0",
|
|
17
|
+
"#bc73e0",
|
|
18
|
+
"#ec82b0",
|
|
19
|
+
"#da1b6a",
|
|
20
|
+
"#c08dd3"
|
|
21
21
|
]
|
|
22
22
|
};
|
|
23
23
|
const heatRampMagenta = (empty) => [
|
|
@@ -42,7 +42,10 @@ const HEAT_RAMPS = {
|
|
|
42
42
|
magenta: heatRampMagenta,
|
|
43
43
|
cyan: heatRampMint
|
|
44
44
|
};
|
|
45
|
-
const heatRamp = (palette, empty) =>
|
|
45
|
+
const heatRamp = (palette, empty, isDark = true) => {
|
|
46
|
+
const [, ...colored] = HEAT_RAMPS[palette](empty);
|
|
47
|
+
return [empty, ...isDark ? colored : colored.reverse()];
|
|
48
|
+
};
|
|
46
49
|
const hexToRgb = (hex) => {
|
|
47
50
|
const value = Number.parseInt(hex.slice(1), 16);
|
|
48
51
|
return [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alpic-ai/ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.145.0",
|
|
4
4
|
"description": "Alpic design system — shared UI components",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"class-variance-authority": "^0.7.1",
|
|
53
53
|
"clsx": "^2.1.1",
|
|
54
54
|
"cmdk": "^1.1.1",
|
|
55
|
-
"recharts": "^3.
|
|
55
|
+
"recharts": "^3.9.0",
|
|
56
56
|
"tailwind-merge": "^3.6.0"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
@@ -15,13 +15,13 @@ import {
|
|
|
15
15
|
YAxis,
|
|
16
16
|
} from "recharts";
|
|
17
17
|
|
|
18
|
-
import { useReducedMotion } from "../hooks/use-reduced-motion";
|
|
19
18
|
import { type ChartSeries, makeXAxisTick, orderByLuminance, resolveSeries } from "../lib/chart";
|
|
20
19
|
import type { ChartPaletteName } from "../lib/chart-palette";
|
|
21
20
|
import { cn } from "../lib/cn";
|
|
22
21
|
import { useChartContext } from "./chart-container";
|
|
23
22
|
import { ChartLegend } from "./chart-legend";
|
|
24
23
|
import { ChartTooltipContent } from "./chart-tooltip";
|
|
24
|
+
import { Skeleton } from "./skeleton";
|
|
25
25
|
|
|
26
26
|
const CURVE_TYPE = { monotone: "monotone", linear: "linear", step: "stepAfter" } as const;
|
|
27
27
|
|
|
@@ -77,8 +77,6 @@ function AreaChart({
|
|
|
77
77
|
}: AreaChartProps) {
|
|
78
78
|
const { palette: paletteColors, theme } = useChartContext(palette);
|
|
79
79
|
const reactId = React.useId().replace(/:/g, "");
|
|
80
|
-
const reducedMotion = useReducedMotion();
|
|
81
|
-
const animated = !reducedMotion;
|
|
82
80
|
|
|
83
81
|
const resolved = resolveSeries(series, paletteColors, theme);
|
|
84
82
|
const stacked = variant === "stacked" || variant === "expand";
|
|
@@ -195,13 +193,17 @@ function AreaChart({
|
|
|
195
193
|
const isEmpty = data.length === 0 || rendered.length === 0;
|
|
196
194
|
|
|
197
195
|
return (
|
|
198
|
-
<div
|
|
196
|
+
<div
|
|
197
|
+
data-slot="area-chart"
|
|
198
|
+
className={cn(
|
|
199
|
+
"flex w-full flex-col gap-3",
|
|
200
|
+
"[&_.recharts-surface]:outline-none [&_.recharts-wrapper]:outline-none",
|
|
201
|
+
className,
|
|
202
|
+
)}
|
|
203
|
+
>
|
|
199
204
|
<div className="w-full" style={{ height }}>
|
|
200
205
|
{loading ? (
|
|
201
|
-
<
|
|
202
|
-
<span className="size-4 animate-spin rounded-full border-2 border-border border-t-foreground" />
|
|
203
|
-
loading…
|
|
204
|
-
</div>
|
|
206
|
+
<Skeleton className="h-full w-full rounded-lg" />
|
|
205
207
|
) : isEmpty ? (
|
|
206
208
|
<div className="flex h-full items-center justify-center rounded-lg border border-border border-dashed font-mono text-quaternary-foreground text-text-xs">
|
|
207
209
|
no data in range
|
|
@@ -241,7 +243,7 @@ function AreaChart({
|
|
|
241
243
|
{...axis}
|
|
242
244
|
tick={makeXAxisTick(theme)}
|
|
243
245
|
interval="preserveStartEnd"
|
|
244
|
-
minTickGap={
|
|
246
|
+
minTickGap={56}
|
|
245
247
|
/>
|
|
246
248
|
<YAxis
|
|
247
249
|
{...axis}
|
|
@@ -304,7 +306,7 @@ function AreaChart({
|
|
|
304
306
|
fill={fillFor(entry, slot)}
|
|
305
307
|
dot={false}
|
|
306
308
|
activeDot={activeDotFor(entry)}
|
|
307
|
-
isAnimationActive={
|
|
309
|
+
isAnimationActive={false}
|
|
308
310
|
animationDuration={650}
|
|
309
311
|
animationEasing="ease-out"
|
|
310
312
|
>
|
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
YAxis,
|
|
16
16
|
} from "recharts";
|
|
17
17
|
|
|
18
|
-
import { useReducedMotion } from "../hooks/use-reduced-motion";
|
|
19
18
|
import { type ChartSeries, makeXAxisTick, orderByLuminance, resolveSeries } from "../lib/chart";
|
|
20
19
|
import type { ChartPaletteName } from "../lib/chart-palette";
|
|
21
20
|
import { cn } from "../lib/cn";
|
|
@@ -23,6 +22,7 @@ import type { ChartMarker } from "./area-chart";
|
|
|
23
22
|
import { useChartContext } from "./chart-container";
|
|
24
23
|
import { ChartLegend } from "./chart-legend";
|
|
25
24
|
import { ChartTooltipContent } from "./chart-tooltip";
|
|
25
|
+
import { Skeleton } from "./skeleton";
|
|
26
26
|
|
|
27
27
|
const BAR_RADIUS = 4;
|
|
28
28
|
const MAX_BAR_SIZE = 48;
|
|
@@ -68,8 +68,6 @@ function BarChart({
|
|
|
68
68
|
}: BarChartProps) {
|
|
69
69
|
const { palette: paletteColors, theme } = useChartContext(palette);
|
|
70
70
|
const reactId = React.useId().replace(/:/g, "");
|
|
71
|
-
const reducedMotion = useReducedMotion();
|
|
72
|
-
const animated = !reducedMotion;
|
|
73
71
|
|
|
74
72
|
const resolved = resolveSeries(series, paletteColors, theme);
|
|
75
73
|
const stacked = variant === "stacked" || variant === "expand";
|
|
@@ -158,13 +156,17 @@ function BarChart({
|
|
|
158
156
|
const isEmpty = data.length === 0 || rendered.length === 0;
|
|
159
157
|
|
|
160
158
|
return (
|
|
161
|
-
<div
|
|
159
|
+
<div
|
|
160
|
+
data-slot="bar-chart"
|
|
161
|
+
className={cn(
|
|
162
|
+
"flex w-full flex-col gap-3",
|
|
163
|
+
"[&_.recharts-surface]:outline-none [&_.recharts-wrapper]:outline-none",
|
|
164
|
+
className,
|
|
165
|
+
)}
|
|
166
|
+
>
|
|
162
167
|
<div className="w-full" style={{ height }}>
|
|
163
168
|
{loading ? (
|
|
164
|
-
<
|
|
165
|
-
<span className="size-4 animate-spin rounded-full border-2 border-border border-t-foreground" />
|
|
166
|
-
loading…
|
|
167
|
-
</div>
|
|
169
|
+
<Skeleton className="h-full w-full rounded-lg" />
|
|
168
170
|
) : isEmpty ? (
|
|
169
171
|
<div className="flex h-full items-center justify-center rounded-lg border border-border border-dashed font-mono text-quaternary-foreground text-text-xs">
|
|
170
172
|
no data in range
|
|
@@ -205,7 +207,7 @@ function BarChart({
|
|
|
205
207
|
{...axis}
|
|
206
208
|
tick={makeXAxisTick(theme)}
|
|
207
209
|
interval="preserveStartEnd"
|
|
208
|
-
minTickGap={
|
|
210
|
+
minTickGap={56}
|
|
209
211
|
/>
|
|
210
212
|
<YAxis
|
|
211
213
|
{...axis}
|
|
@@ -271,8 +273,8 @@ function BarChart({
|
|
|
271
273
|
strokeWidth={0}
|
|
272
274
|
radius={radiusFor(slot)}
|
|
273
275
|
maxBarSize={MAX_BAR_SIZE}
|
|
274
|
-
activeBar={
|
|
275
|
-
isAnimationActive={
|
|
276
|
+
activeBar={false}
|
|
277
|
+
isAnimationActive={false}
|
|
276
278
|
animationDuration={650}
|
|
277
279
|
animationEasing="ease-out"
|
|
278
280
|
>
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
|
|
5
|
-
import { useReducedMotion } from "../hooks/use-reduced-motion";
|
|
6
5
|
import { formatShare } from "../lib/chart";
|
|
7
6
|
import type { ChartPaletteName } from "../lib/chart-palette";
|
|
8
7
|
import { rampColor } from "../lib/chart-palette";
|
|
@@ -47,14 +46,8 @@ function BarList({
|
|
|
47
46
|
}: BarListProps) {
|
|
48
47
|
const { paletteName, theme } = useChartContext(palette);
|
|
49
48
|
const accent = semantic ? theme[SEMANTIC_KEY[semantic]] : null;
|
|
50
|
-
const reducedMotion = useReducedMotion();
|
|
51
|
-
const [mounted, setMounted] = React.useState(false);
|
|
52
49
|
const [active, setActive] = React.useState<number | null>(null);
|
|
53
50
|
|
|
54
|
-
React.useEffect(() => {
|
|
55
|
-
setMounted(true);
|
|
56
|
-
}, []);
|
|
57
|
-
|
|
58
51
|
const total = React.useMemo(() => data.reduce((sum, row) => sum + (Number(row[dataKey]) || 0), 0), [data, dataKey]);
|
|
59
52
|
|
|
60
53
|
const rows = React.useMemo(() => {
|
|
@@ -113,7 +106,7 @@ function BarList({
|
|
|
113
106
|
<div data-slot="bar-list" className={cn("flex w-full flex-col", className)}>
|
|
114
107
|
{rows.map((row, slot) => {
|
|
115
108
|
const fraction = maxValue > 0 ? row.value / maxValue : 0;
|
|
116
|
-
const fillWidth =
|
|
109
|
+
const fillWidth = `${fraction * 100}%`;
|
|
117
110
|
const dimmed = active !== null && active !== slot;
|
|
118
111
|
const isActive = active === slot;
|
|
119
112
|
return (
|
|
@@ -135,7 +128,6 @@ function BarList({
|
|
|
135
128
|
background: `linear-gradient(90deg, ${row.color}, color-mix(in oklab, ${row.color} 82%, transparent))`,
|
|
136
129
|
boxShadow: `inset 0 0 0 1px ${row.color}`,
|
|
137
130
|
opacity: dimmed ? 0.45 : 1,
|
|
138
|
-
transition: reducedMotion ? undefined : "width 700ms ease-out",
|
|
139
131
|
}}
|
|
140
132
|
>
|
|
141
133
|
<span
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import { cn } from "../lib/cn";
|
|
4
4
|
import { useChartContext } from "./chart-container";
|
|
5
5
|
|
|
6
|
+
const MAX_TOOLTIP_ROWS = 8;
|
|
7
|
+
|
|
6
8
|
export interface ChartTooltipItem {
|
|
7
9
|
name?: string | number;
|
|
8
10
|
value?: number | string;
|
|
@@ -41,10 +43,14 @@ function ChartTooltipContent({
|
|
|
41
43
|
return null;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
);
|
|
46
|
+
const toNumber = (value: ChartTooltipItem["value"]) => (typeof value === "number" ? value : Number(value ?? 0));
|
|
47
|
+
const total = payload.reduce((sum, item) => sum + toNumber(item.value), 0);
|
|
48
|
+
|
|
49
|
+
const sorted = [...payload].sort((lower, upper) => toNumber(upper.value) - toNumber(lower.value));
|
|
50
|
+
|
|
51
|
+
const rows = sorted.slice(0, MAX_TOOLTIP_ROWS);
|
|
52
|
+
const overflow = sorted.slice(MAX_TOOLTIP_ROWS);
|
|
53
|
+
const overflowTotal = overflow.reduce((sum, item) => sum + toNumber(item.value), 0);
|
|
48
54
|
|
|
49
55
|
return (
|
|
50
56
|
<div
|
|
@@ -60,13 +66,13 @@ function ChartTooltipContent({
|
|
|
60
66
|
</p>
|
|
61
67
|
)}
|
|
62
68
|
<div className="flex flex-col gap-1">
|
|
63
|
-
{
|
|
69
|
+
{rows.map((item, index) => {
|
|
64
70
|
const swatch =
|
|
65
71
|
[item.color, item.stroke, item.fill].find(
|
|
66
72
|
(candidate): candidate is string =>
|
|
67
73
|
typeof candidate === "string" && candidate.length > 0 && !candidate.startsWith("url("),
|
|
68
74
|
) ?? theme.mutedForeground;
|
|
69
|
-
const numeric =
|
|
75
|
+
const numeric = toNumber(item.value);
|
|
70
76
|
return (
|
|
71
77
|
<div key={`${item.dataKey ?? index}`} className="flex items-center justify-between gap-4 text-text-xs">
|
|
72
78
|
<span className="inline-flex items-center gap-2 text-muted-foreground">
|
|
@@ -77,6 +83,16 @@ function ChartTooltipContent({
|
|
|
77
83
|
</div>
|
|
78
84
|
);
|
|
79
85
|
})}
|
|
86
|
+
{overflow.length > 0 && (
|
|
87
|
+
<div className="flex items-center justify-between gap-4 text-text-xs">
|
|
88
|
+
<span className="inline-flex items-center gap-2 text-quaternary-foreground">
|
|
89
|
+
<span aria-hidden className="size-2 shrink-0" />+{overflow.length} more
|
|
90
|
+
</span>
|
|
91
|
+
<span className="font-mono font-semibold tabular-nums text-quaternary-foreground">
|
|
92
|
+
{valueFormatter(overflowTotal)}
|
|
93
|
+
</span>
|
|
94
|
+
</div>
|
|
95
|
+
)}
|
|
80
96
|
{showTotal && (
|
|
81
97
|
<div className="mt-1 flex items-center justify-between gap-4 border-border border-t pt-1.5 text-text-xs">
|
|
82
98
|
<span className="font-mono text-quaternary-foreground uppercase tracking-wider text-[10px]">
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { Cell, Pie, PieChart as RechartsPieChart, ResponsiveContainer } from "recharts";
|
|
5
5
|
|
|
6
|
-
import { useReducedMotion } from "../hooks/use-reduced-motion";
|
|
7
6
|
import { formatShare } from "../lib/chart";
|
|
8
7
|
import type { ChartPaletteName } from "../lib/chart-palette";
|
|
9
8
|
import { paletteColor } from "../lib/chart-palette";
|
|
@@ -48,8 +47,6 @@ function DonutChart({
|
|
|
48
47
|
}: DonutChartProps) {
|
|
49
48
|
const { palette: paletteColors, theme } = useChartContext(palette);
|
|
50
49
|
const reactId = React.useId().replace(/:/g, "");
|
|
51
|
-
const reducedMotion = useReducedMotion();
|
|
52
|
-
const animated = !reducedMotion;
|
|
53
50
|
const [active, setActive] = React.useState<number | null>(null);
|
|
54
51
|
const rowRefs = React.useRef<Array<HTMLDivElement | null>>([]);
|
|
55
52
|
|
|
@@ -121,7 +118,7 @@ function DonutChart({
|
|
|
121
118
|
cornerRadius={2}
|
|
122
119
|
stroke={theme.card}
|
|
123
120
|
strokeWidth={1.5}
|
|
124
|
-
isAnimationActive={
|
|
121
|
+
isAnimationActive={false}
|
|
125
122
|
animationDuration={650}
|
|
126
123
|
animationEasing="ease-out"
|
|
127
124
|
onMouseEnter={(_entry, sliceIndex) => setActive(sliceIndex)}
|
package/src/components/form.tsx
CHANGED
|
@@ -78,10 +78,12 @@ const FormItemContext = React.createContext<FormItemContextValue>({} as FormItem
|
|
|
78
78
|
|
|
79
79
|
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
|
|
80
80
|
const id = React.useId();
|
|
81
|
+
const { name } = React.useContext(FormFieldContext);
|
|
81
82
|
|
|
82
83
|
return (
|
|
83
84
|
<FormItemContext.Provider value={{ id }}>
|
|
84
|
-
|
|
85
|
+
{/* Expose the field name as generic metadata so hosts can react to focus without per-field wiring. */}
|
|
86
|
+
<div data-slot="form-item" data-field-name={name} className={cn("grid gap-2", className)} {...props} />
|
|
85
87
|
</FormItemContext.Provider>
|
|
86
88
|
);
|
|
87
89
|
}
|
|
@@ -24,6 +24,9 @@ export interface HeatmapChartProps {
|
|
|
24
24
|
xTickFormatter?: (label: string) => string;
|
|
25
25
|
yTickFormatter?: (label: string) => string;
|
|
26
26
|
tooltipMetrics?: ReadonlyArray<{ key: string; label: string; format?: (value: number) => string }>;
|
|
27
|
+
tooltipTitleKey?: string;
|
|
28
|
+
colorScale?: "linear" | "rank";
|
|
29
|
+
showLegend?: boolean;
|
|
27
30
|
ariaLabel?: string;
|
|
28
31
|
className?: string;
|
|
29
32
|
}
|
|
@@ -83,6 +86,9 @@ function HeatmapChart({
|
|
|
83
86
|
xTickFormatter,
|
|
84
87
|
yTickFormatter,
|
|
85
88
|
tooltipMetrics,
|
|
89
|
+
tooltipTitleKey,
|
|
90
|
+
colorScale = "linear",
|
|
91
|
+
showLegend = false,
|
|
86
92
|
ariaLabel = "Activity heatmap",
|
|
87
93
|
className,
|
|
88
94
|
}: HeatmapChartProps) {
|
|
@@ -123,6 +129,28 @@ function HeatmapChart({
|
|
|
123
129
|
const { columns, rows, cells, maxValue, peakKey } = layout;
|
|
124
130
|
const isEmpty = columns.length === 0 || rows.length === 0 || maxValue <= 0;
|
|
125
131
|
|
|
132
|
+
// "rank" spreads clustered values across the whole ramp by colouring each cell
|
|
133
|
+
// by its position among the distinct non-zero values rather than its raw ratio
|
|
134
|
+
// to the max — the right read for skewed activity counts (à la GitHub).
|
|
135
|
+
const colorFractionOf = React.useMemo(() => {
|
|
136
|
+
if (colorScale === "rank") {
|
|
137
|
+
const sorted = [...new Set(cells.map((cell) => cell.value).filter((value) => value > 0))].sort(
|
|
138
|
+
(lower, upper) => lower - upper,
|
|
139
|
+
);
|
|
140
|
+
// Floor non-zero ranks above the empty colour so the quietest active day
|
|
141
|
+
// still reads as coloured, never mistaken for no data.
|
|
142
|
+
const RANK_FLOOR = 0.18;
|
|
143
|
+
const fractionByValue = new Map(
|
|
144
|
+
sorted.map((value, index) => [
|
|
145
|
+
value,
|
|
146
|
+
sorted.length > 1 ? RANK_FLOOR + (1 - RANK_FLOOR) * (index / (sorted.length - 1)) : 1,
|
|
147
|
+
]),
|
|
148
|
+
);
|
|
149
|
+
return (value: number) => (value > 0 ? (fractionByValue.get(value) ?? 0) : 0);
|
|
150
|
+
}
|
|
151
|
+
return (value: number) => (maxValue > 0 ? value / maxValue : 0);
|
|
152
|
+
}, [cells, colorScale, maxValue]);
|
|
153
|
+
|
|
126
154
|
if (loading) {
|
|
127
155
|
return (
|
|
128
156
|
<div
|
|
@@ -152,7 +180,7 @@ function HeatmapChart({
|
|
|
152
180
|
);
|
|
153
181
|
}
|
|
154
182
|
|
|
155
|
-
const ramp = heatRamp(paletteName, theme.isDark ? HEAT_EMPTY.dark : HEAT_EMPTY.light);
|
|
183
|
+
const ramp = heatRamp(paletteName, theme.isDark ? HEAT_EMPTY.dark : HEAT_EMPTY.light, theme.isDark);
|
|
156
184
|
const totalWidth = PAD.left + columns.length * CELL + (columns.length - 1) * GAP + PAD.right;
|
|
157
185
|
const totalHeight = PAD.top + rows.length * CELL + (rows.length - 1) * GAP + PAD.bottom;
|
|
158
186
|
const xStride = showAllXLabels ? 1 : Math.ceil(columns.length / MAX_X_TICKS);
|
|
@@ -209,7 +237,7 @@ function HeatmapChart({
|
|
|
209
237
|
</text>
|
|
210
238
|
))}
|
|
211
239
|
{cells.map((cell) => {
|
|
212
|
-
const fraction = cell.value
|
|
240
|
+
const fraction = colorFractionOf(cell.value);
|
|
213
241
|
const fill = heatColor(ramp, fraction);
|
|
214
242
|
const key = pairKey(cell.rowLabel, cell.colLabel);
|
|
215
243
|
const isPeak = highlightPeak && key === peakKey;
|
|
@@ -266,6 +294,18 @@ function HeatmapChart({
|
|
|
266
294
|
</svg>
|
|
267
295
|
</div>
|
|
268
296
|
|
|
297
|
+
{showLegend && (
|
|
298
|
+
<div className="mt-3 flex items-center justify-end gap-2">
|
|
299
|
+
<span className="font-mono text-[9px] text-quaternary-foreground uppercase tracking-[0.14em]">Less</span>
|
|
300
|
+
<span
|
|
301
|
+
aria-hidden
|
|
302
|
+
className="h-2 w-24 rounded-full border border-border"
|
|
303
|
+
style={{ background: `linear-gradient(90deg, ${ramp.join(", ")})` }}
|
|
304
|
+
/>
|
|
305
|
+
<span className="font-mono text-[9px] text-quaternary-foreground uppercase tracking-[0.14em]">More</span>
|
|
306
|
+
</div>
|
|
307
|
+
)}
|
|
308
|
+
|
|
269
309
|
{mounted &&
|
|
270
310
|
hovered &&
|
|
271
311
|
createPortal(
|
|
@@ -274,7 +314,9 @@ function HeatmapChart({
|
|
|
274
314
|
style={{ left: tooltipLeft, top: tooltipTop }}
|
|
275
315
|
>
|
|
276
316
|
<p className="mb-1.5 font-mono text-[10px] text-quaternary-foreground uppercase tracking-wider">
|
|
277
|
-
{
|
|
317
|
+
{tooltipTitleKey && typeof hovered.record?.[tooltipTitleKey] === "string"
|
|
318
|
+
? hovered.record[tooltipTitleKey]
|
|
319
|
+
: `${formatY(hovered.rowLabel)} · ${formatX(hovered.colLabel)}`}
|
|
278
320
|
</p>
|
|
279
321
|
{tooltipMetrics && tooltipMetrics.length > 0 ? (
|
|
280
322
|
<div className="flex flex-col gap-1">
|
|
@@ -288,7 +330,9 @@ function HeatmapChart({
|
|
|
288
330
|
aria-hidden
|
|
289
331
|
className="size-2 shrink-0 rounded-[2px]"
|
|
290
332
|
style={{
|
|
291
|
-
background: isActive
|
|
333
|
+
background: isActive
|
|
334
|
+
? heatColor(ramp, colorFractionOf(hovered.value))
|
|
335
|
+
: theme.mutedForeground,
|
|
292
336
|
opacity: isActive ? 1 : 0.35,
|
|
293
337
|
}}
|
|
294
338
|
/>
|
|
@@ -312,7 +356,7 @@ function HeatmapChart({
|
|
|
312
356
|
<span
|
|
313
357
|
aria-hidden
|
|
314
358
|
className="size-2 shrink-0 rounded-[2px]"
|
|
315
|
-
style={{ background: heatColor(ramp, hovered.value
|
|
359
|
+
style={{ background: heatColor(ramp, colorFractionOf(hovered.value)) }}
|
|
316
360
|
/>
|
|
317
361
|
activity
|
|
318
362
|
</span>
|
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
YAxis,
|
|
16
16
|
} from "recharts";
|
|
17
17
|
|
|
18
|
-
import { useReducedMotion } from "../hooks/use-reduced-motion";
|
|
19
18
|
import { type ChartSeries, makeXAxisTick, resolveSeries } from "../lib/chart";
|
|
20
19
|
import type { ChartPaletteName } from "../lib/chart-palette";
|
|
21
20
|
import { cn } from "../lib/cn";
|
|
@@ -23,6 +22,7 @@ import type { ChartMarker } from "./area-chart";
|
|
|
23
22
|
import { useChartContext } from "./chart-container";
|
|
24
23
|
import { ChartLegend } from "./chart-legend";
|
|
25
24
|
import { ChartTooltipContent } from "./chart-tooltip";
|
|
25
|
+
import { Skeleton } from "./skeleton";
|
|
26
26
|
|
|
27
27
|
const CURVE_TYPE = { monotone: "monotone", linear: "linear", step: "stepAfter" } as const;
|
|
28
28
|
|
|
@@ -68,8 +68,6 @@ function LineChart({
|
|
|
68
68
|
className,
|
|
69
69
|
}: LineChartProps) {
|
|
70
70
|
const { palette: paletteColors, theme } = useChartContext(palette);
|
|
71
|
-
const reducedMotion = useReducedMotion();
|
|
72
|
-
const animated = !reducedMotion;
|
|
73
71
|
|
|
74
72
|
const resolved = resolveSeries(series, paletteColors, theme);
|
|
75
73
|
|
|
@@ -160,13 +158,17 @@ function LineChart({
|
|
|
160
158
|
const isEmpty = data.length === 0 || resolved.length === 0;
|
|
161
159
|
|
|
162
160
|
return (
|
|
163
|
-
<div
|
|
161
|
+
<div
|
|
162
|
+
data-slot="line-chart"
|
|
163
|
+
className={cn(
|
|
164
|
+
"flex w-full flex-col gap-3",
|
|
165
|
+
"[&_.recharts-surface]:outline-none [&_.recharts-wrapper]:outline-none",
|
|
166
|
+
className,
|
|
167
|
+
)}
|
|
168
|
+
>
|
|
164
169
|
<div className="w-full" style={{ height }}>
|
|
165
170
|
{loading ? (
|
|
166
|
-
<
|
|
167
|
-
<span className="size-4 animate-spin rounded-full border-2 border-border border-t-foreground" />
|
|
168
|
-
loading…
|
|
169
|
-
</div>
|
|
171
|
+
<Skeleton className="h-full w-full rounded-lg" />
|
|
170
172
|
) : isEmpty ? (
|
|
171
173
|
<div className="flex h-full items-center justify-center rounded-lg border border-border border-dashed font-mono text-quaternary-foreground text-text-xs">
|
|
172
174
|
no data in range
|
|
@@ -180,7 +182,7 @@ function LineChart({
|
|
|
180
182
|
{...axis}
|
|
181
183
|
tick={makeXAxisTick(theme)}
|
|
182
184
|
interval="preserveStartEnd"
|
|
183
|
-
minTickGap={
|
|
185
|
+
minTickGap={56}
|
|
184
186
|
/>
|
|
185
187
|
<YAxis
|
|
186
188
|
{...axis}
|
|
@@ -234,7 +236,7 @@ function LineChart({
|
|
|
234
236
|
strokeDasharray={entry.dashed ? "5 3" : undefined}
|
|
235
237
|
dot={dots ? { r: 2.5, fill: entry.color, strokeWidth: 0 } : false}
|
|
236
238
|
activeDot={activeDotFor(entry)}
|
|
237
|
-
isAnimationActive={
|
|
239
|
+
isAnimationActive={false}
|
|
238
240
|
animationDuration={650}
|
|
239
241
|
animationEasing="ease-out"
|
|
240
242
|
>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { cn } from "@alpic-ai/ui/lib/cn";
|
|
4
4
|
import { cva, type VariantProps } from "class-variance-authority";
|
|
5
5
|
|
|
6
|
-
const skeletonVariants = cva("bg-
|
|
6
|
+
const skeletonVariants = cva("bg-foreground/10 motion-safe:animate-pulse", {
|
|
7
7
|
variants: {
|
|
8
8
|
shape: {
|
|
9
9
|
rectangle: "rounded-md",
|
package/src/components/stat.tsx
CHANGED
|
@@ -55,9 +55,9 @@ function Stat({ value, unit, delta, sparkline, semantic, className, ...props }:
|
|
|
55
55
|
delta && (delta.invert ? delta.direction === "down" : delta.direction === "up") ? "positive" : "negative";
|
|
56
56
|
|
|
57
57
|
return (
|
|
58
|
-
<div data-slot="stat" className={cn("flex flex-col gap-2.5", className)} {...props}>
|
|
59
|
-
<div className="flex items-baseline gap-
|
|
60
|
-
<span className="type-display-sm font-bold leading-none tracking-tight tabular-nums text-foreground">
|
|
58
|
+
<div data-slot="stat" className={cn("flex min-w-0 flex-col gap-2.5", className)} {...props}>
|
|
59
|
+
<div className="flex min-w-0 items-baseline gap-x-2.5">
|
|
60
|
+
<span className="type-display-sm min-w-0 truncate font-bold leading-none tracking-tight tabular-nums text-foreground">
|
|
61
61
|
{value}
|
|
62
62
|
</span>
|
|
63
63
|
{unit && <span className="font-mono text-[11px] leading-none text-quaternary-foreground mb-0.5">{unit}</span>}
|
|
@@ -69,7 +69,7 @@ function Stat({ value, unit, delta, sparkline, semantic, className, ...props }:
|
|
|
69
69
|
)}
|
|
70
70
|
</div>
|
|
71
71
|
{hasSpark && (
|
|
72
|
-
<div className="h-9 w-full">
|
|
72
|
+
<div className="pointer-events-none h-9 w-full">
|
|
73
73
|
<ResponsiveContainer width="100%" height="100%" initialDimension={{ width: 0, height: 36 }}>
|
|
74
74
|
<AreaChart data={sparkData} margin={{ top: 4, right: 2, bottom: 0, left: 2 }}>
|
|
75
75
|
<defs>
|