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

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.
@@ -9,7 +9,7 @@ const client$2 = require("@better-auth/infra/client");
9
9
  const client$1 = require("better-auth/client");
10
10
  const icons = require("@strapi/icons");
11
11
  const admin = require("@strapi/strapi/admin");
12
- const index = require("./index-A9PUvldu.js");
12
+ const index = require("./index-xZ2FHX3i.js");
13
13
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
14
14
  const styled__default = /* @__PURE__ */ _interopDefault(styled);
15
15
  const dashPathMethods = () => ({
@@ -458,7 +458,7 @@ function UserCombobox({
458
458
  ]) : void 0;
459
459
  const result = await client.dash.listUsers({
460
460
  query: {
461
- limit: 20,
461
+ limit: search ? 100 : 20,
462
462
  offset: 0,
463
463
  sortBy: "createdAt",
464
464
  sortOrder: "desc",
@@ -633,6 +633,159 @@ function CreateOrganizationDialog({ teamsEnabled, onClose }) {
633
633
  function slugify(str) {
634
634
  return str.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
635
635
  }
636
+ const EntryRow = styled__default.default.div`
637
+ display: flex;
638
+ align-items: center;
639
+ justify-content: space-between;
640
+ gap: 8px;
641
+ padding: 8px 12px;
642
+ width: 100%;
643
+ box-sizing: border-box;
644
+ background: #ffffff;
645
+ border: 1px solid ${({ theme }) => theme.colors?.neutral200 ?? "#dcdce4"};
646
+ border-radius: 4px;
647
+ min-width: 0;
648
+ `;
649
+ function getDisplayLabel(doc) {
650
+ for (const key of ["name", "title", "email", "username", "label", "slug"]) {
651
+ const v = doc[key];
652
+ if (typeof v === "string" && v.trim()) return v;
653
+ }
654
+ return String(doc.documentId ?? doc.id ?? "");
655
+ }
656
+ function normalizeValue(val, cache) {
657
+ if (!val) return [];
658
+ if (typeof val === "object" && !Array.isArray(val) && val !== null && "set" in val) {
659
+ const items2 = val.set ?? [];
660
+ return items2.filter((item) => item?.documentId).map((item) => ({
661
+ documentId: item.documentId,
662
+ label: cache.get(item.documentId) ?? item.documentId
663
+ }));
664
+ }
665
+ const items = Array.isArray(val) ? val : [val];
666
+ return items.filter(
667
+ (item) => typeof item === "object" && item !== null && "documentId" in item
668
+ ).map((item) => ({
669
+ documentId: String(item.documentId),
670
+ label: getDisplayLabel(item)
671
+ }));
672
+ }
673
+ function RelationField({
674
+ name,
675
+ label,
676
+ attribute,
677
+ value,
678
+ onChange,
679
+ readOnly = false
680
+ }) {
681
+ const { get } = admin.useFetchClient();
682
+ const cache = react.useRef(/* @__PURE__ */ new Map());
683
+ const [search, setSearch] = react.useState("");
684
+ const [addKey, setAddKey] = react.useState(0);
685
+ const target = attribute.target ?? "";
686
+ const relationType = attribute.relation ?? "manyToOne";
687
+ const isMulti = relationType === "oneToMany" || relationType === "manyToMany";
688
+ const currentDocs = normalizeValue(value, cache.current);
689
+ const selectedIds = new Set(currentDocs.map((d) => d.documentId));
690
+ const { data: searchResults = [], isLoading } = reactQuery.useQuery({
691
+ queryKey: ["relation-search", target, search],
692
+ queryFn: async () => {
693
+ const params = new URLSearchParams({
694
+ uid: target,
695
+ "pagination[pageSize]": "50"
696
+ });
697
+ if (search) params.set("filters[name][$containsi]", search);
698
+ const { data } = await get(
699
+ `/better-auth-dashboard/db?${params}`
700
+ );
701
+ const docs = data.results ?? [];
702
+ for (const doc of docs) {
703
+ if (doc.documentId) {
704
+ cache.current.set(String(doc.documentId), getDisplayLabel(doc));
705
+ }
706
+ }
707
+ return docs.map((doc) => ({
708
+ documentId: String(doc.documentId ?? ""),
709
+ label: getDisplayLabel(doc)
710
+ })).filter((d) => d.documentId);
711
+ },
712
+ keepPreviousData: true,
713
+ enabled: !readOnly
714
+ });
715
+ const availableOptions = searchResults.filter(
716
+ (r) => !selectedIds.has(r.documentId)
717
+ );
718
+ const commit = (ids) => {
719
+ onChange(name, { set: ids.map((id) => ({ documentId: id })) });
720
+ };
721
+ const handleSelect = (val) => {
722
+ if (!val) return;
723
+ const opt = searchResults.find((r) => r.documentId === val);
724
+ if (opt) cache.current.set(val, opt.label);
725
+ if (isMulti) {
726
+ if (selectedIds.has(val)) return;
727
+ commit([...currentDocs.map((d) => d.documentId), val]);
728
+ } else {
729
+ commit([val]);
730
+ }
731
+ setSearch("");
732
+ setAddKey((k) => k + 1);
733
+ };
734
+ const handleRemove = (docId) => {
735
+ commit(
736
+ currentDocs.filter((d) => d.documentId !== docId).map((d) => d.documentId)
737
+ );
738
+ };
739
+ return /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { style: { width: "100%" }, children: [
740
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: label }),
741
+ !readOnly && /* @__PURE__ */ jsxRuntime.jsx(
742
+ designSystem.Combobox,
743
+ {
744
+ value: "",
745
+ onChange: (val) => handleSelect(val),
746
+ onInputChange: (e) => setSearch(e.target.value),
747
+ loading: isLoading,
748
+ placeholder: "Search…",
749
+ children: availableOptions.map((opt) => /* @__PURE__ */ jsxRuntime.jsx(designSystem.ComboboxOption, { value: opt.documentId, children: opt.label }, opt.documentId))
750
+ },
751
+ addKey
752
+ ),
753
+ currentDocs.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
754
+ designSystem.Flex,
755
+ {
756
+ direction: "column",
757
+ gap: 1,
758
+ style: { marginTop: 8, width: "100%" },
759
+ children: currentDocs.map((doc) => /* @__PURE__ */ jsxRuntime.jsxs(EntryRow, { children: [
760
+ /* @__PURE__ */ jsxRuntime.jsx(
761
+ designSystem.Typography,
762
+ {
763
+ variant: "omega",
764
+ textColor: "neutral800",
765
+ style: {
766
+ overflow: "hidden",
767
+ textOverflow: "ellipsis",
768
+ whiteSpace: "nowrap",
769
+ flex: 1,
770
+ minWidth: 0
771
+ },
772
+ children: doc.label
773
+ }
774
+ ),
775
+ !readOnly && /* @__PURE__ */ jsxRuntime.jsx(
776
+ designSystem.IconButton,
777
+ {
778
+ label: "Remove",
779
+ size: "S",
780
+ onClick: () => handleRemove(doc.documentId),
781
+ children: /* @__PURE__ */ jsxRuntime.jsx(icons.Trash, {})
782
+ }
783
+ )
784
+ ] }, doc.documentId))
785
+ }
786
+ )
787
+ ] });
788
+ }
636
789
  function makeLabel(name) {
637
790
  return name.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()).trim();
638
791
  }
@@ -756,24 +909,15 @@ function DynamicField({
756
909
  ] });
757
910
  }
758
911
  if (type === "relation") {
759
- return /* @__PURE__ */ jsxRuntime.jsxs(
760
- designSystem.Field.Root,
912
+ return /* @__PURE__ */ jsxRuntime.jsx(
913
+ RelationField,
761
914
  {
762
- style: fieldStyle,
763
- hint: `Relation → ${attribute.target ?? "unknown"}`,
764
- children: [
765
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: label }),
766
- /* @__PURE__ */ jsxRuntime.jsx(
767
- designSystem.TextInput,
768
- {
769
- value: value != null ? String(value) : "",
770
- onChange: (e) => onChange(name, e.target.value),
771
- disabled: readOnly,
772
- placeholder: "ID…"
773
- }
774
- ),
775
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {})
776
- ]
915
+ name,
916
+ label,
917
+ attribute,
918
+ value,
919
+ onChange,
920
+ readOnly
777
921
  }
778
922
  );
779
923
  }
@@ -844,6 +988,8 @@ function useModelSchema(model) {
844
988
  if (SYSTEM_FIELDS.has(name)) continue;
845
989
  if (attr.type === "relation" && typeof attr.target === "string" && (attr.target.startsWith("plugin::users-permissions") || attr.target.startsWith("admin::")))
846
990
  continue;
991
+ const baOptions = attr.pluginOptions?.["better-auth"];
992
+ if (baOptions?.managed === true) continue;
847
993
  attributes[name] = attr;
848
994
  }
849
995
  return attributes;
@@ -934,6 +1080,14 @@ function OrganizationDetail({
934
1080
  return result.data ?? [];
935
1081
  }
936
1082
  });
1083
+ const invitationsQuery = reactQuery.useQuery({
1084
+ queryKey: ["dash-org-invitations", organizationId],
1085
+ queryFn: async () => {
1086
+ const result = await client.dash.organization[organizationId].invitations({}, withContext({ organizationId }));
1087
+ if (result.error) throw new Error(result.error.message ?? "Failed");
1088
+ return result.data ?? [];
1089
+ }
1090
+ });
937
1091
  const [activeTab, setActiveTab] = react.useState("details");
938
1092
  const [editName, setEditName] = react.useState(void 0);
939
1093
  const [editSlug, setEditSlug] = react.useState(void 0);
@@ -946,6 +1100,9 @@ function OrganizationDetail({
946
1100
  const [confirmDeleteSsoId, setConfirmDeleteSsoId] = react.useState(
947
1101
  null
948
1102
  );
1103
+ const [inviteEmail, setInviteEmail] = react.useState("");
1104
+ const [inviteRole, setInviteRole] = react.useState("member");
1105
+ const [confirmCancelInvitationId, setConfirmCancelInvitationId] = react.useState(null);
949
1106
  const handleExtraChange = (name, value) => {
950
1107
  setEditExtra((prev) => ({ ...prev, [name]: value }));
951
1108
  };
@@ -1116,9 +1273,74 @@ function OrganizationDetail({
1116
1273
  });
1117
1274
  }
1118
1275
  });
1276
+ const inviteMemberMutation = reactQuery.useMutation({
1277
+ mutationFn: async () => {
1278
+ const result = await client.dash.organization.inviteMember(
1279
+ { email: inviteEmail, role: inviteRole, invitedBy: "" },
1280
+ withContext({ organizationId })
1281
+ );
1282
+ if (result.error) throw new Error(result.error.message ?? "Failed");
1283
+ return result.data;
1284
+ },
1285
+ onSuccess: () => {
1286
+ qc.invalidateQueries({
1287
+ queryKey: ["dash-org-invitations", organizationId]
1288
+ });
1289
+ setInviteEmail("");
1290
+ setInviteRole("member");
1291
+ toggleNotification({ type: "success", message: "Invitation sent" });
1292
+ },
1293
+ onError: (err) => {
1294
+ toggleNotification({
1295
+ type: "danger",
1296
+ message: err.message ?? "Failed to invite"
1297
+ });
1298
+ }
1299
+ });
1300
+ const cancelInvitationMutation = reactQuery.useMutation({
1301
+ mutationFn: async (invitationId) => {
1302
+ const result = await client.dash.organization.cancelInvitation(
1303
+ { invitationId },
1304
+ withContext({ organizationId })
1305
+ );
1306
+ if (result.error) throw new Error(result.error.message ?? "Failed");
1307
+ },
1308
+ onSuccess: () => {
1309
+ setConfirmCancelInvitationId(null);
1310
+ qc.invalidateQueries({
1311
+ queryKey: ["dash-org-invitations", organizationId]
1312
+ });
1313
+ toggleNotification({ type: "success", message: "Invitation cancelled" });
1314
+ },
1315
+ onError: (err) => {
1316
+ toggleNotification({
1317
+ type: "danger",
1318
+ message: err.message ?? "Failed to cancel"
1319
+ });
1320
+ }
1321
+ });
1322
+ const resendInvitationMutation = reactQuery.useMutation({
1323
+ mutationFn: async (invitationId) => {
1324
+ const result = await client.dash.organization.resendInvitation(
1325
+ { invitationId },
1326
+ withContext({ organizationId })
1327
+ );
1328
+ if (result.error) throw new Error(result.error.message ?? "Failed");
1329
+ },
1330
+ onSuccess: () => {
1331
+ toggleNotification({ type: "success", message: "Invitation resent" });
1332
+ },
1333
+ onError: (err) => {
1334
+ toggleNotification({
1335
+ type: "danger",
1336
+ message: err.message ?? "Failed to resend"
1337
+ });
1338
+ }
1339
+ });
1119
1340
  const members = membersQuery.data ?? [];
1120
1341
  const teams = teamsQuery.data ?? [];
1121
1342
  const ssoProviders = ssoQuery.data ?? [];
1343
+ const invitations = invitationsQuery.data ?? [];
1122
1344
  const hasOrgEdits = editName !== void 0 || editSlug !== void 0 || editLogo !== void 0 || Object.keys(editExtra).length > 0;
1123
1345
  const customFields = Object.entries(schemaQuery.data ?? {}).filter(([name]) => !STANDARD_ORG_FIELDS.has(name)).map(([name, attribute]) => ({ name, attribute }));
1124
1346
  const detailsFooter = activeTab === "details" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
@@ -1175,6 +1397,11 @@ function OrganizationDetail({
1175
1397
  teams.length,
1176
1398
  ")"
1177
1399
  ] }),
1400
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Tabs.Trigger, { value: "invitations", children: [
1401
+ "Invitations (",
1402
+ invitations.length,
1403
+ ")"
1404
+ ] }),
1178
1405
  /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Tabs.Trigger, { value: "sso", children: [
1179
1406
  "SSO (",
1180
1407
  ssoProviders.length,
@@ -1430,7 +1657,127 @@ function OrganizationDetail({
1430
1657
  },
1431
1658
  provider.id
1432
1659
  )) })
1433
- ] }) }) })
1660
+ ] }) }) }),
1661
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "invitations", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 5, paddingTop: 6, children: [
1662
+ /* @__PURE__ */ jsxRuntime.jsxs(FormSection, { children: [
1663
+ /* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { children: "Invite member by email" }),
1664
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Grid.Root, { gap: 3, children: [
1665
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 8, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { children: [
1666
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Email address" }),
1667
+ /* @__PURE__ */ jsxRuntime.jsx(
1668
+ designSystem.TextInput,
1669
+ {
1670
+ type: "email",
1671
+ value: inviteEmail,
1672
+ onChange: (e) => setInviteEmail(e.target.value),
1673
+ placeholder: "user@example.com"
1674
+ }
1675
+ )
1676
+ ] }) }),
1677
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 4, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { children: [
1678
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Role" }),
1679
+ /* @__PURE__ */ jsxRuntime.jsxs(
1680
+ designSystem.SingleSelect,
1681
+ {
1682
+ value: inviteRole,
1683
+ onChange: (v) => setInviteRole(String(v)),
1684
+ "aria-label": "Invite role",
1685
+ children: [
1686
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "member", children: "Member" }),
1687
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "admin", children: "Admin" }),
1688
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "owner", children: "Owner" })
1689
+ ]
1690
+ }
1691
+ )
1692
+ ] }) })
1693
+ ] }),
1694
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(
1695
+ designSystem.Button,
1696
+ {
1697
+ size: "S",
1698
+ startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Plus, {}),
1699
+ disabled: !inviteEmail,
1700
+ loading: inviteMemberMutation.isLoading,
1701
+ onClick: () => inviteMemberMutation.mutate(),
1702
+ children: "Send invitation"
1703
+ }
1704
+ ) })
1705
+ ] }),
1706
+ /* @__PURE__ */ jsxRuntime.jsxs(FormSection, { children: [
1707
+ /* @__PURE__ */ jsxRuntime.jsxs(SectionLabel, { children: [
1708
+ "Invitations (",
1709
+ invitations.length,
1710
+ ")"
1711
+ ] }),
1712
+ invitationsQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: "neutral500", children: "Loading…" }) : invitations.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: "No invitations yet." }) : /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { direction: "column", gap: 2, children: invitations.map((inv) => {
1713
+ const statusColor = {
1714
+ pending: "#f59e0b",
1715
+ accepted: "#5cb176",
1716
+ rejected: "#d02b20",
1717
+ canceled: "#8e8ea9"
1718
+ };
1719
+ const color = statusColor[inv.status] ?? "#8e8ea9";
1720
+ const isPending = inv.status === "pending";
1721
+ return /* @__PURE__ */ jsxRuntime.jsxs(AccountRow, { children: [
1722
+ /* @__PURE__ */ jsxRuntime.jsxs(
1723
+ designSystem.Flex,
1724
+ {
1725
+ direction: "column",
1726
+ gap: 1,
1727
+ alignItems: "flex-start",
1728
+ style: { flex: 1 },
1729
+ children: [
1730
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", fontWeight: "semiBold", children: inv.user?.name ?? inv.email }),
1731
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: [
1732
+ inv.email,
1733
+ " · role: ",
1734
+ inv.role
1735
+ ] }),
1736
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: [
1737
+ "Expires",
1738
+ " ",
1739
+ new Date(inv.expiresAt).toLocaleDateString()
1740
+ ] })
1741
+ ]
1742
+ }
1743
+ ),
1744
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "center", children: [
1745
+ /* @__PURE__ */ jsxRuntime.jsx(
1746
+ MonoChip,
1747
+ {
1748
+ style: {
1749
+ color,
1750
+ borderColor: color,
1751
+ background: `${color}18`
1752
+ },
1753
+ children: inv.status
1754
+ }
1755
+ ),
1756
+ isPending && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1757
+ /* @__PURE__ */ jsxRuntime.jsx(
1758
+ designSystem.Button,
1759
+ {
1760
+ size: "S",
1761
+ variant: "secondary",
1762
+ loading: resendInvitationMutation.isLoading,
1763
+ onClick: () => resendInvitationMutation.mutate(inv.id),
1764
+ children: "Resend"
1765
+ }
1766
+ ),
1767
+ /* @__PURE__ */ jsxRuntime.jsx(
1768
+ designSystem.IconButton,
1769
+ {
1770
+ label: "Cancel invitation",
1771
+ onClick: () => setConfirmCancelInvitationId(inv.id),
1772
+ children: /* @__PURE__ */ jsxRuntime.jsx(icons.Trash, {})
1773
+ }
1774
+ )
1775
+ ] })
1776
+ ] })
1777
+ ] }, inv.id);
1778
+ }) })
1779
+ ] })
1780
+ ] }) })
1434
1781
  ] }),
