@hexclave/dashboard-ui-components 1.0.3 → 1.0.6
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/alert.js +4 -4
- package/dist/components/alert.js.map +1 -1
- package/dist/components/badge.js +1 -1
- package/dist/components/badge.js.map +1 -1
- package/dist/components/button.d.ts +4 -4
- package/dist/components/button.js +3 -3
- package/dist/components/button.js.map +1 -1
- package/dist/components/data-grid/data-grid-toolbar.js +5 -5
- package/dist/components/data-grid/data-grid-toolbar.js.map +1 -1
- package/dist/components/data-grid/data-grid.d.ts.map +1 -1
- package/dist/components/data-grid/data-grid.js +11 -5
- package/dist/components/data-grid/data-grid.js.map +1 -1
- package/dist/components/data-grid/data-grid.test.js +34 -1
- package/dist/components/data-grid/data-grid.test.js.map +1 -1
- package/dist/components/dialog.d.ts +6 -2
- package/dist/components/dialog.d.ts.map +1 -1
- package/dist/components/dialog.js +14 -10
- package/dist/components/dialog.js.map +1 -1
- package/dist/components/input.js +12 -9
- package/dist/components/input.js.map +1 -1
- package/dist/components/pill-toggle.js +3 -3
- package/dist/components/pill-toggle.js.map +1 -1
- package/dist/components/tabs.js +2 -2
- package/dist/components/tabs.js.map +1 -1
- package/dist/dashboard-ui-components.global.js +15639 -17400
- package/dist/dashboard-ui-components.global.js.map +4 -4
- package/dist/esm/components/alert.js +4 -4
- package/dist/esm/components/alert.js.map +1 -1
- package/dist/esm/components/badge.js +1 -1
- package/dist/esm/components/badge.js.map +1 -1
- package/dist/esm/components/button.d.ts +4 -4
- package/dist/esm/components/button.js +3 -3
- package/dist/esm/components/button.js.map +1 -1
- package/dist/esm/components/data-grid/data-grid-toolbar.js +5 -5
- package/dist/esm/components/data-grid/data-grid-toolbar.js.map +1 -1
- package/dist/esm/components/data-grid/data-grid.d.ts.map +1 -1
- package/dist/esm/components/data-grid/data-grid.js +11 -5
- package/dist/esm/components/data-grid/data-grid.js.map +1 -1
- package/dist/esm/components/data-grid/data-grid.test.js +34 -1
- package/dist/esm/components/data-grid/data-grid.test.js.map +1 -1
- package/dist/esm/components/dialog.d.ts +6 -2
- package/dist/esm/components/dialog.d.ts.map +1 -1
- package/dist/esm/components/dialog.js +13 -11
- package/dist/esm/components/dialog.js.map +1 -1
- package/dist/esm/components/input.js +12 -9
- package/dist/esm/components/input.js.map +1 -1
- package/dist/esm/components/pill-toggle.js +4 -4
- package/dist/esm/components/pill-toggle.js.map +1 -1
- package/dist/esm/components/tabs.js +2 -2
- package/dist/esm/components/tabs.js.map +1 -1
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/index.js +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +12 -0
- package/package.json +4 -3
- package/src/components/alert.tsx +120 -0
- package/src/components/analytics-chart/analytics-chart-pie.tsx +369 -0
- package/src/components/analytics-chart/analytics-chart.tsx +1585 -0
- package/src/components/analytics-chart/default-analytics-chart-tooltip.tsx +265 -0
- package/src/components/analytics-chart/format.ts +101 -0
- package/src/components/analytics-chart/index.ts +75 -0
- package/src/components/analytics-chart/palette.ts +68 -0
- package/src/components/analytics-chart/render-data-series.tsx +169 -0
- package/src/components/analytics-chart/state.ts +165 -0
- package/src/components/analytics-chart/strings.ts +72 -0
- package/src/components/analytics-chart/types.ts +220 -0
- package/src/components/badge.tsx +108 -0
- package/src/components/button.tsx +104 -0
- package/src/components/card.tsx +263 -0
- package/src/components/chart-card.tsx +101 -0
- package/src/components/chart-container.tsx +155 -0
- package/src/components/chart-legend.tsx +65 -0
- package/src/components/chart-theme.tsx +52 -0
- package/src/components/chart-tooltip.tsx +165 -0
- package/src/components/cursor-blast-effect.tsx +334 -0
- package/src/components/data-grid/data-grid-sizing.ts +51 -0
- package/src/components/data-grid/data-grid-toolbar.tsx +373 -0
- package/src/components/data-grid/data-grid.test.tsx +407 -0
- package/src/components/data-grid/data-grid.tsx +1231 -0
- package/src/components/data-grid/index.ts +64 -0
- package/src/components/data-grid/state.ts +235 -0
- package/src/components/data-grid/strings.ts +45 -0
- package/src/components/data-grid/types.ts +401 -0
- package/src/components/data-grid/use-data-source.ts +413 -0
- package/src/components/data-grid/use-url-state.test.tsx +88 -0
- package/src/components/data-grid/use-url-state.ts +298 -0
- package/src/components/dialog.tsx +218 -0
- package/src/components/edit-mode.tsx +17 -0
- package/src/components/empty-state.tsx +64 -0
- package/src/components/input.tsx +97 -0
- package/src/components/metric-card.tsx +142 -0
- package/src/components/pill-toggle.tsx +159 -0
- package/src/components/progress-bar.tsx +82 -0
- package/src/components/separator.tsx +36 -0
- package/src/components/skeleton.tsx +30 -0
- package/src/components/table.tsx +113 -0
- package/src/components/tabs.tsx +217 -0
- package/src/index.ts +71 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import { MagnifyingGlassMinusIcon } from "@phosphor-icons/react";
|
|
2
|
+
import { cn } from "@hexclave/ui";
|
|
3
|
+
import { type DesignChartConfig, DesignChartContainer } from "../chart-container";
|
|
4
|
+
import { type CSSProperties, type Ref, useEffect, useMemo, useState } from "react";
|
|
5
|
+
import { Cell, Pie, PieChart } from "recharts";
|
|
6
|
+
import { TrendPill } from "./default-analytics-chart-tooltip";
|
|
7
|
+
import { formatDelta } from "./format";
|
|
8
|
+
import type { AnalyticsChartStrings } from "./strings";
|
|
9
|
+
import {
|
|
10
|
+
cssIdent,
|
|
11
|
+
type AnalyticsChartPieProps,
|
|
12
|
+
type AnalyticsChartSeries,
|
|
13
|
+
type FormatKind,
|
|
14
|
+
type Point,
|
|
15
|
+
} from "./types";
|
|
16
|
+
|
|
17
|
+
type SegmentColors = {
|
|
18
|
+
primary: { light: string[], dark: string[] },
|
|
19
|
+
compare: { light: string[], dark: string[] },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type AnalyticsChartPieBodyProps = {
|
|
23
|
+
wrapperRef: Ref<HTMLDivElement>,
|
|
24
|
+
primarySegmentSeries: readonly AnalyticsChartSeries[],
|
|
25
|
+
compareSegmentSeries: readonly AnalyticsChartSeries[],
|
|
26
|
+
aggregatedPrimarySegments: number[],
|
|
27
|
+
aggregatedCompareSegments: number[],
|
|
28
|
+
aggregatedPrimaryTotal: number,
|
|
29
|
+
aggregatedCompareTotal: number,
|
|
30
|
+
segmentColors: SegmentColors,
|
|
31
|
+
showPrimary: boolean,
|
|
32
|
+
showCompare: boolean,
|
|
33
|
+
xFormatKind: FormatKind,
|
|
34
|
+
yFormatKind: FormatKind,
|
|
35
|
+
hoverKey: string | null,
|
|
36
|
+
setHoverKey: (k: string | null) => void,
|
|
37
|
+
zoomRange: [number, number] | null,
|
|
38
|
+
onResetZoom: () => void,
|
|
39
|
+
visibleStart: number,
|
|
40
|
+
visibleEnd: number,
|
|
41
|
+
fullData: Point[],
|
|
42
|
+
strings: AnalyticsChartStrings,
|
|
43
|
+
fmtValue: (value: number, kind: FormatKind) => string,
|
|
44
|
+
pie: AnalyticsChartPieProps | undefined,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const DEFAULT_PIE_CLASSNAME = "aspect-square h-[220px] w-[220px] sm:h-[240px] sm:w-[240px]";
|
|
48
|
+
|
|
49
|
+
export function AnalyticsChartPie({
|
|
50
|
+
wrapperRef,
|
|
51
|
+
primarySegmentSeries,
|
|
52
|
+
compareSegmentSeries,
|
|
53
|
+
aggregatedPrimarySegments,
|
|
54
|
+
aggregatedCompareSegments,
|
|
55
|
+
aggregatedPrimaryTotal,
|
|
56
|
+
aggregatedCompareTotal,
|
|
57
|
+
segmentColors,
|
|
58
|
+
showPrimary,
|
|
59
|
+
showCompare,
|
|
60
|
+
xFormatKind,
|
|
61
|
+
yFormatKind,
|
|
62
|
+
hoverKey,
|
|
63
|
+
setHoverKey,
|
|
64
|
+
zoomRange,
|
|
65
|
+
onResetZoom,
|
|
66
|
+
visibleStart,
|
|
67
|
+
visibleEnd,
|
|
68
|
+
fullData,
|
|
69
|
+
strings,
|
|
70
|
+
fmtValue,
|
|
71
|
+
pie,
|
|
72
|
+
}: AnalyticsChartPieBodyProps) {
|
|
73
|
+
const innerRadius = pie?.innerRadius ?? 60;
|
|
74
|
+
const outerRadius = pie?.outerRadius ?? 84;
|
|
75
|
+
const compareInnerRadius = pie?.compareInnerRadius ?? 36;
|
|
76
|
+
const compareOuterRadius = pie?.compareOuterRadius ?? 52;
|
|
77
|
+
const containerClassName = pie?.className ?? DEFAULT_PIE_CLASSNAME;
|
|
78
|
+
const showDateRange = pie?.showDateRange ?? true;
|
|
79
|
+
const centerTextMaxWidth = Math.max(64, innerRadius * 2 - 28);
|
|
80
|
+
const segmentTransitionStyle: CSSProperties = {
|
|
81
|
+
transition: "opacity 180ms ease-out",
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const canonicalSeries = primarySegmentSeries.length > 0
|
|
85
|
+
? primarySegmentSeries
|
|
86
|
+
: compareSegmentSeries;
|
|
87
|
+
const usePrimaryForCanonical = primarySegmentSeries.length > 0;
|
|
88
|
+
|
|
89
|
+
const compareValueByKey = useMemo(() => {
|
|
90
|
+
const m = new Map<string, number>();
|
|
91
|
+
compareSegmentSeries.forEach((s, sIdx) => {
|
|
92
|
+
m.set(s.key, aggregatedCompareSegments[sIdx] ?? 0);
|
|
93
|
+
});
|
|
94
|
+
return m;
|
|
95
|
+
}, [compareSegmentSeries, aggregatedCompareSegments]);
|
|
96
|
+
const primaryValueByKey = useMemo(() => {
|
|
97
|
+
const m = new Map<string, number>();
|
|
98
|
+
primarySegmentSeries.forEach((s, sIdx) => {
|
|
99
|
+
m.set(s.key, aggregatedPrimarySegments[sIdx] ?? 0);
|
|
100
|
+
});
|
|
101
|
+
return m;
|
|
102
|
+
}, [primarySegmentSeries, aggregatedPrimarySegments]);
|
|
103
|
+
|
|
104
|
+
const canonicalTotal = usePrimaryForCanonical
|
|
105
|
+
? aggregatedPrimaryTotal
|
|
106
|
+
: aggregatedCompareTotal;
|
|
107
|
+
|
|
108
|
+
const legendRows = useMemo(
|
|
109
|
+
() =>
|
|
110
|
+
canonicalSeries
|
|
111
|
+
.map((s, sIdx) => {
|
|
112
|
+
const value = usePrimaryForCanonical
|
|
113
|
+
? (aggregatedPrimarySegments[sIdx] ?? 0)
|
|
114
|
+
: (aggregatedCompareSegments[sIdx] ?? 0);
|
|
115
|
+
const prevValue = usePrimaryForCanonical
|
|
116
|
+
? (compareValueByKey.get(s.key) ?? 0)
|
|
117
|
+
: (primaryValueByKey.get(s.key) ?? 0);
|
|
118
|
+
return {
|
|
119
|
+
key: s.key,
|
|
120
|
+
label: s.label,
|
|
121
|
+
sIdx,
|
|
122
|
+
value,
|
|
123
|
+
prevValue,
|
|
124
|
+
pct: canonicalTotal > 0 ? value / canonicalTotal : 0,
|
|
125
|
+
fill: segmentColors.primary.light[sIdx],
|
|
126
|
+
fillDark: segmentColors.primary.dark[sIdx],
|
|
127
|
+
fillCompare: segmentColors.compare.light[sIdx],
|
|
128
|
+
fillCompareDark: segmentColors.compare.dark[sIdx],
|
|
129
|
+
};
|
|
130
|
+
})
|
|
131
|
+
.sort((a, b) => b.value - a.value),
|
|
132
|
+
[
|
|
133
|
+
canonicalSeries,
|
|
134
|
+
usePrimaryForCanonical,
|
|
135
|
+
aggregatedPrimarySegments,
|
|
136
|
+
aggregatedCompareSegments,
|
|
137
|
+
compareValueByKey,
|
|
138
|
+
primaryValueByKey,
|
|
139
|
+
canonicalTotal,
|
|
140
|
+
segmentColors,
|
|
141
|
+
],
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const chartConfig = useMemo<DesignChartConfig>(() => {
|
|
145
|
+
const config: DesignChartConfig = {};
|
|
146
|
+
canonicalSeries.forEach((s, sIdx) => {
|
|
147
|
+
config[cssIdent(s.key)] = {
|
|
148
|
+
label: s.label,
|
|
149
|
+
theme: {
|
|
150
|
+
light: segmentColors.primary.light[sIdx],
|
|
151
|
+
dark: segmentColors.primary.dark[sIdx],
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
config[`compare-${cssIdent(s.key)}`] = {
|
|
155
|
+
label: s.label,
|
|
156
|
+
theme: {
|
|
157
|
+
light: segmentColors.compare.light[sIdx],
|
|
158
|
+
dark: segmentColors.compare.dark[sIdx],
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
return config;
|
|
163
|
+
}, [canonicalSeries, segmentColors]);
|
|
164
|
+
|
|
165
|
+
const activeRow = hoverKey != null
|
|
166
|
+
? legendRows.find((r) => r.key === hoverKey) ?? null
|
|
167
|
+
: null;
|
|
168
|
+
const [centerDisplayKey, setCenterDisplayKey] = useState<string | null>(null);
|
|
169
|
+
const [centerDisplayVisible, setCenterDisplayVisible] = useState(true);
|
|
170
|
+
const centerDisplayRow = centerDisplayKey != null
|
|
171
|
+
? legendRows.find((r) => r.key === centerDisplayKey) ?? null
|
|
172
|
+
: null;
|
|
173
|
+
useEffect(() => {
|
|
174
|
+
const nextKey = activeRow?.key ?? null;
|
|
175
|
+
if (nextKey === centerDisplayKey) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
setCenterDisplayVisible(false);
|
|
180
|
+
const timeoutId = window.setTimeout(() => {
|
|
181
|
+
setCenterDisplayKey(nextKey);
|
|
182
|
+
setCenterDisplayVisible(true);
|
|
183
|
+
}, 120);
|
|
184
|
+
|
|
185
|
+
return () => {
|
|
186
|
+
window.clearTimeout(timeoutId);
|
|
187
|
+
};
|
|
188
|
+
}, [activeRow?.key, centerDisplayKey]);
|
|
189
|
+
|
|
190
|
+
const activeDelta = activeRow
|
|
191
|
+
? formatDelta(activeRow.value, activeRow.prevValue)
|
|
192
|
+
: formatDelta(aggregatedPrimaryTotal, aggregatedCompareTotal);
|
|
193
|
+
|
|
194
|
+
const windowDays = visibleEnd - visibleStart + 1;
|
|
195
|
+
const startLabel = fmtValue(fullData[visibleStart]!.ts, xFormatKind);
|
|
196
|
+
const endLabel = fmtValue(fullData[visibleEnd]!.ts, xFormatKind);
|
|
197
|
+
|
|
198
|
+
const outerData = legendRows.map((r) => ({ name: cssIdent(r.key), hoverKey: r.key, value: r.value, fill: r.fill }));
|
|
199
|
+
const innerData = legendRows.map((r) => ({ name: cssIdent(r.key), hoverKey: r.key, value: r.prevValue, fill: r.fillCompare }));
|
|
200
|
+
const activeIdx = hoverKey != null ? legendRows.findIndex((r) => r.key === hoverKey) : -1;
|
|
201
|
+
|
|
202
|
+
return (
|
|
203
|
+
<div
|
|
204
|
+
ref={wrapperRef}
|
|
205
|
+
className="relative flex h-full min-h-0 w-full min-w-0 flex-1 flex-col select-none"
|
|
206
|
+
onClick={(e) => e.stopPropagation()}
|
|
207
|
+
>
|
|
208
|
+
{zoomRange && (
|
|
209
|
+
<div className="absolute right-2 top-2 z-20">
|
|
210
|
+
<button
|
|
211
|
+
type="button"
|
|
212
|
+
onClick={onResetZoom}
|
|
213
|
+
className="inline-flex items-center gap-1.5 rounded-full bg-blue-500/10 px-2.5 py-1 font-mono text-[10px] font-semibold uppercase tracking-wider text-blue-600 ring-1 ring-blue-500/30 transition-colors duration-150 hover:bg-blue-500/15 hover:transition-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/50 dark:text-blue-300 dark:ring-blue-400/30"
|
|
214
|
+
>
|
|
215
|
+
<MagnifyingGlassMinusIcon weight="bold" className="size-3" aria-hidden="true" />
|
|
216
|
+
<span>{strings.resetZoom}</span>
|
|
217
|
+
</button>
|
|
218
|
+
</div>
|
|
219
|
+
)}
|
|
220
|
+
|
|
221
|
+
<div className="flex h-full min-h-0 w-full flex-1 flex-wrap content-center items-center justify-center gap-x-10 gap-y-6">
|
|
222
|
+
<div className="flex shrink-0 flex-col items-center gap-3">
|
|
223
|
+
<div className="relative">
|
|
224
|
+
<DesignChartContainer config={chartConfig} className={containerClassName}>
|
|
225
|
+
<PieChart
|
|
226
|
+
role="img"
|
|
227
|
+
aria-label={strings.pieAriaLabel({
|
|
228
|
+
segmentCount: canonicalSeries.length,
|
|
229
|
+
windowDays,
|
|
230
|
+
})}
|
|
231
|
+
>
|
|
232
|
+
{showPrimary && (
|
|
233
|
+
<Pie
|
|
234
|
+
data={outerData}
|
|
235
|
+
dataKey="value"
|
|
236
|
+
nameKey="name"
|
|
237
|
+
cx="50%"
|
|
238
|
+
cy="50%"
|
|
239
|
+
innerRadius={innerRadius}
|
|
240
|
+
outerRadius={outerRadius}
|
|
241
|
+
paddingAngle={1}
|
|
242
|
+
startAngle={90}
|
|
243
|
+
endAngle={-270}
|
|
244
|
+
isAnimationActive={false}
|
|
245
|
+
stroke="none"
|
|
246
|
+
>
|
|
247
|
+
{outerData.map((d, i) => {
|
|
248
|
+
const inactive = activeIdx >= 0 && activeIdx !== i;
|
|
249
|
+
return (
|
|
250
|
+
<Cell
|
|
251
|
+
key={`outer-${d.name}`}
|
|
252
|
+
fill={`var(--color-${d.name})`}
|
|
253
|
+
opacity={inactive ? 0.22 : 1}
|
|
254
|
+
style={segmentTransitionStyle}
|
|
255
|
+
onMouseEnter={() => setHoverKey(d.hoverKey)}
|
|
256
|
+
onMouseLeave={() => setHoverKey(null)}
|
|
257
|
+
/>
|
|
258
|
+
);
|
|
259
|
+
})}
|
|
260
|
+
</Pie>
|
|
261
|
+
)}
|
|
262
|
+
{showCompare && (
|
|
263
|
+
<Pie
|
|
264
|
+
data={innerData}
|
|
265
|
+
dataKey="value"
|
|
266
|
+
nameKey="name"
|
|
267
|
+
cx="50%"
|
|
268
|
+
cy="50%"
|
|
269
|
+
innerRadius={compareInnerRadius}
|
|
270
|
+
outerRadius={compareOuterRadius}
|
|
271
|
+
paddingAngle={1}
|
|
272
|
+
startAngle={90}
|
|
273
|
+
endAngle={-270}
|
|
274
|
+
isAnimationActive={false}
|
|
275
|
+
stroke="none"
|
|
276
|
+
>
|
|
277
|
+
{innerData.map((d, i) => {
|
|
278
|
+
const inactive = activeIdx >= 0 && activeIdx !== i;
|
|
279
|
+
return (
|
|
280
|
+
<Cell
|
|
281
|
+
key={`inner-${d.name}`}
|
|
282
|
+
fill={`var(--color-compare-${d.name})`}
|
|
283
|
+
opacity={inactive ? 0.22 : 0.95}
|
|
284
|
+
style={segmentTransitionStyle}
|
|
285
|
+
onMouseEnter={() => setHoverKey(d.hoverKey)}
|
|
286
|
+
onMouseLeave={() => setHoverKey(null)}
|
|
287
|
+
/>
|
|
288
|
+
);
|
|
289
|
+
})}
|
|
290
|
+
</Pie>
|
|
291
|
+
)}
|
|
292
|
+
</PieChart>
|
|
293
|
+
</DesignChartContainer>
|
|
294
|
+
|
|
295
|
+
<div
|
|
296
|
+
className={cn(
|
|
297
|
+
"pointer-events-none absolute inset-0 flex flex-col items-center justify-center text-center transition-opacity duration-200 ease-out",
|
|
298
|
+
centerDisplayVisible ? "opacity-100" : "opacity-35",
|
|
299
|
+
)}
|
|
300
|
+
>
|
|
301
|
+
<span className="block truncate font-mono text-[9px] uppercase tracking-wider text-muted-foreground" style={{ maxWidth: centerTextMaxWidth }}>
|
|
302
|
+
{centerDisplayRow ? centerDisplayRow.label : strings.pieTotalCenter}
|
|
303
|
+
</span>
|
|
304
|
+
<span className="mt-0.5 block truncate font-mono text-xl font-semibold leading-none tabular-nums text-foreground" style={{ maxWidth: centerTextMaxWidth }}>
|
|
305
|
+
{fmtValue(centerDisplayRow ? centerDisplayRow.value : canonicalTotal, yFormatKind)}
|
|
306
|
+
</span>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
|
|
310
|
+
{(showDateRange || showCompare) && (
|
|
311
|
+
<div className="flex items-center gap-2 text-center">
|
|
312
|
+
{showDateRange && (
|
|
313
|
+
<span className="font-mono text-[10px] uppercase tracking-wider text-muted-foreground/80">
|
|
314
|
+
{startLabel} – {endLabel}
|
|
315
|
+
</span>
|
|
316
|
+
)}
|
|
317
|
+
{showCompare && (
|
|
318
|
+
<TrendPill delta={activeDelta} size="sm" label={strings.pieVsPrev} />
|
|
319
|
+
)}
|
|
320
|
+
</div>
|
|
321
|
+
)}
|
|
322
|
+
</div>
|
|
323
|
+
|
|
324
|
+
<ul className="flex min-w-[200px] max-w-[300px] flex-col gap-1">
|
|
325
|
+
{legendRows.map((r) => {
|
|
326
|
+
const isActive = hoverKey === r.key;
|
|
327
|
+
const dimmed = hoverKey != null && !isActive;
|
|
328
|
+
const rowDelta = formatDelta(r.value, r.prevValue);
|
|
329
|
+
return (
|
|
330
|
+
<li key={r.key}>
|
|
331
|
+
<button
|
|
332
|
+
type="button"
|
|
333
|
+
onMouseEnter={() => setHoverKey(r.key)}
|
|
334
|
+
onMouseLeave={() => setHoverKey(null)}
|
|
335
|
+
onFocus={() => setHoverKey(r.key)}
|
|
336
|
+
onBlur={() => setHoverKey(null)}
|
|
337
|
+
className={cn(
|
|
338
|
+
"flex w-full items-center gap-2.5 rounded-md px-2 py-1.5 text-left transition-[background-color,opacity] duration-200 ease-out hover:bg-foreground/[0.04] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground/20",
|
|
339
|
+
isActive && "bg-foreground/[0.05]",
|
|
340
|
+
dimmed && "opacity-50",
|
|
341
|
+
)}
|
|
342
|
+
>
|
|
343
|
+
<span
|
|
344
|
+
className="size-2.5 shrink-0 rounded-[3px] bg-[var(--c-l)] dark:bg-[var(--c-d)]"
|
|
345
|
+
style={{ "--c-l": r.fill, "--c-d": r.fillDark } as CSSProperties}
|
|
346
|
+
/>
|
|
347
|
+
<span className="min-w-0 flex-1 truncate text-[12px] font-medium text-foreground">
|
|
348
|
+
{r.label}
|
|
349
|
+
</span>
|
|
350
|
+
<span className="shrink-0 font-mono text-[11px] tabular-nums text-muted-foreground">
|
|
351
|
+
{(r.pct * 100).toFixed(1)}%
|
|
352
|
+
</span>
|
|
353
|
+
<span className="shrink-0 min-w-[48px] text-right font-mono text-[11px] font-semibold tabular-nums text-foreground">
|
|
354
|
+
{fmtValue(r.value, yFormatKind)}
|
|
355
|
+
</span>
|
|
356
|
+
{showCompare && (
|
|
357
|
+
<span className="shrink-0">
|
|
358
|
+
<TrendPill delta={rowDelta} size="sm" />
|
|
359
|
+
</span>
|
|
360
|
+
)}
|
|
361
|
+
</button>
|
|
362
|
+
</li>
|
|
363
|
+
);
|
|
364
|
+
})}
|
|
365
|
+
</ul>
|
|
366
|
+
</div>
|
|
367
|
+
</div>
|
|
368
|
+
);
|
|
369
|
+
}
|