@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.
Files changed (98) hide show
  1. package/dist/components/alert.js +4 -4
  2. package/dist/components/alert.js.map +1 -1
  3. package/dist/components/badge.js +1 -1
  4. package/dist/components/badge.js.map +1 -1
  5. package/dist/components/button.d.ts +4 -4
  6. package/dist/components/button.js +3 -3
  7. package/dist/components/button.js.map +1 -1
  8. package/dist/components/data-grid/data-grid-toolbar.js +5 -5
  9. package/dist/components/data-grid/data-grid-toolbar.js.map +1 -1
  10. package/dist/components/data-grid/data-grid.d.ts.map +1 -1
  11. package/dist/components/data-grid/data-grid.js +11 -5
  12. package/dist/components/data-grid/data-grid.js.map +1 -1
  13. package/dist/components/data-grid/data-grid.test.js +34 -1
  14. package/dist/components/data-grid/data-grid.test.js.map +1 -1
  15. package/dist/components/dialog.d.ts +6 -2
  16. package/dist/components/dialog.d.ts.map +1 -1
  17. package/dist/components/dialog.js +14 -10
  18. package/dist/components/dialog.js.map +1 -1
  19. package/dist/components/input.js +12 -9
  20. package/dist/components/input.js.map +1 -1
  21. package/dist/components/pill-toggle.js +3 -3
  22. package/dist/components/pill-toggle.js.map +1 -1
  23. package/dist/components/tabs.js +2 -2
  24. package/dist/components/tabs.js.map +1 -1
  25. package/dist/dashboard-ui-components.global.js +15639 -17400
  26. package/dist/dashboard-ui-components.global.js.map +4 -4
  27. package/dist/esm/components/alert.js +4 -4
  28. package/dist/esm/components/alert.js.map +1 -1
  29. package/dist/esm/components/badge.js +1 -1
  30. package/dist/esm/components/badge.js.map +1 -1
  31. package/dist/esm/components/button.d.ts +4 -4
  32. package/dist/esm/components/button.js +3 -3
  33. package/dist/esm/components/button.js.map +1 -1
  34. package/dist/esm/components/data-grid/data-grid-toolbar.js +5 -5
  35. package/dist/esm/components/data-grid/data-grid-toolbar.js.map +1 -1
  36. package/dist/esm/components/data-grid/data-grid.d.ts.map +1 -1
  37. package/dist/esm/components/data-grid/data-grid.js +11 -5
  38. package/dist/esm/components/data-grid/data-grid.js.map +1 -1
  39. package/dist/esm/components/data-grid/data-grid.test.js +34 -1
  40. package/dist/esm/components/data-grid/data-grid.test.js.map +1 -1
  41. package/dist/esm/components/dialog.d.ts +6 -2
  42. package/dist/esm/components/dialog.d.ts.map +1 -1
  43. package/dist/esm/components/dialog.js +13 -11
  44. package/dist/esm/components/dialog.js.map +1 -1
  45. package/dist/esm/components/input.js +12 -9
  46. package/dist/esm/components/input.js.map +1 -1
  47. package/dist/esm/components/pill-toggle.js +4 -4
  48. package/dist/esm/components/pill-toggle.js.map +1 -1
  49. package/dist/esm/components/tabs.js +2 -2
  50. package/dist/esm/components/tabs.js.map +1 -1
  51. package/dist/esm/index.d.ts +2 -2
  52. package/dist/esm/index.js +2 -2
  53. package/dist/index.d.ts +2 -2
  54. package/dist/index.js +12 -0
  55. package/package.json +4 -3
  56. package/src/components/alert.tsx +120 -0
  57. package/src/components/analytics-chart/analytics-chart-pie.tsx +369 -0
  58. package/src/components/analytics-chart/analytics-chart.tsx +1585 -0
  59. package/src/components/analytics-chart/default-analytics-chart-tooltip.tsx +265 -0
  60. package/src/components/analytics-chart/format.ts +101 -0
  61. package/src/components/analytics-chart/index.ts +75 -0
  62. package/src/components/analytics-chart/palette.ts +68 -0
  63. package/src/components/analytics-chart/render-data-series.tsx +169 -0
  64. package/src/components/analytics-chart/state.ts +165 -0
  65. package/src/components/analytics-chart/strings.ts +72 -0
  66. package/src/components/analytics-chart/types.ts +220 -0
  67. package/src/components/badge.tsx +108 -0
  68. package/src/components/button.tsx +104 -0
  69. package/src/components/card.tsx +263 -0
  70. package/src/components/chart-card.tsx +101 -0
  71. package/src/components/chart-container.tsx +155 -0
  72. package/src/components/chart-legend.tsx +65 -0
  73. package/src/components/chart-theme.tsx +52 -0
  74. package/src/components/chart-tooltip.tsx +165 -0
  75. package/src/components/cursor-blast-effect.tsx +334 -0
  76. package/src/components/data-grid/data-grid-sizing.ts +51 -0
  77. package/src/components/data-grid/data-grid-toolbar.tsx +373 -0
  78. package/src/components/data-grid/data-grid.test.tsx +407 -0
  79. package/src/components/data-grid/data-grid.tsx +1231 -0
  80. package/src/components/data-grid/index.ts +64 -0
  81. package/src/components/data-grid/state.ts +235 -0
  82. package/src/components/data-grid/strings.ts +45 -0
  83. package/src/components/data-grid/types.ts +401 -0
  84. package/src/components/data-grid/use-data-source.ts +413 -0
  85. package/src/components/data-grid/use-url-state.test.tsx +88 -0
  86. package/src/components/data-grid/use-url-state.ts +298 -0
  87. package/src/components/dialog.tsx +218 -0
  88. package/src/components/edit-mode.tsx +17 -0
  89. package/src/components/empty-state.tsx +64 -0
  90. package/src/components/input.tsx +97 -0
  91. package/src/components/metric-card.tsx +142 -0
  92. package/src/components/pill-toggle.tsx +159 -0
  93. package/src/components/progress-bar.tsx +82 -0
  94. package/src/components/separator.tsx +36 -0
  95. package/src/components/skeleton.tsx +30 -0
  96. package/src/components/table.tsx +113 -0
  97. package/src/components/tabs.tsx +217 -0
  98. 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
+ }