1435
1782
  confirmRemoveMemberId && /* @__PURE__ */ jsxRuntime.jsx(
1436
1783
  ConfirmDialog,
@@ -1464,6 +1811,17 @@ function OrganizationDetail({
1464
1811
  onConfirm: () => deleteSsoMutation.mutate(confirmDeleteSsoId),
1465
1812
  onCancel: () => setConfirmDeleteSsoId(null)
1466
1813
  }
1814
+ ),
1815
+ confirmCancelInvitationId && /* @__PURE__ */ jsxRuntime.jsx(
1816
+ ConfirmDialog,
1817
+ {
1818
+ title: "Cancel invitation",
1819
+ message: "Are you sure you want to cancel this invitation? The invite link will no longer work.",
1820
+ confirmLabel: "Cancel invitation",
1821
+ loading: cancelInvitationMutation.isLoading,
1822
+ onConfirm: () => cancelInvitationMutation.mutate(confirmCancelInvitationId),
1823
+ onCancel: () => setConfirmCancelInvitationId(null)
1824
+ }
1467
1825
  )
1468
1826
  ]
1469
1827
  }
@@ -1609,12 +1967,12 @@ function TeamRow({
1609
1967
  )
1610
1968
  ] });
1611
1969
  }
1612
- const PAGE_SIZE$2 = 25;
1613
- const fadeUp$3 = styled.keyframes`
1970
+ const PAGE_SIZE$1 = 25;
1971
+ const fadeUp$2 = styled.keyframes`
1614
1972
  from { opacity: 0; transform: translateY(6px); }
1615
1973
  to { opacity: 1; transform: translateY(0); }
1616
1974
  `;
1617
- const Wrap$3 = styled__default.default.div`
1975
+ const Wrap$2 = styled__default.default.div`
1618
1976
  padding: 28px 32px;
1619
1977
  background: #f6f6f9;
1620
1978
  min-height: 100%;
@@ -1622,39 +1980,39 @@ const Wrap$3 = styled__default.default.div`
1622
1980
  flex-direction: column;
1623
1981
  gap: 20px;
1624
1982
  `;
1625
- const PageHeader$2 = styled__default.default.div`
1983
+ const PageHeader$1 = styled__default.default.div`
1626
1984
  display: flex;
1627
1985
  justify-content: space-between;
1628
1986
  align-items: flex-start;
1629
1987
  `;
1630
- const TitleBlock$2 = styled__default.default.div`
1988
+ const TitleBlock$1 = styled__default.default.div`
1631
1989
  display: flex;
1632
1990
  flex-direction: column;
1633
1991
  gap: 4px;
1634
1992
  `;
1635
- const PageTitle$2 = styled__default.default.h1`
1993
+ const PageTitle$1 = styled__default.default.h1`
1636
1994
  margin: 0;
1637
1995
  font-size: 22px;
1638
1996
  font-weight: 800;
1639
1997
  color: #32324d;
1640
1998
  letter-spacing: -0.03em;
1641
1999
  `;
1642
- const PageSubtitle$2 = styled__default.default.p`
2000
+ const PageSubtitle$1 = styled__default.default.p`
1643
2001
  margin: 0;
1644
2002
  font-size: 12px;
1645
2003
  color: #8e8ea9;
1646
2004
  `;
1647
- const TableCard$2 = styled__default.default.div`
2005
+ const TableCard$1 = styled__default.default.div`
1648
2006
  background: #ffffff;
1649
2007
  border: 1px solid #eaeaef;
1650
2008
  border-radius: 10px;
1651
2009
  overflow: hidden;
1652
2010
  `;
1653
- const Table$2 = styled__default.default.table`
2011
+ const Table$1 = styled__default.default.table`
1654
2012
  width: 100%;
1655
2013
  border-collapse: collapse;
1656
2014
  `;
1657
- const TH$2 = styled__default.default.th`
2015
+ const TH$1 = styled__default.default.th`
1658
2016
  text-align: left;
1659
2017
  padding: 10px 14px;
1660
2018
  font-size: 10px;
@@ -1668,13 +2026,13 @@ const TH$2 = styled__default.default.th`
1668
2026
  &:first-child { padding-left: 20px; }
1669
2027
  &:last-child { padding-right: 20px; }
1670
2028
  `;
1671
- const THCheck$1 = styled__default.default(TH$2)`
2029
+ const THCheck$1 = styled__default.default(TH$1)`
1672
2030
  width: 44px;
1673
2031
  `;
1674
- const THActions$1 = styled__default.default(TH$2)`
2032
+ const THActions$1 = styled__default.default(TH$1)`
1675
2033
  width: 80px;
1676
2034
  `;
1677
- const TD$2 = styled__default.default.td`
2035
+ const TD$1 = styled__default.default.td`
1678
2036
  padding: 11px 14px;
1679
2037
  font-size: 12px;
1680
2038
  color: #32324d;
@@ -1683,14 +2041,14 @@ const TD$2 = styled__default.default.td`
1683
2041
  &:first-child { padding-left: 20px; }
1684
2042
  &:last-child { padding-right: 20px; }
1685
2043
  `;
1686
- const TDCheck$1 = styled__default.default(TD$2)`
2044
+ const TDCheck$1 = styled__default.default(TD$1)`
1687
2045
  width: 44px;
1688
2046
  `;
1689
- const TDActions$1 = styled__default.default(TD$2)`
2047
+ const TDActions$1 = styled__default.default(TD$1)`
1690
2048
  width: 80px;
1691
2049
  `;
1692
- const TR$2 = styled__default.default.tr`
1693
- animation: ${fadeUp$3} 280ms ease both;
2050
+ const TR$1 = styled__default.default.tr`
2051
+ animation: ${fadeUp$2} 280ms ease both;
1694
2052
  animation-delay: ${(p) => (p.$i ?? 0) * 25}ms;
1695
2053
  background: ${(p) => p.$selected ? "#f0f0ff" : "transparent"};
1696
2054
  transition: background 120ms ease;
@@ -1786,13 +2144,13 @@ function OrganizationsPage({ teamsEnabled }) {
1786
2144
  const [detailOrgId, setDetailOrgId] = react.useState(null);
1787
2145
  const [confirmDelete, setConfirmDelete] = react.useState(null);
1788
2146
  const [confirmDeleteMany, setConfirmDeleteMany] = react.useState(false);
1789
- const offset = (page - 1) * PAGE_SIZE$2;
2147
+ const offset = (page - 1) * PAGE_SIZE$1;
1790
2148
  const orgsQuery = reactQuery.useQuery({
1791
2149
  queryKey: ["dash-organizations", page, search],
1792
2150
  queryFn: async () => {
1793
2151
  const result = await client.dash.listOrganizations({
1794
2152
  query: {
1795
- limit: PAGE_SIZE$2,
2153
+ limit: PAGE_SIZE$1,
1796
2154
  offset,
1797
2155
  sortBy: "createdAt",
1798
2156
  sortOrder: "desc",
@@ -1837,7 +2195,7 @@ function OrganizationsPage({ teamsEnabled }) {
1837
2195
  });
1838
2196
  const orgs = orgsQuery.data && "organizations" in orgsQuery.data ? orgsQuery.data.organizations : [];
1839
2197
  const total = orgsQuery.data && "total" in orgsQuery.data ? orgsQuery.data.total : 0;
1840
- const pageCount = Math.ceil(total / PAGE_SIZE$2);
2198
+ const pageCount = Math.ceil(total / PAGE_SIZE$1);
1841
2199
  const allSelected = orgs.length > 0 && orgs.every((o) => selected.has(o.id));
1842
2200
  const someSelected = selected.size > 0;
1843
2201
  const toggleSelect = (id) => {
@@ -1857,11 +2215,11 @@ function OrganizationsPage({ teamsEnabled }) {
1857
2215
  setSearch(searchInput);
1858
2216
  setPage(1);
1859
2217
  };
1860
- return /* @__PURE__ */ jsxRuntime.jsxs(Wrap$3, { "data-testid": "organizations-page", children: [
1861
- /* @__PURE__ */ jsxRuntime.jsxs(PageHeader$2, { children: [
1862
- /* @__PURE__ */ jsxRuntime.jsxs(TitleBlock$2, { children: [
1863
- /* @__PURE__ */ jsxRuntime.jsx(PageTitle$2, { children: "Organizations" }),
1864
- /* @__PURE__ */ jsxRuntime.jsxs(PageSubtitle$2, { children: [
2218
+ return /* @__PURE__ */ jsxRuntime.jsxs(Wrap$2, { "data-testid": "organizations-page", children: [
2219
+ /* @__PURE__ */ jsxRuntime.jsxs(PageHeader$1, { children: [
2220
+ /* @__PURE__ */ jsxRuntime.jsxs(TitleBlock$1, { children: [
2221
+ /* @__PURE__ */ jsxRuntime.jsx(PageTitle$1, { children: "Organizations" }),
2222
+ /* @__PURE__ */ jsxRuntime.jsxs(PageSubtitle$1, { children: [
1865
2223
  total.toLocaleString(),
1866
2224
  " total"
1867
2225
  ] })
@@ -1909,7 +2267,7 @@ function OrganizationsPage({ teamsEnabled }) {
1909
2267
  )
1910
2268
  ] }),
1911
2269
  orgsQuery.isError && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { color: "#d02b20", fontSize: 12, padding: "8px 0" }, children: orgsQuery.error instanceof Error ? orgsQuery.error.message : "An error occurred" }),
1912
- /* @__PURE__ */ jsxRuntime.jsx(TableCard$2, { children: orgsQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", padding: 8, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading organizations…" }) }) : /* @__PURE__ */ jsxRuntime.jsxs(Table$2, { children: [
2270
+ /* @__PURE__ */ jsxRuntime.jsx(TableCard$1, { children: orgsQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", padding: 8, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading organizations…" }) }) : /* @__PURE__ */ jsxRuntime.jsxs(Table$1, { children: [
1913
2271
  /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
1914
2272
  /* @__PURE__ */ jsxRuntime.jsx(THCheck$1, { children: /* @__PURE__ */ jsxRuntime.jsx(
1915
2273
  designSystem.Checkbox,
@@ -1919,14 +2277,14 @@ function OrganizationsPage({ teamsEnabled }) {
1919
2277
  "aria-label": "Select all"
1920
2278
  }
1921
2279
  ) }),
1922
- /* @__PURE__ */ jsxRuntime.jsx(TH$2, { children: "Name" }),
1923
- /* @__PURE__ */ jsxRuntime.jsx(TH$2, { children: "Slug" }),
1924
- /* @__PURE__ */ jsxRuntime.jsx(TH$2, { children: "Members" }),
1925
- /* @__PURE__ */ jsxRuntime.jsx(TH$2, { children: "Created" }),
2280
+ /* @__PURE__ */ jsxRuntime.jsx(TH$1, { children: "Name" }),
2281
+ /* @__PURE__ */ jsxRuntime.jsx(TH$1, { children: "Slug" }),
2282
+ /* @__PURE__ */ jsxRuntime.jsx(TH$1, { children: "Members" }),
2283
+ /* @__PURE__ */ jsxRuntime.jsx(TH$1, { children: "Created" }),
1926
2284
  /* @__PURE__ */ jsxRuntime.jsx(THActions$1, {})
1927
2285
  ] }) }),
1928
2286
  /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: orgs.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsx(
1929
- TD$2,
2287
+ TD$1,
1930
2288
  {
1931
2289
  colSpan: 6,
1932
2290
  style: {
@@ -1938,7 +2296,7 @@ function OrganizationsPage({ teamsEnabled }) {
1938
2296
  children: search ? `No organizations matching "${search}"` : "No organizations found"
1939
2297
  }
1940
2298
  ) }) : orgs.map((org, i) => /* @__PURE__ */ jsxRuntime.jsxs(
1941
- TR$2,
2299
+ TR$1,
1942
2300
  {
1943
2301
  $selected: selected.has(org.id),
1944
2302
  $i: i,
@@ -1952,13 +2310,13 @@ function OrganizationsPage({ teamsEnabled }) {
1952
2310
  "aria-label": `Select ${org.name}`
1953
2311
  }
1954
2312
  ) }),
1955
- /* @__PURE__ */ jsxRuntime.jsx(TD$2, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { alignItems: "center", gap: 2, children: [
2313
+ /* @__PURE__ */ jsxRuntime.jsx(TD$1, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { alignItems: "center", gap: 2, children: [
1956
2314
  /* @__PURE__ */ jsxRuntime.jsx(OrgAvatar, { name: org.name, logo: org.logo }),
1957
2315
  /* @__PURE__ */ jsxRuntime.jsx(OrgName, { children: org.name })
1958
2316
  ] }) }),
1959
- /* @__PURE__ */ jsxRuntime.jsx(TD$2, { children: /* @__PURE__ */ jsxRuntime.jsx(SlugChip, { children: org.slug }) }),
1960
- /* @__PURE__ */ jsxRuntime.jsx(TD$2, { children: /* @__PURE__ */ jsxRuntime.jsx(CountChip, { children: org.memberCount }) }),
1961
- /* @__PURE__ */ jsxRuntime.jsx(TD$2, { children: /* @__PURE__ */ jsxRuntime.jsx(DateText$1, { children: new Date(org.createdAt).toLocaleDateString() }) }),
2317
+ /* @__PURE__ */ jsxRuntime.jsx(TD$1, { children: /* @__PURE__ */ jsxRuntime.jsx(SlugChip, { children: org.slug }) }),
2318
+ /* @__PURE__ */ jsxRuntime.jsx(TD$1, { children: /* @__PURE__ */ jsxRuntime.jsx(CountChip, { children: org.memberCount }) }),
2319
+ /* @__PURE__ */ jsxRuntime.jsx(TD$1, { children: /* @__PURE__ */ jsxRuntime.jsx(DateText$1, { children: new Date(org.createdAt).toLocaleDateString() }) }),
1962
2320
  /* @__PURE__ */ jsxRuntime.jsx(TDActions$1, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 1, justifyContent: "flex-end", children: [
1963
2321
  /* @__PURE__ */ jsxRuntime.jsx(
1964
2322
  designSystem.IconButton,
@@ -2105,95 +2463,143 @@ function Avatar({
2105
2463
  const color = getColor(name || "?");
2106
2464
  return /* @__PURE__ */ jsxRuntime.jsx(Circle, { $bg: color.bg, $fg: color.fg, $size: size, children: getInitials(name || "?") });
2107
2465
  }
2108
- const fadeUp$2 = styled.keyframes`
2109
- from { opacity: 0; transform: translateY(8px); }
2466
+ const T = {
2467
+ bg: "#f6f6f9",
2468
+ bgCard: "#ffffff",
2469
+ border: "#eaeaef",
2470
+ borderHover: "rgba(73,69,255,0.35)",
2471
+ accent: "#4945ff",
2472
+ green: "#5cb176",
2473
+ greenDim: "rgba(92,177,118,0.12)",
2474
+ amber: "#d9822f",
2475
+ red: "#d02b20",
2476
+ redDim: "rgba(208,43,32,0.1)",
2477
+ purple: "#8460b8",
2478
+ textPrimary: "#32324d",
2479
+ textSecondary: "#666687",
2480
+ textMuted: "#b8b8c7",
2481
+ mono: `'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace`
2482
+ };
2483
+ const fadeUp$1 = styled.keyframes`
2484
+ from { opacity: 0; transform: translateY(10px); }
2110
2485
  to { opacity: 1; transform: translateY(0); }
2111
2486
  `;
