@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,265 @@
1
+ import { cn } from "@hexclave/ui";
2
+ import {
3
+ ArrowDownIcon,
4
+ ArrowUpIcon,
5
+ CursorClickIcon,
6
+ MinusIcon,
7
+ PushPinSimpleIcon,
8
+ } from "@phosphor-icons/react";
9
+ import type { CSSProperties } from "react";
10
+ import type { AnalyticsChartStrings } from "./strings";
11
+ import type { AnalyticsChartDelta, Point } from "./types";
12
+
13
+ /** Trend pill — small rounded badge with an up/down/flat arrow, a signed
14
+ * percentage, and an optional trailing label. Shared between the default
15
+ * tooltip, the pie view, and the demo panels (which re-export it). */
16
+ export function TrendPill({
17
+ delta,
18
+ label,
19
+ size = "sm",
20
+ }: {
21
+ delta: AnalyticsChartDelta,
22
+ label?: string,
23
+ size?: "sm" | "md",
24
+ }) {
25
+ const { pct, sign } = delta;
26
+ const tone =
27
+ sign === "up" ? "text-emerald-600 dark:text-emerald-400 bg-emerald-500/10"
28
+ : sign === "down" ? "text-rose-600 dark:text-rose-400 bg-rose-500/10"
29
+ : "text-muted-foreground bg-foreground/[0.06]";
30
+ const Icon = sign === "up" ? ArrowUpIcon : sign === "down" ? ArrowDownIcon : MinusIcon;
31
+ const text = pct == null ? "—" : `${pct > 0 ? "+" : ""}${pct}%`;
32
+ return (
33
+ <span className={cn(
34
+ "inline-flex items-center gap-1 rounded-full font-mono tabular-nums font-medium",
35
+ size === "md" ? "px-2 py-0.5 text-[11px]" : "px-1.5 py-[1px] text-[10px]",
36
+ tone,
37
+ )}>
38
+ <Icon weight="bold" className={size === "md" ? "size-3" : "size-2.5"} aria-hidden="true" />
39
+ {text}
40
+ {label && <span className="ml-0.5 text-muted-foreground font-normal">{label}</span>}
41
+ </span>
42
+ );
43
+ }
44
+
45
+ export type AnalyticsChartTooltipSegmentRow = {
46
+ key: string,
47
+ label: string,
48
+ value: number,
49
+ /** Light-theme color for the dot / swatch. */
50
+ color: string,
51
+ /** Dark-theme color for the dot / swatch. */
52
+ colorDark: string,
53
+ };
54
+
55
+ export type AnalyticsChartTooltipLayerView = {
56
+ /** Stable layer id (e.g. `"signups"`, `"previous"`). */
57
+ id: string,
58
+ /** Consumer-provided layer label. */
59
+ label: string,
60
+ /** Resolved layer color (light theme). */
61
+ color: string,
62
+ /** Resolved layer color (dark theme). */
63
+ colorDark: string,
64
+ /** Flat total for this layer at the hovered index. Populated regardless
65
+ * of segmentation so consumers can render the same number in either mode. */
66
+ total: number,
67
+ /** True iff this layer is rendered as a stacked breakdown. */
68
+ segmented: boolean,
69
+ /** Per-segment rows — empty when `segmented === false`. Order matches
70
+ * `segmentSeries`. */
71
+ segments: AnalyticsChartTooltipSegmentRow[],
72
+ };
73
+
74
+ export type AnalyticsChartTooltipContext = {
75
+ /** Index into the visible window. */
76
+ activeIndex: number,
77
+ /** Raw point at `activeIndex` — convenient for `.ts` access. */
78
+ point: Point,
79
+ /** True when the tooltip is pinned (via click) and stable under hover. */
80
+ isPinned: boolean,
81
+ /** Primary layer view or null when the primary layer is hidden. */
82
+ primary: AnalyticsChartTooltipLayerView | null,
83
+ /** Compare layer view or null when the compare layer is hidden. */
84
+ compare: AnalyticsChartTooltipLayerView | null,
85
+ /** Flat-mode delta between primary and compare totals. Null when either
86
+ * side is hidden. Consumers should feed this into their trend pill. */
87
+ delta: AnalyticsChartDelta | null,
88
+ /** Pre-bound value formatter for y-axis values. */
89
+ formatValue: (v: number) => string,
90
+ /** Pre-bound formatter for x-axis values. */
91
+ formatDate: (ts: number) => string,
92
+ /** Resolved strings — already merged with defaults. */
93
+ strings: AnalyticsChartStrings,
94
+ };
95
+
96
+ export type DefaultAnalyticsChartTooltipProps = {
97
+ ctx: AnalyticsChartTooltipContext,
98
+ };
99
+
100
+ /** The default tooltip body. The tooltip is rendered as an
101
+ * absolutely-positioned sibling of `<ChartContainer>`, which means it sits
102
+ * OUTSIDE the `[data-chart=…]` subtree that scopes shadcn's `--color-${key}`
103
+ * CSS variables. We therefore cannot reference those variables for segment
104
+ * swatches — instead, every swatch uses a single span with `--c-l`/`--c-d`
105
+ * custom properties + Tailwind arbitrary variants so one DOM element covers
106
+ * both themes. */
107
+ export function DefaultAnalyticsChartTooltip({ ctx }: DefaultAnalyticsChartTooltipProps) {
108
+ const { point, isPinned, primary, compare, delta, formatValue: fv, formatDate: fd, strings } = ctx;
109
+ const anySegmented = (primary?.segmented ?? false) || (compare?.segmented ?? false);
110
+ return (
111
+ <div className="rounded-xl border border-foreground/10 bg-background/95 px-3 py-2.5 shadow-[0_10px_28px_rgba(15,23,42,0.18)] backdrop-blur-xl dark:shadow-[0_10px_28px_rgba(0,0,0,0.55)] min-w-[180px]">
112
+ <div className="flex items-center justify-between gap-3">
113
+ <span className="font-mono text-[10px] uppercase tracking-wider text-muted-foreground">
114
+ {fd(point.ts)}
115
+ </span>
116
+ {isPinned && (
117
+ <span className="inline-flex items-center gap-1 rounded-full bg-blue-500/10 px-1.5 py-[1px] text-[9px] font-semibold uppercase tracking-wider text-blue-600 dark:text-blue-300">
118
+ <PushPinSimpleIcon weight="fill" className="size-2.5" aria-hidden="true" />
119
+ {strings.pinnedBadge}
120
+ </span>
121
+ )}
122
+ </div>
123
+ <div className="mt-2 flex flex-col gap-1.5">
124
+ <TooltipLayerRows view={primary} keyPrefix="p" fv={fv} muted={false} />
125
+ <TooltipLayerRows view={compare} keyPrefix="c" fv={fv} muted />
126
+ </div>
127
+ {anySegmented && (
128
+ <div className="mt-2 flex flex-col gap-1 border-t border-foreground/[0.07] pt-2">
129
+ {primary?.segmented && (
130
+ <LayerSummaryRow
131
+ label={`${primary.label}${strings.layerTotalSuffix}`}
132
+ value={fv(primary.total)}
133
+ muted={false}
134
+ />
135
+ )}
136
+ {compare?.segmented && (
137
+ <LayerSummaryRow
138
+ label={`${compare.label}${strings.layerTotalSuffix}`}
139
+ value={fv(compare.total)}
140
+ muted
141
+ />
142
+ )}
143
+ {primary?.segmented && compare?.segmented && delta && (
144
+ <div className="mt-1 flex items-center justify-between">
145
+ <span className="font-mono text-[10px] uppercase tracking-wider text-muted-foreground">
146
+ {strings.deltaVsPrev}
147
+ </span>
148
+ <TrendPill delta={delta} size="sm" />
149
+ </div>
150
+ )}
151
+ </div>
152
+ )}
153
+ {!anySegmented && delta && primary && compare && (
154
+ <div className="mt-2 flex items-center justify-between border-t border-foreground/[0.07] pt-2">
155
+ <span className="font-mono text-[10px] uppercase tracking-wider text-muted-foreground">
156
+ {strings.deltaVsPrev}
157
+ </span>
158
+ <TrendPill delta={delta} size="sm" />
159
+ </div>
160
+ )}
161
+ <div className="mt-2 flex items-center gap-1 text-[10px] text-muted-foreground">
162
+ <CursorClickIcon weight="bold" className="size-2.5" aria-hidden="true" />
163
+ <span>{isPinned ? strings.hintClickAnywhereUnpin : strings.hintClickToPin}</span>
164
+ </div>
165
+ </div>
166
+ );
167
+ }
168
+
169
+ /** Render either a single flat row or the per-segment breakdown rows for
170
+ * one tooltip layer view. Collapses what were formerly two parallel
171
+ * primary/compare branches into one component. */
172
+ function TooltipLayerRows({
173
+ view,
174
+ keyPrefix,
175
+ fv,
176
+ muted,
177
+ }: {
178
+ view: AnalyticsChartTooltipLayerView | null,
179
+ keyPrefix: string,
180
+ fv: (v: number) => string,
181
+ muted: boolean,
182
+ }) {
183
+ if (!view) return null;
184
+ if (!view.segmented) {
185
+ return (
186
+ <LayerTotalRow
187
+ light={view.color}
188
+ dark={view.colorDark}
189
+ label={view.label}
190
+ value={fv(view.total)}
191
+ muted={muted}
192
+ />
193
+ );
194
+ }
195
+ return (
196
+ <>
197
+ {view.segments.map((s) => (
198
+ <LayerTotalRow
199
+ key={`${keyPrefix}-${s.key}`}
200
+ light={s.color}
201
+ dark={s.colorDark}
202
+ label={s.label}
203
+ value={fv(s.value)}
204
+ muted={muted}
205
+ />
206
+ ))}
207
+ </>
208
+ );
209
+ }
210
+
211
+ /** Single row: swatch + label + value. One DOM element for the swatch —
212
+ * dark-mode color is picked up via `--c-d`. */
213
+ function LayerTotalRow({
214
+ light,
215
+ dark,
216
+ label,
217
+ value,
218
+ muted,
219
+ }: {
220
+ light: string,
221
+ dark: string,
222
+ label: string,
223
+ value: string,
224
+ muted: boolean,
225
+ }) {
226
+ return (
227
+ <div className="flex items-center gap-2.5">
228
+ <span
229
+ className="size-2 rounded-full bg-[var(--c-l)] dark:bg-[var(--c-d)]"
230
+ style={{ "--c-l": light, "--c-d": dark } as CSSProperties}
231
+ />
232
+ <span className="text-[11px] text-muted-foreground">{label}</span>
233
+ <span className={cn(
234
+ "ml-auto font-mono text-xs tabular-nums",
235
+ muted ? "text-muted-foreground" : "font-semibold text-foreground",
236
+ )}>
237
+ {value}
238
+ </span>
239
+ </div>
240
+ );
241
+ }
242
+
243
+ function LayerSummaryRow({
244
+ label,
245
+ value,
246
+ muted,
247
+ }: {
248
+ label: string,
249
+ value: string,
250
+ muted: boolean,
251
+ }) {
252
+ return (
253
+ <div className="flex items-center justify-between">
254
+ <span className="font-mono text-[10px] uppercase tracking-wider text-muted-foreground">
255
+ {label}
256
+ </span>
257
+ <span className={cn(
258
+ "font-mono text-xs tabular-nums",
259
+ muted ? "text-muted-foreground" : "font-semibold text-foreground",
260
+ )}>
261
+ {value}
262
+ </span>
263
+ </div>
264
+ );
265
+ }
@@ -0,0 +1,101 @@
1
+ import type { FormatKind, FormatKindType, AnalyticsChartDelta } from "./types";
2
+
3
+ export const FORMAT_KIND_TYPES: FormatKindType[] = [
4
+ "numeric",
5
+ "short",
6
+ "currency",
7
+ "duration",
8
+ "datetime",
9
+ "percent",
10
+ ];
11
+
12
+ export const DEFAULT_FORMAT_KIND: { [K in FormatKindType]: Extract<FormatKind, { type: K }> } = {
13
+ numeric: { type: "numeric", locale: "en-US", decimals: 0 },
14
+ short: { type: "short", precision: 1, locale: "en-US" },
15
+ currency: { type: "currency", currency: "USD", divisor: 100, locale: "en-US" },
16
+ duration: { type: "duration", unit: "s", showZero: false },
17
+ datetime: { type: "datetime", style: "short", locale: "en-US" },
18
+ percent: { type: "percent", source: "fraction", decimals: 1 },
19
+ };
20
+
21
+ function formatRelative(value: number, locale: string): string {
22
+ const diff = value - Date.now();
23
+ const absSec = Math.abs(diff) / 1000;
24
+ const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
25
+ if (absSec >= 86_400) return rtf.format(Math.round(diff / 86_400_000), "day");
26
+ if (absSec >= 3600) return rtf.format(Math.round(diff / 3_600_000), "hour");
27
+ if (absSec >= 60) return rtf.format(Math.round(diff / 60_000), "minute");
28
+ return "just now";
29
+ }
30
+
31
+ /** `short` uses compact notation (e.g. `1.2K`), not a custom `k` suffix. */
32
+ export function formatValue(value: number, kind: FormatKind): string {
33
+ switch (kind.type) {
34
+ case "numeric": {
35
+ const decimals = kind.decimals ?? 0;
36
+ return value.toLocaleString(kind.locale ?? "en-US", {
37
+ minimumFractionDigits: decimals,
38
+ maximumFractionDigits: decimals,
39
+ });
40
+ }
41
+ case "short": {
42
+ const precision = kind.precision ?? 1;
43
+ return new Intl.NumberFormat(kind.locale ?? "en-US", {
44
+ notation: "compact",
45
+ compactDisplay: "short",
46
+ minimumFractionDigits: precision,
47
+ maximumFractionDigits: precision,
48
+ }).format(value);
49
+ }
50
+ case "currency": {
51
+ const divisor = kind.divisor ?? 1;
52
+ return new Intl.NumberFormat(kind.locale ?? "en-US", {
53
+ style: "currency",
54
+ currency: kind.currency ?? "USD",
55
+ }).format(value / divisor);
56
+ }
57
+ case "duration": {
58
+ const unit = kind.unit ?? "s";
59
+ const seconds = unit === "ms" ? value / 1000
60
+ : unit === "m" ? value * 60
61
+ : unit === "h" ? value * 3600
62
+ : value;
63
+ const h = Math.floor(seconds / 3600);
64
+ const m = Math.floor((seconds % 3600) / 60);
65
+ const s = Math.floor(seconds % 60);
66
+ if (h > 0) return `${h}h ${m}m ${s}s`;
67
+ if (m > 0) return `${m}m ${s}s`;
68
+ if (unit === "ms" && seconds < 1) return `${Math.round(value)}ms`;
69
+ if (s > 0 || kind.showZero) return `${s}s`;
70
+ return "0s";
71
+ }
72
+ case "datetime": {
73
+ const d = new Date(value);
74
+ const style = kind.style ?? "short";
75
+ const locale = kind.locale ?? "en-US";
76
+ if (style === "iso") return d.toISOString();
77
+ if (style === "relative") return formatRelative(value, locale);
78
+ if (style === "long") return d.toLocaleString(locale, {
79
+ dateStyle: "medium",
80
+ timeStyle: "short",
81
+ });
82
+ return d.toLocaleDateString(locale, { month: "short", day: "numeric" });
83
+ }
84
+ case "percent": {
85
+ const source = kind.source ?? "fraction";
86
+ const decimals = kind.decimals ?? 1;
87
+ const pct = source === "basis" ? value / 100
88
+ : source === "whole" ? value
89
+ : value * 100;
90
+ return `${pct.toFixed(decimals)}%`;
91
+ }
92
+ }
93
+ }
94
+
95
+ export function formatDelta(current: number, previous: number): AnalyticsChartDelta {
96
+ if (!Number.isFinite(current) || !Number.isFinite(previous)) return { pct: null, sign: "na" };
97
+ if (previous === 0) return current === 0 ? { pct: 0, sign: "flat" } : { pct: null, sign: "na" };
98
+ const pct = Number((((current - previous) / previous) * 100).toFixed(1));
99
+ const sign = pct > 0 ? "up" : pct < 0 ? "down" : "flat";
100
+ return { pct, sign };
101
+ }
@@ -0,0 +1,75 @@
1
+ export { AnalyticsChart, type AnalyticsChartProps, type Margin } from "./analytics-chart";
2
+ export {
3
+ DefaultAnalyticsChartTooltip,
4
+ TrendPill,
5
+ type AnalyticsChartTooltipContext,
6
+ type AnalyticsChartTooltipLayerView,
7
+ type AnalyticsChartTooltipSegmentRow,
8
+ type DefaultAnalyticsChartTooltipProps,
9
+ } from "./default-analytics-chart-tooltip";
10
+ export {
11
+ formatDelta,
12
+ formatValue,
13
+ DEFAULT_FORMAT_KIND,
14
+ FORMAT_KIND_TYPES,
15
+ } from "./format";
16
+ export {
17
+ ANALYTICS_CHART_DEFAULT_PALETTE,
18
+ buildRampColors,
19
+ buildSegmentThemeMap,
20
+ resolveAnalyticsChartPalette,
21
+ } from "./palette";
22
+ export {
23
+ ANALYTICS_CHART_DEFAULT_LAYERS,
24
+ ANALYTICS_CHART_DEFAULT_STATE,
25
+ computeLocalInProgressIdx,
26
+ EMPTY_MATRIX,
27
+ EMPTY_SERIES,
28
+ findAnnotationsLayer,
29
+ findCompareLayer,
30
+ findLayerById,
31
+ findPrimaryLayer,
32
+ isAnalyticsChartDataLayer,
33
+ isTimeseriesState,
34
+ patchLayerById,
35
+ resolveDataLayerStyle,
36
+ setLayerById,
37
+ STROKE_DASHARRAY,
38
+ type ResolvedDataLayerStyle,
39
+ } from "./state";
40
+ export {
41
+ ANALYTICS_CHART_DEFAULT_STRINGS,
42
+ resolveAnalyticsChartStrings,
43
+ type AnalyticsChartStrings,
44
+ } from "./strings";
45
+ export {
46
+ pointValue,
47
+ type AnalyticsChartAnnotationsLayer,
48
+ type AnalyticsChartAreaLayer,
49
+ type AnalyticsChartBarLayer,
50
+ type AnalyticsChartDataLayer,
51
+ type AnalyticsChartDelta,
52
+ type AnalyticsChartLayer,
53
+ type AnalyticsChartLayers,
54
+ type AnalyticsChartLayerType,
55
+ type AnalyticsChartLineLayer,
56
+ type AnalyticsChartPalette,
57
+ type AnalyticsChartPieProps,
58
+ type AnalyticsChartPieState,
59
+ type AnalyticsChartSegmentRamp,
60
+ type AnalyticsChartSeries,
61
+ type AnalyticsChartState,
62
+ type AnalyticsChartStrokeStyle,
63
+ type AnalyticsChartTimeseriesState,
64
+ type AnalyticsChartView,
65
+ type Annotation,
66
+ type FormatKind,
67
+ type FormatKindCurrency,
68
+ type FormatKindDatetime,
69
+ type FormatKindDuration,
70
+ type FormatKindNumeric,
71
+ type FormatKindPercent,
72
+ type FormatKindShort,
73
+ type FormatKindType,
74
+ type Point,
75
+ } from "./types";
@@ -0,0 +1,68 @@
1
+ import type {
2
+ AnalyticsChartPalette,
3
+ AnalyticsChartSegmentRamp,
4
+ AnalyticsChartSeries,
5
+ } from "./types";
6
+
7
+ export const ANALYTICS_CHART_DEFAULT_PALETTE: AnalyticsChartPalette = {
8
+ primary: {
9
+ kind: "procedural",
10
+ hue: 220,
11
+ sat: 78,
12
+ shadeRangeLight: [28, 62],
13
+ shadeRangeDark: [52, 82],
14
+ },
15
+ compare: {
16
+ kind: "procedural",
17
+ hue: 38,
18
+ sat: 92,
19
+ shadeRangeLight: [28, 62],
20
+ shadeRangeDark: [52, 82],
21
+ },
22
+ };
23
+
24
+ export function resolveAnalyticsChartPalette(
25
+ override: Partial<AnalyticsChartPalette> | undefined,
26
+ ): AnalyticsChartPalette {
27
+ if (!override) return ANALYTICS_CHART_DEFAULT_PALETTE;
28
+ return {
29
+ primary: override.primary ?? ANALYTICS_CHART_DEFAULT_PALETTE.primary,
30
+ compare: override.compare ?? ANALYTICS_CHART_DEFAULT_PALETTE.compare,
31
+ };
32
+ }
33
+
34
+ /** Expand a ramp into N colors for a given theme. */
35
+ export function buildRampColors(
36
+ ramp: AnalyticsChartSegmentRamp,
37
+ count: number,
38
+ theme: "light" | "dark",
39
+ ): string[] {
40
+ if (ramp.kind === "explicit") {
41
+ const list = theme === "light" ? ramp.light : ramp.dark;
42
+ if (list.length === 0) return Array.from({ length: count }, () => "#888");
43
+ return Array.from(
44
+ { length: count },
45
+ (_, i) => list[i < list.length ? i : list.length - 1]!,
46
+ );
47
+ }
48
+ const range = theme === "light" ? ramp.shadeRangeLight : ramp.shadeRangeDark;
49
+ return Array.from({ length: count }, (_, i) => {
50
+ const t = count <= 1 ? 0.5 : i / (count - 1);
51
+ const l = range[0] + t * (range[1] - range[0]);
52
+ return `hsl(${ramp.hue} ${ramp.sat}% ${l.toFixed(1)}%)`;
53
+ });
54
+ }
55
+
56
+ /** Per-segment light/dark colors for `ChartConfig.theme` (SVG only; siblings use inline vars). */
57
+ export function buildSegmentThemeMap(
58
+ series: readonly AnalyticsChartSeries[],
59
+ ramp: AnalyticsChartSegmentRamp,
60
+ ): Record<string, { light: string, dark: string }> {
61
+ const light = buildRampColors(ramp, series.length, "light");
62
+ const dark = buildRampColors(ramp, series.length, "dark");
63
+ const out: Record<string, { light: string, dark: string }> = {};
64
+ series.forEach((s, i) => {
65
+ out[s.key] = { light: light[i]!, dark: dark[i]! };
66
+ });
67
+ return out;
68
+ }
@@ -0,0 +1,169 @@
1
+ import type { ReactNode } from "react";
2
+ import { Area, Bar, Line } from "recharts";
3
+ import type {
4
+ AnalyticsChartDataLayer,
5
+ AnalyticsChartSeries,
6
+ } from "./types";
7
+
8
+ /** Area layers use fill-only `<Area>` plus `<Line>` for the top edge (not Recharts' closed-path stroke). */
9
+ export type RenderDataSeriesArgs = {
10
+ layer: AnalyticsChartDataLayer,
11
+ segmented: boolean,
12
+ segmentSeries: readonly AnalyticsChartSeries[],
13
+ segKey: (segKey: string) => string,
14
+ stackId: string,
15
+ strokeDasharray: string | undefined,
16
+ segmentedStrokeDasharray: string | undefined,
17
+ fillOpacity: number,
18
+ segmentedFillOpacity: number,
19
+ baseOpacity?: number,
20
+ strokeWidth: number,
21
+ segmentedStrokeWidth: number,
22
+ inProgressKeys: { solid: string, dashed: string } | null,
23
+ };
24
+
25
+ /** Return value must be spread into `<ComposedChart>` as siblings — do not wrap in `<Fragment>`. */
26
+ export function renderDataSeries(args: RenderDataSeriesArgs): ReactNode[] {
27
+ const {
28
+ layer,
29
+ segmented,
30
+ segmentSeries,
31
+ segKey,
32
+ stackId,
33
+ strokeDasharray,
34
+ segmentedStrokeDasharray,
35
+ fillOpacity,
36
+ segmentedFillOpacity,
37
+ baseOpacity = 1,
38
+ strokeWidth,
39
+ segmentedStrokeWidth,
40
+ inProgressKeys,
41
+ } = args;
42
+
43
+ const nodes: ReactNode[] = [];
44
+
45
+ if (segmented) {
46
+ segmentSeries.forEach((s, sIdx) => {
47
+ const key = segKey(s.key);
48
+ const nodeKey = `${layer.id}_seg_${s.key}`;
49
+ if (layer.type === "bar") {
50
+ const isTop = sIdx === segmentSeries.length - 1;
51
+ nodes.push(
52
+ <Bar
53
+ key={nodeKey}
54
+ dataKey={key}
55
+ stackId={stackId}
56
+ fill={`var(--color-${key})`}
57
+ radius={isTop ? [2, 2, 0, 0] : 0}
58
+ isAnimationActive={false}
59
+ opacity={baseOpacity}
60
+ />,
61
+ );
62
+ } else if (layer.type === "area") {
63
+ nodes.push(
64
+ <Area
65
+ key={nodeKey}
66
+ dataKey={key}
67
+ stackId={stackId}
68
+ type="linear"
69
+ fill={`var(--color-${key})`}
70
+ fillOpacity={segmentedFillOpacity}
71
+ stroke={`var(--color-${key})`}
72
+ strokeWidth={segmentedStrokeWidth}
73
+ strokeDasharray={segmentedStrokeDasharray}
74
+ isAnimationActive={false}
75
+ opacity={baseOpacity}
76
+ />,
77
+ );
78
+ } else {
79
+ nodes.push(
80
+ <Line
81
+ key={nodeKey}
82
+ dataKey={key}
83
+ type="linear"
84
+ stroke={`var(--color-${key})`}
85
+ strokeWidth={strokeWidth}
86
+ strokeDasharray={segmentedStrokeDasharray}
87
+ dot={false}
88
+ isAnimationActive={false}
89
+ opacity={baseOpacity}
90
+ />,
91
+ );
92
+ }
93
+ });
94
+ return nodes;
95
+ }
96
+
97
+ if (layer.type === "bar") {
98
+ nodes.push(
99
+ <Bar
100
+ key={`${layer.id}_main`}
101
+ dataKey={layer.id}
102
+ fill={`var(--color-${layer.id})`}
103
+ radius={2}
104
+ isAnimationActive={false}
105
+ />,
106
+ );
107
+ return nodes;
108
+ }
109
+
110
+ if (layer.type === "area") {
111
+ nodes.push(
112
+ <Area
113
+ key={`${layer.id}_area`}
114
+ dataKey={layer.id}
115
+ type="linear"
116
+ fill={`var(--color-${layer.id})`}
117
+ fillOpacity={fillOpacity}
118
+ stroke="none"
119
+ isAnimationActive={false}
120
+ />,
121
+ );
122
+ }
123
+
124
+ if (inProgressKeys) {
125
+ nodes.push(
126
+ <Line
127
+ key={`${layer.id}_solid`}
128
+ dataKey={inProgressKeys.solid}
129
+ type="linear"
130
+ stroke={`var(--color-${layer.id})`}
131
+ strokeWidth={strokeWidth}
132
+ strokeDasharray={strokeDasharray}
133
+ dot={false}
134
+ isAnimationActive={false}
135
+ connectNulls
136
+ />,
137
+ );
138
+ nodes.push(
139
+ <Line
140
+ key={`${layer.id}_dashed`}
141
+ dataKey={inProgressKeys.dashed}
142
+ type="linear"
143
+ stroke={`var(--color-${layer.id})`}
144
+ strokeWidth={strokeWidth}
145
+ strokeDasharray="4 4"
146
+ dot={false}
147
+ isAnimationActive={false}
148
+ connectNulls
149
+ opacity={0.85}
150
+ />,
151
+ );
152
+ } else {
153
+ nodes.push(
154
+ <Line
155
+ key={`${layer.id}_line`}
156
+ dataKey={layer.id}
157
+ type="linear"
158
+ stroke={`var(--color-${layer.id})`}
159
+ strokeWidth={strokeWidth}
160
+ strokeDasharray={strokeDasharray}
161
+ dot={false}
162
+ isAnimationActive={false}
163
+ opacity={baseOpacity}
164
+ />,
165
+ );
166
+ }
167
+
168
+ return nodes;
169
+ }