@unicitylabs/sphere-ui 0.1.13 → 0.1.14

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.
@@ -0,0 +1,121 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+
4
+ type DateRangePreset = '1d' | '7d' | '30d' | '90d';
5
+ type DateRangeLabel = DateRangePreset | 'custom';
6
+ interface DateRangeValue {
7
+ label: DateRangeLabel;
8
+ /** ISO date string (YYYY-MM-DD). Only present when label === 'custom'. */
9
+ from?: string;
10
+ to?: string;
11
+ }
12
+ interface DateRangePickerProps {
13
+ value: DateRangeValue;
14
+ onChange: (next: DateRangeValue) => void;
15
+ /**
16
+ * Presets to show. Defaults to the full set the analytics API accepts.
17
+ * Reduce if a particular view doesn't benefit from every preset.
18
+ */
19
+ presets?: DateRangePreset[];
20
+ className?: string;
21
+ }
22
+ declare const PRESET_LABELS: Record<DateRangePreset, string>;
23
+ /**
24
+ * Unified range selector used across admin and developer analytics.
25
+ * Emits a normalized DateRangeValue that maps 1:1 to the backend
26
+ * ?range=... query: presets pass the label, "custom" passes from+to.
27
+ */
28
+ declare function DateRangePicker({ value, onChange, presets, className, }: DateRangePickerProps): react_jsx_runtime.JSX.Element;
29
+
30
+ interface TimeseriesSeries {
31
+ /** Key inside each data point (e.g. 'installs'). */
32
+ dataKey: string;
33
+ /** Human label shown in legend and tooltip. */
34
+ name: string;
35
+ /** Hex color; defaults are applied when omitted. */
36
+ color?: string;
37
+ }
38
+ interface TimeseriesChartProps {
39
+ /** Array of points. Each entry must include a 'date' field (YYYY-MM-DD). */
40
+ data: Array<Record<string, string | number>>;
41
+ series: TimeseriesSeries[];
42
+ /** Visual style. 'area' stacks series with translucent fill, 'line' draws plain lines. */
43
+ variant?: 'line' | 'area';
44
+ /** px height. Defaults to 240 to match dev-portal cards. */
45
+ height?: number;
46
+ stacked?: boolean;
47
+ showLegend?: boolean;
48
+ showGrid?: boolean;
49
+ className?: string;
50
+ /** Empty-state content when data is length 0. Defaults to a subtle placeholder. */
51
+ emptyState?: React.ReactNode;
52
+ }
53
+ /**
54
+ * Shared timeseries chart used for completions, installs, achievements,
55
+ * etc. Encapsulates the Recharts wiring so both admin and developer
56
+ * views render charts identically.
57
+ */
58
+ declare function TimeseriesChart({ data, series, variant, height, stacked, showLegend, showGrid, className, emptyState, }: TimeseriesChartProps): react_jsx_runtime.JSX.Element;
59
+
60
+ interface KPICardProps {
61
+ /** Short uppercase label above the value. */
62
+ label: string;
63
+ /** Primary number to display. Formatted with toLocaleString() by default. */
64
+ value: number | string;
65
+ /** Optional icon (lucide-react component). Rendered in accent color at 16px. */
66
+ icon?: ReactNode;
67
+ /**
68
+ * Previous-period value; when provided, renders a trend pill showing
69
+ * percent delta (+23%) colored green/red/neutral accordingly.
70
+ */
71
+ previousValue?: number;
72
+ /** Hint shown below the value (e.g. "vs previous 30 days"). */
73
+ hint?: string;
74
+ /** Tooltip for the whole card — useful to explain derived metrics. */
75
+ title?: string;
76
+ /** Custom value formatter (e.g. percent "{v}%" or decimal formatting). */
77
+ format?: (value: number | string) => string;
78
+ /** Override accent — defaults to --accent CSS var. */
79
+ accentColor?: string;
80
+ className?: string;
81
+ }
82
+ /**
83
+ * Single KPI tile — title, value, optional delta-vs-previous badge.
84
+ * Same shape as dev-portal's admin-card pattern so both frontends
85
+ * render KPIs identically.
86
+ */
87
+ declare function KPICard({ label, value, icon, previousValue, hint, title, format, accentColor, className, }: KPICardProps): react_jsx_runtime.JSX.Element;
88
+
89
+ interface TopEntity {
90
+ id: string;
91
+ title: string;
92
+ /** Primary numeric value — drives the ranking and the bar fill. */
93
+ value: number;
94
+ /** Optional secondary metric (e.g. points per quest). */
95
+ secondary?: number;
96
+ /** Optional prefix icon. */
97
+ icon?: ReactNode;
98
+ }
99
+ interface TopEntitiesTableProps {
100
+ entities: TopEntity[];
101
+ /** Shown above the list. */
102
+ title?: string;
103
+ /** Label for the primary value column. Default: "Value". */
104
+ valueLabel?: string;
105
+ /** Label for the secondary column (when any row has secondary). */
106
+ secondaryLabel?: string;
107
+ /** Hide the horizontal progress bars. */
108
+ hideBars?: boolean;
109
+ /** Empty-state content when entities is empty. */
110
+ emptyState?: ReactNode;
111
+ /** Override accent for bar fill. */
112
+ accentColor?: string;
113
+ className?: string;
114
+ }
115
+ /**
116
+ * Ranked list with a horizontal bar fill per row. Used for "top quests",
117
+ * "top installed projects", "top users", etc. — any cross-entity ranking.
118
+ */
119
+ declare function TopEntitiesTable({ entities, title, valueLabel, secondaryLabel, hideBars, emptyState, accentColor, className, }: TopEntitiesTableProps): react_jsx_runtime.JSX.Element;
120
+
121
+ export { type DateRangeLabel, DateRangePicker, type DateRangePickerProps, type DateRangePreset, type DateRangeValue, KPICard, type KPICardProps, TimeseriesChart, type TimeseriesChartProps, type TimeseriesSeries, TopEntitiesTable, type TopEntitiesTableProps, type TopEntity, PRESET_LABELS as dateRangePresetLabels };
@@ -0,0 +1,423 @@
1
+ // src/analytics/DateRangePicker.tsx
2
+ import { useState, useRef, useEffect } from "react";
3
+ import { Calendar } from "lucide-react";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ var DEFAULT_PRESETS = ["1d", "7d", "30d", "90d"];
6
+ var PRESET_LABELS = {
7
+ "1d": "1 day",
8
+ "7d": "7 days",
9
+ "30d": "30 days",
10
+ "90d": "90 days"
11
+ };
12
+ function todayIso() {
13
+ return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
14
+ }
15
+ function daysAgoIso(days) {
16
+ return new Date(Date.now() - days * 24 * 60 * 60 * 1e3).toISOString().slice(0, 10);
17
+ }
18
+ function DateRangePicker({
19
+ value,
20
+ onChange,
21
+ presets = DEFAULT_PRESETS,
22
+ className = ""
23
+ }) {
24
+ const [customOpen, setCustomOpen] = useState(false);
25
+ const [draftFrom, setDraftFrom] = useState(value.from ?? daysAgoIso(30));
26
+ const [draftTo, setDraftTo] = useState(value.to ?? todayIso());
27
+ const panelRef = useRef(null);
28
+ useEffect(() => {
29
+ if (!customOpen) return;
30
+ function onDocClick(e) {
31
+ if (panelRef.current && !panelRef.current.contains(e.target)) {
32
+ setCustomOpen(false);
33
+ }
34
+ }
35
+ document.addEventListener("mousedown", onDocClick);
36
+ return () => document.removeEventListener("mousedown", onDocClick);
37
+ }, [customOpen]);
38
+ function selectPreset(preset) {
39
+ setCustomOpen(false);
40
+ onChange({ label: preset });
41
+ }
42
+ function applyCustom() {
43
+ if (!draftFrom || !draftTo) return;
44
+ if (draftFrom >= draftTo) return;
45
+ setCustomOpen(false);
46
+ onChange({ label: "custom", from: draftFrom, to: draftTo });
47
+ }
48
+ const customInvalid = !draftFrom || !draftTo || draftFrom >= draftTo;
49
+ const isCustom = value.label === "custom";
50
+ return /* @__PURE__ */ jsxs("div", { className: `relative inline-flex items-center ${className}`, children: [
51
+ /* @__PURE__ */ jsxs(
52
+ "div",
53
+ {
54
+ role: "group",
55
+ "aria-label": "Date range",
56
+ className: "inline-flex items-center rounded-lg border border-[var(--border,rgba(255,255,255,0.1))] bg-[var(--surface,rgba(255,255,255,0.02))] overflow-hidden",
57
+ children: [
58
+ presets.map((preset) => {
59
+ const active = value.label === preset;
60
+ return /* @__PURE__ */ jsx(
61
+ "button",
62
+ {
63
+ type: "button",
64
+ onClick: () => selectPreset(preset),
65
+ "aria-pressed": active,
66
+ className: `px-3 py-1.5 text-xs font-medium transition-colors ${active ? "bg-[var(--accent,#FF6F00)] text-white" : "text-[var(--text-muted,rgba(255,255,255,0.6))] hover:text-[var(--text-primary,#fff)] hover:bg-[var(--surface-hover,rgba(255,255,255,0.04))]"}`,
67
+ children: preset
68
+ },
69
+ preset
70
+ );
71
+ }),
72
+ /* @__PURE__ */ jsxs(
73
+ "button",
74
+ {
75
+ type: "button",
76
+ onClick: () => setCustomOpen((v) => !v),
77
+ "aria-pressed": isCustom,
78
+ "aria-expanded": customOpen,
79
+ "aria-label": "Custom date range",
80
+ className: `px-3 py-1.5 text-xs font-medium inline-flex items-center gap-1.5 transition-colors border-l border-[var(--border,rgba(255,255,255,0.1))] ${isCustom ? "bg-[var(--accent,#FF6F00)] text-white" : "text-[var(--text-muted,rgba(255,255,255,0.6))] hover:text-[var(--text-primary,#fff)] hover:bg-[var(--surface-hover,rgba(255,255,255,0.04))]"}`,
81
+ children: [
82
+ /* @__PURE__ */ jsx(Calendar, { className: "w-3.5 h-3.5" }),
83
+ isCustom && value.from && value.to ? `${value.from} \u2014 ${value.to}` : "Custom"
84
+ ]
85
+ }
86
+ )
87
+ ]
88
+ }
89
+ ),
90
+ customOpen && /* @__PURE__ */ jsxs(
91
+ "div",
92
+ {
93
+ ref: panelRef,
94
+ className: "absolute top-full right-0 mt-2 p-3 rounded-lg border border-[var(--border,rgba(255,255,255,0.1))] bg-[var(--surface,#0f0f13)] shadow-xl z-50 min-w-[260px]",
95
+ children: [
96
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
97
+ /* @__PURE__ */ jsxs("label", { className: "block text-[11px] uppercase tracking-wider text-[var(--text-muted,rgba(255,255,255,0.5))]", children: [
98
+ "From",
99
+ /* @__PURE__ */ jsx(
100
+ "input",
101
+ {
102
+ type: "date",
103
+ value: draftFrom,
104
+ max: draftTo,
105
+ onChange: (e) => setDraftFrom(e.target.value),
106
+ className: "mt-1 w-full px-2 py-1.5 text-sm rounded border border-[var(--border,rgba(255,255,255,0.1))] bg-[var(--input-bg,rgba(255,255,255,0.03))] text-[var(--text-primary,#fff)] focus:outline-none focus:border-[var(--accent,#FF6F00)]"
107
+ }
108
+ )
109
+ ] }),
110
+ /* @__PURE__ */ jsxs("label", { className: "block text-[11px] uppercase tracking-wider text-[var(--text-muted,rgba(255,255,255,0.5))]", children: [
111
+ "To",
112
+ /* @__PURE__ */ jsx(
113
+ "input",
114
+ {
115
+ type: "date",
116
+ value: draftTo,
117
+ min: draftFrom,
118
+ max: todayIso(),
119
+ onChange: (e) => setDraftTo(e.target.value),
120
+ className: "mt-1 w-full px-2 py-1.5 text-sm rounded border border-[var(--border,rgba(255,255,255,0.1))] bg-[var(--input-bg,rgba(255,255,255,0.03))] text-[var(--text-primary,#fff)] focus:outline-none focus:border-[var(--accent,#FF6F00)]"
121
+ }
122
+ )
123
+ ] })
124
+ ] }),
125
+ /* @__PURE__ */ jsx(
126
+ "button",
127
+ {
128
+ type: "button",
129
+ onClick: applyCustom,
130
+ disabled: customInvalid,
131
+ className: "mt-3 w-full py-1.5 text-xs font-semibold rounded bg-[var(--accent,#FF6F00)] text-white disabled:opacity-40 disabled:cursor-not-allowed hover:opacity-90",
132
+ children: "Apply"
133
+ }
134
+ )
135
+ ]
136
+ }
137
+ )
138
+ ] });
139
+ }
140
+
141
+ // src/analytics/TimeseriesChart.tsx
142
+ import {
143
+ LineChart,
144
+ Line,
145
+ AreaChart,
146
+ Area,
147
+ XAxis,
148
+ YAxis,
149
+ Tooltip,
150
+ ResponsiveContainer,
151
+ CartesianGrid,
152
+ Legend
153
+ } from "recharts";
154
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
155
+ var DEFAULT_COLORS = ["#10b981", "#f43f5e", "#60a5fa", "#a78bfa", "#fbbf24", "#ec4899"];
156
+ function TimeseriesChart({
157
+ data,
158
+ series,
159
+ variant = "area",
160
+ height = 240,
161
+ stacked = false,
162
+ showLegend = true,
163
+ showGrid = true,
164
+ className = "",
165
+ emptyState
166
+ }) {
167
+ if (!data || data.length === 0) {
168
+ return /* @__PURE__ */ jsx2(
169
+ "div",
170
+ {
171
+ className: `flex items-center justify-center text-xs text-[var(--text-muted,rgba(255,255,255,0.4))] ${className}`,
172
+ style: { height },
173
+ children: emptyState ?? "No data for this period"
174
+ }
175
+ );
176
+ }
177
+ const Chart = variant === "area" ? AreaChart : LineChart;
178
+ const gridStroke = "rgba(255,255,255,0.06)";
179
+ const axisColor = "rgba(255,255,255,0.4)";
180
+ return /* @__PURE__ */ jsx2("div", { className, style: { width: "100%", height }, children: /* @__PURE__ */ jsx2(ResponsiveContainer, { children: /* @__PURE__ */ jsxs2(Chart, { data, margin: { top: 8, right: 12, left: -12, bottom: 0 }, children: [
181
+ showGrid && /* @__PURE__ */ jsx2(CartesianGrid, { strokeDasharray: "3 3", stroke: gridStroke }),
182
+ /* @__PURE__ */ jsx2(
183
+ XAxis,
184
+ {
185
+ dataKey: "date",
186
+ stroke: axisColor,
187
+ tick: { fontSize: 11, fill: axisColor },
188
+ tickLine: false,
189
+ axisLine: false
190
+ }
191
+ ),
192
+ /* @__PURE__ */ jsx2(
193
+ YAxis,
194
+ {
195
+ stroke: axisColor,
196
+ tick: { fontSize: 11, fill: axisColor },
197
+ tickLine: false,
198
+ axisLine: false,
199
+ width: 32,
200
+ allowDecimals: false
201
+ }
202
+ ),
203
+ /* @__PURE__ */ jsx2(
204
+ Tooltip,
205
+ {
206
+ contentStyle: {
207
+ background: "var(--surface, #0f0f13)",
208
+ border: "1px solid var(--border, rgba(255,255,255,0.1))",
209
+ borderRadius: 8,
210
+ fontSize: 12
211
+ },
212
+ labelStyle: { color: "var(--text-muted, rgba(255,255,255,0.6))" }
213
+ }
214
+ ),
215
+ showLegend && /* @__PURE__ */ jsx2(
216
+ Legend,
217
+ {
218
+ wrapperStyle: { fontSize: 11, color: axisColor },
219
+ iconType: "circle",
220
+ iconSize: 8
221
+ }
222
+ ),
223
+ series.map((s, i) => {
224
+ const color = s.color ?? DEFAULT_COLORS[i % DEFAULT_COLORS.length];
225
+ if (variant === "area") {
226
+ return /* @__PURE__ */ jsx2(
227
+ Area,
228
+ {
229
+ type: "monotone",
230
+ dataKey: s.dataKey,
231
+ name: s.name,
232
+ stroke: color,
233
+ strokeWidth: 2,
234
+ fill: color,
235
+ fillOpacity: 0.18,
236
+ stackId: stacked ? "stack" : void 0
237
+ },
238
+ s.dataKey
239
+ );
240
+ }
241
+ return /* @__PURE__ */ jsx2(
242
+ Line,
243
+ {
244
+ type: "monotone",
245
+ dataKey: s.dataKey,
246
+ name: s.name,
247
+ stroke: color,
248
+ strokeWidth: 2,
249
+ dot: false,
250
+ activeDot: { r: 4 }
251
+ },
252
+ s.dataKey
253
+ );
254
+ })
255
+ ] }) }) });
256
+ }
257
+
258
+ // src/analytics/KPICard.tsx
259
+ import { TrendingUp, TrendingDown, Minus } from "lucide-react";
260
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
261
+ function defaultFormat(value) {
262
+ if (typeof value === "number") return value.toLocaleString();
263
+ return value;
264
+ }
265
+ function KPICard({
266
+ label,
267
+ value,
268
+ icon,
269
+ previousValue,
270
+ hint,
271
+ title,
272
+ format = defaultFormat,
273
+ accentColor,
274
+ className = ""
275
+ }) {
276
+ const trend = computeTrend(value, previousValue);
277
+ return /* @__PURE__ */ jsxs3(
278
+ "div",
279
+ {
280
+ title,
281
+ className: `rounded-xl border border-[var(--border,rgba(255,255,255,0.08))] bg-[var(--surface,rgba(255,255,255,0.02))] p-5 ${className}`,
282
+ children: [
283
+ /* @__PURE__ */ jsxs3("div", { className: "flex items-center justify-between gap-2 mb-2", children: [
284
+ /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2 min-w-0", children: [
285
+ icon && /* @__PURE__ */ jsx3("span", { className: "shrink-0", style: { color: accentColor ?? "var(--accent, #FF6F00)" }, children: icon }),
286
+ /* @__PURE__ */ jsx3("span", { className: "text-[11px] font-medium uppercase tracking-wider text-[var(--text-muted,rgba(255,255,255,0.6))] truncate", children: label })
287
+ ] }),
288
+ trend && /* @__PURE__ */ jsxs3(
289
+ "span",
290
+ {
291
+ className: `inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded text-[10px] font-semibold ${trend.className}`,
292
+ "aria-label": `${trend.direction} ${trend.absPercent}% vs previous period`,
293
+ children: [
294
+ trend.Icon,
295
+ trend.sign,
296
+ trend.absPercent,
297
+ "%"
298
+ ]
299
+ }
300
+ )
301
+ ] }),
302
+ /* @__PURE__ */ jsx3("div", { className: "text-2xl font-bold text-[var(--text-primary,#fff)] font-mono tabular-nums", children: format(value) }),
303
+ hint && /* @__PURE__ */ jsx3("div", { className: "text-[10px] text-[var(--text-muted,rgba(255,255,255,0.4))] mt-1", children: hint })
304
+ ]
305
+ }
306
+ );
307
+ }
308
+ function computeTrend(value, previousValue) {
309
+ if (previousValue === void 0) return null;
310
+ if (typeof value !== "number") return null;
311
+ if (previousValue === 0) {
312
+ if (value === 0) {
313
+ return {
314
+ direction: "flat",
315
+ sign: "",
316
+ absPercent: 0,
317
+ className: "bg-[var(--surface-hover,rgba(255,255,255,0.04))] text-[var(--text-muted,rgba(255,255,255,0.5))]",
318
+ Icon: /* @__PURE__ */ jsx3(Minus, { className: "w-2.5 h-2.5" })
319
+ };
320
+ }
321
+ return {
322
+ direction: "up",
323
+ sign: "+",
324
+ absPercent: 100,
325
+ className: "bg-emerald-500/10 text-emerald-400",
326
+ Icon: /* @__PURE__ */ jsx3(TrendingUp, { className: "w-2.5 h-2.5" })
327
+ };
328
+ }
329
+ const delta = (value - previousValue) / previousValue * 100;
330
+ const absPercent = Math.round(Math.abs(delta));
331
+ if (absPercent === 0) {
332
+ return {
333
+ direction: "flat",
334
+ sign: "",
335
+ absPercent: 0,
336
+ className: "bg-[var(--surface-hover,rgba(255,255,255,0.04))] text-[var(--text-muted,rgba(255,255,255,0.5))]",
337
+ Icon: /* @__PURE__ */ jsx3(Minus, { className: "w-2.5 h-2.5" })
338
+ };
339
+ }
340
+ if (delta > 0) {
341
+ return {
342
+ direction: "up",
343
+ sign: "+",
344
+ absPercent,
345
+ className: "bg-emerald-500/10 text-emerald-400",
346
+ Icon: /* @__PURE__ */ jsx3(TrendingUp, { className: "w-2.5 h-2.5" })
347
+ };
348
+ }
349
+ return {
350
+ direction: "down",
351
+ sign: "-",
352
+ absPercent,
353
+ className: "bg-rose-500/10 text-rose-400",
354
+ Icon: /* @__PURE__ */ jsx3(TrendingDown, { className: "w-2.5 h-2.5" })
355
+ };
356
+ }
357
+
358
+ // src/analytics/TopEntitiesTable.tsx
359
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
360
+ function TopEntitiesTable({
361
+ entities,
362
+ title,
363
+ valueLabel = "Value",
364
+ secondaryLabel,
365
+ hideBars = false,
366
+ emptyState,
367
+ accentColor,
368
+ className = ""
369
+ }) {
370
+ const hasAnySecondary = entities.some((e) => e.secondary !== void 0);
371
+ const maxValue = entities.length > 0 ? Math.max(...entities.map((e) => e.value)) || 1 : 1;
372
+ return /* @__PURE__ */ jsxs4("div", { className: `rounded-xl border border-[var(--border,rgba(255,255,255,0.08))] bg-[var(--surface,rgba(255,255,255,0.02))] p-5 ${className}`, children: [
373
+ title && /* @__PURE__ */ jsx4("h3", { className: "text-sm font-semibold text-[var(--text-primary,#fff)] mb-4", children: title }),
374
+ entities.length === 0 ? /* @__PURE__ */ jsx4("div", { className: "py-8 text-center text-xs text-[var(--text-muted,rgba(255,255,255,0.4))]", children: emptyState ?? "No entries" }) : /* @__PURE__ */ jsx4("ol", { className: "space-y-2", children: entities.map((e, i) => {
375
+ const pct = hideBars ? 0 : Math.round(e.value / maxValue * 100);
376
+ return /* @__PURE__ */ jsxs4("li", { className: "group", children: [
377
+ /* @__PURE__ */ jsxs4("div", { className: "flex items-center justify-between gap-3 mb-1", children: [
378
+ /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-2.5 min-w-0 flex-1", children: [
379
+ /* @__PURE__ */ jsxs4("span", { className: "text-[10px] font-mono text-[var(--text-muted,rgba(255,255,255,0.35))] w-5 shrink-0", children: [
380
+ i + 1,
381
+ "."
382
+ ] }),
383
+ e.icon && /* @__PURE__ */ jsx4("span", { className: "shrink-0", children: e.icon }),
384
+ /* @__PURE__ */ jsx4("span", { className: "text-xs font-medium text-[var(--text-primary,#fff)] truncate", children: e.title })
385
+ ] }),
386
+ /* @__PURE__ */ jsxs4("div", { className: "flex items-baseline gap-3 shrink-0", children: [
387
+ hasAnySecondary && e.secondary !== void 0 && /* @__PURE__ */ jsxs4("span", { className: "text-[10px] text-[var(--text-muted,rgba(255,255,255,0.5))] font-mono tabular-nums", children: [
388
+ e.secondary.toLocaleString(),
389
+ secondaryLabel ? ` ${secondaryLabel}` : ""
390
+ ] }),
391
+ /* @__PURE__ */ jsx4(
392
+ "span",
393
+ {
394
+ className: "text-xs font-bold font-mono tabular-nums",
395
+ style: { color: accentColor ?? "var(--accent, #FF6F00)" },
396
+ "aria-label": `${e.value} ${valueLabel}`,
397
+ children: e.value.toLocaleString()
398
+ }
399
+ )
400
+ ] })
401
+ ] }),
402
+ !hideBars && /* @__PURE__ */ jsx4("div", { className: "h-1.5 rounded-full bg-[var(--surface-hover,rgba(255,255,255,0.04))] overflow-hidden", children: /* @__PURE__ */ jsx4(
403
+ "div",
404
+ {
405
+ className: "h-full rounded-full transition-all duration-500",
406
+ style: {
407
+ width: `${pct}%`,
408
+ background: accentColor ?? "var(--accent, #FF6F00)",
409
+ opacity: 0.7
410
+ }
411
+ }
412
+ ) })
413
+ ] }, e.id);
414
+ }) })
415
+ ] });
416
+ }
417
+ export {
418
+ DateRangePicker,
419
+ KPICard,
420
+ TimeseriesChart,
421
+ TopEntitiesTable,
422
+ PRESET_LABELS as dateRangePresetLabels
423
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unicitylabs/sphere-ui",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -25,6 +25,10 @@
25
25
  "./hooks": {
26
26
  "import": "./dist/hooks/index.js",
27
27
  "types": "./dist/hooks/index.d.ts"
28
+ },
29
+ "./analytics": {
30
+ "import": "./dist/analytics/index.js",
31
+ "types": "./dist/analytics/index.d.ts"
28
32
  }
29
33
  },
30
34
  "files": [
@@ -46,7 +50,13 @@
46
50
  "@tanstack/react-table": "^8.0.0",
47
51
  "lucide-react": ">=0.400.0",
48
52
  "react": "^19.0.0",
49
- "react-dom": "^19.0.0"
53
+ "react-dom": "^19.0.0",
54
+ "recharts": "^3.0.0"
55
+ },
56
+ "peerDependenciesMeta": {
57
+ "recharts": {
58
+ "optional": true
59
+ }
50
60
  },
51
61
  "devDependencies": {
52
62
  "@dnd-kit/core": "^6.0.0",
@@ -64,6 +74,7 @@
64
74
  "lucide-react": "^0.400.0",
65
75
  "react": "^19.0.0",
66
76
  "react-dom": "^19.0.0",
77
+ "recharts": "^3.8.1",
67
78
  "tsup": "^8.0.0",
68
79
  "typescript": "~5.9.0",
69
80
  "vitest": "^2.1.9"