2112
- const Wrap$2 = styled__default.default.div`
2113
- padding: 28px 32px;
2114
- background: #f6f6f9;
2487
+ const Wrap$1 = styled__default.default.div`
2488
+ padding: 28px 32px 48px;
2489
+ background: ${T.bg};
2115
2490
  min-height: 100%;
2116
2491
  display: flex;
2117
2492
  flex-direction: column;
2118
- gap: 20px;
2493
+ gap: 22px;
2119
2494
  `;
2120
- const StatRow = styled__default.default.div`
2121
- display: grid;
2122
- grid-template-columns: repeat(5, 1fr);
2123
- gap: 12px;
2124
- @media (max-width: 1200px) { grid-template-columns: repeat(3, 1fr); }
2125
- @media (max-width: 768px) { grid-template-columns: repeat(2, 1fr); }
2495
+ const SectionDivider = styled__default.default.div`
2496
+ display: flex;
2497
+ align-items: center;
2498
+ gap: 10px;
2499
+ margin-bottom: -6px;
2126
2500
  `;
2127
- const MainRow = styled__default.default.div`
2128
- display: grid;
2129
- grid-template-columns: 1fr 300px;
2130
- gap: 12px;
2131
- @media (max-width: 960px) { grid-template-columns: 1fr; }
2501
+ const DivLabel = styled__default.default.span`
2502
+ font-size: 9px;
2503
+ font-weight: 700;
2504
+ letter-spacing: 0.14em;
2505
+ text-transform: uppercase;
2506
+ color: ${T.textMuted};
2507
+ white-space: nowrap;
2132
2508
  `;
2133
- const ActiveRow = styled__default.default.div`
2134
- display: grid;
2135
- grid-template-columns: repeat(3, 1fr);
2136
- gap: 12px;
2509
+ const DivLine = styled__default.default.div`
2510
+ flex: 1;
2511
+ height: 1px;
2512
+ background: ${T.border};
2137
2513
  `;
2138
2514
  const Card = styled__default.default.div`
2139
- background: #ffffff;
2140
- border: 1px solid #eaeaef;
2141
- border-radius: 10px;
2515
+ background: ${T.bgCard};
2516
+ border: 1px solid ${T.border};
2517
+ border-radius: 12px;
2142
2518
  overflow: hidden;
2143
2519
  position: relative;
2144
- animation: ${fadeUp$2} 360ms ease both;
2520
+ animation: ${fadeUp$1} 380ms ease both;
2145
2521
  animation-delay: ${(p) => (p.$delay ?? 0) * 55}ms;
2146
- transition: border-color 180ms ease, box-shadow 180ms ease;
2147
-
2148
- ${(p) => p.$accent && `
2149
- &::before {
2150
- content: '';
2151
- position: absolute;
2152
- top: 0; left: 0; right: 0;
2153
- height: 3px;
2154
- background: ${p.$accent};
2155
- z-index: 1;
2156
- }
2157
- `}
2158
-
2522
+ transition: border-color 200ms, box-shadow 200ms;
2523
+ &:hover {
2524
+ border-color: ${T.borderHover};
2525
+ box-shadow: 0 0 0 1px ${T.borderHover}, 0 8px 32px rgba(124,109,250,0.07);
2526
+ }
2527
+ `;
2528
+ const PillGroup = styled__default.default.div`
2529
+ display: flex;
2530
+ background: rgba(0,0,0,0.03);
2531
+ border: 1px solid ${T.border};
2532
+ border-radius: 9px;
2533
+ padding: 3px;
2534
+ gap: 2px;
2535
+ `;
2536
+ const Pill = styled__default.default.button`
2537
+ appearance: none;
2538
+ border: none;
2539
+ cursor: pointer;
2540
+ padding: 5px 16px;
2541
+ border-radius: 6px;
2542
+ font-size: 11px;
2543
+ font-weight: 600;
2544
+ letter-spacing: 0.03em;
2545
+ transition: background 180ms, color 180ms;
2546
+ background: ${(p) => p.$active ? T.accent : "transparent"};
2547
+ color: ${(p) => p.$active ? "#fff" : T.textSecondary};
2159
2548
  &:hover {
2160
- border-color: #c0bfff;
2161
- box-shadow: 0 2px 16px rgba(73,69,255,0.08);
2549
+ background: ${(p) => p.$active ? T.accent : "rgba(0,0,0,0.05)"};
2550
+ color: ${(p) => p.$active ? "#fff" : T.textPrimary};
2162
2551
  }
2163
2552
  `;
2164
- const StatTop = styled__default.default.div`
2165
- padding: 18px 18px 10px;
2553
+ const StatGrid = styled__default.default.div`
2554
+ display: grid;
2555
+ grid-template-columns: repeat(5, 1fr);
2556
+ gap: 10px;
2557
+ @media (max-width: 1200px) { grid-template-columns: repeat(3, 1fr); }
2558
+ `;
2559
+ const StatCard = styled__default.default(Card)`
2560
+ padding: 18px 20px 0;
2561
+ &::after {
2562
+ content: '';
2563
+ position: absolute;
2564
+ top: 0; left: 0; right: 0;
2565
+ height: 2px;
2566
+ background: ${(p) => p.$accent};
2567
+ opacity: 0.75;
2568
+ }
2166
2569
  `;
2167
2570
  const StatLabel = styled__default.default.div`
2168
- font-size: 10px;
2571
+ font-size: 9px;
2169
2572
  font-weight: 700;
2170
- letter-spacing: 0.08em;
2573
+ letter-spacing: 0.1em;
2171
2574
  text-transform: uppercase;
2172
- color: #8e8ea9;
2173
- margin-bottom: 8px;
2575
+ color: ${T.textMuted};
2576
+ margin-bottom: 10px;
2174
2577
  `;
2175
2578
  const StatValue = styled__default.default.div`
2176
2579
  font-size: 30px;
2177
2580
  font-weight: 800;
2178
- color: #32324d;
2581
+ color: ${T.textPrimary};
2179
2582
  line-height: 1;
2180
- letter-spacing: -0.05em;
2583
+ letter-spacing: -0.045em;
2181
2584
  font-variant-numeric: tabular-nums;
2182
2585
  margin-bottom: 10px;
2183
2586
  `;
2184
2587
  const TrendBadge = styled__default.default.span`
2185
2588
  display: inline-flex;
2186
2589
  align-items: center;
2187
- gap: 2px;
2188
- padding: 2px 7px;
2590
+ gap: 3px;
2591
+ padding: 2px 8px;
2189
2592
  border-radius: 20px;
2190
- font-size: 10px;
2593
+ font-size: 9px;
2191
2594
  font-weight: 700;
2192
- background: ${(p) => p.$pos ? "#eafbe7" : "#fcecea"};
2193
- color: ${(p) => p.$pos ? "#5cb176" : "#d02b20"};
2595
+ letter-spacing: 0.03em;
2596
+ background: ${(p) => p.$pos ? T.greenDim : T.redDim};
2597
+ color: ${(p) => p.$pos ? T.green : T.red};
2598
+ margin-bottom: 12px;
2194
2599
  `;
2195
2600
  const SparkWrap = styled__default.default.div`
2196
- height: 48px;
2601
+ height: 44px;
2602
+ margin: 0 -20px;
2197
2603
  `;
2198
2604
  const ChartCard = styled__default.default(Card)`
2199
2605
  padding: 20px 20px 12px;
@@ -2205,31 +2611,42 @@ const ChartHeader = styled__default.default.div`
2205
2611
  display: flex;
2206
2612
  align-items: center;
2207
2613
  justify-content: space-between;
2614
+ gap: 12px;
2208
2615
  flex-shrink: 0;
2209
2616
  `;
2210
2617
  const ChartTitle = styled__default.default.div`
2211
- font-size: 13px;
2618
+ font-size: 12px;
2212
2619
  font-weight: 700;
2213
- color: #32324d;
2620
+ color: ${T.textPrimary};
2621
+ letter-spacing: 0.01em;
2214
2622
  `;
2215
- const LegendRow = styled__default.default.div`
2623
+ const SeriesRow = styled__default.default.div`
2216
2624
  display: flex;
2217
2625
  align-items: center;
2218
- gap: 14px;
2626
+ gap: 6px;
2219
2627
  `;
2220
- const LegendItem = styled__default.default.div`
2628
+ const SeriesBtn = styled__default.default.button`
2629
+ appearance: none;
2630
+ border: 1px solid ${(p) => p.$on ? `${p.$c}55` : T.border};
2631
+ background: ${(p) => p.$on ? `${p.$c}18` : "transparent"};
2632
+ color: ${(p) => p.$on ? p.$c : T.textMuted};
2633
+ font-size: 10px;
2634
+ font-weight: 600;
2635
+ padding: 3px 10px 3px 8px;
2636
+ border-radius: 6px;
2637
+ cursor: pointer;
2221
2638
  display: flex;
2222
2639
  align-items: center;
2223
2640
  gap: 5px;
2224
- font-size: 10px;
2225
- color: #8e8ea9;
2226
- font-weight: 600;
2227
- text-transform: uppercase;
2228
- letter-spacing: 0.04em;
2641
+ transition: all 160ms;
2642
+ &:hover {
2643
+ border-color: ${(p) => `${p.$c}88`};
2644
+ color: ${(p) => p.$c};
2645
+ background: ${(p) => `${p.$c}22`};
2646
+ }
2229
2647
  `;
2230
- const LegendDot = styled__default.default.span`
2231
- width: 8px;
2232
- height: 8px;
2648
+ const SDot = styled__default.default.span`
2649
+ width: 6px; height: 6px;
2233
2650
  border-radius: 50%;
2234
2651
  background: ${(p) => p.$c};
2235
2652
  display: inline-block;
@@ -2237,140 +2654,188 @@ const LegendDot = styled__default.default.span`
2237
2654
  `;
2238
2655
  const HoverInfo = styled__default.default.div`
2239
2656
  font-size: 11px;
2240
- color: #8e8ea9;
2657
+ color: ${T.textSecondary};
2241
2658
  font-variant-numeric: tabular-nums;
2242
- min-height: 16px;
2659
+ font-family: ${T.mono};
2660
+ min-height: 15px;
2661
+ `;
2662
+ const TwoPanel = styled__default.default.div`
2663
+ display: grid;
2664
+ grid-template-columns: ${(p) => p.$ratio ?? "1fr 288px"};
2665
+ gap: 10px;
2666
+ align-items: stretch;
2667
+ @media (max-width: 900px) { grid-template-columns: 1fr; }
2243
2668
  `;
2244
2669
  const FeedCard = styled__default.default(Card)`
2245
2670
  display: flex;
2246
2671
  flex-direction: column;
2247
- max-height: 380px;
2672
+ overflow: hidden;
2248
2673
  `;
2249
- const FeedHeader = styled__default.default.div`
2250
- padding: 16px 18px 12px;
2251
- border-bottom: 1px solid #f0f0f7;
2252
- flex-shrink: 0;
2253
- font-size: 10px;
2674
+ const FeedHead = styled__default.default.div`
2675
+ padding: 13px 16px 9px;
2676
+ border-bottom: 1px solid ${T.border};
2677
+ font-size: 11px;
2254
2678
  font-weight: 700;
2255
- letter-spacing: 0.08em;
2256
- text-transform: uppercase;
2257
- color: #8e8ea9;
2679
+ color: ${T.textPrimary};
2680
+ flex-shrink: 0;
2681
+ display: flex;
2682
+ justify-content: space-between;
2683
+ align-items: center;
2258
2684
  `;
2259
- const FeedList = styled__default.default.div`
2685
+ const FeedSelect = styled__default.default.select`
2686
+ appearance: none;
2687
+ border: 1px solid ${T.border};
2688
+ border-radius: 6px;
2689
+ background: ${T.bgCard};
2690
+ color: ${T.textSecondary};
2691
+ font-size: 10px;
2692
+ font-weight: 600;
2693
+ padding: 3px 22px 3px 8px;
2694
+ cursor: pointer;
2695
+ outline: none;
2696
+ 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");
2697
+ background-repeat: no-repeat;
2698
+ background-position: right 7px center;
2699
+ transition: border-color 160ms;
2700
+ &:hover, &:focus { border-color: ${T.accent}; color: ${T.textPrimary}; }
2701
+ `;
2702
+ const FeedScroll = styled__default.default.div`
2260
2703
  flex: 1;
2261
2704
  overflow-y: auto;
2262
- padding: 6px 0;
2263
- &::-webkit-scrollbar { width: 3px; }
2264
- &::-webkit-scrollbar-track { background: transparent; }
2265
- &::-webkit-scrollbar-thumb { background: #d9d8ff; border-radius: 2px; }
2705
+ min-height: 0;
2706
+ &::-webkit-scrollbar { width: 4px; }
2707
+ &::-webkit-scrollbar-thumb { background: ${T.textMuted}; border-radius: 2px; }
2266
2708
  `;
2267
2709
  const FeedItem = styled__default.default.div`
2710
+ padding: 9px 14px;
2711
+ border-bottom: 1px solid #f0f0f5;
2268
2712
  display: flex;
2269
- align-items: flex-start;
2270
- gap: 10px;
2271
- padding: 9px 18px;
2272
- &:hover { background: #fafafe; }
2713
+ flex-direction: column;
2714
+ gap: 3px;
2715
+ transition: background 130ms;
2716
+ &:hover { background: #fafaff; }
2717
+ &:last-child { border-bottom: none; }
2273
2718
  `;
2274
- const FeedContent = styled__default.default.div`
2275
- flex: 1;
2276
- min-width: 0;
2719
+ const FeedTop = styled__default.default.div`
2720
+ display: flex;
2721
+ align-items: center;
2722
+ justify-content: space-between;
2723
+ gap: 6px;
2277
2724
  `;
2278
- const FeedName = styled__default.default.div`
2279
- font-size: 12px;
2725
+ const FeedName = styled__default.default.span`
2726
+ font-size: 11px;
2280
2727
  font-weight: 600;
2281
- color: #32324d;
2282
- white-space: nowrap;
2728
+ color: ${T.textPrimary};
2283
2729
  overflow: hidden;
2284
2730
  text-overflow: ellipsis;
2731
+ white-space: nowrap;
2285
2732
  `;
2286
- const FeedSub = styled__default.default.div`
2733
+ const FeedEmail = styled__default.default.span`
2287
2734
  font-size: 10px;
2288
- color: #8e8ea9;
2289
- white-space: nowrap;
2735
+ color: ${T.textSecondary};
2290
2736
  overflow: hidden;
2291
2737
  text-overflow: ellipsis;
2292
- margin-top: 1px;
2738
+ white-space: nowrap;
2739
+ display: block;
2293
2740
  `;
2294
- const FeedTime = styled__default.default.div`
2295
- font-size: 10px;
2296
- color: #b8b8c7;
2741
+ const FeedMeta = styled__default.default.span`
2742
+ font-size: 9px;
2743
+ color: ${T.textMuted};
2297
2744
  white-space: nowrap;
2298
2745
  font-variant-numeric: tabular-nums;
2299
- margin-top: 1px;
2300
2746
  `;
2301
- const UsersCard = styled__default.default(Card)``;
2302
- const TableWrap = styled__default.default.div`
2303
- overflow-x: auto;
2747
+ const RtnCard = styled__default.default(Card)`
2748
+ padding: 20px;
2749
+ display: flex;
2750
+ flex-direction: column;
2751
+ gap: 12px;
2304
2752
  `;
2305
- const Table$1 = styled__default.default.table`
2306
- width: 100%;
2307
- border-collapse: collapse;
2753
+ const RtnRow = styled__default.default.div`
2754
+ display: flex;
2755
+ align-items: center;
2756
+ gap: 10px;
2757
+ padding: 4px 6px;
2758
+ border-radius: 7px;
2759
+ transition: background 130ms;
2760
+ background: ${(p) => p.$hov ? "#f0f0f6" : "transparent"};
2761
+ cursor: default;
2308
2762
  `;
