@strapi-community/plugin-better-auth-dashboard 1.0.0-alpha.9 → 1.0.0-beta.1

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,1217 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const jsxRuntime = require("react/jsx-runtime");
4
+ const designSystem = require("@strapi/design-system");
5
+ const admin = require("@strapi/strapi/admin");
6
+ const react = require("react");
7
+ const reactQuery = require("react-query");
8
+ const styled = require("styled-components");
9
+ const index = require("./index-BHfYu0uA.js");
10
+ const Avatar = require("./Avatar-Cvk89aMx.js");
11
+ const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
12
+ const styled__default = /* @__PURE__ */ _interopDefault(styled);
13
+ const T = {
14
+ bg: "#f6f6f9",
15
+ bgCard: "#ffffff",
16
+ border: "#eaeaef",
17
+ borderHover: "rgba(73,69,255,0.35)",
18
+ accent: "#4945ff",
19
+ green: "#5cb176",
20
+ greenDim: "rgba(92,177,118,0.12)",
21
+ amber: "#d9822f",
22
+ red: "#d02b20",
23
+ redDim: "rgba(208,43,32,0.1)",
24
+ purple: "#8460b8",
25
+ textPrimary: "#32324d",
26
+ textSecondary: "#666687",
27
+ textMuted: "#b8b8c7",
28
+ mono: `'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace`
29
+ };
30
+ const fadeUp = styled.keyframes`
31
+ from { opacity: 0; transform: translateY(10px); }
32
+ to { opacity: 1; transform: translateY(0); }
33
+ `;
34
+ const Wrap = styled__default.default.div`
35
+ padding: 28px 32px 48px;
36
+ background: ${T.bg};
37
+ min-height: 100%;
38
+ display: flex;
39
+ flex-direction: column;
40
+ gap: 22px;
41
+ `;
42
+ const SectionDivider = styled__default.default.div`
43
+ display: flex;
44
+ align-items: center;
45
+ gap: 10px;
46
+ margin-bottom: -6px;
47
+ `;
48
+ const DivLabel = styled__default.default.span`
49
+ font-size: 9px;
50
+ font-weight: 700;
51
+ letter-spacing: 0.14em;
52
+ text-transform: uppercase;
53
+ color: ${T.textMuted};
54
+ white-space: nowrap;
55
+ `;
56
+ const DivLine = styled__default.default.div`
57
+ flex: 1;
58
+ height: 1px;
59
+ background: ${T.border};
60
+ `;
61
+ const Card = styled__default.default.div`
62
+ background: ${T.bgCard};
63
+ border: 1px solid ${T.border};
64
+ border-radius: 12px;
65
+ overflow: hidden;
66
+ position: relative;
67
+ animation: ${fadeUp} 380ms ease both;
68
+ animation-delay: ${(p) => (p.$delay ?? 0) * 55}ms;
69
+ transition: border-color 200ms, box-shadow 200ms;
70
+ &:hover {
71
+ border-color: ${T.borderHover};
72
+ box-shadow: 0 0 0 1px ${T.borderHover}, 0 8px 32px rgba(124,109,250,0.07);
73
+ }
74
+ `;
75
+ const PillGroup = styled__default.default.div`
76
+ display: flex;
77
+ background: rgba(0,0,0,0.03);
78
+ border: 1px solid ${T.border};
79
+ border-radius: 9px;
80
+ padding: 3px;
81
+ gap: 2px;
82
+ `;
83
+ const Pill = styled__default.default.button`
84
+ appearance: none;
85
+ border: none;
86
+ cursor: pointer;
87
+ padding: 5px 16px;
88
+ border-radius: 6px;
89
+ font-size: 11px;
90
+ font-weight: 600;
91
+ letter-spacing: 0.03em;
92
+ transition: background 180ms, color 180ms;
93
+ background: ${(p) => p.$active ? T.accent : "transparent"};
94
+ color: ${(p) => p.$active ? "#fff" : T.textSecondary};
95
+ &:hover {
96
+ background: ${(p) => p.$active ? T.accent : "rgba(0,0,0,0.05)"};
97
+ color: ${(p) => p.$active ? "#fff" : T.textPrimary};
98
+ }
99
+ `;
100
+ const StatGrid = styled__default.default.div`
101
+ display: grid;
102
+ grid-template-columns: repeat(5, 1fr);
103
+ gap: 10px;
104
+ @media (max-width: 1200px) { grid-template-columns: repeat(3, 1fr); }
105
+ `;
106
+ const StatCard = styled__default.default(Card)`
107
+ padding: 18px 20px 0;
108
+ &::after {
109
+ content: '';
110
+ position: absolute;
111
+ top: 0; left: 0; right: 0;
112
+ height: 2px;
113
+ background: ${(p) => p.$accent};
114
+ opacity: 0.75;
115
+ }
116
+ `;
117
+ const StatLabel = styled__default.default.div`
118
+ font-size: 9px;
119
+ font-weight: 700;
120
+ letter-spacing: 0.1em;
121
+ text-transform: uppercase;
122
+ color: ${T.textMuted};
123
+ margin-bottom: 10px;
124
+ `;
125
+ const StatValue = styled__default.default.div`
126
+ font-size: 30px;
127
+ font-weight: 800;
128
+ color: ${T.textPrimary};
129
+ line-height: 1;
130
+ letter-spacing: -0.045em;
131
+ font-variant-numeric: tabular-nums;
132
+ margin-bottom: 10px;
133
+ `;
134
+ const TrendBadge = styled__default.default.span`
135
+ display: inline-flex;
136
+ align-items: center;
137
+ gap: 3px;
138
+ padding: 2px 8px;
139
+ border-radius: 20px;
140
+ font-size: 9px;
141
+ font-weight: 700;
142
+ letter-spacing: 0.03em;
143
+ background: ${(p) => p.$pos ? T.greenDim : T.redDim};
144
+ color: ${(p) => p.$pos ? T.green : T.red};
145
+ margin-bottom: 12px;
146
+ `;
147
+ const SparkWrap = styled__default.default.div`
148
+ height: 44px;
149
+ margin: 0 -20px;
150
+ `;
151
+ const ChartCard = styled__default.default(Card)`
152
+ padding: 20px 20px 12px;
153
+ display: flex;
154
+ flex-direction: column;
155
+ gap: 10px;
156
+ `;
157
+ const ChartHeader = styled__default.default.div`
158
+ display: flex;
159
+ align-items: center;
160
+ justify-content: space-between;
161
+ gap: 12px;
162
+ flex-shrink: 0;
163
+ `;
164
+ const ChartTitle = styled__default.default.div`
165
+ font-size: 12px;
166
+ font-weight: 700;
167
+ color: ${T.textPrimary};
168
+ letter-spacing: 0.01em;
169
+ `;
170
+ const SeriesRow = styled__default.default.div`
171
+ display: flex;
172
+ align-items: center;
173
+ gap: 6px;
174
+ `;
175
+ const SeriesBtn = styled__default.default.button`
176
+ appearance: none;
177
+ border: 1px solid ${(p) => p.$on ? `${p.$c}55` : T.border};
178
+ background: ${(p) => p.$on ? `${p.$c}18` : "transparent"};
179
+ color: ${(p) => p.$on ? p.$c : T.textMuted};
180
+ font-size: 10px;
181
+ font-weight: 600;
182
+ padding: 3px 10px 3px 8px;
183
+ border-radius: 6px;
184
+ cursor: pointer;
185
+ display: flex;
186
+ align-items: center;
187
+ gap: 5px;
188
+ transition: all 160ms;
189
+ &:hover {
190
+ border-color: ${(p) => `${p.$c}88`};
191
+ color: ${(p) => p.$c};
192
+ background: ${(p) => `${p.$c}22`};
193
+ }
194
+ `;
195
+ const SDot = styled__default.default.span`
196
+ width: 6px; height: 6px;
197
+ border-radius: 50%;
198
+ background: ${(p) => p.$c};
199
+ display: inline-block;
200
+ flex-shrink: 0;
201
+ `;
202
+ const HoverInfo = styled__default.default.div`
203
+ font-size: 11px;
204
+ color: ${T.textSecondary};
205
+ font-variant-numeric: tabular-nums;
206
+ font-family: ${T.mono};
207
+ min-height: 15px;
208
+ `;
209
+ const TwoPanel = styled__default.default.div`
210
+ display: grid;
211
+ grid-template-columns: ${(p) => p.$ratio ?? "1fr 288px"};
212
+ gap: 10px;
213
+ align-items: stretch;
214
+ @media (max-width: 900px) { grid-template-columns: 1fr; }
215
+ `;
216
+ const FeedCard = styled__default.default(Card)`
217
+ display: flex;
218
+ flex-direction: column;
219
+ overflow: hidden;
220
+ `;
221
+ const FeedHead = styled__default.default.div`
222
+ padding: 13px 16px 9px;
223
+ border-bottom: 1px solid ${T.border};
224
+ font-size: 11px;
225
+ font-weight: 700;
226
+ color: ${T.textPrimary};
227
+ flex-shrink: 0;
228
+ display: flex;
229
+ justify-content: space-between;
230
+ align-items: center;
231
+ `;
232
+ const FeedSelect = styled__default.default.select`
233
+ appearance: none;
234
+ border: 1px solid ${T.border};
235
+ border-radius: 6px;
236
+ background: ${T.bgCard};
237
+ color: ${T.textSecondary};
238
+ font-size: 10px;
239
+ font-weight: 600;
240
+ padding: 3px 22px 3px 8px;
241
+ cursor: pointer;
242
+ outline: none;
243
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23666687'/%3E%3C/svg%3E");
244
+ background-repeat: no-repeat;
245
+ background-position: right 7px center;
246
+ transition: border-color 160ms;
247
+ &:hover, &:focus { border-color: ${T.accent}; color: ${T.textPrimary}; }
248
+ `;
249
+ const FeedScroll = styled__default.default.div`
250
+ flex: 1;
251
+ overflow-y: auto;
252
+ min-height: 0;
253
+ &::-webkit-scrollbar { width: 4px; }
254
+ &::-webkit-scrollbar-thumb { background: ${T.textMuted}; border-radius: 2px; }
255
+ `;
256
+ const FeedItem = styled__default.default.div`
257
+ padding: 9px 14px;
258
+ border-bottom: 1px solid #f0f0f5;
259
+ display: flex;
260
+ flex-direction: column;
261
+ gap: 3px;
262
+ transition: background 130ms;
263
+ &:hover { background: #fafaff; }
264
+ &:last-child { border-bottom: none; }
265
+ `;
266
+ const FeedTop = styled__default.default.div`
267
+ display: flex;
268
+ align-items: center;
269
+ justify-content: space-between;
270
+ gap: 6px;
271
+ `;
272
+ const FeedName = styled__default.default.span`
273
+ font-size: 11px;
274
+ font-weight: 600;
275
+ color: ${T.textPrimary};
276
+ overflow: hidden;
277
+ text-overflow: ellipsis;
278
+ white-space: nowrap;
279
+ `;
280
+ const FeedEmail = styled__default.default.span`
281
+ font-size: 10px;
282
+ color: ${T.textSecondary};
283
+ overflow: hidden;
284
+ text-overflow: ellipsis;
285
+ white-space: nowrap;
286
+ display: block;
287
+ `;
288
+ const FeedMeta = styled__default.default.span`
289
+ font-size: 9px;
290
+ color: ${T.textMuted};
291
+ white-space: nowrap;
292
+ font-variant-numeric: tabular-nums;
293
+ `;
294
+ const RtnCard = styled__default.default(Card)`
295
+ padding: 20px;
296
+ display: flex;
297
+ flex-direction: column;
298
+ gap: 12px;
299
+ `;
300
+ const RtnRow = styled__default.default.div`
301
+ display: flex;
302
+ align-items: center;
303
+ gap: 10px;
304
+ padding: 4px 6px;
305
+ border-radius: 7px;
306
+ transition: background 130ms;
307
+ background: ${(p) => p.$hov ? "#f0f0f6" : "transparent"};
308
+ cursor: default;
309
+ `;
310
+ const RtnLabel = styled__default.default.span`
311
+ font-size: 10px;
312
+ color: ${T.textSecondary};
313
+ min-width: 80px;
314
+ text-align: right;
315
+ white-space: nowrap;
316
+ `;
317
+ const RtnSize = styled__default.default.span`
318
+ font-size: 9px;
319
+ color: ${T.textMuted};
320
+ min-width: 52px;
321
+ text-align: right;
322
+ white-space: nowrap;
323
+ font-variant-numeric: tabular-nums;
324
+ `;
325
+ const RtnTrack = styled__default.default.div`
326
+ flex: 1;
327
+ height: 10px;
328
+ background: #eaeaef;
329
+ border-radius: 4px;
330
+ overflow: hidden;
331
+ `;
332
+ const RtnBar = styled__default.default.div`
333
+ width: ${(p) => Math.max(p.$w, 0.5)}%;
334
+ height: 100%;
335
+ background: hsl(${(p) => p.$hue}, 60%, 50%);
336
+ border-radius: 4px;
337
+ transition: width 0.55s cubic-bezier(0.25, 0.46, 0.45, 0.94);
338
+ `;
339
+ const RtnPct = styled__default.default.span`
340
+ font-size: 10px;
341
+ font-weight: 700;
342
+ color: hsl(${(p) => p.$hue}, 60%, 60%);
343
+ min-width: 38px;
344
+ text-align: right;
345
+ font-variant-numeric: tabular-nums;
346
+ font-family: ${T.mono};
347
+ `;
348
+ const RtnTip = styled__default.default.div`
349
+ font-size: 10px;
350
+ color: ${T.textSecondary};
351
+ min-height: 14px;
352
+ font-variant-numeric: tabular-nums;
353
+ padding: 0 6px;
354
+ `;
355
+ const RingGrid = styled__default.default.div`
356
+ display: flex;
357
+ flex-direction: column;
358
+ gap: 10px;
359
+ `;
360
+ const RingCard = styled__default.default(Card)`
361
+ padding: 18px 20px;
362
+ display: flex;
363
+ align-items: center;
364
+ gap: 16px;
365
+ flex: 1;
366
+ `;
367
+ const RingInfo = styled__default.default.div`
368
+ display: flex;
369
+ flex-direction: column;
370
+ gap: 4px;
371
+ `;
372
+ const RingVal = styled__default.default.div`
373
+ font-size: 24px;
374
+ font-weight: 800;
375
+ color: ${T.textPrimary};
376
+ letter-spacing: -0.04em;
377
+ font-variant-numeric: tabular-nums;
378
+ line-height: 1;
379
+ `;
380
+ const RingLabel = styled__default.default.div`
381
+ font-size: 9px;
382
+ font-weight: 700;
383
+ text-transform: uppercase;
384
+ letter-spacing: 0.1em;
385
+ color: ${T.textMuted};
386
+ `;
387
+ const Empty = styled__default.default.div`
388
+ display: flex;
389
+ flex-direction: column;
390
+ align-items: center;
391
+ justify-content: center;
392
+ padding: 32px;
393
+ color: ${T.textMuted};
394
+ font-size: 12px;
395
+ gap: 8px;
396
+ `;
397
+ function useCountUp(target, duration = 850) {
398
+ const [count, setCount] = react.useState(0);
399
+ const prevRef = react.useRef(0);
400
+ react.useEffect(() => {
401
+ let raf;
402
+ let start = 0;
403
+ const from = prevRef.current;
404
+ const diff = target - from;
405
+ const step = (ts) => {
406
+ if (!start) start = ts;
407
+ const progress = Math.min((ts - start) / duration, 1);
408
+ const eased = 1 - (1 - progress) ** 3;
409
+ setCount(Math.round(from + eased * diff));
410
+ if (progress < 1) raf = requestAnimationFrame(step);
411
+ else prevRef.current = target;
412
+ };
413
+ raf = requestAnimationFrame(step);
414
+ return () => cancelAnimationFrame(raf);
415
+ }, [target, duration]);
416
+ return count;
417
+ }
418
+ function Sparkline({
419
+ data,
420
+ color,
421
+ id
422
+ }) {
423
+ const W = 300;
424
+ const H = 44;
425
+ if (data.length < 2) return /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "100%", height: H });
426
+ const min = Math.min(...data);
427
+ const max = Math.max(...data, min + 1);
428
+ const pts = data.map((v, i) => ({
429
+ x: i / (data.length - 1) * W,
430
+ y: H - 3 - (v - min) / (max - min) * (H - 8)
431
+ }));
432
+ const line = pts.reduce((a, p, i) => {
433
+ if (i === 0) return `M ${p.x} ${p.y}`;
434
+ const pr = pts[i - 1];
435
+ const cx = (pr.x + p.x) / 2;
436
+ return `${a} C ${cx} ${pr.y} ${cx} ${p.y} ${p.x} ${p.y}`;
437
+ }, "");
438
+ const area = `${line} L ${pts.at(-1).x} ${H} L 0 ${H} Z`;
439
+ const gid = `spk-${id}`;
440
+ return /* @__PURE__ */ jsxRuntime.jsxs(
441
+ "svg",
442
+ {
443
+ width: "100%",
444
+ height: H,
445
+ viewBox: `0 0 ${W} ${H}`,
446
+ preserveAspectRatio: "none",
447
+ "aria-hidden": true,
448
+ children: [
449
+ /* @__PURE__ */ jsxRuntime.jsx("defs", { children: /* @__PURE__ */ jsxRuntime.jsxs("linearGradient", { id: gid, x1: "0", y1: "0", x2: "0", y2: "1", children: [
450
+ /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "0%", stopColor: color, stopOpacity: "0.32" }),
451
+ /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "100%", stopColor: color, stopOpacity: "0" })
452
+ ] }) }),
453
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: area, fill: `url(#${gid})` }),
454
+ /* @__PURE__ */ jsxRuntime.jsx(
455
+ "path",
456
+ {
457
+ d: line,
458
+ stroke: color,
459
+ strokeWidth: "1.6",
460
+ fill: "none",
461
+ strokeLinecap: "round"
462
+ }
463
+ )
464
+ ]
465
+ }
466
+ );
467
+ }
468
+ const ALL_SERIES = [
469
+ { key: "totalUsers", color: T.accent, label: "Total" },
470
+ { key: "newUsers", color: T.green, label: "New" },
471
+ { key: "activeUsers", color: T.amber, label: "Active" }
472
+ ];
473
+ function GrowthChart({
474
+ data,
475
+ hovered,
476
+ onHover,
477
+ activeSeries
478
+ }) {
479
+ const W = 600;
480
+ const H = 200;
481
+ const PL = 40;
482
+ const PR = 12;
483
+ const PT = 12;
484
+ const PB = 28;
485
+ const CW = W - PL - PR;
486
+ const CH = H - PT - PB;
487
+ if (!data.length) {
488
+ return /* @__PURE__ */ jsxRuntime.jsxs(Empty, { children: [
489
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 24 }, children: "📊" }),
490
+ /* @__PURE__ */ jsxRuntime.jsx("div", { children: "No growth data for this period" })
491
+ ] });
492
+ }
493
+ const active = ALL_SERIES.filter((s) => activeSeries.has(s.key));
494
+ const allVals = data.flatMap((d) => active.map((s) => d[s.key]));
495
+ const maxV = Math.max(...allVals, 10);
496
+ const yMax = Math.ceil(maxV * 1.15);
497
+ const xp = (i) => PL + i / Math.max(data.length - 1, 1) * CW;
498
+ const yp = (v) => PT + (1 - v / yMax) * CH;
499
+ const smooth = (vals) => {
500
+ const pts = vals.map((v, i) => ({ x: xp(i), y: yp(v) }));
501
+ const path = pts.reduce((a, p, i) => {
502
+ if (i === 0) return `M ${p.x} ${p.y}`;
503
+ const pr = pts[i - 1];
504
+ const cx = (pr.x + p.x) / 2;
505
+ return `${a} C ${cx} ${pr.y} ${cx} ${p.y} ${p.x} ${p.y}`;
506
+ }, "");
507
+ const area = `${path} L ${pts.at(-1).x} ${PT + CH} L ${pts[0].x} ${PT + CH} Z`;
508
+ return { pts, path, area };
509
+ };
510
+ const lines = active.map((s) => ({
511
+ ...s,
512
+ ...smooth(data.map((d) => d[s.key]))
513
+ }));
514
+ const yTicks = Array.from({ length: 5 }, (_, i) => {
515
+ const v = yMax * (4 - i) / 4;
516
+ return { v: Math.round(v), y: yp(v) };
517
+ });
518
+ const step = Math.max(1, Math.ceil(data.length / 8));
519
+ const segW = CW / Math.max(data.length - 1, 1);
520
+ return /* @__PURE__ */ jsxRuntime.jsxs(
521
+ "svg",
522
+ {
523
+ width: "100%",
524
+ viewBox: `0 0 ${W} ${H}`,
525
+ preserveAspectRatio: "xMidYMid meet",
526
+ style: { display: "block" },
527
+ "aria-label": "User growth chart",
528
+ role: "img",
529
+ children: [
530
+ /* @__PURE__ */ jsxRuntime.jsx("defs", { children: lines.map((s) => /* @__PURE__ */ jsxRuntime.jsxs(
531
+ "linearGradient",
532
+ {
533
+ id: `ag-${s.key}`,
534
+ x1: "0",
535
+ y1: "0",
536
+ x2: "0",
537
+ y2: "1",
538
+ children: [
539
+ /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "0%", stopColor: s.color, stopOpacity: "0.18" }),
540
+ /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "100%", stopColor: s.color, stopOpacity: "0" })
541
+ ]
542
+ },
543
+ s.key
544
+ )) }),
545
+ yTicks.map((t) => /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
546
+ /* @__PURE__ */ jsxRuntime.jsx(
547
+ "line",
548
+ {
549
+ x1: PL,
550
+ y1: t.y,
551
+ x2: W - PR,
552
+ y2: t.y,
553
+ stroke: "#eaeaef",
554
+ strokeWidth: "1"
555
+ }
556
+ ),
557
+ /* @__PURE__ */ jsxRuntime.jsx(
558
+ "text",
559
+ {
560
+ x: PL - 6,
561
+ y: t.y + 4,
562
+ textAnchor: "end",
563
+ fill: T.textMuted,
564
+ fontSize: "9",
565
+ children: t.v >= 1e3 ? `${(t.v / 1e3).toFixed(1)}k` : t.v
566
+ }
567
+ )
568
+ ] }, t.v)),
569
+ /* @__PURE__ */ jsxRuntime.jsx(
570
+ "line",
571
+ {
572
+ x1: PL,
573
+ y1: PT + CH,
574
+ x2: W - PR,
575
+ y2: PT + CH,
576
+ stroke: "#eaeaef",
577
+ strokeWidth: "1"
578
+ }
579
+ ),
580
+ lines.map((s) => /* @__PURE__ */ jsxRuntime.jsx("path", { d: s.area, fill: `url(#ag-${s.key})` }, `area-${s.key}`)),
581
+ lines.map((s) => /* @__PURE__ */ jsxRuntime.jsx(
582
+ "path",
583
+ {
584
+ d: s.path,
585
+ stroke: s.color,
586
+ strokeWidth: "2",
587
+ fill: "none",
588
+ strokeLinecap: "round",
589
+ strokeLinejoin: "round"
590
+ },
591
+ `line-${s.key}`
592
+ )),
593
+ data.map((d, i) => {
594
+ if (i % step !== 0 && i !== data.length - 1) return null;
595
+ return /* @__PURE__ */ jsxRuntime.jsx(
596
+ "text",
597
+ {
598
+ x: xp(i),
599
+ y: H - 6,
600
+ textAnchor: "middle",
601
+ fill: T.textMuted,
602
+ fontSize: "9",
603
+ children: d.label
604
+ },
605
+ i
606
+ );
607
+ }),
608
+ data.map((d, i) => {
609
+ const isHov = hovered === i;
610
+ return (
611
+ // biome-ignore lint/a11y/noStaticElementInteractions: SVG chart hover
612
+ /* @__PURE__ */ jsxRuntime.jsxs(
613
+ "g",
614
+ {
615
+ onMouseEnter: () => onHover(i),
616
+ onMouseLeave: () => onHover(null),
617
+ style: { cursor: "crosshair" },
618
+ children: [
619
+ /* @__PURE__ */ jsxRuntime.jsx(
620
+ "rect",
621
+ {
622
+ x: xp(i) - segW / 2,
623
+ y: PT,
624
+ width: segW,
625
+ height: CH,
626
+ fill: "transparent"
627
+ }
628
+ ),
629
+ isHov && /* @__PURE__ */ jsxRuntime.jsx(
630
+ "line",
631
+ {
632
+ x1: xp(i),
633
+ y1: PT,
634
+ x2: xp(i),
635
+ y2: PT + CH,
636
+ stroke: "rgba(50,50,77,0.2)",
637
+ strokeWidth: "1",
638
+ strokeDasharray: "3 3"
639
+ }
640
+ ),
641
+ lines.map((s) => /* @__PURE__ */ jsxRuntime.jsx(
642
+ "circle",
643
+ {
644
+ cx: xp(i),
645
+ cy: yp(d[s.key]),
646
+ r: isHov ? 4 : 2.5,
647
+ fill: s.color,
648
+ stroke: T.bg,
649
+ strokeWidth: isHov ? 2 : 1,
650
+ opacity: isHov ? 1 : 0.5
651
+ },
652
+ s.key
653
+ ))
654
+ ]
655
+ },
656
+ i
657
+ )
658
+ );
659
+ })
660
+ ]
661
+ }
662
+ );
663
+ }
664
+ function ProgressRing({
665
+ value,
666
+ max,
667
+ color,
668
+ size = 56
669
+ }) {
670
+ const r = (size - 9) / 2;
671
+ const circ = 2 * Math.PI * r;
672
+ const pct = Math.min(value / Math.max(max, 1), 1);
673
+ const offset = circ * (1 - pct);
674
+ return /* @__PURE__ */ jsxRuntime.jsxs(
675
+ "svg",
676
+ {
677
+ width: size,
678
+ height: size,
679
+ style: { flexShrink: 0 },
680
+ "aria-hidden": true,
681
+ children: [
682
+ /* @__PURE__ */ jsxRuntime.jsx(
683
+ "circle",
684
+ {
685
+ cx: size / 2,
686
+ cy: size / 2,
687
+ r,
688
+ fill: "none",
689
+ stroke: "#e5e7eb",
690
+ strokeWidth: "7"
691
+ }
692
+ ),
693
+ /* @__PURE__ */ jsxRuntime.jsx(
694
+ "circle",
695
+ {
696
+ cx: size / 2,
697
+ cy: size / 2,
698
+ r,
699
+ fill: "none",
700
+ stroke: color,
701
+ strokeWidth: "7",
702
+ strokeDasharray: `${circ} ${circ}`,
703
+ strokeDashoffset: offset,
704
+ strokeLinecap: "round",
705
+ transform: `rotate(-90 ${size / 2} ${size / 2})`,
706
+ style: {
707
+ transition: "stroke-dashoffset 0.9s cubic-bezier(0.4,0,0.2,1)"
708
+ }
709
+ }
710
+ )
711
+ ]
712
+ }
713
+ );
714
+ }
715
+ function relTime(date) {
716
+ const diff = Date.now() - new Date(date).getTime();
717
+ const mins = Math.floor(diff / 6e4);
718
+ if (mins < 1) return "just now";
719
+ if (mins < 60) return `${mins}m`;
720
+ const hrs = Math.floor(mins / 60);
721
+ if (hrs < 24) return `${hrs}h`;
722
+ return `${Math.floor(hrs / 24)}d`;
723
+ }
724
+ function rateHue(r) {
725
+ return r >= 70 ? 142 : r >= 40 ? 38 : 4;
726
+ }
727
+ function StatItem({
728
+ id,
729
+ label,
730
+ value,
731
+ pct,
732
+ sparkline,
733
+ color,
734
+ delay = 0
735
+ }) {
736
+ const animated = useCountUp(value);
737
+ const isPos = pct === void 0 || pct >= 0;
738
+ return /* @__PURE__ */ jsxRuntime.jsxs(StatCard, { $delay: delay, $accent: color, children: [
739
+ /* @__PURE__ */ jsxRuntime.jsx(StatLabel, { children: label }),
740
+ /* @__PURE__ */ jsxRuntime.jsx(StatValue, { children: animated.toLocaleString() }),
741
+ pct !== void 0 && /* @__PURE__ */ jsxRuntime.jsxs(TrendBadge, { $pos: isPos, children: [
742
+ isPos ? "↑" : "↓",
743
+ " ",
744
+ Math.abs(pct).toFixed(1),
745
+ "%"
746
+ ] }),
747
+ sparkline && sparkline.length > 1 && /* @__PURE__ */ jsxRuntime.jsx(SparkWrap, { children: /* @__PURE__ */ jsxRuntime.jsx(Sparkline, { data: sparkline, color, id }) })
748
+ ] });
749
+ }
750
+ function OverviewPage() {
751
+ const { get } = admin.useFetchClient();
752
+ const [period, setPeriod] = react.useState(
753
+ "weekly"
754
+ );
755
+ const [feedMode, setFeedMode] = react.useState("signups");
756
+ const [hovIdx, setHovIdx] = react.useState(null);
757
+ const [rtnHov, setRtnHov] = react.useState(null);
758
+ const [activeSeries, setActiveSeries] = react.useState(
759
+ () => /* @__PURE__ */ new Set(["totalUsers", "newUsers", "activeUsers"])
760
+ );
761
+ const toggleSeries = (key) => {
762
+ setActiveSeries((prev) => {
763
+ const next = new Set(prev);
764
+ if (next.has(key) && next.size > 1) next.delete(key);
765
+ else next.add(key);
766
+ return next;
767
+ });
768
+ };
769
+ const statsQuery = reactQuery.useQuery({
770
+ queryKey: ["dash-user-stats"],
771
+ queryFn: async () => {
772
+ const r = await index.client.dash.userStats();
773
+ if (r.error) throw new Error(r.error.message ?? "Failed");
774
+ return r.data;
775
+ }
776
+ });
777
+ const graphQuery = reactQuery.useQuery({
778
+ queryKey: ["dash-user-graph", period],
779
+ queryFn: async () => {
780
+ const r = await index.client.dash.userGraphData({ query: { period } });
781
+ if (r.error) throw new Error(r.error.message ?? "Failed");
782
+ return r.data;
783
+ }
784
+ });
785
+ const retentionQuery = reactQuery.useQuery({
786
+ queryKey: ["dash-user-retention", period],
787
+ queryFn: async () => {
788
+ const r = await index.client.dash.userRetentionData({ query: { period } });
789
+ if (r.error) throw new Error(r.error.message ?? "Failed");
790
+ return r.data;
791
+ }
792
+ });
793
+ const usersQuery = reactQuery.useQuery({
794
+ queryKey: ["dash-recent-users"],
795
+ queryFn: async () => {
796
+ const r = await index.client.dash.listUsers({
797
+ query: { limit: 12, offset: 0, sortBy: "createdAt", sortOrder: "desc" }
798
+ });
799
+ if (r.error) throw new Error(r.error.message ?? "Failed");
800
+ return r.data;
801
+ }
802
+ });
803
+ const sessionsQuery = reactQuery.useQuery({
804
+ queryKey: ["dash-recent-sessions"],
805
+ queryFn: async () => {
806
+ const { data } = await get(
807
+ "/better-auth-dashboard/db?uid=plugin::better-auth.session&pagination[pageSize]=12&sort[0]=updatedAt:desc"
808
+ );
809
+ return data.results ?? [];
810
+ },
811
+ refetchInterval: 3e4
812
+ });
813
+ const orgsQuery = reactQuery.useQuery({
814
+ queryKey: ["dash-orgs-count"],
815
+ queryFn: async () => {
816
+ const r = await index.client.dash.listOrganizations({
817
+ query: { page: 1, limit: 1 }
818
+ });
819
+ if (r.error) throw new Error(r.error.message ?? "Failed");
820
+ return r.data;
821
+ }
822
+ });
823
+ const sessionsRaw = sessionsQuery.data ?? [];
824
+ const activeUserIds = [];
825
+ const lastActiveByUserId = /* @__PURE__ */ new Map();
826
+ for (const s of sessionsRaw) {
827
+ const uid = String(s.userId);
828
+ if (!lastActiveByUserId.has(uid)) {
829
+ lastActiveByUserId.set(uid, s.updatedAt ?? s.createdAt);
830
+ activeUserIds.push(uid);
831
+ }
832
+ }
833
+ const activeUsersQuery = reactQuery.useQuery({
834
+ queryKey: ["dash-active-users", activeUserIds],
835
+ queryFn: async () => {
836
+ if (activeUserIds.length === 0) return [];
837
+ const params = new URLSearchParams({ uid: "plugin::better-auth.user" });
838
+ for (let i = 0; i < activeUserIds.length; i++) {
839
+ params.set(`filters[id][$in][${i}]`, activeUserIds[i]);
840
+ }
841
+ params.set("pagination[pageSize]", "12");
842
+ const { data } = await get(
843
+ `/better-auth-dashboard/db?${params}`
844
+ );
845
+ return data.results ?? [];
846
+ },
847
+ enabled: feedMode === "active" && activeUserIds.length > 0
848
+ });
849
+ const chartRef = react.useRef(null);
850
+ const [chartHeight, setChartHeight] = react.useState(0);
851
+ react.useEffect(() => {
852
+ const el = chartRef.current;
853
+ if (!el) return;
854
+ const ro = new ResizeObserver((entries) => {
855
+ for (const entry of entries) setChartHeight(entry.contentRect.height);
856
+ });
857
+ ro.observe(el);
858
+ return () => ro.disconnect();
859
+ }, []);
860
+ if (statsQuery.isLoading) {
861
+ return /* @__PURE__ */ jsxRuntime.jsx(
862
+ designSystem.Flex,
863
+ {
864
+ justifyContent: "center",
865
+ alignItems: "center",
866
+ padding: 12,
867
+ style: { background: T.bg, minHeight: "100%" },
868
+ children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading…" })
869
+ }
870
+ );
871
+ }
872
+ const stats = statsQuery.data;
873
+ if (!stats) return null;
874
+ const graphData = graphQuery.data?.data ?? [];
875
+ const rtnData = retentionQuery.data?.data ?? [];
876
+ const users = usersQuery.data?.users ?? [];
877
+ const orgCount = orgsQuery.data?.total ?? 0;
878
+ const activeUserMap = /* @__PURE__ */ new Map();
879
+ for (const u of activeUsersQuery.data ?? []) {
880
+ activeUserMap.set(String(u.id), u);
881
+ }
882
+ const sortedActiveUsers = activeUserIds.map((uid) => activeUserMap.get(uid)).filter((u) => u !== void 0);
883
+ const totalSpark = graphData.map((d) => d.totalUsers);
884
+ const newSpark = graphData.map((d) => d.newUsers);
885
+ const activeSpark = graphData.map((d) => d.activeUsers);
886
+ const hovRow = hovIdx !== null ? graphData[hovIdx] : null;
887
+ const rtnHovRow = rtnHov !== null ? rtnData[rtnHov] : null;
888
+ const activeMax = Math.max(
889
+ stats.activeUsers.daily.active ?? 0,
890
+ stats.activeUsers.weekly.active ?? 0,
891
+ stats.activeUsers.monthly.active ?? 0,
892
+ 1
893
+ );
894
+ return /* @__PURE__ */ jsxRuntime.jsxs(Wrap, { "data-testid": "overview-page", children: [
895
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "flex-end", children: [
896
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
897
+ /* @__PURE__ */ jsxRuntime.jsx(
898
+ "div",
899
+ {
900
+ style: {
901
+ fontSize: 22,
902
+ fontWeight: 800,
903
+ color: T.textPrimary,
904
+ letterSpacing: "-0.03em"
905
+ },
906
+ children: "Overview"
907
+ }
908
+ ),
909
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 11, color: T.textSecondary, marginTop: 4 }, children: (/* @__PURE__ */ new Date()).toLocaleDateString(void 0, {
910
+ weekday: "long",
911
+ year: "numeric",
912
+ month: "long",
913
+ day: "numeric"
914
+ }) })
915
+ ] }),
916
+ /* @__PURE__ */ jsxRuntime.jsx(PillGroup, { children: ["daily", "weekly", "monthly"].map((p) => /* @__PURE__ */ jsxRuntime.jsx(Pill, { $active: period === p, onClick: () => setPeriod(p), children: p[0].toUpperCase() + p.slice(1) }, p)) })
917
+ ] }),
918
+ /* @__PURE__ */ jsxRuntime.jsxs(SectionDivider, { children: [
919
+ /* @__PURE__ */ jsxRuntime.jsx(DivLabel, { children: "Metrics" }),
920
+ /* @__PURE__ */ jsxRuntime.jsx(DivLine, {})
921
+ ] }),
922
+ /* @__PURE__ */ jsxRuntime.jsxs(StatGrid, { children: [
923
+ /* @__PURE__ */ jsxRuntime.jsx(
924
+ StatItem,
925
+ {
926
+ id: "total",
927
+ label: "Total Users",
928
+ value: stats.total ?? 0,
929
+ sparkline: totalSpark,
930
+ color: T.accent,
931
+ delay: 0
932
+ }
933
+ ),
934
+ /* @__PURE__ */ jsxRuntime.jsx(
935
+ StatItem,
936
+ {
937
+ id: "d-sig",
938
+ label: "Daily Sign-ups",
939
+ value: stats.daily.signUps ?? 0,
940
+ pct: stats.daily.percentage ?? void 0,
941
+ sparkline: newSpark,
942
+ color: T.green,
943
+ delay: 1
944
+ }
945
+ ),
946
+ /* @__PURE__ */ jsxRuntime.jsx(
947
+ StatItem,
948
+ {
949
+ id: "w-sig",
950
+ label: "Weekly Sign-ups",
951
+ value: stats.weekly.signUps ?? 0,
952
+ pct: stats.weekly.percentage ?? void 0,
953
+ sparkline: newSpark,
954
+ color: T.green,
955
+ delay: 2
956
+ }
957
+ ),
958
+ /* @__PURE__ */ jsxRuntime.jsx(
959
+ StatItem,
960
+ {
961
+ id: "m-sig",
962
+ label: "Monthly Sign-ups",
963
+ value: stats.monthly.signUps ?? 0,
964
+ pct: stats.monthly.percentage ?? void 0,
965
+ sparkline: newSpark,
966
+ color: T.green,
967
+ delay: 3
968
+ }
969
+ ),
970
+ /* @__PURE__ */ jsxRuntime.jsx(
971
+ StatItem,
972
+ {
973
+ id: "orgs",
974
+ label: "Organizations",
975
+ value: orgCount,
976
+ color: T.purple,
977
+ delay: 4
978
+ }
979
+ )
980
+ ] }),
981
+ /* @__PURE__ */ jsxRuntime.jsxs(SectionDivider, { children: [
982
+ /* @__PURE__ */ jsxRuntime.jsx(DivLabel, { children: "Growth" }),
983
+ /* @__PURE__ */ jsxRuntime.jsx(DivLine, {})
984
+ ] }),
985
+ /* @__PURE__ */ jsxRuntime.jsxs(TwoPanel, { children: [
986
+ /* @__PURE__ */ jsxRuntime.jsxs(ChartCard, { ref: chartRef, $delay: 5, children: [
987
+ /* @__PURE__ */ jsxRuntime.jsxs(ChartHeader, { children: [
988
+ /* @__PURE__ */ jsxRuntime.jsx(ChartTitle, { children: "User Growth" }),
989
+ /* @__PURE__ */ jsxRuntime.jsx(SeriesRow, { children: ALL_SERIES.map((s) => /* @__PURE__ */ jsxRuntime.jsxs(
990
+ SeriesBtn,
991
+ {
992
+ $on: activeSeries.has(s.key),
993
+ $c: s.color,
994
+ onClick: () => toggleSeries(s.key),
995
+ children: [
996
+ /* @__PURE__ */ jsxRuntime.jsx(SDot, { $c: s.color }),
997
+ s.label
998
+ ]
999
+ },
1000
+ s.key
1001
+ )) })
1002
+ ] }),
1003
+ /* @__PURE__ */ jsxRuntime.jsx(HoverInfo, { children: hovRow ? `${hovRow.label} · ${hovRow.totalUsers.toLocaleString()} total · +${hovRow.newUsers.toLocaleString()} new · ${hovRow.activeUsers.toLocaleString()} active` : "Hover the chart to inspect a data point" }),
1004
+ graphQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(
1005
+ designSystem.Flex,
1006
+ {
1007
+ justifyContent: "center",
1008
+ alignItems: "center",
1009
+ style: { flex: 1, minHeight: 170 },
1010
+ children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading…" })
1011
+ }
1012
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
1013
+ GrowthChart,
1014
+ {
1015
+ data: graphData,
1016
+ hovered: hovIdx,
1017
+ onHover: setHovIdx,
1018
+ activeSeries
1019
+ }
1020
+ )
1021
+ ] }),
1022
+ /* @__PURE__ */ jsxRuntime.jsxs(
1023
+ FeedCard,
1024
+ {
1025
+ $delay: 6,
1026
+ style: chartHeight > 0 ? { height: chartHeight } : void 0,
1027
+ children: [
1028
+ /* @__PURE__ */ jsxRuntime.jsxs(FeedHead, { children: [
1029
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: feedMode === "signups" ? "Recent Sign-ups" : "Recently Active" }),
1030
+ /* @__PURE__ */ jsxRuntime.jsxs(
1031
+ FeedSelect,
1032
+ {
1033
+ value: feedMode,
1034
+ onChange: (e) => setFeedMode(e.target.value),
1035
+ children: [
1036
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "signups", children: "Recent Sign-ups" }),
1037
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "active", children: "Recently Active" })
1038
+ ]
1039
+ }
1040
+ )
1041
+ ] }),
1042
+ /* @__PURE__ */ jsxRuntime.jsx(FeedScroll, { children: feedMode === "signups" ? usersQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading…" }) }) : users.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(Empty, { children: "No users yet" }) : users.map((u) => /* @__PURE__ */ jsxRuntime.jsxs(FeedItem, { children: [
1043
+ /* @__PURE__ */ jsxRuntime.jsxs(FeedTop, { children: [
1044
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { alignItems: "center", gap: 1, style: { minWidth: 0 }, children: [
1045
+ /* @__PURE__ */ jsxRuntime.jsx(Avatar.Avatar, { name: u.name, src: u.image, size: 20 }),
1046
+ /* @__PURE__ */ jsxRuntime.jsx(FeedName, { title: u.name, children: u.name })
1047
+ ] }),
1048
+ /* @__PURE__ */ jsxRuntime.jsx(FeedMeta, { children: relTime(u.createdAt) })
1049
+ ] }),
1050
+ /* @__PURE__ */ jsxRuntime.jsx(FeedEmail, { title: u.email, children: u.email })
1051
+ ] }, u.id)) : activeUsersQuery.isLoading || sessionsQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading…" }) }) : sortedActiveUsers.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(Empty, { children: "No recent activity" }) : sortedActiveUsers.map((u) => /* @__PURE__ */ jsxRuntime.jsxs(FeedItem, { children: [
1052
+ /* @__PURE__ */ jsxRuntime.jsxs(FeedTop, { children: [
1053
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { alignItems: "center", gap: 1, style: { minWidth: 0 }, children: [
1054
+ /* @__PURE__ */ jsxRuntime.jsx(
1055
+ Avatar.Avatar,
1056
+ {
1057
+ name: u.name,
1058
+ src: u.image ?? void 0,
1059
+ size: 20
1060
+ }
1061
+ ),
1062
+ /* @__PURE__ */ jsxRuntime.jsx(FeedName, { title: u.name, children: u.name })
1063
+ ] }),
1064
+ /* @__PURE__ */ jsxRuntime.jsx(FeedMeta, { children: relTime(
1065
+ lastActiveByUserId.get(String(u.id)) ?? u.createdAt
1066
+ ) })
1067
+ ] }),
1068
+ /* @__PURE__ */ jsxRuntime.jsx(FeedEmail, { title: u.email, children: u.email })
1069
+ ] }, u.documentId)) })
1070
+ ]
1071
+ }
1072
+ )
1073
+ ] }),
1074
+ /* @__PURE__ */ jsxRuntime.jsxs(SectionDivider, { children: [
1075
+ /* @__PURE__ */ jsxRuntime.jsx(DivLabel, { children: "Retention & Activity" }),
1076
+ /* @__PURE__ */ jsxRuntime.jsx(DivLine, {})
1077
+ ] }),
1078
+ /* @__PURE__ */ jsxRuntime.jsxs(TwoPanel, { children: [
1079
+ /* @__PURE__ */ jsxRuntime.jsxs(RtnCard, { $delay: 7, children: [
1080
+ /* @__PURE__ */ jsxRuntime.jsxs(ChartHeader, { children: [
1081
+ /* @__PURE__ */ jsxRuntime.jsx(ChartTitle, { children: "Cohort Retention" }),
1082
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { alignItems: "center", gap: 2, children: [
1083
+ { hue: 142, label: "≥70%" },
1084
+ { hue: 38, label: "40–70%" },
1085
+ { hue: 4, label: "<40%" }
1086
+ ].map(({ hue, label }) => /* @__PURE__ */ jsxRuntime.jsxs(
1087
+ designSystem.Flex,
1088
+ {
1089
+ alignItems: "center",
1090
+ gap: 1,
1091
+ style: { marginLeft: 8 },
1092
+ children: [
1093
+ /* @__PURE__ */ jsxRuntime.jsx(
1094
+ "span",
1095
+ {
1096
+ style: {
1097
+ width: 8,
1098
+ height: 8,
1099
+ borderRadius: "50%",
1100
+ background: `hsl(${hue},60%,50%)`,
1101
+ display: "inline-block",
1102
+ flexShrink: 0
1103
+ }
1104
+ }
1105
+ ),
1106
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: 9, color: T.textMuted }, children: label })
1107
+ ]
1108
+ },
1109
+ hue
1110
+ )) })
1111
+ ] }),
1112
+ /* @__PURE__ */ jsxRuntime.jsx(RtnTip, { children: rtnHovRow ? `Cohort ${rtnHovRow.label} · ${rtnHovRow.cohortSize.toLocaleString()} users · active ${rtnHovRow.activeStart} – ${rtnHovRow.activeEnd}` : "Hover a row to see cohort details" }),
1113
+ retentionQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(
1114
+ designSystem.Flex,
1115
+ {
1116
+ justifyContent: "center",
1117
+ alignItems: "center",
1118
+ style: { minHeight: 80 },
1119
+ children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading…" })
1120
+ }
1121
+ ) : rtnData.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs(Empty, { children: [
1122
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 22 }, children: "📉" }),
1123
+ /* @__PURE__ */ jsxRuntime.jsx("div", { children: "No retention data for this period" })
1124
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", flexDirection: "column", gap: 5 }, children: rtnData.map((row, i) => {
1125
+ const hue = rateHue(row.retentionRate);
1126
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1127
+ RtnRow,
1128
+ {
1129
+ $hov: rtnHov === i,
1130
+ onMouseEnter: () => setRtnHov(i),
1131
+ onMouseLeave: () => setRtnHov(null),
1132
+ children: [
1133
+ /* @__PURE__ */ jsxRuntime.jsx(RtnLabel, { children: row.label }),
1134
+ /* @__PURE__ */ jsxRuntime.jsx(RtnSize, { children: row.cohortSize.toLocaleString() }),
1135
+ /* @__PURE__ */ jsxRuntime.jsx(RtnTrack, { children: /* @__PURE__ */ jsxRuntime.jsx(RtnBar, { $w: row.retentionRate, $hue: hue }) }),
1136
+ /* @__PURE__ */ jsxRuntime.jsxs(RtnPct, { $hue: hue, children: [
1137
+ row.retentionRate.toFixed(1),
1138
+ "%"
1139
+ ] })
1140
+ ]
1141
+ },
1142
+ row.n
1143
+ );
1144
+ }) })
1145
+ ] }),
1146
+ /* @__PURE__ */ jsxRuntime.jsx(RingGrid, { children: [
1147
+ {
1148
+ label: "Daily Active",
1149
+ value: stats.activeUsers.daily.active,
1150
+ pct: stats.activeUsers.daily.percentage,
1151
+ color: T.amber,
1152
+ sparkline: activeSpark,
1153
+ delay: 8
1154
+ },
1155
+ {
1156
+ label: "Weekly Active",
1157
+ value: stats.activeUsers.weekly.active,
1158
+ pct: stats.activeUsers.weekly.percentage,
1159
+ color: T.green,
1160
+ sparkline: activeSpark,
1161
+ delay: 9
1162
+ },
1163
+ {
1164
+ label: "Monthly Active",
1165
+ value: stats.activeUsers.monthly.active,
1166
+ pct: stats.activeUsers.monthly.percentage,
1167
+ color: T.accent,
1168
+ sparkline: activeSpark,
1169
+ delay: 10
1170
+ }
1171
+ ].map(({ label, value, pct, color, delay }) => {
1172
+ const isPos = pct == null || pct >= 0;
1173
+ return /* @__PURE__ */ jsxRuntime.jsxs(RingCard, { $delay: delay, children: [
1174
+ /* @__PURE__ */ jsxRuntime.jsx(
1175
+ ProgressRing,
1176
+ {
1177
+ value: value ?? 0,
1178
+ max: activeMax,
1179
+ color,
1180
+ size: 56
1181
+ }
1182
+ ),
1183
+ /* @__PURE__ */ jsxRuntime.jsxs(RingInfo, { children: [
1184
+ /* @__PURE__ */ jsxRuntime.jsx(RingLabel, { children: label }),
1185
+ /* @__PURE__ */ jsxRuntime.jsx(RingVal, { children: (value ?? 0).toLocaleString() }),
1186
+ pct != null && /* @__PURE__ */ jsxRuntime.jsxs(
1187
+ TrendBadge,
1188
+ {
1189
+ $pos: isPos,
1190
+ style: { marginTop: 4, marginBottom: 0 },
1191
+ children: [
1192
+ isPos ? "↑" : "↓",
1193
+ " ",
1194
+ Math.abs(pct).toFixed(1),
1195
+ "%"
1196
+ ]
1197
+ }
1198
+ )
1199
+ ] })
1200
+ ] }, label);
1201
+ }) })
1202
+ ] })
1203
+ ] });
1204
+ }
1205
+ function ProtectedOverviewPage() {
1206
+ return /* @__PURE__ */ jsxRuntime.jsx(
1207
+ admin.Page.Protect,
1208
+ {
1209
+ permissions: index.PERMISSIONS.overview.filter(
1210
+ (p) => p.action === `plugin::${index.PLUGIN_ID}.overview.read`
1211
+ ),
1212
+ children: /* @__PURE__ */ jsxRuntime.jsx(OverviewPage, {})
1213
+ }
1214
+ );
1215
+ }
1216
+ exports.OverviewPage = OverviewPage;
1217
+ exports.default = ProtectedOverviewPage;