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