2309
- const TH$1 = styled__default.default.th`
2310
- text-align: left;
2311
- padding: 10px 14px;
2763
+ const RtnLabel = styled__default.default.span`
2312
2764
  font-size: 10px;
2313
- font-weight: 700;
2314
- color: #8e8ea9;
2315
- text-transform: uppercase;
2316
- letter-spacing: 0.06em;
2317
- border-bottom: 1px solid #eaeaef;
2318
- background: #fafafa;
2765
+ color: ${T.textSecondary};
2766
+ min-width: 80px;
2767
+ text-align: right;
2319
2768
  white-space: nowrap;
2320
- &:first-child { padding-left: 20px; }
2321
- &:last-child { padding-right: 20px; }
2322
2769
  `;
2323
- const TR$1 = styled__default.default.tr`
2324
- &:hover td { background: #fafafe; }
2325
- &:last-child td { border-bottom: none; }
2770
+ const RtnSize = styled__default.default.span`
2771
+ font-size: 9px;
2772
+ color: ${T.textMuted};
2773
+ min-width: 52px;
2774
+ text-align: right;
2775
+ white-space: nowrap;
2776
+ font-variant-numeric: tabular-nums;
2326
2777
  `;
2327
- const TD$1 = styled__default.default.td`
2328
- padding: 10px 14px;
2329
- font-size: 12px;
2330
- color: #32324d;
2331
- border-bottom: 1px solid #f5f5f9;
2332
- vertical-align: middle;
2333
- &:first-child { padding-left: 20px; }
2334
- &:last-child { padding-right: 20px; }
2778
+ const RtnTrack = styled__default.default.div`
2779
+ flex: 1;
2780
+ height: 10px;
2781
+ background: #eaeaef;
2782
+ border-radius: 4px;
2783
+ overflow: hidden;
2335
2784
  `;
2336
- const StatusChip$1 = styled__default.default.span`
2337
- display: inline-flex;
2338
- align-items: center;
2339
- gap: 4px;
2340
- padding: 2px 8px;
2341
- border-radius: 20px;
2785
+ const RtnBar = styled__default.default.div`
2786
+ width: ${(p) => Math.max(p.$w, 0.5)}%;
2787
+ height: 100%;
2788
+ background: hsl(${(p) => p.$hue}, 60%, 50%);
2789
+ border-radius: 4px;
2790
+ transition: width 0.55s cubic-bezier(0.25, 0.46, 0.45, 0.94);
2791
+ `;
2792
+ const RtnPct = styled__default.default.span`
2342
2793
  font-size: 10px;
2343
2794
  font-weight: 700;
2344
- background: ${(p) => p.$banned ? "#fcecea" : p.$verified ? "#eafbe7" : "#f0f0ff"};
2345
- color: ${(p) => p.$banned ? "#d02b20" : p.$verified ? "#5cb176" : "#8e8ea9"};
2346
-
2347
- &::before {
2348
- content: '';
2349
- display: inline-block;
2350
- width: 5px;
2351
- height: 5px;
2352
- border-radius: 50%;
2353
- background: ${(p) => p.$banned ? "#d02b20" : p.$verified ? "#5cb176" : "#b8b8c7"};
2354
- }
2795
+ color: hsl(${(p) => p.$hue}, 60%, 60%);
2796
+ min-width: 38px;
2797
+ text-align: right;
2798
+ font-variant-numeric: tabular-nums;
2799
+ font-family: ${T.mono};
2800
+ `;
2801
+ const RtnTip = styled__default.default.div`
2802
+ font-size: 10px;
2803
+ color: ${T.textSecondary};
2804
+ min-height: 14px;
2805
+ font-variant-numeric: tabular-nums;
2806
+ padding: 0 6px;
2355
2807
  `;
2356
- const Divider = styled__default.default.div`
2808
+ const RingGrid = styled__default.default.div`
2357
2809
  display: flex;
2358
- align-items: center;
2810
+ flex-direction: column;
2359
2811
  gap: 10px;
2360
- margin-bottom: -8px;
2361
2812
  `;
2362
- const DivLabel = styled__default.default.span`
2363
- font-size: 10px;
2813
+ const RingCard = styled__default.default(Card)`
2814
+ padding: 18px 20px;
2815
+ display: flex;
2816
+ align-items: center;
2817
+ gap: 16px;
2818
+ flex: 1;
2819
+ `;
2820
+ const RingInfo = styled__default.default.div`
2821
+ display: flex;
2822
+ flex-direction: column;
2823
+ gap: 4px;
2824
+ `;
2825
+ const RingVal = styled__default.default.div`
2826
+ font-size: 24px;
2827
+ font-weight: 800;
2828
+ color: ${T.textPrimary};
2829
+ letter-spacing: -0.04em;
2830
+ font-variant-numeric: tabular-nums;
2831
+ line-height: 1;
2832
+ `;
2833
+ const RingLabel = styled__default.default.div`
2834
+ font-size: 9px;
2364
2835
  font-weight: 700;
2365
2836
  text-transform: uppercase;
2366
- letter-spacing: 0.08em;
2367
- color: #b8b8c7;
2368
- white-space: nowrap;
2369
- `;
2370
- const DivLine = styled__default.default.div`
2371
- flex: 1;
2372
- height: 1px;
2373
- background: #eaeaef;
2837
+ letter-spacing: 0.1em;
2838
+ color: ${T.textMuted};
2374
2839
  `;
2375
2840
  const Empty = styled__default.default.div`
2376
2841
  display: flex;
@@ -2378,32 +2843,52 @@ const Empty = styled__default.default.div`
2378
2843
  align-items: center;
2379
2844
  justify-content: center;
2380
2845
  padding: 32px;
2381
- color: #8e8ea9;
2846
+ color: ${T.textMuted};
2382
2847
  font-size: 12px;
2383
- gap: 6px;
2848
+ gap: 8px;
2384
2849
  `;
2850
+ function useCountUp(target, duration = 850) {
2851
+ const [count, setCount] = react.useState(0);
2852
+ const prevRef = react.useRef(0);
2853
+ react.useEffect(() => {
2854
+ let raf;
2855
+ let start = 0;
2856
+ const from = prevRef.current;
2857
+ const diff = target - from;
2858
+ const step = (ts) => {
2859
+ if (!start) start = ts;
2860
+ const progress = Math.min((ts - start) / duration, 1);
2861
+ const eased = 1 - (1 - progress) ** 3;
2862
+ setCount(Math.round(from + eased * diff));
2863
+ if (progress < 1) raf = requestAnimationFrame(step);
2864
+ else prevRef.current = target;
2865
+ };
2866
+ raf = requestAnimationFrame(step);
2867
+ return () => cancelAnimationFrame(raf);
2868
+ }, [target, duration]);
2869
+ return count;
2870
+ }
2385
2871
  function Sparkline({
2386
2872
  data,
2387
2873
  color,
2388
2874
  id
2389
2875
  }) {
2390
2876
  const W = 300;
2391
- const H = 48;
2877
+ const H = 44;
2392
2878
  if (data.length < 2) return /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "100%", height: H });
2393
2879
  const min = Math.min(...data);
2394
2880
  const max = Math.max(...data, min + 1);
2395
- const range = max - min;
2396
2881
  const pts = data.map((v, i) => ({
2397
2882
  x: i / (data.length - 1) * W,
2398
- y: H - 4 - (v - min) / range * (H - 10)
2883
+ y: H - 3 - (v - min) / (max - min) * (H - 8)
2399
2884
  }));
2400
- const line = pts.reduce((acc, p, i) => {
2885
+ const line = pts.reduce((a, p, i) => {
2401
2886
  if (i === 0) return `M ${p.x} ${p.y}`;
2402
2887
  const pr = pts[i - 1];
2403
2888
  const cx = (pr.x + p.x) / 2;
2404
- return `${acc} C ${cx} ${pr.y} ${cx} ${p.y} ${p.x} ${p.y}`;
2889
+ return `${a} C ${cx} ${pr.y} ${cx} ${p.y} ${p.x} ${p.y}`;
2405
2890
  }, "");
2406
- const area = `${line} L ${pts[pts.length - 1].x} ${H} L 0 ${H} Z`;
2891
+ const area = `${line} L ${pts.at(-1).x} ${H} L 0 ${H} Z`;
2407
2892
  const gid = `spk-${id}`;
2408
2893
  return /* @__PURE__ */ jsxRuntime.jsxs(
2409
2894
  "svg",
@@ -2412,10 +2897,10 @@ function Sparkline({
2412
2897
  height: H,
2413
2898
  viewBox: `0 0 ${W} ${H}`,
2414
2899
  preserveAspectRatio: "none",
2415
- "aria-hidden": "true",
2900
+ "aria-hidden": true,
2416
2901
  children: [
2417
2902
  /* @__PURE__ */ jsxRuntime.jsx("defs", { children: /* @__PURE__ */ jsxRuntime.jsxs("linearGradient", { id: gid, x1: "0", y1: "0", x2: "0", y2: "1", children: [
2418
- /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "0%", stopColor: color, stopOpacity: "0.25" }),
2903
+ /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "0%", stopColor: color, stopOpacity: "0.32" }),
2419
2904
  /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "100%", stopColor: color, stopOpacity: "0" })
2420
2905
  ] }) }),
2421
2906
  /* @__PURE__ */ jsxRuntime.jsx("path", { d: area, fill: `url(#${gid})` }),
@@ -2424,57 +2909,58 @@ function Sparkline({
2424
2909
  {
2425
2910
  d: line,
2426
2911
  stroke: color,
2427
- strokeWidth: "2",
2912
+ strokeWidth: "1.6",
2428
2913
  fill: "none",
2429
- strokeLinecap: "round",
2430
- strokeLinejoin: "round"
2914
+ strokeLinecap: "round"
2431
2915
  }
2432
2916
  )
2433
2917
  ]
2434
2918
  }
2435
2919
  );
2436
2920
  }
2437
- const SERIES = [
2438
- { key: "totalUsers", color: "#4945FF", label: "Total" },
2439
- { key: "newUsers", color: "#5CB176", label: "New" },
2440
- { key: "activeUsers", color: "#E57553", label: "Active" }
2921
+ const ALL_SERIES = [
2922
+ { key: "totalUsers", color: T.accent, label: "Total" },
2923
+ { key: "newUsers", color: T.green, label: "New" },
2924
+ { key: "activeUsers", color: T.amber, label: "Active" }
2441
2925
  ];
2442
- function AreaChart({
2926
+ function GrowthChart({
2443
2927
  data,
2444
2928
  hovered,
2445
- onHover
2929
+ onHover,
2930
+ activeSeries
2446
2931
  }) {
2447
2932
  const W = 600;
2448
- const H = 220;
2449
- const PL = 44;
2933
+ const H = 200;
2934
+ const PL = 40;
2450
2935
  const PR = 12;
2451
2936
  const PT = 12;
2452
2937
  const PB = 28;
2453
2938
  const CW = W - PL - PR;
2454
2939
  const CH = H - PT - PB;
2455
- if (data.length === 0) {
2940
+ if (!data.length) {
2456
2941
  return /* @__PURE__ */ jsxRuntime.jsxs(Empty, { children: [
2457
2942
  /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 24 }, children: "📊" }),
2458
2943
  /* @__PURE__ */ jsxRuntime.jsx("div", { children: "No growth data for this period" })
2459
2944
  ] });
2460
2945
  }
2461
- const allVals = data.flatMap((d) => SERIES.map((s) => d[s.key]));
2946
+ const active = ALL_SERIES.filter((s) => activeSeries.has(s.key));
2947
+ const allVals = data.flatMap((d) => active.map((s) => d[s.key]));
2462
2948
  const maxV = Math.max(...allVals, 10);
2463
2949
  const yMax = Math.ceil(maxV * 1.15);
2464
2950
  const xp = (i) => PL + i / Math.max(data.length - 1, 1) * CW;
2465
2951
  const yp = (v) => PT + (1 - v / yMax) * CH;
2466
2952
  const smooth = (vals) => {
2467
2953
  const pts = vals.map((v, i) => ({ x: xp(i), y: yp(v) }));
2468
- const path = pts.reduce((acc, p, i) => {
2954
+ const path = pts.reduce((a, p, i) => {
2469
2955
  if (i === 0) return `M ${p.x} ${p.y}`;
2470
2956
  const pr = pts[i - 1];
2471
2957
  const cx = (pr.x + p.x) / 2;
2472
- return `${acc} C ${cx} ${pr.y} ${cx} ${p.y} ${p.x} ${p.y}`;
2958
+ return `${a} C ${cx} ${pr.y} ${cx} ${p.y} ${p.x} ${p.y}`;
2473
2959
  }, "");
2474
- const area = `${path} L ${pts[pts.length - 1].x} ${PT + CH} L ${pts[0].x} ${PT + CH} Z`;
2960
+ const area = `${path} L ${pts.at(-1).x} ${PT + CH} L ${pts[0].x} ${PT + CH} Z`;
2475
2961
  return { pts, path, area };
2476
2962
  };
2477
- const lines = SERIES.map((s) => ({
2963
+ const lines = active.map((s) => ({
2478
2964
  ...s,
2479
2965
  ...smooth(data.map((d) => d[s.key]))
2480
2966
  }));
@@ -2497,17 +2983,17 @@ function AreaChart({
2497
2983
  /* @__PURE__ */ jsxRuntime.jsx("defs", { children: lines.map((s) => /* @__PURE__ */ jsxRuntime.jsxs(
2498
2984
  "linearGradient",
2499
2985
  {
2500
- id: `ag-${s.color.replace("#", "")}`,
2986
+ id: `ag-${s.key}`,
2501
2987
  x1: "0",
2502
2988
  y1: "0",
2503
2989
  x2: "0",
2504
2990
  y2: "1",
2505
2991
  children: [
2506
- /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "0%", stopColor: s.color, stopOpacity: "0.13" }),
2992
+ /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "0%", stopColor: s.color, stopOpacity: "0.18" }),
2507
2993
  /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "100%", stopColor: s.color, stopOpacity: "0" })
2508
2994
  ]
2509
2995
  },
2510
- s.color
2996
+ s.key
2511
2997
  )) }),
2512
2998
  yTicks.map((t) => /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
2513
2999
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -2527,7 +3013,7 @@ function AreaChart({
2527
3013
  x: PL - 6,
2528
3014
  y: t.y + 4,
2529
3015
  textAnchor: "end",
2530
- fill: "#b8b8c7",
3016
+ fill: T.textMuted,
2531
3017
  fontSize: "9",
2532
3018
  children: t.v >= 1e3 ? `${(t.v / 1e3).toFixed(1)}k` : t.v
2533
3019
  }
@@ -2544,25 +3030,18 @@ function AreaChart({
2544
3030
  strokeWidth: "1"
2545
3031
  }
2546
3032
  ),
2547
- lines.map((s) => /* @__PURE__ */ jsxRuntime.jsx(
2548
- "path",
2549
- {
2550
- d: s.area,
2551
- fill: `url(#ag-${s.color.replace("#", "")})`
2552
- },
2553
- `a-${s.color}`
2554
- )),
3033
+ lines.map((s) => /* @__PURE__ */ jsxRuntime.jsx("path", { d: s.area, fill: `url(#ag-${s.key})` }, `area-${s.key}`)),
2555
3034
  lines.map((s) => /* @__PURE__ */ jsxRuntime.jsx(
2556
3035
  "path",
2557
3036
  {
2558
3037
  d: s.path,
2559
3038
  stroke: s.color,
2560
- strokeWidth: "1.8",
3039
+ strokeWidth: "2",
2561
3040
  fill: "none",
2562
3041
  strokeLinecap: "round",
2563
3042
  strokeLinejoin: "round"
2564
3043
  },
2565
- `l-${s.color}`
3044
+ `line-${s.key}`
2566
3045
  )),
2567
3046
  data.map((d, i) => {
2568
3047
  if (i % step !== 0 && i !== data.length - 1) return null;
@@ -2572,7 +3051,7 @@ function AreaChart({
2572
3051
  x: xp(i),
2573
3052
  y: H - 6,
2574
3053
  textAnchor: "middle",
2575
- fill: "#b8b8c7",
3054
+ fill: T.textMuted,
2576
3055
  fontSize: "9",
2577
3056
  children: d.label
2578
3057
  },
@@ -2582,7 +3061,7 @@ function AreaChart({
2582
3061
  data.map((d, i) => {
2583
3062
  const isHov = hovered === i;
2584
3063
  return (
2585
- // biome-ignore lint/a11y/noStaticElementInteractions: SVG chart hover hit area
3064
+ // biome-ignore lint/a11y/noStaticElementInteractions: SVG chart hover
2586
3065
  /* @__PURE__ */ jsxRuntime.jsxs(
2587
3066
  "g",
2588
3067
  {
@@ -2607,10 +3086,9 @@ function AreaChart({
2607
3086
  y1: PT,
2608
3087
  x2: xp(i),
2609
3088
  y2: PT + CH,
2610
- stroke: "#32324d",
3089
+ stroke: "rgba(50,50,77,0.2)",
2611
3090
  strokeWidth: "1",
2612
- strokeDasharray: "3 3",
2613
- opacity: "0.35"
3091
+ strokeDasharray: "3 3"
2614
3092
  }
2615
3093
  ),
2616
3094
  lines.map((s) => /* @__PURE__ */ jsxRuntime.jsx(
@@ -2620,11 +3098,11 @@ function AreaChart({
2620
3098
  cy: yp(d[s.key]),
2621
3099
  r: isHov ? 4 : 2.5,
2622
3100
  fill: s.color,
2623
- stroke: "white",
3101
+ stroke: T.bg,
2624
3102
  strokeWidth: isHov ? 2 : 1,
2625
- opacity: isHov ? 1 : 0.45
3103
+ opacity: isHov ? 1 : 0.5
2626
3104
  },
2627
- s.color
3105
+ s.key
2628
3106
  ))
2629
3107
  ]
2630
3108
  },
@@ -2636,23 +3114,70 @@ function AreaChart({
2636
3114
  }
2637
3115
  );
2638
3116
  }
3117
+ function ProgressRing({
3118
+ value,
3119
+ max,
3120
+ color,
3121
+ size = 56
3122
+ }) {
3123
+ const r = (size - 9) / 2;
3124
+ const circ = 2 * Math.PI * r;
3125
+ const pct = Math.min(value / Math.max(max, 1), 1);
3126
+ const offset = circ * (1 - pct);
3127
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3128
+ "svg",
3129
+ {
3130
+ width: size,
3131
+ height: size,
3132
+ style: { flexShrink: 0 },
3133
+ "aria-hidden": true,
3134
+ children: [
3135
+ /* @__PURE__ */ jsxRuntime.jsx(
3136
+ "circle",
3137
+ {
3138
+ cx: size / 2,
3139
+ cy: size / 2,
3140
+ r,
3141
+ fill: "none",
3142
+ stroke: "#e5e7eb",
3143
+ strokeWidth: "7"
3144
+ }
3145
+ ),
3146
+ /* @__PURE__ */ jsxRuntime.jsx(
3147
+ "circle",
3148
+ {
3149
+ cx: size / 2,
3150
+ cy: size / 2,
3151
+ r,
3152
+ fill: "none",
3153
+ stroke: color,
3154
+ strokeWidth: "7",
3155
+ strokeDasharray: `${circ} ${circ}`,
3156
+ strokeDashoffset: offset,
3157
+ strokeLinecap: "round",
3158
+ transform: `rotate(-90 ${size / 2} ${size / 2})`,
3159
+ style: {
3160
+ transition: "stroke-dashoffset 0.9s cubic-bezier(0.4,0,0.2,1)"
3161
+ }
3162
+ }
3163
+ )
3164
+ ]
3165
+ }
3166
+ );
3167
+ }
2639
3168
  function relTime(date) {
2640
- const d = typeof date === "string" ? new Date(date) : date;
2641
- const m = Math.floor((Date.now() - d.getTime()) / 6e4);
2642
- if (m < 1) return "just now";
2643
- if (m < 60) return `${m}m ago`;
2644
- const h = Math.floor(m / 60);
2645
- if (h < 24) return `${h}h ago`;
2646
- return `${Math.floor(h / 24)}d ago`;
3169
+ const diff = Date.now() - new Date(date).getTime();
3170
+ const mins = Math.floor(diff / 6e4);
3171
+ if (mins < 1) return "just now";
3172
+ if (mins < 60) return `${mins}m`;
3173
+ const hrs = Math.floor(mins / 60);
3174
+ if (hrs < 24) return `${hrs}h`;
3175
+ return `${Math.floor(hrs / 24)}d`;
2647
3176
  }
2648
- function fmtDate(date) {
2649
- return new Date(date).toLocaleDateString(void 0, {
2650
- month: "short",
2651
- day: "numeric",
2652
- year: "numeric"
2653
- });
3177
+ function rateHue(r) {
3178
+ return r >= 70 ? 142 : r >= 40 ? 38 : 4;
2654
3179
  }
2655
- function StatCardItem({
3180
+ function StatItem({
2656
3181
  id,
2657
3182
  label,
2658
3183
  value,
@@ -2661,26 +3186,39 @@ function StatCardItem({
2661
3186
  color,
2662
3187
  delay = 0
2663
3188
  }) {
3189
+ const animated = useCountUp(value);
2664
3190
  const isPos = pct === void 0 || pct >= 0;
2665
- return /* @__PURE__ */ jsxRuntime.jsxs(Card, { $delay: delay, $accent: color, children: [
2666
- /* @__PURE__ */ jsxRuntime.jsxs(StatTop, { children: [
2667
- /* @__PURE__ */ jsxRuntime.jsx(StatLabel, { children: label }),
2668
- /* @__PURE__ */ jsxRuntime.jsx(StatValue, { children: typeof value === "number" ? value.toLocaleString() : value }),
2669
- pct !== void 0 && /* @__PURE__ */ jsxRuntime.jsxs(TrendBadge, { $pos: isPos, children: [
2670
- isPos ? "↑" : "↓",
2671
- " ",
2672
- Math.abs(pct).toFixed(1),
2673
- "%"
2674
- ] })
3191
+ return /* @__PURE__ */ jsxRuntime.jsxs(StatCard, { $delay: delay, $accent: color, children: [
3192
+ /* @__PURE__ */ jsxRuntime.jsx(StatLabel, { children: label }),
3193
+ /* @__PURE__ */ jsxRuntime.jsx(StatValue, { children: animated.toLocaleString() }),
3194
+ pct !== void 0 && /* @__PURE__ */ jsxRuntime.jsxs(TrendBadge, { $pos: isPos, children: [
3195
+ isPos ? "↑" : "↓",
3196
+ " ",
3197
+ Math.abs(pct).toFixed(1),
3198
+ "%"
2675
3199
  ] }),
2676
3200
  sparkline && sparkline.length > 1 && /* @__PURE__ */ jsxRuntime.jsx(SparkWrap, { children: /* @__PURE__ */ jsxRuntime.jsx(Sparkline, { data: sparkline, color, id }) })
2677
3201
  ] });
2678
3202
  }
2679
3203
  function OverviewPage() {
3204
+ const { get } = admin.useFetchClient();
2680
3205
  const [period, setPeriod] = react.useState(
2681
3206
  "weekly"
2682
3207
  );
3208
+ const [feedMode, setFeedMode] = react.useState("signups");
2683
3209
  const [hovIdx, setHovIdx] = react.useState(null);
3210
+ const [rtnHov, setRtnHov] = react.useState(null);
3211
+ const [activeSeries, setActiveSeries] = react.useState(
3212
+ () => /* @__PURE__ */ new Set(["totalUsers", "newUsers", "activeUsers"])
3213
+ );
3214
+ const toggleSeries = (key) => {
3215
+ setActiveSeries((prev) => {
3216
+ const next = new Set(prev);
3217
+ if (next.has(key) && next.size > 1) next.delete(key);
3218
+ else next.add(key);
3219
+ return next;
3220
+ });
3221
+ };
2684
3222
  const statsQuery = reactQuery.useQuery({
2685
3223
  queryKey: ["dash-user-stats"],
2686
3224
  queryFn: async () => {
@@ -2697,12 +3235,10 @@ function OverviewPage() {
2697
3235
  return r.data;
2698
3236
  }
2699
3237
  });
2700
- const sessionsQuery = reactQuery.useQuery({
2701
- queryKey: ["dash-recent-sessions"],
3238
+ const retentionQuery = reactQuery.useQuery({
3239
+ queryKey: ["dash-user-retention", period],
2702
3240
  queryFn: async () => {
2703
- const r = await client.dash.listAllSessions({
2704
- query: { page: 1, limit: 10 }
2705
- });
3241
+ const r = await client.dash.userRetentionData({ query: { period } });
2706
3242
  if (r.error) throw new Error(r.error.message ?? "Failed");
2707
3243
  return r.data;
2708
3244
  }
@@ -2711,12 +3247,22 @@ function OverviewPage() {
2711
3247
  queryKey: ["dash-recent-users"],
2712
3248
  queryFn: async () => {
2713
3249
  const r = await client.dash.listUsers({
2714
- query: { page: 1, limit: 8, search: "" }
3250
+ query: { limit: 12, offset: 0, sortBy: "createdAt", sortOrder: "desc" }
2715
3251
  });
2716
3252
  if (r.error) throw new Error(r.error.message ?? "Failed");
2717
3253
  return r.data;
2718
3254
  }
2719
3255
  });
3256
+ const sessionsQuery = reactQuery.useQuery({
3257
+ queryKey: ["dash-recent-sessions"],
3258
+ queryFn: async () => {
3259
+ const { data } = await get(
3260
+ "/better-auth-dashboard/db?uid=plugin::better-auth.session&pagination[pageSize]=12&sort[0]=createdAt:desc"
3261
+ );
3262
+ return data.results ?? [];
3263
+ },
3264
+ refetchInterval: 3e4
3265
+ });
2720
3266
  const orgsQuery = reactQuery.useQuery({
2721
3267
  queryKey: ["dash-orgs-count"],
2722
3268
  queryFn: async () => {
@@ -2728,594 +3274,344 @@ function OverviewPage() {
2728
3274
  }
2729
3275
  });
2730
3276
  if (statsQuery.isLoading) {
2731
- return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", alignItems: "center", padding: 12, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading…" }) });
3277
+ return /* @__PURE__ */ jsxRuntime.jsx(
3278
+ designSystem.Flex,
3279
+ {
3280
+ justifyContent: "center",
3281
+ alignItems: "center",
3282
+ padding: 12,
3283
+ style: { background: T.bg, minHeight: "100%" },
3284
+ children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading…" })
3285
+ }
3286
+ );
2732
3287
  }
2733
3288
  const stats = statsQuery.data;
2734
3289
  if (!stats) return null;
2735
3290
  const graphData = graphQuery.data?.data ?? [];
2736
- const sessions = sessionsQuery.data?.sessions ?? [];
2737
- const recentUsers = usersQuery.data?.users ?? [];
3291
+ const rtnData = retentionQuery.data?.data ?? [];
3292
+ const users = usersQuery.data?.users ?? [];
3293
+ const sessions = sessionsQuery.data ?? [];
2738
3294
  const orgCount = orgsQuery.data?.total ?? 0;
2739
3295
  const totalSpark = graphData.map((d) => d.totalUsers);
2740
3296
  const newSpark = graphData.map((d) => d.newUsers);
2741
3297
  const activeSpark = graphData.map((d) => d.activeUsers);
2742
3298
  const hovRow = hovIdx !== null ? graphData[hovIdx] : null;
2743
- return /* @__PURE__ */ jsxRuntime.jsxs(Wrap$2, { "data-testid": "overview-page", children: [
3299
+ const rtnHovRow = rtnHov !== null ? rtnData[rtnHov] : null;
3300
+ const activeMax = Math.max(
3301
+ stats.activeUsers.daily.active ?? 0,
3302
+ stats.activeUsers.weekly.active ?? 0,
3303
+ stats.activeUsers.monthly.active ?? 0,
3304
+ 1
3305
+ );
3306
+ return /* @__PURE__ */ jsxRuntime.jsxs(Wrap$1, { "data-testid": "overview-page", children: [
2744
3307
  /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "flex-end", children: [
2745
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { children: [
2746
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "alpha", textColor: "neutral800", children: "Overview" }),
2747
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { paddingTop: 1, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: (/* @__PURE__ */ new Date()).toLocaleDateString(void 0, {
3308
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
3309
+ /* @__PURE__ */ jsxRuntime.jsx(
3310
+ "div",
3311
+ {
3312
+ style: {
3313
+ fontSize: 22,
3314
+ fontWeight: 800,
3315
+ color: T.textPrimary,
3316
+ letterSpacing: "-0.03em"
3317
+ },
3318
+ children: "Overview"
3319
+ }
3320
+ ),
3321
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 11, color: T.textSecondary, marginTop: 4 }, children: (/* @__PURE__ */ new Date()).toLocaleDateString(void 0, {
2748
3322
  weekday: "long",
2749
3323
  year: "numeric",
2750
3324
  month: "long",
2751
3325
  day: "numeric"
2752
- }) }) })
3326
+ }) })
2753
3327
  ] }),
2754
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { width: "150px", children: /* @__PURE__ */ jsxRuntime.jsxs(
2755
- designSystem.SingleSelect,
2756
- {
2757
- value: period,
2758
- onChange: (v) => setPeriod(v),
2759
- size: "S",
2760
- "aria-label": "Select period",
2761
- children: [
2762
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "daily", children: "Daily" }),
2763
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "weekly", children: "Weekly" }),
2764
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "monthly", children: "Monthly" })
2765
- ]
2766
- }
2767
- ) })
3328
+ /* @__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)) })
2768
3329
  ] }),
2769
- /* @__PURE__ */ jsxRuntime.jsxs(Divider, { children: [
2770
- /* @__PURE__ */ jsxRuntime.jsx(DivLabel, { children: "Sign-ups" }),
3330
+ /* @__PURE__ */ jsxRuntime.jsxs(SectionDivider, { children: [
3331
+ /* @__PURE__ */ jsxRuntime.jsx(DivLabel, { children: "Metrics" }),
2771
3332
  /* @__PURE__ */ jsxRuntime.jsx(DivLine, {})
2772
3333
  ] }),
2773
- /* @__PURE__ */ jsxRuntime.jsxs(StatRow, { children: [
3334
+ /* @__PURE__ */ jsxRuntime.jsxs(StatGrid, { children: [
2774
3335
  /* @__PURE__ */ jsxRuntime.jsx(
2775
- StatCardItem,
3336
+ StatItem,
2776
3337
  {
2777
3338
  id: "total",
2778
3339
  label: "Total Users",
2779
3340
  value: stats.total ?? 0,
2780
3341
  sparkline: totalSpark,
2781
- color: "#4945FF",
3342
+ color: T.accent,
2782
3343
  delay: 0
2783
3344
  }
2784
3345
  ),
2785
3346
  /* @__PURE__ */ jsxRuntime.jsx(
2786
- StatCardItem,
3347
+ StatItem,
2787
3348
  {
2788
3349
  id: "d-sig",
2789
3350
  label: "Daily Sign-ups",
2790
3351
  value: stats.daily.signUps ?? 0,
2791
3352
  pct: stats.daily.percentage ?? void 0,
2792
3353
  sparkline: newSpark,
2793
- color: "#5CB176",
3354
+ color: T.green,
2794
3355
  delay: 1
2795
3356
  }
2796
3357
  ),
2797
3358
  /* @__PURE__ */ jsxRuntime.jsx(
2798
- StatCardItem,
3359
+ StatItem,
2799
3360
  {
2800
3361
  id: "w-sig",
2801
3362
  label: "Weekly Sign-ups",
2802
3363
  value: stats.weekly.signUps ?? 0,
2803
3364
  pct: stats.weekly.percentage ?? void 0,
2804
3365
  sparkline: newSpark,
2805
- color: "#5CB176",
3366
+ color: T.green,
2806
3367
  delay: 2
2807
3368
  }
2808
- ),
2809
- /* @__PURE__ */ jsxRuntime.jsx(
2810
- StatCardItem,
2811
- {
2812
- id: "m-sig",
2813
- label: "Monthly Sign-ups",
2814
- value: stats.monthly.signUps ?? 0,
2815
- pct: stats.monthly.percentage ?? void 0,
2816
- sparkline: newSpark,
2817
- color: "#5CB176",
2818
- delay: 3
2819
- }
2820
- ),
2821
- /* @__PURE__ */ jsxRuntime.jsx(
2822
- StatCardItem,
2823
- {
2824
- id: "orgs",
2825
- label: "Organizations",
2826
- value: orgCount,
2827
- color: "#9E6BF9",
2828
- delay: 4
2829
- }
2830
- )
2831
- ] }),
2832
- /* @__PURE__ */ jsxRuntime.jsxs(MainRow, { children: [
2833
- /* @__PURE__ */ jsxRuntime.jsxs(ChartCard, { $delay: 5, children: [
2834
- /* @__PURE__ */ jsxRuntime.jsxs(ChartHeader, { children: [
2835
- /* @__PURE__ */ jsxRuntime.jsx(ChartTitle, { children: "User Growth" }),
2836
- /* @__PURE__ */ jsxRuntime.jsx(LegendRow, { children: SERIES.map((s) => /* @__PURE__ */ jsxRuntime.jsxs(LegendItem, { children: [
2837
- /* @__PURE__ */ jsxRuntime.jsx(LegendDot, { $c: s.color }),
2838
- s.label
2839
- ] }, s.color)) })
2840
- ] }),
2841
- /* @__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" }),
2842
- graphQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(
2843
- designSystem.Flex,
2844
- {
2845
- justifyContent: "center",
2846
- alignItems: "center",
2847
- style: { flex: 1, minHeight: 180 },
2848
- children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading…" })
2849
- }
2850
- ) : /* @__PURE__ */ jsxRuntime.jsx(AreaChart, { data: graphData, hovered: hovIdx, onHover: setHovIdx })
2851
- ] }),
2852
- /* @__PURE__ */ jsxRuntime.jsxs(FeedCard, { $delay: 6, children: [
2853
- /* @__PURE__ */ jsxRuntime.jsx(FeedHeader, { children: "Recent Sessions" }),
2854
- /* @__PURE__ */ jsxRuntime.jsx(FeedList, { children: sessionsQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(Empty, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading…" }) }) : sessions.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(Empty, { children: "No recent sessions" }) : sessions.map((s) => /* @__PURE__ */ jsxRuntime.jsxs(FeedItem, { children: [
2855
- /* @__PURE__ */ jsxRuntime.jsx(
2856
- Avatar,
2857
- {
2858
- name: s.user?.name ?? "?",
2859
- src: s.user?.image,
2860
- size: 28
2861
- }
2862
- ),
2863
- /* @__PURE__ */ jsxRuntime.jsxs(FeedContent, { children: [
2864
- /* @__PURE__ */ jsxRuntime.jsx(FeedName, { children: s.user?.name ?? "Unknown" }),
2865
- /* @__PURE__ */ jsxRuntime.jsx(FeedSub, { children: s.user?.email ?? "" }),
2866
- /* @__PURE__ */ jsxRuntime.jsx(FeedTime, { children: relTime(s.createdAt) })
2867
- ] })
2868
- ] }, s.id)) })
2869
- ] })
2870
- ] }),
2871
- /* @__PURE__ */ jsxRuntime.jsxs(Divider, { children: [
2872
- /* @__PURE__ */ jsxRuntime.jsx(DivLabel, { children: "Active Users" }),
2873
- /* @__PURE__ */ jsxRuntime.jsx(DivLine, {})
2874
- ] }),
2875
- /* @__PURE__ */ jsxRuntime.jsxs(ActiveRow, { children: [
2876
- /* @__PURE__ */ jsxRuntime.jsx(
2877
- StatCardItem,
2878
- {
2879
- id: "d-act",
2880
- label: "Daily Active",
2881
- value: stats.activeUsers.daily.active ?? 0,
2882
- pct: stats.activeUsers.daily.percentage ?? void 0,
2883
- sparkline: activeSpark,
2884
- color: "#E57553",
2885
- delay: 7
2886
- }
2887
- ),
2888
- /* @__PURE__ */ jsxRuntime.jsx(
2889
- StatCardItem,
2890
- {
2891
- id: "w-act",
2892
- label: "Weekly Active",
2893
- value: stats.activeUsers.weekly.active ?? 0,
2894
- pct: stats.activeUsers.weekly.percentage ?? void 0,
2895
- sparkline: activeSpark,
2896
- color: "#E57553",
2897
- delay: 8
2898
- }
2899
- ),
2900
- /* @__PURE__ */ jsxRuntime.jsx(
2901
- StatCardItem,
2902
- {
2903
- id: "m-act",
2904
- label: "Monthly Active",
2905
- value: stats.activeUsers.monthly.active ?? 0,
2906
- pct: stats.activeUsers.monthly.percentage ?? void 0,
2907
- sparkline: activeSpark,
2908
- color: "#E57553",
2909
- delay: 9
2910
- }
2911
- )
2912
- ] }),
2913
- /* @__PURE__ */ jsxRuntime.jsxs(Divider, { children: [
2914
- /* @__PURE__ */ jsxRuntime.jsx(DivLabel, { children: "Recent Users" }),
2915
- /* @__PURE__ */ jsxRuntime.jsx(DivLine, {})
2916
- ] }),
2917
- /* @__PURE__ */ jsxRuntime.jsx(UsersCard, { $delay: 10, children: /* @__PURE__ */ jsxRuntime.jsx(TableWrap, { children: /* @__PURE__ */ jsxRuntime.jsxs(Table$1, { children: [
2918
- /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsx("tr", { children: ["User", "Email", "Joined", "Status"].map((h) => /* @__PURE__ */ jsxRuntime.jsx(TH$1, { children: h }, h)) }) }),
2919
- /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: usersQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsx(TD$1, { colSpan: 4, style: { textAlign: "center", padding: 28 }, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading users…" }) }) }) : recentUsers.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsx(
2920
- TD$1,
2921
- {
2922
- colSpan: 4,
2923
- style: {
2924
- textAlign: "center",
2925
- padding: 28,
2926
- color: "#8e8ea9"
2927
- },
2928
- children: "No users yet"
2929
- }
2930
- ) }) : recentUsers.map((u) => /* @__PURE__ */ jsxRuntime.jsxs(TR$1, { children: [
2931
- /* @__PURE__ */ jsxRuntime.jsx(TD$1, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { alignItems: "center", gap: 2, children: [
2932
- /* @__PURE__ */ jsxRuntime.jsx(Avatar, { name: u.name, src: u.image, size: 26 }),
2933
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontWeight: 600, fontSize: 12 }, children: u.name })
2934
- ] }) }),
2935
- /* @__PURE__ */ jsxRuntime.jsx(
2936
- TD$1,
2937
- {
2938
- style: {
2939
- color: "#8e8ea9",
2940
- fontFamily: "monospace",
2941
- fontSize: 11
2942
- },
2943
- children: u.email
2944
- }
2945
- ),
2946
- /* @__PURE__ */ jsxRuntime.jsx(
2947
- TD$1,
2948
- {
2949
- style: {
2950
- color: "#8e8ea9",
2951
- fontSize: 11,
2952
- whiteSpace: "nowrap"
2953
- },
2954
- children: fmtDate(u.createdAt)
2955
- }
2956
- ),
2957
- /* @__PURE__ */ jsxRuntime.jsx(TD$1, { children: /* @__PURE__ */ jsxRuntime.jsx(
2958
- StatusChip$1,
2959
- {
2960
- $verified: u.emailVerified,
2961
- $banned: u.banned,
2962
- children: u.banned ? "Banned" : u.emailVerified ? "Verified" : "Unverified"
2963
- }
2964
- ) })
2965
- ] }, u.id)) })
2966
- ] }) }) })
2967
- ] });
2968
- }
2969
- const PAGE_SIZE$1 = 25;
2970
- const fadeUp$1 = styled.keyframes`
2971
- from { opacity: 0; transform: translateY(6px); }
2972
- to { opacity: 1; transform: translateY(0); }
2973
- `;
2974
- const Wrap$1 = styled__default.default.div`
2975
- padding: 28px 32px;
2976
- background: #f6f6f9;
2977
- min-height: 100%;
2978
- display: flex;
2979
- flex-direction: column;
2980
- gap: 20px;
2981
- `;
2982
- const PageHeader$1 = styled__default.default.div`
2983
- display: flex;
2984
- justify-content: space-between;
2985
- align-items: flex-start;
2986
- `;
2987
- const TitleBlock$1 = styled__default.default.div`
2988
- display: flex;
2989
- flex-direction: column;
2990
- gap: 4px;
2991
- `;
2992
- const PageTitle$1 = styled__default.default.h1`
2993
- margin: 0;
2994
- font-size: 22px;
2995
- font-weight: 800;
2996
- color: #32324d;
2997
- letter-spacing: -0.03em;
2998
- `;
2999
- const PageSubtitle$1 = styled__default.default.p`
3000
- margin: 0;
3001
- font-size: 12px;
3002
- color: #8e8ea9;
3003
- `;
3004
- const TableCard$1 = styled__default.default.div`
3005
- background: #ffffff;
3006
- border: 1px solid #eaeaef;
3007
- border-radius: 10px;
3008
- overflow: hidden;
3009
- `;
3010
- const ColumnHeader = styled__default.default.div`
3011
- padding: 10px 20px;
3012
- display: flex;
3013
- align-items: center;
3014
- gap: 12px;
3015
- background: #fafafa;
3016
- border-bottom: 1px solid #eaeaef;
3017
- font-size: 10px;
3018
- font-weight: 700;
3019
- text-transform: uppercase;
3020
- letter-spacing: 0.08em;
3021
- color: #8e8ea9;
3022
- `;
3023
- const ColumnHeaderRight = styled__default.default.div`
3024
- margin-left: auto;
3025
- font-size: 10px;
3026
- font-weight: 700;
3027
- text-transform: uppercase;
3028
- letter-spacing: 0.08em;
3029
- color: #8e8ea9;
3030
- `;
3031
- const UserGroup = styled__default.default.div`
3032
- border-bottom: 1px solid #eaeaef;
3033
- animation: ${fadeUp$1} 280ms ease both;
3034
- animation-delay: ${(p) => (p.$i ?? 0) * 30}ms;
3035
- &:last-child { border-bottom: none; }
3036
- `;
3037
- const UserRowHeader = styled__default.default.div`
3038
- padding: 14px 20px;
3039
- display: flex;
3040
- align-items: center;
3041
- gap: 12px;
3042
- border-bottom: 1px solid #f5f5f9;
3043
- cursor: default;
3044
- &:hover { background: #fafafe; }
3045
- `;
3046
- const UserInfo = styled__default.default.div`
3047
- flex: 1;
3048
- display: flex;
3049
- flex-direction: column;
3050
- gap: 2px;
3051
- `;
3052
- const UserName$1 = styled__default.default.span`
3053
- font-size: 12px;
3054
- font-weight: 600;
3055
- color: #32324d;
3056
- `;
3057
- const UserEmail = styled__default.default.span`
3058
- font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
3059
- font-size: 11px;
3060
- color: #8e8ea9;
3061
- `;
3062
- const SessionCountChip = styled__default.default.span`
3063
- display: inline-flex;
3064
- align-items: center;
3065
- padding: 2px 8px;
3066
- border-radius: 20px;
3067
- font-size: 10px;
3068
- font-weight: 700;
3069
- background: #f0f0ff;
3070
- color: #4945ff;
3071
- `;
3072
- const SessionCard = styled__default.default.div`
3073
- padding: 10px 20px 10px 64px;
3074
- border-top: 1px solid #f5f5f9;
3075
- background: #fafafa;
3076
- display: flex;
3077
- align-items: flex-start;
3078
- justify-content: space-between;
3079
- gap: 16px;
3080
- &:hover { background: #f5f5ff; }
3081
- `;
3082
- const SessionMeta = styled__default.default.div`
3083
- display: flex;
3084
- flex-direction: column;
3085
- gap: 3px;
3086
- flex: 1;
3087
- min-width: 0;
3088
- `;
3089
- const IpChip = styled__default.default.span`
3090
- display: inline-block;
3091
- background: #f0f0ff;
3092
- color: #4945ff;
3093
- border-radius: 5px;
3094
- padding: 2px 6px;
3095
- font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
3096
- font-size: 11px;
3097
- font-weight: 600;
3098
- `;
3099
- const TimestampText = styled__default.default.span`
3100
- font-size: 11px;
3101
- color: #8e8ea9;
3102
- font-variant-numeric: tabular-nums;
3103
- `;
3104
- const AgentText = styled__default.default.span`
3105
- font-size: 11px;
3106
- color: #b8b8c7;
3107
- overflow: hidden;
3108
- text-overflow: ellipsis;
3109
- white-space: nowrap;
3110
- display: block;
3111
- max-width: 400px;
3112
- `;
3113
- const EmptyState = styled__default.default.div`
3114
- display: flex;
3115
- justify-content: center;
3116
- align-items: center;
3117
- padding: 48px;
3118
- font-size: 12px;
3119
- color: #8e8ea9;
3120
- `;
3121
- function SessionsPage() {
3122
- const qc = reactQuery.useQueryClient();
3123
- const [page, setPage] = react.useState(1);
3124
- const [selected, setSelected] = react.useState(/* @__PURE__ */ new Set());
3125
- const [confirmRevokeSessionId, setConfirmRevokeSessionId] = react.useState(null);
3126
- const [confirmRevokeMany, setConfirmRevokeMany] = react.useState(false);
3127
- const sessionsQuery = reactQuery.useQuery({
3128
- queryKey: ["dash-all-sessions", page],
3129
- queryFn: async () => {
3130
- const result = await client.dash.listAllSessions({});
3131
- if (result.error)
3132
- throw new Error(result.error.message ?? "Failed to load sessions");
3133
- return result.data ?? [];
3134
- },
3135
- keepPreviousData: true
3136
- });
3137
- const revokeSessionMutation = reactQuery.useMutation({
3138
- mutationFn: async (sessionId) => {
3139
- const result = await client.dash.sessions.revoke(
3140
- {},
3141
- withContext({ sessionId })
3142
- );
3143
- if (result.error)
3144
- throw new Error(result.error.message ?? "Revoke failed");
3145
- },
3146
- onSuccess: () => {
3147
- setConfirmRevokeSessionId(null);
3148
- qc.invalidateQueries({ queryKey: ["dash-all-sessions"] });
3149
- }
3150
- });
3151
- const revokeManyMutation = reactQuery.useMutation({
3152
- mutationFn: async (userIds) => {
3153
- const result = await client.dash.sessions.revokeMany(
3154
- {},
3155
- withContext({ userIds })
3156
- );
3157
- if (result.error)
3158
- throw new Error(result.error.message ?? "Revoke failed");
3159
- return result.data;
3160
- },
3161
- onSuccess: () => {
3162
- setConfirmRevokeMany(false);
3163
- setSelected(/* @__PURE__ */ new Set());
3164
- qc.invalidateQueries({ queryKey: ["dash-all-sessions"] });
3165
- }
3166
- });
3167
- const usersWithSessions = sessionsQuery.data ?? [];
3168
- const allUserIds = usersWithSessions.map((u) => u.id);
3169
- const allSelected = allUserIds.length > 0 && allUserIds.every((id) => selected.has(id));
3170
- const someSelected = selected.size > 0;
3171
- const toggleSelect = (id) => {
3172
- setSelected((prev) => {
3173
- const next = new Set(prev);
3174
- if (next.has(id)) next.delete(id);
3175
- else next.add(id);
3176
- return next;
3177
- });
3178
- };
3179
- const handleSelectAll = () => {
3180
- if (allSelected) setSelected(/* @__PURE__ */ new Set());
3181
- else setSelected(new Set(allUserIds));
3182
- };
3183
- return /* @__PURE__ */ jsxRuntime.jsxs(Wrap$1, { "data-testid": "sessions-page", children: [
3184
- /* @__PURE__ */ jsxRuntime.jsxs(PageHeader$1, { children: [
3185
- /* @__PURE__ */ jsxRuntime.jsxs(TitleBlock$1, { children: [
3186
- /* @__PURE__ */ jsxRuntime.jsx(PageTitle$1, { children: "Sessions" }),
3187
- /* @__PURE__ */ jsxRuntime.jsx(PageSubtitle$1, { children: usersWithSessions.length > 0 ? `${usersWithSessions.length} user${usersWithSessions.length !== 1 ? "s" : ""} with active sessions` : "Active sessions across all users" })
3188
- ] }),
3189
- someSelected && /* @__PURE__ */ jsxRuntime.jsxs(
3190
- designSystem.Button,
3369
+ ),
3370
+ /* @__PURE__ */ jsxRuntime.jsx(
3371
+ StatItem,
3191
3372
  {
3192
- variant: "danger-light",
3193
- size: "S",
3194
- onClick: () => setConfirmRevokeMany(true),
3195
- "data-testid": "revoke-selected-btn",
3196
- children: [
3197
- "Revoke sessions for ",
3198
- selected.size,
3199
- " user",
3200
- selected.size !== 1 ? "s" : ""
3201
- ]
3373
+ id: "m-sig",
3374
+ label: "Monthly Sign-ups",
3375
+ value: stats.monthly.signUps ?? 0,
3376
+ pct: stats.monthly.percentage ?? void 0,
3377
+ sparkline: newSpark,
3378
+ color: T.green,
3379
+ delay: 3
3380
+ }
3381
+ ),
3382
+ /* @__PURE__ */ jsxRuntime.jsx(
3383
+ StatItem,
3384
+ {
3385
+ id: "orgs",
3386
+ label: "Organizations",
3387
+ value: orgCount,
3388
+ color: T.purple,
3389
+ delay: 4
3202
3390
  }
3203
3391
  )
3204
3392
  ] }),
3205
- sessionsQuery.isError && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { color: "#d02b20", fontSize: 12, padding: "8px 0" }, children: sessionsQuery.error instanceof Error ? sessionsQuery.error.message : "An error occurred" }),
3206
- /* @__PURE__ */ jsxRuntime.jsx(TableCard$1, { children: sessionsQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", padding: 8, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading sessions…" }) }) : usersWithSessions.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(EmptyState, { children: "No active sessions" }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3207
- /* @__PURE__ */ jsxRuntime.jsxs(ColumnHeader, { children: [
3208
- /* @__PURE__ */ jsxRuntime.jsx(
3209
- designSystem.Checkbox,
3393
+ /* @__PURE__ */ jsxRuntime.jsxs(SectionDivider, { children: [
3394
+ /* @__PURE__ */ jsxRuntime.jsx(DivLabel, { children: "Growth" }),
3395
+ /* @__PURE__ */ jsxRuntime.jsx(DivLine, {})
3396
+ ] }),
3397
+ /* @__PURE__ */ jsxRuntime.jsxs(TwoPanel, { children: [
3398
+ /* @__PURE__ */ jsxRuntime.jsxs(ChartCard, { $delay: 5, children: [
3399
+ /* @__PURE__ */ jsxRuntime.jsxs(ChartHeader, { children: [
3400
+ /* @__PURE__ */ jsxRuntime.jsx(ChartTitle, { children: "User Growth" }),
3401
+ /* @__PURE__ */ jsxRuntime.jsx(SeriesRow, { children: ALL_SERIES.map((s) => /* @__PURE__ */ jsxRuntime.jsxs(
3402
+ SeriesBtn,
3403
+ {
3404
+ $on: activeSeries.has(s.key),
3405
+ $c: s.color,
3406
+ onClick: () => toggleSeries(s.key),
3407
+ children: [
3408
+ /* @__PURE__ */ jsxRuntime.jsx(SDot, { $c: s.color }),
3409
+ s.label
3410
+ ]
3411
+ },
3412
+ s.key
3413
+ )) })
3414
+ ] }),
3415
+ /* @__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" }),
3416
+ graphQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(
3417
+ designSystem.Flex,
3210
3418
  {
3211
- checked: allSelected,
3212
- onCheckedChange: handleSelectAll,
3213
- "aria-label": "Select all users"
3419
+ justifyContent: "center",
3420
+ alignItems: "center",
3421
+ style: { flex: 1, minHeight: 170 },
3422
+ children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading…" })
3214
3423
  }
3215
- ),
3216
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: "User / Sessions" }),
3217
- /* @__PURE__ */ jsxRuntime.jsx(ColumnHeaderRight, { children: "Sessions" })
3424
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
3425
+ GrowthChart,
3426
+ {
3427
+ data: graphData,
3428
+ hovered: hovIdx,
3429
+ onHover: setHovIdx,
3430
+ activeSeries
3431
+ }
3432
+ )
3218
3433
  ] }),
3219
- usersWithSessions.map((userRow, i) => /* @__PURE__ */ jsxRuntime.jsxs(UserGroup, { $i: i, "data-testid": "session-user-row", children: [
3220
- /* @__PURE__ */ jsxRuntime.jsxs(UserRowHeader, { children: [
3221
- /* @__PURE__ */ jsxRuntime.jsx(
3222
- designSystem.Checkbox,
3434
+ /* @__PURE__ */ jsxRuntime.jsxs(FeedCard, { $delay: 6, children: [
3435
+ /* @__PURE__ */ jsxRuntime.jsxs(FeedHead, { children: [
3436
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: feedMode === "signups" ? "Recent Sign-ups" : "Recent Active" }),
3437
+ /* @__PURE__ */ jsxRuntime.jsxs(
3438
+ FeedSelect,
3223
3439
  {
3224
- checked: selected.has(userRow.id),
3225
- onCheckedChange: () => toggleSelect(userRow.id),
3226
- "aria-label": `Select ${userRow.name}`
3440
+ value: feedMode,
3441
+ onChange: (e) => setFeedMode(e.target.value),
3442
+ children: [
3443
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "signups", children: "Recent Sign-ups" }),
3444
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "active", children: "Recent Active" })
3445
+ ]
3227
3446
  }
3228
- ),
3229
- /* @__PURE__ */ jsxRuntime.jsx(Avatar, { name: userRow.name ?? "", src: null, size: 30 }),
3230
- /* @__PURE__ */ jsxRuntime.jsxs(UserInfo, { children: [
3231
- /* @__PURE__ */ jsxRuntime.jsx(UserName$1, { children: userRow.name }),
3232
- /* @__PURE__ */ jsxRuntime.jsx(UserEmail, { children: userRow.email })
3233
- ] }),
3234
- /* @__PURE__ */ jsxRuntime.jsxs(SessionCountChip, { children: [
3235
- userRow.sessions.length,
3236
- " session",
3237
- userRow.sessions.length !== 1 ? "s" : ""
3238
- ] })
3447
+ )
3239
3448
  ] }),
3240
- userRow.sessions.map((session) => /* @__PURE__ */ jsxRuntime.jsxs(SessionCard, { "data-testid": "session-row", children: [
3241
- /* @__PURE__ */ jsxRuntime.jsxs(SessionMeta, { children: [
3242
- /* @__PURE__ */ jsxRuntime.jsxs(
3243
- designSystem.Flex,
3449
+ /* @__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: [
3450
+ /* @__PURE__ */ jsxRuntime.jsxs(FeedTop, { children: [
3451
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { alignItems: "center", gap: 1, style: { minWidth: 0 }, children: [
3452
+ /* @__PURE__ */ jsxRuntime.jsx(Avatar, { name: u.name, src: u.image, size: 20 }),
3453
+ /* @__PURE__ */ jsxRuntime.jsx(FeedName, { title: u.name, children: u.name })
3454
+ ] }),
3455
+ /* @__PURE__ */ jsxRuntime.jsx(FeedMeta, { children: relTime(u.createdAt) })
3456
+ ] }),
3457
+ /* @__PURE__ */ jsxRuntime.jsx(FeedEmail, { title: u.email, children: u.email })
3458
+ ] }, u.id)) : sessionsQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading…" }) }) : sessions.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(Empty, { children: "No sessions yet" }) : sessions.map((s) => /* @__PURE__ */ jsxRuntime.jsxs(FeedItem, { children: [
3459
+ /* @__PURE__ */ jsxRuntime.jsxs(FeedTop, { children: [
3460
+ /* @__PURE__ */ jsxRuntime.jsx(
3461
+ FeedName,
3244
3462
  {
3245
- gap: 2,
3246
- alignItems: "center",
3247
- style: { flexWrap: "wrap" },
3248
- children: [
3249
- session.ipAddress && /* @__PURE__ */ jsxRuntime.jsx(IpChip, { children: session.ipAddress }),
3250
- /* @__PURE__ */ jsxRuntime.jsxs(TimestampText, { children: [
3251
- "Created ",
3252
- new Date(session.createdAt).toLocaleString(),
3253
- " ",
3254
- "· Expires",
3255
- " ",
3256
- new Date(session.expiresAt).toLocaleString()
3257
- ] })
3258
- ]
3463
+ title: s.userAgent ?? void 0,
3464
+ style: { fontFamily: T.mono, fontSize: 10 },
3465
+ children: s.ipAddress ?? ""
3259
3466
  }
3260
3467
  ),
3261
- session.userAgent && /* @__PURE__ */ jsxRuntime.jsx(AgentText, { children: session.userAgent })
3468
+ /* @__PURE__ */ jsxRuntime.jsx(FeedMeta, { children: relTime(s.createdAt) })
3262
3469
  ] }),
3263
3470
  /* @__PURE__ */ jsxRuntime.jsx(
3264
- designSystem.IconButton,
3471
+ FeedEmail,
3265
3472
  {
3266
- label: "Revoke session",
3267
- onClick: () => setConfirmRevokeSessionId(session.id),
3268
- style: { flexShrink: 0 },
3269
- "data-testid": "revoke-session-btn",
3270
- children: /* @__PURE__ */ jsxRuntime.jsx(icons.Trash, {})
3473
+ title: s.userAgent ?? void 0,
3474
+ style: {
3475
+ fontSize: 9,
3476
+ overflow: "hidden",
3477
+ textOverflow: "ellipsis",
3478
+ whiteSpace: "nowrap"
3479
+ },
3480
+ children: s.userAgent ?? "Unknown agent"
3271
3481
  }
3272
3482
  )
3273
- ] }, session.id))
3274
- ] }, userRow.id))
3275
- ] }) }),
3276
- usersWithSessions.length === PAGE_SIZE$1 && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "flex-end", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, children: [
3277
- /* @__PURE__ */ jsxRuntime.jsx(
3278
- designSystem.Button,
3483
+ ] }, s.documentId)) })
3484
+ ] })
3485
+ ] }),
3486
+ /* @__PURE__ */ jsxRuntime.jsxs(SectionDivider, { children: [
3487
+ /* @__PURE__ */ jsxRuntime.jsx(DivLabel, { children: "Retention & Activity" }),
3488
+ /* @__PURE__ */ jsxRuntime.jsx(DivLine, {})
3489
+ ] }),
3490
+ /* @__PURE__ */ jsxRuntime.jsxs(TwoPanel, { children: [
3491
+ /* @__PURE__ */ jsxRuntime.jsxs(RtnCard, { $delay: 7, children: [
3492
+ /* @__PURE__ */ jsxRuntime.jsxs(ChartHeader, { children: [
3493
+ /* @__PURE__ */ jsxRuntime.jsx(ChartTitle, { children: "Cohort Retention" }),
3494
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { alignItems: "center", gap: 2, children: [
3495
+ { hue: 142, label: "≥70%" },
3496
+ { hue: 38, label: "40–70%" },
3497
+ { hue: 4, label: "<40%" }
3498
+ ].map(({ hue, label }) => /* @__PURE__ */ jsxRuntime.jsxs(
3499
+ designSystem.Flex,
3500
+ {
3501
+ alignItems: "center",
3502
+ gap: 1,
3503
+ style: { marginLeft: 8 },
3504
+ children: [
3505
+ /* @__PURE__ */ jsxRuntime.jsx(
3506
+ "span",
3507
+ {
3508
+ style: {
3509
+ width: 8,
3510
+ height: 8,
3511
+ borderRadius: "50%",
3512
+ background: `hsl(${hue},60%,50%)`,
3513
+ display: "inline-block",
3514
+ flexShrink: 0
3515
+ }
3516
+ }
3517
+ ),
3518
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: 9, color: T.textMuted }, children: label })
3519
+ ]
3520
+ },
3521
+ hue
3522
+ )) })
3523
+ ] }),
3524
+ /* @__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" }),
3525
+ retentionQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(
3526
+ designSystem.Flex,
3527
+ {
3528
+ justifyContent: "center",
3529
+ alignItems: "center",
3530
+ style: { minHeight: 80 },
3531
+ children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading…" })
3532
+ }
3533
+ ) : rtnData.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs(Empty, { children: [
3534
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 22 }, children: "📉" }),
3535
+ /* @__PURE__ */ jsxRuntime.jsx("div", { children: "No retention data for this period" })
3536
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", flexDirection: "column", gap: 5 }, children: rtnData.map((row, i) => {
3537
+ const hue = rateHue(row.retentionRate);
3538
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3539
+ RtnRow,
3540
+ {
3541
+ $hov: rtnHov === i,
3542
+ onMouseEnter: () => setRtnHov(i),
3543
+ onMouseLeave: () => setRtnHov(null),
3544
+ children: [
3545
+ /* @__PURE__ */ jsxRuntime.jsx(RtnLabel, { children: row.label }),
3546
+ /* @__PURE__ */ jsxRuntime.jsx(RtnSize, { children: row.cohortSize.toLocaleString() }),
3547
+ /* @__PURE__ */ jsxRuntime.jsx(RtnTrack, { children: /* @__PURE__ */ jsxRuntime.jsx(RtnBar, { $w: row.retentionRate, $hue: hue }) }),
3548
+ /* @__PURE__ */ jsxRuntime.jsxs(RtnPct, { $hue: hue, children: [
3549
+ row.retentionRate.toFixed(1),
3550
+ "%"
3551
+ ] })
3552
+ ]
3553
+ },
3554
+ row.n
3555
+ );
3556
+ }) })
3557
+ ] }),
3558
+ /* @__PURE__ */ jsxRuntime.jsx(RingGrid, { children: [
3279
3559
  {
3280
- variant: "tertiary",
3281
- size: "S",
3282
- disabled: page === 1,
3283
- onClick: () => setPage((p) => p - 1),
3284
- children: "Previous"
3285
- }
3286
- ),
3287
- /* @__PURE__ */ jsxRuntime.jsx(
3288
- designSystem.Button,
3560
+ label: "Daily Active",
3561
+ value: stats.activeUsers.daily.active,
3562
+ pct: stats.activeUsers.daily.percentage,
3563
+ color: T.amber,
3564
+ sparkline: activeSpark,
3565
+ delay: 8
3566
+ },
3289
3567
  {
3290
- variant: "tertiary",
3291
- size: "S",
3292
- onClick: () => setPage((p) => p + 1),
3293
- children: "Next"
3568
+ label: "Weekly Active",
3569
+ value: stats.activeUsers.weekly.active,
3570
+ pct: stats.activeUsers.weekly.percentage,
3571
+ color: T.green,
3572
+ sparkline: activeSpark,
3573
+ delay: 9
3574
+ },
3575
+ {
3576
+ label: "Monthly Active",
3577
+ value: stats.activeUsers.monthly.active,
3578
+ pct: stats.activeUsers.monthly.percentage,
3579
+ color: T.accent,
3580
+ sparkline: activeSpark,
3581
+ delay: 10
3294
3582
  }
3295
- )
3296
- ] }) }),
3297
- confirmRevokeSessionId && /* @__PURE__ */ jsxRuntime.jsx(
3298
- ConfirmDialog,
3299
- {
3300
- title: "Revoke session",
3301
- message: "Are you sure you want to revoke this session? The user will be signed out on this device.",
3302
- confirmLabel: "Revoke",
3303
- loading: revokeSessionMutation.isLoading,
3304
- onConfirm: () => revokeSessionMutation.mutate(confirmRevokeSessionId),
3305
- onCancel: () => setConfirmRevokeSessionId(null)
3306
- }
3307
- ),
3308
- confirmRevokeMany && /* @__PURE__ */ jsxRuntime.jsx(
3309
- ConfirmDialog,
3310
- {
3311
- title: `Revoke sessions for ${selected.size} user${selected.size !== 1 ? "s" : ""}`,
3312
- message: `Are you sure you want to revoke all sessions for ${selected.size} user${selected.size !== 1 ? "s" : ""}? They will be signed out on all their devices.`,
3313
- confirmLabel: "Revoke all",
3314
- loading: revokeManyMutation.isLoading,
3315
- onConfirm: () => revokeManyMutation.mutate([...selected]),
3316
- onCancel: () => setConfirmRevokeMany(false)
3317
- }
3318
- )
3583
+ ].map(({ label, value, pct, color, delay }) => {
3584
+ const isPos = pct == null || pct >= 0;
3585
+ return /* @__PURE__ */ jsxRuntime.jsxs(RingCard, { $delay: delay, children: [
3586
+ /* @__PURE__ */ jsxRuntime.jsx(
3587
+ ProgressRing,
3588
+ {
3589
+ value: value ?? 0,
3590
+ max: activeMax,
3591
+ color,
3592
+ size: 56
3593
+ }
3594
+ ),
3595
+ /* @__PURE__ */ jsxRuntime.jsxs(RingInfo, { children: [
3596
+ /* @__PURE__ */ jsxRuntime.jsx(RingLabel, { children: label }),
3597
+ /* @__PURE__ */ jsxRuntime.jsx(RingVal, { children: (value ?? 0).toLocaleString() }),
3598
+ pct != null && /* @__PURE__ */ jsxRuntime.jsxs(
3599
+ TrendBadge,
3600
+ {
3601
+ $pos: isPos,
3602
+ style: { marginTop: 4, marginBottom: 0 },
3603
+ children: [
3604
+ isPos ? "↑" : "↓",
3605
+ " ",
3606
+ Math.abs(pct).toFixed(1),
3607
+ "%"
3608
+ ]
3609
+ }
3610
+ )
3611
+ ] })
3612
+ ] }, label);
3613
+ }) })
3614
+ ] })
3319
3615
  ] });
3320
3616
  }
3321
3617
  function useUsers(options = {}) {
@@ -3556,6 +3852,44 @@ const ReadOnlyCodeInput = styled__default.default.div`
3556
3852
  word-break: break-all;
3557
3853
  overflow-wrap: anywhere;
3558
3854
  `;
3855
+ const SessionCard = styled__default.default.div`
3856
+ display: flex;
3857
+ align-items: flex-start;
3858
+ justify-content: space-between;
3859
+ gap: 12px;
3860
+ padding: 10px 0;
3861
+ border-bottom: 1px solid #f5f5f9;
3862
+ &:last-child { border-bottom: none; }
3863
+ `;
3864
+ const SessionMeta = styled__default.default.div`
3865
+ display: flex;
3866
+ flex-direction: column;
3867
+ gap: 3px;
3868
+ flex: 1;
3869
+ min-width: 0;
3870
+ `;
3871
+ const IpChip = styled__default.default.span`
3872
+ display: inline-block;
3873
+ background: #f0f0ff;
3874
+ color: #4945ff;
3875
+ border-radius: 5px;
3876
+ padding: 2px 6px;
3877
+ font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
3878
+ font-size: 11px;
3879
+ font-weight: 600;
3880
+ `;
3881
+ const TimestampText = styled__default.default.span`
3882
+ font-size: 11px;
3883
+ color: #8e8ea9;
3884
+ `;
3885
+ const AgentText = styled__default.default.span`
3886
+ font-size: 11px;
3887
+ color: #b8b8c7;
3888
+ overflow: hidden;
3889
+ text-overflow: ellipsis;
3890
+ white-space: nowrap;
3891
+ display: block;
3892
+ `;
3559
3893
  const STANDARD_FIELDS = /* @__PURE__ */ new Set([
3560
3894
  "id",
3561
3895
  "name",
@@ -3577,6 +3911,7 @@ function UserDetailDrawer({
3577
3911
  }) {
3578
3912
  const qc = reactQuery.useQueryClient();
3579
3913
  const { toggleNotification } = admin.useNotification();
3914
+ const { get, put } = admin.useFetchClient();
3580
3915
  const schemaQuery = useModelSchema("user");
3581
3916
  const userQuery = reactQuery.useQuery({
3582
3917
  queryKey: ["dash-user", userId],
@@ -3598,6 +3933,27 @@ function UserDetailDrawer({
3598
3933
  return result.data?.organizations ?? [];
3599
3934
  }
3600
3935
  });
3936
+ const sessionsQuery = reactQuery.useQuery({
3937
+ queryKey: ["dash-user-sessions", userId],
3938
+ queryFn: async () => {
3939
+ const { data } = await get(
3940
+ `/better-auth-dashboard/db?uid=plugin::better-auth.session&filters[userId][$eq]=${userId}&sort[0]=createdAt:desc&pagination[pageSize]=50`
3941
+ );
3942
+ return data.results ?? [];
3943
+ }
3944
+ });
3945
+ const strapiUserQuery = reactQuery.useQuery({
3946
+ queryKey: ["dash-strapi-user", userId],
3947
+ enabled: !!schemaQuery.data,
3948
+ queryFn: async () => {
3949
+ const relationFields = Object.entries(schemaQuery.data).filter(([, attr]) => attr.type === "relation").map(([fieldName]) => fieldName);
3950
+ const populateParam = relationFields.length > 0 ? `&populate=${encodeURIComponent(relationFields.join(","))}` : "";
3951
+ const { data } = await get(
3952
+ `/better-auth-dashboard/db?uid=plugin::better-auth.user&filters[id][$eq]=${userId}&pagination[pageSize]=1${populateParam}`
3953
+ );
3954
+ return data.results?.[0] ?? null;
3955
+ }
3956
+ });
3601
3957
  const [activeTab, setActiveTab] = react.useState("profile");
3602
3958
  const [editName, setEditName] = react.useState(void 0);
3603
3959
  const [editEmail, setEditEmail] = react.useState(void 0);
@@ -3608,6 +3964,7 @@ function UserDetailDrawer({
3608
3964
  const [banReason, setBanReason] = react.useState("");
3609
3965
  const [banExpiresDays, setBanExpiresDays] = react.useState("");
3610
3966
  const [confirmRevokeAll, setConfirmRevokeAll] = react.useState(false);
3967
+ const [confirmRevokeSessionId, setConfirmRevokeSessionId] = react.useState(null);
3611
3968
  const [confirmUnban, setConfirmUnban] = react.useState(false);
3612
3969
  const [confirmUnlinkAccountId, setConfirmUnlinkAccountId] = react.useState(null);
3613
3970
  const [confirmDisable2FA, setConfirmDisable2FA] = react.useState(false);
@@ -3626,28 +3983,43 @@ function UserDetailDrawer({
3626
3983
  setEditExtra((prev) => ({ ...prev, [name]: value }));
3627
3984
  };
3628
3985
  const extraData = {
3629
- ...user,
3986
+ ...strapiUserQuery.data ?? {},
3630
3987
  ...editExtra
3631
3988
  };
3632
3989
  const updateMutation = reactQuery.useMutation({
3633
3990
  mutationFn: async () => {
3634
- const body = { ...editExtra };
3635
- if (editName !== void 0) body.name = editName;
3636
- if (editEmail !== void 0) body.email = editEmail;
3991
+ const baBody = {};
3992
+ if (editName !== void 0) baBody.name = editName;
3993
+ if (editEmail !== void 0) baBody.email = editEmail;
3637
3994
  if (editEmailVerified !== void 0)
3638
- body.emailVerified = editEmailVerified;
3639
- if (editImage !== void 0) body.image = editImage;
3640
- const result = await client.dash.updateUser(
3641
- body,
3642
- withContext({ userId })
3643
- );
3644
- if (result.error)
3645
- throw new Error(result.error.message ?? "Update failed");
3646
- return result.data;
3995
+ baBody.emailVerified = editEmailVerified;
3996
+ if (editImage !== void 0) baBody.image = editImage;
3997
+ const ops = [];
3998
+ if (Object.keys(baBody).length > 0) {
3999
+ ops.push(
4000
+ client.dash.updateUser(baBody, withContext({ userId })).then((result) => {
4001
+ if (result.error)
4002
+ throw new Error(result.error.message ?? "Update failed");
4003
+ })
4004
+ );
4005
+ }
4006
+ if (Object.keys(editExtra).length > 0) {
4007
+ const documentId = strapiUserQuery.data?.documentId;
4008
+ if (!documentId)
4009
+ throw new Error("Could not resolve documentId for user");
4010
+ ops.push(
4011
+ put(
4012
+ `/better-auth-dashboard/db/${documentId}?uid=plugin::better-auth.user`,
4013
+ editExtra
4014
+ )
4015
+ );
4016
+ }
4017
+ await Promise.all(ops);
3647
4018
  },
3648
4019
  onSuccess: () => {
3649
4020
  qc.invalidateQueries({ queryKey: ["dash-user", userId] });
3650
4021
  qc.invalidateQueries({ queryKey: ["dash-users"] });
4022
+ qc.invalidateQueries({ queryKey: ["dash-strapi-user", userId] });
3651
4023
  setEditName(void 0);
3652
4024
  setEditEmail(void 0);
3653
4025
  setEditEmailVerified(void 0);
@@ -3666,6 +4038,27 @@ function UserDetailDrawer({
3666
4038
  });
3667
4039
  }
3668
4040
  });
4041
+ const revokeSessionMutation = reactQuery.useMutation({
4042
+ mutationFn: async (sessionId) => {
4043
+ const result = await client.dash.sessions.revoke(
4044
+ {},
4045
+ withContext({ sessionId })
4046
+ );
4047
+ if (result.error)
4048
+ throw new Error(result.error.message ?? "Revoke failed");
4049
+ },
4050
+ onSuccess: () => {
4051
+ setConfirmRevokeSessionId(null);
4052
+ qc.invalidateQueries({ queryKey: ["dash-user-sessions", userId] });
4053
+ toggleNotification({ type: "success", message: "Session revoked" });
4054
+ },
4055
+ onError: (err) => {
4056
+ toggleNotification({
4057
+ type: "danger",
4058
+ message: err.message ?? "Failed to revoke session"
4059
+ });
4060
+ }
4061
+ });
3669
4062
  const passwordMutation = reactQuery.useMutation({
3670
4063
  mutationFn: async () => {
3671
4064
  const result = await client.dash.setPassword(
@@ -4428,25 +4821,55 @@ function UserDetailDrawer({
4428
4821
  }
4429
4822
  ) })
4430
4823
  ] }) }) }),
4431
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "sessions", children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { direction: "column", gap: 5, paddingTop: 6, children: /* @__PURE__ */ jsxRuntime.jsxs(FormSection, { children: [
4432
- /* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { children: "Session management" }),
4433
- /* @__PURE__ */ jsxRuntime.jsxs(AccountRow, { children: [
4434
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 1, alignItems: "flex-start", children: [
4435
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", fontWeight: "semiBold", children: "Revoke all sessions" }),
4436
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: "Signs the user out immediately on all devices." })
4437
- ] }),
4438
- /* @__PURE__ */ jsxRuntime.jsx(
4439
- designSystem.Button,
4440
- {
4441
- variant: "danger-light",
4442
- size: "S",
4443
- onClick: () => setConfirmRevokeAll(true),
4444
- style: { flexShrink: 0 },
4445
- children: "Revoke all"
4446
- }
4447
- )
4824
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "sessions", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 5, paddingTop: 6, children: [
4825
+ /* @__PURE__ */ jsxRuntime.jsxs(FormSection, { children: [
4826
+ /* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { children: "Session management" }),
4827
+ /* @__PURE__ */ jsxRuntime.jsxs(AccountRow, { children: [
4828
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 1, alignItems: "flex-start", children: [
4829
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", fontWeight: "semiBold", children: "Revoke all sessions" }),
4830
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: "Signs the user out immediately on all devices." })
4831
+ ] }),
4832
+ /* @__PURE__ */ jsxRuntime.jsx(
4833
+ designSystem.Button,
4834
+ {
4835
+ variant: "danger-light",
4836
+ size: "S",
4837
+ onClick: () => setConfirmRevokeAll(true),
4838
+ style: { flexShrink: 0 },
4839
+ children: "Revoke all"
4840
+ }
4841
+ )
4842
+ ] })
4843
+ ] }),
4844
+ /* @__PURE__ */ jsxRuntime.jsxs(FormSection, { children: [
4845
+ /* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { children: "Active sessions" }),
4846
+ sessionsQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading sessions…" }) }) : (sessionsQuery.data ?? []).length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: "No active sessions." }) : /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { direction: "column", gap: 0, alignItems: "stretch", children: (sessionsQuery.data ?? []).map((session) => /* @__PURE__ */ jsxRuntime.jsxs(SessionCard, { children: [
4847
+ /* @__PURE__ */ jsxRuntime.jsxs(SessionMeta, { children: [
4848
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "center", style: { flexWrap: "wrap" }, children: [
4849
+ session.ipAddress && /* @__PURE__ */ jsxRuntime.jsx(IpChip, { children: session.ipAddress }),
4850
+ /* @__PURE__ */ jsxRuntime.jsxs(TimestampText, { children: [
4851
+ "Created",
4852
+ " ",
4853
+ new Date(session.createdAt).toLocaleString(),
4854
+ " · Expires",
4855
+ " ",
4856
+ new Date(session.expiresAt).toLocaleString()
4857
+ ] })
4858
+ ] }),
4859
+ session.userAgent && /* @__PURE__ */ jsxRuntime.jsx(AgentText, { children: session.userAgent })
4860
+ ] }),
4861
+ /* @__PURE__ */ jsxRuntime.jsx(
4862
+ designSystem.IconButton,
4863
+ {
4864
+ label: "Revoke session",
4865
+ onClick: () => setConfirmRevokeSessionId(String(session.id)),
4866
+ style: { flexShrink: 0 },
4867
+ children: /* @__PURE__ */ jsxRuntime.jsx(icons.Trash, {})
4868
+ }
4869
+ )
4870
+ ] }, session.documentId)) })
4448
4871
  ] })
4449
- ] }) }) }),
4872
+ ] }) }),
4450
4873
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "organizations", children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { direction: "column", gap: 5, paddingTop: 6, children: /* @__PURE__ */ jsxRuntime.jsxs(FormSection, { children: [
4451
4874
  /* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { children: "Memberships" }),
4452
4875
  orgsQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: "neutral500", children: "Loading…" }) : (orgsQuery.data ?? []).length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: "Not a member of any organizations." }) : /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { direction: "column", gap: 2, children: (orgsQuery.data ?? []).map(
@@ -4483,6 +4906,17 @@ function UserDetailDrawer({
4483
4906
  onCancel: () => setConfirmRevokeAll(false)
4484
4907
  }
4485
4908
  ),
4909
+ confirmRevokeSessionId && /* @__PURE__ */ jsxRuntime.jsx(
4910
+ ConfirmDialog,
4911
+ {
4912
+ title: "Revoke session",
4913
+ message: "Are you sure you want to revoke this session? The user will be signed out on this device.",
4914
+ confirmLabel: "Revoke",
4915
+ loading: revokeSessionMutation.isLoading,
4916
+ onConfirm: () => revokeSessionMutation.mutate(confirmRevokeSessionId),
4917
+ onCancel: () => setConfirmRevokeSessionId(null)
4918
+ }
4919
+ ),
4486
4920
  confirmUnban && /* @__PURE__ */ jsxRuntime.jsx(
4487
4921
  ConfirmDialog,
4488
4922
  {
@@ -5017,8 +5451,7 @@ const Accent = styled__default.default.div`
5017
5451
  const BrandIcon = styled__default.default.div`
5018
5452
  width: 30px;
5019
5453
  height: 30px;
5020
- border-radius: 7px;
5021
- background: linear-gradient(135deg, #4945ff 0%, #7b79ff 100%);
5454
+ background: black;
5022
5455
  display: flex;
5023
5456
  align-items: center;
5024
5457
  justify-content: center;
@@ -5094,7 +5527,7 @@ function App() {
5094
5527
  paddingBottom: 4,
5095
5528
  children: [
5096
5529
  /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "center", children: [
5097
- /* @__PURE__ */ jsxRuntime.jsx(BrandIcon, { children: "BA" }),
5530
+ /* @__PURE__ */ jsxRuntime.jsx(BrandIcon, { children: /* @__PURE__ */ jsxRuntime.jsx(index.PluginIcon, {}) }),
5098
5531
  /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { children: [
5099
5532
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "beta", textColor: "neutral800", children: "Better Auth" }),
5100
5533
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { paddingTop: "2px", children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: "Authentication Dashboard" }) })
@@ -5114,8 +5547,7 @@ function App() {
5114
5547
  "data-testid": "nav-organizations",
5115
5548
  children: "Organizations"
5116
5549
  }
5117
- ),
5118
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Trigger, { value: "sessions", "data-testid": "nav-sessions", children: "Sessions" })
5550
+ )
5119
5551
  ] })
5120
5552
  ]
5121
5553
  }
@@ -5125,8 +5557,7 @@ function App() {
5125
5557
  ),
5126
5558
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "overview", "data-testid": "tab-overview", children: /* @__PURE__ */ jsxRuntime.jsx(OverviewPage, {}) }),
5127
5559
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "users", "data-testid": "tab-users", children: /* @__PURE__ */ jsxRuntime.jsx(UsersPage, { config }) }),
5128
- orgEnabled && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "organizations", "data-testid": "tab-organizations", children: /* @__PURE__ */ jsxRuntime.jsx(OrganizationsPage, { teamsEnabled }) }),
5129
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "sessions", "data-testid": "tab-sessions", children: /* @__PURE__ */ jsxRuntime.jsx(SessionsPage, {}) })
5560
+ orgEnabled && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "organizations", "data-testid": "tab-organizations", children: /* @__PURE__ */ jsxRuntime.jsx(OrganizationsPage, { teamsEnabled }) })
5130
5561
  ] }) });
5131
5562
  }
5132
5563
  function Root() {