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

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-CIxfFlzU.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;
@@ -896,6 +1042,7 @@ function OrganizationDetail({
896
1042
  const schemaQuery = useModelSchema("organization");
897
1043
  const qc = reactQuery.useQueryClient();
898
1044
  const { toggleNotification } = admin.useNotification();
1045
+ const { get, put } = admin.useFetchClient();
899
1046
  const orgQuery = reactQuery.useQuery({
900
1047
  queryKey: ["dash-org", organizationId],
901
1048
  queryFn: async () => {
@@ -934,6 +1081,24 @@ function OrganizationDetail({
934
1081
  return result.data ?? [];
935
1082
  }
936
1083
  });
1084
+ const invitationsQuery = reactQuery.useQuery({
1085
+ queryKey: ["dash-org-invitations", organizationId],
1086
+ queryFn: async () => {
1087
+ const result = await client.dash.organization[organizationId].invitations({}, withContext({ organizationId }));
1088
+ if (result.error) throw new Error(result.error.message ?? "Failed");
1089
+ return result.data ?? [];
1090
+ }
1091
+ });
1092
+ const strapiOrgQuery = reactQuery.useQuery({
1093
+ queryKey: ["dash-strapi-org", organizationId],
1094
+ enabled: !!orgQuery.data,
1095
+ queryFn: async () => {
1096
+ const { data } = await get(
1097
+ `/better-auth-dashboard/db?uid=plugin::better-auth.organization&filters[id][$eq]=${organizationId}&pagination[pageSize]=1`
1098
+ );
1099
+ return data.results?.[0] ?? null;
1100
+ }
1101
+ });
937
1102
  const [activeTab, setActiveTab] = react.useState("details");
938
1103
  const [editName, setEditName] = react.useState(void 0);
939
1104
  const [editSlug, setEditSlug] = react.useState(void 0);
@@ -946,6 +1111,9 @@ function OrganizationDetail({
946
1111
  const [confirmDeleteSsoId, setConfirmDeleteSsoId] = react.useState(
947
1112
  null
948
1113
  );
1114
+ const [inviteEmail, setInviteEmail] = react.useState("");
1115
+ const [inviteRole, setInviteRole] = react.useState("member");
1116
+ const [confirmCancelInvitationId, setConfirmCancelInvitationId] = react.useState(null);
949
1117
  const handleExtraChange = (name, value) => {
950
1118
  setEditExtra((prev) => ({ ...prev, [name]: value }));
951
1119
  };
@@ -960,13 +1128,13 @@ function OrganizationDetail({
960
1128
  if (editName !== void 0) body.name = editName;
961
1129
  if (editSlug !== void 0) body.slug = editSlug;
962
1130
  if (editLogo !== void 0) body.logo = editLogo;
963
- const result = await client.dash.organization.update(
964
- body,
965
- withContext({ organizationId })
1131
+ const documentId = strapiOrgQuery.data?.documentId;
1132
+ if (!documentId)
1133
+ throw new Error("Could not resolve documentId for organization");
1134
+ await put(
1135
+ `/better-auth-dashboard/db/${documentId}?uid=plugin::better-auth.organization`,
1136
+ body
966
1137
  );
967
- if (result.error)
968
- throw new Error(result.error.message ?? "Update failed");
969
- return result.data;
970
1138
  },
971
1139
  onSuccess: () => {
972
1140
  qc.invalidateQueries({ queryKey: ["dash-org", organizationId] });
@@ -1116,9 +1284,74 @@ function OrganizationDetail({
1116
1284
  });
1117
1285
  }
1118
1286
  });
1287
+ const inviteMemberMutation = reactQuery.useMutation({
1288
+ mutationFn: async () => {
1289
+ const result = await client.dash.organization.inviteMember(
1290
+ { email: inviteEmail, role: inviteRole, invitedBy: "" },
1291
+ withContext({ organizationId })
1292
+ );
1293
+ if (result.error) throw new Error(result.error.message ?? "Failed");
1294
+ return result.data;
1295
+ },
1296
+ onSuccess: () => {
1297
+ qc.invalidateQueries({
1298
+ queryKey: ["dash-org-invitations", organizationId]
1299
+ });
1300
+ setInviteEmail("");
1301
+ setInviteRole("member");
1302
+ toggleNotification({ type: "success", message: "Invitation sent" });
1303
+ },
1304
+ onError: (err) => {
1305
+ toggleNotification({
1306
+ type: "danger",
1307
+ message: err.message ?? "Failed to invite"
1308
+ });
1309
+ }
1310
+ });
1311
+ const cancelInvitationMutation = reactQuery.useMutation({
1312
+ mutationFn: async (invitationId) => {
1313
+ const result = await client.dash.organization.cancelInvitation(
1314
+ { invitationId },
1315
+ withContext({ organizationId })
1316
+ );
1317
+ if (result.error) throw new Error(result.error.message ?? "Failed");
1318
+ },
1319
+ onSuccess: () => {
1320
+ setConfirmCancelInvitationId(null);
1321
+ qc.invalidateQueries({
1322
+ queryKey: ["dash-org-invitations", organizationId]
1323
+ });
1324
+ toggleNotification({ type: "success", message: "Invitation cancelled" });
1325
+ },
1326
+ onError: (err) => {
1327
+ toggleNotification({
1328
+ type: "danger",
1329
+ message: err.message ?? "Failed to cancel"
1330
+ });
1331
+ }
1332
+ });
1333
+ const resendInvitationMutation = reactQuery.useMutation({
1334
+ mutationFn: async (invitationId) => {
1335
+ const result = await client.dash.organization.resendInvitation(
1336
+ { invitationId },
1337
+ withContext({ organizationId })
1338
+ );
1339
+ if (result.error) throw new Error(result.error.message ?? "Failed");
1340
+ },
1341
+ onSuccess: () => {
1342
+ toggleNotification({ type: "success", message: "Invitation resent" });
1343
+ },
1344
+ onError: (err) => {
1345
+ toggleNotification({
1346
+ type: "danger",
1347
+ message: err.message ?? "Failed to resend"
1348
+ });
1349
+ }
1350
+ });
1119
1351
  const members = membersQuery.data ?? [];
1120
1352
  const teams = teamsQuery.data ?? [];
1121
1353
  const ssoProviders = ssoQuery.data ?? [];
1354
+ const invitations = invitationsQuery.data ?? [];
1122
1355
  const hasOrgEdits = editName !== void 0 || editSlug !== void 0 || editLogo !== void 0 || Object.keys(editExtra).length > 0;
1123
1356
  const customFields = Object.entries(schemaQuery.data ?? {}).filter(([name]) => !STANDARD_ORG_FIELDS.has(name)).map(([name, attribute]) => ({ name, attribute }));
1124
1357
  const detailsFooter = activeTab === "details" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
@@ -1175,6 +1408,11 @@ function OrganizationDetail({
1175
1408
  teams.length,
1176
1409
  ")"
1177
1410
  ] }),
1411
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Tabs.Trigger, { value: "invitations", children: [
1412
+ "Invitations (",
1413
+ invitations.length,
1414
+ ")"
1415
+ ] }),
1178
1416
  /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Tabs.Trigger, { value: "sso", children: [
1179
1417
  "SSO (",
1180
1418
  ssoProviders.length,
@@ -1430,7 +1668,127 @@ function OrganizationDetail({
1430
1668
  },
1431
1669
  provider.id
1432
1670
  )) })
1433
- ] }) }) })
1671
+ ] }) }) }),
1672
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "invitations", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 5, paddingTop: 6, children: [
1673
+ /* @__PURE__ */ jsxRuntime.jsxs(FormSection, { children: [
1674
+ /* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { children: "Invite member by email" }),
1675
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Grid.Root, { gap: 3, children: [
1676
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 8, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { children: [
1677
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Email address" }),
1678
+ /* @__PURE__ */ jsxRuntime.jsx(
1679
+ designSystem.TextInput,
1680
+ {
1681
+ type: "email",
1682
+ value: inviteEmail,
1683
+ onChange: (e) => setInviteEmail(e.target.value),
1684
+ placeholder: "user@example.com"
1685
+ }
1686
+ )
1687
+ ] }) }),
1688
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 4, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { children: [
1689
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Role" }),
1690
+ /* @__PURE__ */ jsxRuntime.jsxs(
1691
+ designSystem.SingleSelect,
1692
+ {
1693
+ value: inviteRole,
1694
+ onChange: (v) => setInviteRole(String(v)),
1695
+ "aria-label": "Invite role",
1696
+ children: [
1697
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "member", children: "Member" }),
1698
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "admin", children: "Admin" }),
1699
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "owner", children: "Owner" })
1700
+ ]
1701
+ }
1702
+ )
1703
+ ] }) })
1704
+ ] }),
1705
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(
1706
+ designSystem.Button,
1707
+ {
1708
+ size: "S",
1709
+ startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Plus, {}),
1710
+ disabled: !inviteEmail,
1711
+ loading: inviteMemberMutation.isLoading,
1712
+ onClick: () => inviteMemberMutation.mutate(),
1713
+ children: "Send invitation"
1714
+ }
1715
+ ) })
1716
+ ] }),
1717
+ /* @__PURE__ */ jsxRuntime.jsxs(FormSection, { children: [
1718
+ /* @__PURE__ */ jsxRuntime.jsxs(SectionLabel, { children: [
1719
+ "Invitations (",
1720
+ invitations.length,
1721
+ ")"
1722
+ ] }),
1723
+ 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) => {
1724
+ const statusColor = {
1725
+ pending: "#f59e0b",
1726
+ accepted: "#5cb176",
1727
+ rejected: "#d02b20",
1728
+ canceled: "#8e8ea9"
1729
+ };
1730
+ const color = statusColor[inv.status] ?? "#8e8ea9";
1731
+ const isPending = inv.status === "pending";
1732
+ return /* @__PURE__ */ jsxRuntime.jsxs(AccountRow, { children: [
1733
+ /* @__PURE__ */ jsxRuntime.jsxs(
1734
+ designSystem.Flex,
1735
+ {
1736
+ direction: "column",
1737
+ gap: 1,
1738
+ alignItems: "flex-start",
1739
+ style: { flex: 1 },
1740
+ children: [
1741
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", fontWeight: "semiBold", children: inv.user?.name ?? inv.email }),
1742
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: [
1743
+ inv.email,
1744
+ " · role: ",
1745
+ inv.role
1746
+ ] }),
1747
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: [
1748
+ "Expires",
1749
+ " ",
1750
+ new Date(inv.expiresAt).toLocaleDateString()
1751
+ ] })
1752
+ ]
1753
+ }
1754
+ ),
1755
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "center", children: [
1756
+ /* @__PURE__ */ jsxRuntime.jsx(
1757
+ MonoChip,
1758
+ {
1759
+ style: {
1760
+ color,
1761
+ borderColor: color,
1762
+ background: `${color}18`
1763
+ },
1764
+ children: inv.status
1765
+ }
1766
+ ),
1767
+ isPending && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1768
+ /* @__PURE__ */ jsxRuntime.jsx(
1769
+ designSystem.Button,
1770
+ {
1771
+ size: "S",
1772
+ variant: "secondary",
1773
+ loading: resendInvitationMutation.isLoading,
1774
+ onClick: () => resendInvitationMutation.mutate(inv.id),
1775
+ children: "Resend"
1776
+ }
1777
+ ),
1778
+ /* @__PURE__ */ jsxRuntime.jsx(
1779
+ designSystem.IconButton,
1780
+ {
1781
+ label: "Cancel invitation",
1782
+ onClick: () => setConfirmCancelInvitationId(inv.id),
1783
+ children: /* @__PURE__ */ jsxRuntime.jsx(icons.Trash, {})
1784
+ }
1785
+ )
1786
+ ] })
1787
+ ] })
1788
+ ] }, inv.id);
1789
+ }) })
1790
+ ] })
1791
+ ] }) })
1434
1792
  ] }),
1435
1793
  confirmRemoveMemberId && /* @__PURE__ */ jsxRuntime.jsx(
1436
1794
  ConfirmDialog,
@@ -1464,6 +1822,17 @@ function OrganizationDetail({
1464
1822
  onConfirm: () => deleteSsoMutation.mutate(confirmDeleteSsoId),
1465
1823
  onCancel: () => setConfirmDeleteSsoId(null)
1466
1824
  }
1825
+ ),
1826
+ confirmCancelInvitationId && /* @__PURE__ */ jsxRuntime.jsx(
1827
+ ConfirmDialog,
1828
+ {
1829
+ title: "Cancel invitation",
1830
+ message: "Are you sure you want to cancel this invitation? The invite link will no longer work.",
1831
+ confirmLabel: "Cancel invitation",
1832
+ loading: cancelInvitationMutation.isLoading,
1833
+ onConfirm: () => cancelInvitationMutation.mutate(confirmCancelInvitationId),
1834
+ onCancel: () => setConfirmCancelInvitationId(null)
1835
+ }
1467
1836
  )
1468
1837
  ]
1469
1838
  }
@@ -1609,12 +1978,12 @@ function TeamRow({
1609
1978
  )
1610
1979
  ] });
1611
1980
  }
1612
- const PAGE_SIZE$2 = 25;
1613
- const fadeUp$3 = styled.keyframes`
1981
+ const PAGE_SIZE$1 = 25;
1982
+ const fadeUp$2 = styled.keyframes`
1614
1983
  from { opacity: 0; transform: translateY(6px); }
1615
1984
  to { opacity: 1; transform: translateY(0); }
1616
1985
  `;
1617
- const Wrap$3 = styled__default.default.div`
1986
+ const Wrap$2 = styled__default.default.div`
1618
1987
  padding: 28px 32px;
1619
1988
  background: #f6f6f9;
1620
1989
  min-height: 100%;
@@ -1622,39 +1991,39 @@ const Wrap$3 = styled__default.default.div`
1622
1991
  flex-direction: column;
1623
1992
  gap: 20px;
1624
1993
  `;
1625
- const PageHeader$2 = styled__default.default.div`
1994
+ const PageHeader$1 = styled__default.default.div`
1626
1995
  display: flex;
1627
1996
  justify-content: space-between;
1628
1997
  align-items: flex-start;
1629
1998
  `;
1630
- const TitleBlock$2 = styled__default.default.div`
1999
+ const TitleBlock$1 = styled__default.default.div`
1631
2000
  display: flex;
1632
2001
  flex-direction: column;
1633
2002
  gap: 4px;
1634
2003
  `;
1635
- const PageTitle$2 = styled__default.default.h1`
2004
+ const PageTitle$1 = styled__default.default.h1`
1636
2005
  margin: 0;
1637
2006
  font-size: 22px;
1638
2007
  font-weight: 800;
1639
2008
  color: #32324d;
1640
2009
  letter-spacing: -0.03em;
1641
2010
  `;
1642
- const PageSubtitle$2 = styled__default.default.p`
2011
+ const PageSubtitle$1 = styled__default.default.p`
1643
2012
  margin: 0;
1644
2013
  font-size: 12px;
1645
2014
  color: #8e8ea9;
1646
2015
  `;
1647
- const TableCard$2 = styled__default.default.div`
2016
+ const TableCard$1 = styled__default.default.div`
1648
2017
  background: #ffffff;
1649
2018
  border: 1px solid #eaeaef;
1650
2019
  border-radius: 10px;
1651
2020
  overflow: hidden;
1652
2021
  `;
1653
- const Table$2 = styled__default.default.table`
2022
+ const Table$1 = styled__default.default.table`
1654
2023
  width: 100%;
1655
2024
  border-collapse: collapse;
1656
2025
  `;
1657
- const TH$2 = styled__default.default.th`
2026
+ const TH$1 = styled__default.default.th`
1658
2027
  text-align: left;
1659
2028
  padding: 10px 14px;
1660
2029
  font-size: 10px;
@@ -1668,13 +2037,13 @@ const TH$2 = styled__default.default.th`
1668
2037
  &:first-child { padding-left: 20px; }
1669
2038
  &:last-child { padding-right: 20px; }
1670
2039
  `;
1671
- const THCheck$1 = styled__default.default(TH$2)`
2040
+ const THCheck$1 = styled__default.default(TH$1)`
1672
2041
  width: 44px;
1673
2042
  `;
1674
- const THActions$1 = styled__default.default(TH$2)`
2043
+ const THActions$1 = styled__default.default(TH$1)`
1675
2044
  width: 80px;
1676
2045
  `;
1677
- const TD$2 = styled__default.default.td`
2046
+ const TD$1 = styled__default.default.td`
1678
2047
  padding: 11px 14px;
1679
2048
  font-size: 12px;
1680
2049
  color: #32324d;
@@ -1683,14 +2052,14 @@ const TD$2 = styled__default.default.td`
1683
2052
  &:first-child { padding-left: 20px; }
1684
2053
  &:last-child { padding-right: 20px; }
1685
2054
  `;
1686
- const TDCheck$1 = styled__default.default(TD$2)`
2055
+ const TDCheck$1 = styled__default.default(TD$1)`
1687
2056
  width: 44px;
1688
2057
  `;
1689
- const TDActions$1 = styled__default.default(TD$2)`
2058
+ const TDActions$1 = styled__default.default(TD$1)`
1690
2059
  width: 80px;
1691
2060
  `;
1692
- const TR$2 = styled__default.default.tr`
1693
- animation: ${fadeUp$3} 280ms ease both;
2061
+ const TR$1 = styled__default.default.tr`
2062
+ animation: ${fadeUp$2} 280ms ease both;
1694
2063
  animation-delay: ${(p) => (p.$i ?? 0) * 25}ms;
1695
2064
  background: ${(p) => p.$selected ? "#f0f0ff" : "transparent"};
1696
2065
  transition: background 120ms ease;
@@ -1778,6 +2147,7 @@ function OrgAvatar({ name, logo }) {
1778
2147
  }
1779
2148
  function OrganizationsPage({ teamsEnabled }) {
1780
2149
  const qc = reactQuery.useQueryClient();
2150
+ const { toggleNotification } = admin.useNotification();
1781
2151
  const [page, setPage] = react.useState(1);
1782
2152
  const [search, setSearch] = react.useState("");
1783
2153
  const [searchInput, setSearchInput] = react.useState("");
@@ -1786,13 +2156,13 @@ function OrganizationsPage({ teamsEnabled }) {
1786
2156
  const [detailOrgId, setDetailOrgId] = react.useState(null);
1787
2157
  const [confirmDelete, setConfirmDelete] = react.useState(null);
1788
2158
  const [confirmDeleteMany, setConfirmDeleteMany] = react.useState(false);
1789
- const offset = (page - 1) * PAGE_SIZE$2;
2159
+ const offset = (page - 1) * PAGE_SIZE$1;
1790
2160
  const orgsQuery = reactQuery.useQuery({
1791
2161
  queryKey: ["dash-organizations", page, search],
1792
2162
  queryFn: async () => {
1793
2163
  const result = await client.dash.listOrganizations({
1794
2164
  query: {
1795
- limit: PAGE_SIZE$2,
2165
+ limit: PAGE_SIZE$1,
1796
2166
  offset,
1797
2167
  sortBy: "createdAt",
1798
2168
  sortOrder: "desc",
@@ -1817,6 +2187,13 @@ function OrganizationsPage({ teamsEnabled }) {
1817
2187
  onSuccess: () => {
1818
2188
  setConfirmDelete(null);
1819
2189
  qc.invalidateQueries({ queryKey: ["dash-organizations"] });
2190
+ toggleNotification({ type: "success", message: "Organization deleted" });
2191
+ },
2192
+ onError: (err) => {
2193
+ toggleNotification({
2194
+ type: "danger",
2195
+ message: err.message ?? "Failed to delete organization"
2196
+ });
1820
2197
  }
1821
2198
  });
1822
2199
  const deleteManyMutation = reactQuery.useMutation({
@@ -1829,15 +2206,25 @@ function OrganizationsPage({ teamsEnabled }) {
1829
2206
  throw new Error(result.error.message ?? "Delete failed");
1830
2207
  return result.data;
1831
2208
  },
1832
- onSuccess: () => {
2209
+ onSuccess: (_data, organizationIds) => {
1833
2210
  setConfirmDeleteMany(false);
1834
2211
  setSelected(/* @__PURE__ */ new Set());
1835
2212
  qc.invalidateQueries({ queryKey: ["dash-organizations"] });
2213
+ toggleNotification({
2214
+ type: "success",
2215
+ message: `${organizationIds.length} organization${organizationIds.length !== 1 ? "s" : ""} deleted`
2216
+ });
2217
+ },
2218
+ onError: (err) => {
2219
+ toggleNotification({
2220
+ type: "danger",
2221
+ message: err.message ?? "Failed to delete organizations"
2222
+ });
1836
2223
  }
1837
2224
  });
1838
2225
  const orgs = orgsQuery.data && "organizations" in orgsQuery.data ? orgsQuery.data.organizations : [];
1839
2226
  const total = orgsQuery.data && "total" in orgsQuery.data ? orgsQuery.data.total : 0;
1840
- const pageCount = Math.ceil(total / PAGE_SIZE$2);
2227
+ const pageCount = Math.ceil(total / PAGE_SIZE$1);
1841
2228
  const allSelected = orgs.length > 0 && orgs.every((o) => selected.has(o.id));
1842
2229
  const someSelected = selected.size > 0;
1843
2230
  const toggleSelect = (id) => {
@@ -1857,11 +2244,11 @@ function OrganizationsPage({ teamsEnabled }) {
1857
2244
  setSearch(searchInput);
1858
2245
  setPage(1);
1859
2246
  };
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: [
2247
+ return /* @__PURE__ */ jsxRuntime.jsxs(Wrap$2, { "data-testid": "organizations-page", children: [
2248
+ /* @__PURE__ */ jsxRuntime.jsxs(PageHeader$1, { children: [
2249
+ /* @__PURE__ */ jsxRuntime.jsxs(TitleBlock$1, { children: [
2250
+ /* @__PURE__ */ jsxRuntime.jsx(PageTitle$1, { children: "Organizations" }),
2251
+ /* @__PURE__ */ jsxRuntime.jsxs(PageSubtitle$1, { children: [
1865
2252
  total.toLocaleString(),
1866
2253
  " total"
1867
2254
  ] })
@@ -1909,7 +2296,7 @@ function OrganizationsPage({ teamsEnabled }) {
1909
2296
  )
1910
2297
  ] }),
1911
2298
  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: [
2299
+ /* @__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
2300
  /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
1914
2301
  /* @__PURE__ */ jsxRuntime.jsx(THCheck$1, { children: /* @__PURE__ */ jsxRuntime.jsx(
1915
2302
  designSystem.Checkbox,
@@ -1919,14 +2306,14 @@ function OrganizationsPage({ teamsEnabled }) {
1919
2306
  "aria-label": "Select all"
1920
2307
  }
1921
2308
  ) }),
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" }),
2309
+ /* @__PURE__ */ jsxRuntime.jsx(TH$1, { children: "Name" }),
2310
+ /* @__PURE__ */ jsxRuntime.jsx(TH$1, { children: "Slug" }),
2311
+ /* @__PURE__ */ jsxRuntime.jsx(TH$1, { children: "Members" }),
2312
+ /* @__PURE__ */ jsxRuntime.jsx(TH$1, { children: "Created" }),
1926
2313
  /* @__PURE__ */ jsxRuntime.jsx(THActions$1, {})
1927
2314
  ] }) }),
1928
2315
  /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: orgs.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsx(
1929
- TD$2,
2316
+ TD$1,
1930
2317
  {
1931
2318
  colSpan: 6,
1932
2319
  style: {
@@ -1938,7 +2325,7 @@ function OrganizationsPage({ teamsEnabled }) {
1938
2325
  children: search ? `No organizations matching "${search}"` : "No organizations found"
1939
2326
  }
1940
2327
  ) }) : orgs.map((org, i) => /* @__PURE__ */ jsxRuntime.jsxs(
1941
- TR$2,
2328
+ TR$1,
1942
2329
  {
1943
2330
  $selected: selected.has(org.id),
1944
2331
  $i: i,
@@ -1952,13 +2339,13 @@ function OrganizationsPage({ teamsEnabled }) {
1952
2339
  "aria-label": `Select ${org.name}`
1953
2340
  }
1954
2341
  ) }),
1955
- /* @__PURE__ */ jsxRuntime.jsx(TD$2, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { alignItems: "center", gap: 2, children: [
2342
+ /* @__PURE__ */ jsxRuntime.jsx(TD$1, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { alignItems: "center", gap: 2, children: [
1956
2343
  /* @__PURE__ */ jsxRuntime.jsx(OrgAvatar, { name: org.name, logo: org.logo }),
1957
2344
  /* @__PURE__ */ jsxRuntime.jsx(OrgName, { children: org.name })
1958
2345
  ] }) }),
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() }) }),
2346
+ /* @__PURE__ */ jsxRuntime.jsx(TD$1, { children: /* @__PURE__ */ jsxRuntime.jsx(SlugChip, { children: org.slug }) }),
2347
+ /* @__PURE__ */ jsxRuntime.jsx(TD$1, { children: /* @__PURE__ */ jsxRuntime.jsx(CountChip, { children: org.memberCount }) }),
2348
+ /* @__PURE__ */ jsxRuntime.jsx(TD$1, { children: /* @__PURE__ */ jsxRuntime.jsx(DateText$1, { children: new Date(org.createdAt).toLocaleDateString() }) }),
1962
2349
  /* @__PURE__ */ jsxRuntime.jsx(TDActions$1, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 1, justifyContent: "flex-end", children: [
1963
2350
  /* @__PURE__ */ jsxRuntime.jsx(
1964
2351
  designSystem.IconButton,
@@ -2105,95 +2492,143 @@ function Avatar({
2105
2492
  const color = getColor(name || "?");
2106
2493
  return /* @__PURE__ */ jsxRuntime.jsx(Circle, { $bg: color.bg, $fg: color.fg, $size: size, children: getInitials(name || "?") });
2107
2494
  }
2108
- const fadeUp$2 = styled.keyframes`
2109
- from { opacity: 0; transform: translateY(8px); }
2495
+ const T = {
2496
+ bg: "#f6f6f9",
2497
+ bgCard: "#ffffff",
2498
+ border: "#eaeaef",
2499
+ borderHover: "rgba(73,69,255,0.35)",
2500
+ accent: "#4945ff",
2501
+ green: "#5cb176",
2502
+ greenDim: "rgba(92,177,118,0.12)",
2503
+ amber: "#d9822f",
2504
+ red: "#d02b20",
2505
+ redDim: "rgba(208,43,32,0.1)",
2506
+ purple: "#8460b8",
2507
+ textPrimary: "#32324d",
2508
+ textSecondary: "#666687",
2509
+ textMuted: "#b8b8c7",
2510
+ mono: `'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace`
2511
+ };
2512
+ const fadeUp$1 = styled.keyframes`
2513
+ from { opacity: 0; transform: translateY(10px); }
2110
2514
  to { opacity: 1; transform: translateY(0); }
2111
2515
  `;
2112
- const Wrap$2 = styled__default.default.div`
2113
- padding: 28px 32px;
2114
- background: #f6f6f9;
2516
+ const Wrap$1 = styled__default.default.div`
2517
+ padding: 28px 32px 48px;
2518
+ background: ${T.bg};
2115
2519
  min-height: 100%;
2116
2520
  display: flex;
2117
2521
  flex-direction: column;
2118
- gap: 20px;
2522
+ gap: 22px;
2119
2523
  `;
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); }
2524
+ const SectionDivider = styled__default.default.div`
2525
+ display: flex;
2526
+ align-items: center;
2527
+ gap: 10px;
2528
+ margin-bottom: -6px;
2126
2529
  `;
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; }
2530
+ const DivLabel = styled__default.default.span`
2531
+ font-size: 9px;
2532
+ font-weight: 700;
2533
+ letter-spacing: 0.14em;
2534
+ text-transform: uppercase;
2535
+ color: ${T.textMuted};
2536
+ white-space: nowrap;
2132
2537
  `;
2133
- const ActiveRow = styled__default.default.div`
2134
- display: grid;
2135
- grid-template-columns: repeat(3, 1fr);
2136
- gap: 12px;
2538
+ const DivLine = styled__default.default.div`
2539
+ flex: 1;
2540
+ height: 1px;
2541
+ background: ${T.border};
2137
2542
  `;
2138
2543
  const Card = styled__default.default.div`
2139
- background: #ffffff;
2140
- border: 1px solid #eaeaef;
2141
- border-radius: 10px;
2544
+ background: ${T.bgCard};
2545
+ border: 1px solid ${T.border};
2546
+ border-radius: 12px;
2142
2547
  overflow: hidden;
2143
2548
  position: relative;
2144
- animation: ${fadeUp$2} 360ms ease both;
2549
+ animation: ${fadeUp$1} 380ms ease both;
2145
2550
  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
-
2551
+ transition: border-color 200ms, box-shadow 200ms;
2159
2552
  &:hover {
2160
- border-color: #c0bfff;
2161
- box-shadow: 0 2px 16px rgba(73,69,255,0.08);
2553
+ border-color: ${T.borderHover};
2554
+ box-shadow: 0 0 0 1px ${T.borderHover}, 0 8px 32px rgba(124,109,250,0.07);
2162
2555
  }
2163
2556
  `;
2164
- const StatTop = styled__default.default.div`
2165
- padding: 18px 18px 10px;
2557
+ const PillGroup = styled__default.default.div`
2558
+ display: flex;
2559
+ background: rgba(0,0,0,0.03);
2560
+ border: 1px solid ${T.border};
2561
+ border-radius: 9px;
2562
+ padding: 3px;
2563
+ gap: 2px;
2166
2564
  `;
2167
- const StatLabel = styled__default.default.div`
2168
- font-size: 10px;
2169
- font-weight: 700;
2170
- letter-spacing: 0.08em;
2565
+ const Pill = styled__default.default.button`
2566
+ appearance: none;
2567
+ border: none;
2568
+ cursor: pointer;
2569
+ padding: 5px 16px;
2570
+ border-radius: 6px;
2571
+ font-size: 11px;
2572
+ font-weight: 600;
2573
+ letter-spacing: 0.03em;
2574
+ transition: background 180ms, color 180ms;
2575
+ background: ${(p) => p.$active ? T.accent : "transparent"};
2576
+ color: ${(p) => p.$active ? "#fff" : T.textSecondary};
2577
+ &:hover {
2578
+ background: ${(p) => p.$active ? T.accent : "rgba(0,0,0,0.05)"};
2579
+ color: ${(p) => p.$active ? "#fff" : T.textPrimary};
2580
+ }
2581
+ `;
2582
+ const StatGrid = styled__default.default.div`
2583
+ display: grid;
2584
+ grid-template-columns: repeat(5, 1fr);
2585
+ gap: 10px;
2586
+ @media (max-width: 1200px) { grid-template-columns: repeat(3, 1fr); }
2587
+ `;
2588
+ const StatCard = styled__default.default(Card)`
2589
+ padding: 18px 20px 0;
2590
+ &::after {
2591
+ content: '';
2592
+ position: absolute;
2593
+ top: 0; left: 0; right: 0;
2594
+ height: 2px;
2595
+ background: ${(p) => p.$accent};
2596
+ opacity: 0.75;
2597
+ }
2598
+ `;
2599
+ const StatLabel = styled__default.default.div`
2600
+ font-size: 9px;
2601
+ font-weight: 700;
2602
+ letter-spacing: 0.1em;
2171
2603
  text-transform: uppercase;
2172
- color: #8e8ea9;
2173
- margin-bottom: 8px;
2604
+ color: ${T.textMuted};
2605
+ margin-bottom: 10px;
2174
2606
  `;
2175
2607
  const StatValue = styled__default.default.div`
2176
2608
  font-size: 30px;
2177
2609
  font-weight: 800;
2178
- color: #32324d;
2610
+ color: ${T.textPrimary};
2179
2611
  line-height: 1;
2180
- letter-spacing: -0.05em;
2612
+ letter-spacing: -0.045em;
2181
2613
  font-variant-numeric: tabular-nums;
2182
2614
  margin-bottom: 10px;
2183
2615
  `;
2184
2616
  const TrendBadge = styled__default.default.span`
2185
2617
  display: inline-flex;
2186
2618
  align-items: center;
2187
- gap: 2px;
2188
- padding: 2px 7px;
2619
+ gap: 3px;
2620
+ padding: 2px 8px;
2189
2621
  border-radius: 20px;
2190
- font-size: 10px;
2622
+ font-size: 9px;
2191
2623
  font-weight: 700;
2192
- background: ${(p) => p.$pos ? "#eafbe7" : "#fcecea"};
2193
- color: ${(p) => p.$pos ? "#5cb176" : "#d02b20"};
2624
+ letter-spacing: 0.03em;
2625
+ background: ${(p) => p.$pos ? T.greenDim : T.redDim};
2626
+ color: ${(p) => p.$pos ? T.green : T.red};
2627
+ margin-bottom: 12px;
2194
2628
  `;
2195
2629
  const SparkWrap = styled__default.default.div`
2196
- height: 48px;
2630
+ height: 44px;
2631
+ margin: 0 -20px;
2197
2632
  `;
2198
2633
  const ChartCard = styled__default.default(Card)`
2199
2634
  padding: 20px 20px 12px;
@@ -2205,31 +2640,42 @@ const ChartHeader = styled__default.default.div`
2205
2640
  display: flex;
2206
2641
  align-items: center;
2207
2642
  justify-content: space-between;
2643
+ gap: 12px;
2208
2644
  flex-shrink: 0;
2209
2645
  `;
2210
2646
  const ChartTitle = styled__default.default.div`
2211
- font-size: 13px;
2647
+ font-size: 12px;
2212
2648
  font-weight: 700;
2213
- color: #32324d;
2649
+ color: ${T.textPrimary};
2650
+ letter-spacing: 0.01em;
2214
2651
  `;
2215
- const LegendRow = styled__default.default.div`
2652
+ const SeriesRow = styled__default.default.div`
2216
2653
  display: flex;
2217
2654
  align-items: center;
2218
- gap: 14px;
2655
+ gap: 6px;
2219
2656
  `;
2220
- const LegendItem = styled__default.default.div`
2657
+ const SeriesBtn = styled__default.default.button`
2658
+ appearance: none;
2659
+ border: 1px solid ${(p) => p.$on ? `${p.$c}55` : T.border};
2660
+ background: ${(p) => p.$on ? `${p.$c}18` : "transparent"};
2661
+ color: ${(p) => p.$on ? p.$c : T.textMuted};
2662
+ font-size: 10px;
2663
+ font-weight: 600;
2664
+ padding: 3px 10px 3px 8px;
2665
+ border-radius: 6px;
2666
+ cursor: pointer;
2221
2667
  display: flex;
2222
2668
  align-items: center;
2223
2669
  gap: 5px;
2224
- font-size: 10px;
2225
- color: #8e8ea9;
2226
- font-weight: 600;
2227
- text-transform: uppercase;
2228
- letter-spacing: 0.04em;
2670
+ transition: all 160ms;
2671
+ &:hover {
2672
+ border-color: ${(p) => `${p.$c}88`};
2673
+ color: ${(p) => p.$c};
2674
+ background: ${(p) => `${p.$c}22`};
2675
+ }
2229
2676
  `;
2230
- const LegendDot = styled__default.default.span`
2231
- width: 8px;
2232
- height: 8px;
2677
+ const SDot = styled__default.default.span`
2678
+ width: 6px; height: 6px;
2233
2679
  border-radius: 50%;
2234
2680
  background: ${(p) => p.$c};
2235
2681
  display: inline-block;
@@ -2237,140 +2683,188 @@ const LegendDot = styled__default.default.span`
2237
2683
  `;
2238
2684
  const HoverInfo = styled__default.default.div`
2239
2685
  font-size: 11px;
2240
- color: #8e8ea9;
2686
+ color: ${T.textSecondary};
2241
2687
  font-variant-numeric: tabular-nums;
2242
- min-height: 16px;
2688
+ font-family: ${T.mono};
2689
+ min-height: 15px;
2690
+ `;
2691
+ const TwoPanel = styled__default.default.div`
2692
+ display: grid;
2693
+ grid-template-columns: ${(p) => p.$ratio ?? "1fr 288px"};
2694
+ gap: 10px;
2695
+ align-items: stretch;
2696
+ @media (max-width: 900px) { grid-template-columns: 1fr; }
2243
2697
  `;
2244
2698
  const FeedCard = styled__default.default(Card)`
2245
2699
  display: flex;
2246
2700
  flex-direction: column;
2247
- max-height: 380px;
2701
+ overflow: hidden;
2248
2702
  `;
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;
2703
+ const FeedHead = styled__default.default.div`
2704
+ padding: 13px 16px 9px;
2705
+ border-bottom: 1px solid ${T.border};
2706
+ font-size: 11px;
2254
2707
  font-weight: 700;
2255
- letter-spacing: 0.08em;
2256
- text-transform: uppercase;
2257
- color: #8e8ea9;
2708
+ color: ${T.textPrimary};
2709
+ flex-shrink: 0;
2710
+ display: flex;
2711
+ justify-content: space-between;
2712
+ align-items: center;
2258
2713
  `;
2259
- const FeedList = styled__default.default.div`
2714
+ const FeedSelect = styled__default.default.select`
2715
+ appearance: none;
2716
+ border: 1px solid ${T.border};
2717
+ border-radius: 6px;
2718
+ background: ${T.bgCard};
2719
+ color: ${T.textSecondary};
2720
+ font-size: 10px;
2721
+ font-weight: 600;
2722
+ padding: 3px 22px 3px 8px;
2723
+ cursor: pointer;
2724
+ outline: none;
2725
+ 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");
2726
+ background-repeat: no-repeat;
2727
+ background-position: right 7px center;
2728
+ transition: border-color 160ms;
2729
+ &:hover, &:focus { border-color: ${T.accent}; color: ${T.textPrimary}; }
2730
+ `;
2731
+ const FeedScroll = styled__default.default.div`
2260
2732
  flex: 1;
2261
2733
  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; }
2734
+ min-height: 0;
2735
+ &::-webkit-scrollbar { width: 4px; }
2736
+ &::-webkit-scrollbar-thumb { background: ${T.textMuted}; border-radius: 2px; }
2266
2737
  `;
2267
2738
  const FeedItem = styled__default.default.div`
2739
+ padding: 9px 14px;
2740
+ border-bottom: 1px solid #f0f0f5;
2268
2741
  display: flex;
2269
- align-items: flex-start;
2270
- gap: 10px;
2271
- padding: 9px 18px;
2272
- &:hover { background: #fafafe; }
2742
+ flex-direction: column;
2743
+ gap: 3px;
2744
+ transition: background 130ms;
2745
+ &:hover { background: #fafaff; }
2746
+ &:last-child { border-bottom: none; }
2273
2747
  `;
2274
- const FeedContent = styled__default.default.div`
2275
- flex: 1;
2276
- min-width: 0;
2748
+ const FeedTop = styled__default.default.div`
2749
+ display: flex;
2750
+ align-items: center;
2751
+ justify-content: space-between;
2752
+ gap: 6px;
2277
2753
  `;
2278
- const FeedName = styled__default.default.div`
2279
- font-size: 12px;
2754
+ const FeedName = styled__default.default.span`
2755
+ font-size: 11px;
2280
2756
  font-weight: 600;
2281
- color: #32324d;
2282
- white-space: nowrap;
2757
+ color: ${T.textPrimary};
2283
2758
  overflow: hidden;
2284
2759
  text-overflow: ellipsis;
2760
+ white-space: nowrap;
2285
2761
  `;
2286
- const FeedSub = styled__default.default.div`
2762
+ const FeedEmail = styled__default.default.span`
2287
2763
  font-size: 10px;
2288
- color: #8e8ea9;
2289
- white-space: nowrap;
2764
+ color: ${T.textSecondary};
2290
2765
  overflow: hidden;
2291
2766
  text-overflow: ellipsis;
2292
- margin-top: 1px;
2767
+ white-space: nowrap;
2768
+ display: block;
2293
2769
  `;
2294
- const FeedTime = styled__default.default.div`
2295
- font-size: 10px;
2296
- color: #b8b8c7;
2770
+ const FeedMeta = styled__default.default.span`
2771
+ font-size: 9px;
2772
+ color: ${T.textMuted};
2297
2773
  white-space: nowrap;
2298
2774
  font-variant-numeric: tabular-nums;
2299
- margin-top: 1px;
2300
2775
  `;
2301
- const UsersCard = styled__default.default(Card)``;
2302
- const TableWrap = styled__default.default.div`
2303
- overflow-x: auto;
2776
+ const RtnCard = styled__default.default(Card)`
2777
+ padding: 20px;
2778
+ display: flex;
2779
+ flex-direction: column;
2780
+ gap: 12px;
2304
2781
  `;
2305
- const Table$1 = styled__default.default.table`
2306
- width: 100%;
2307
- border-collapse: collapse;
2782
+ const RtnRow = styled__default.default.div`
2783
+ display: flex;
2784
+ align-items: center;
2785
+ gap: 10px;
2786
+ padding: 4px 6px;
2787
+ border-radius: 7px;
2788
+ transition: background 130ms;
2789
+ background: ${(p) => p.$hov ? "#f0f0f6" : "transparent"};
2790
+ cursor: default;
2308
2791
  `;
2309
- const TH$1 = styled__default.default.th`
2310
- text-align: left;
2311
- padding: 10px 14px;
2792
+ const RtnLabel = styled__default.default.span`
2312
2793
  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;
2794
+ color: ${T.textSecondary};
2795
+ min-width: 80px;
2796
+ text-align: right;
2319
2797
  white-space: nowrap;
2320
- &:first-child { padding-left: 20px; }
2321
- &:last-child { padding-right: 20px; }
2322
2798
  `;
2323
- const TR$1 = styled__default.default.tr`
2324
- &:hover td { background: #fafafe; }
2325
- &:last-child td { border-bottom: none; }
2799
+ const RtnSize = styled__default.default.span`
2800
+ font-size: 9px;
2801
+ color: ${T.textMuted};
2802
+ min-width: 52px;
2803
+ text-align: right;
2804
+ white-space: nowrap;
2805
+ font-variant-numeric: tabular-nums;
2326
2806
  `;
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; }
2807
+ const RtnTrack = styled__default.default.div`
2808
+ flex: 1;
2809
+ height: 10px;
2810
+ background: #eaeaef;
2811
+ border-radius: 4px;
2812
+ overflow: hidden;
2335
2813
  `;
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;
2814
+ const RtnBar = styled__default.default.div`
2815
+ width: ${(p) => Math.max(p.$w, 0.5)}%;
2816
+ height: 100%;
2817
+ background: hsl(${(p) => p.$hue}, 60%, 50%);
2818
+ border-radius: 4px;
2819
+ transition: width 0.55s cubic-bezier(0.25, 0.46, 0.45, 0.94);
2820
+ `;
2821
+ const RtnPct = styled__default.default.span`
2342
2822
  font-size: 10px;
2343
2823
  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
- }
2824
+ color: hsl(${(p) => p.$hue}, 60%, 60%);
2825
+ min-width: 38px;
2826
+ text-align: right;
2827
+ font-variant-numeric: tabular-nums;
2828
+ font-family: ${T.mono};
2829
+ `;
2830
+ const RtnTip = styled__default.default.div`
2831
+ font-size: 10px;
2832
+ color: ${T.textSecondary};
2833
+ min-height: 14px;
2834
+ font-variant-numeric: tabular-nums;
2835
+ padding: 0 6px;
2355
2836
  `;
2356
- const Divider = styled__default.default.div`
2837
+ const RingGrid = styled__default.default.div`
2357
2838
  display: flex;
2358
- align-items: center;
2839
+ flex-direction: column;
2359
2840
  gap: 10px;
2360
- margin-bottom: -8px;
2361
2841
  `;
2362
- const DivLabel = styled__default.default.span`
2363
- font-size: 10px;
2842
+ const RingCard = styled__default.default(Card)`
2843
+ padding: 18px 20px;
2844
+ display: flex;
2845
+ align-items: center;
2846
+ gap: 16px;
2847
+ flex: 1;
2848
+ `;
2849
+ const RingInfo = styled__default.default.div`
2850
+ display: flex;
2851
+ flex-direction: column;
2852
+ gap: 4px;
2853
+ `;
2854
+ const RingVal = styled__default.default.div`
2855
+ font-size: 24px;
2856
+ font-weight: 800;
2857
+ color: ${T.textPrimary};
2858
+ letter-spacing: -0.04em;
2859
+ font-variant-numeric: tabular-nums;
2860
+ line-height: 1;
2861
+ `;
2862
+ const RingLabel = styled__default.default.div`
2863
+ font-size: 9px;
2364
2864
  font-weight: 700;
2365
2865
  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;
2866
+ letter-spacing: 0.1em;
2867
+ color: ${T.textMuted};
2374
2868
  `;
2375
2869
  const Empty = styled__default.default.div`
2376
2870
  display: flex;
@@ -2378,32 +2872,52 @@ const Empty = styled__default.default.div`
2378
2872
  align-items: center;
2379
2873
  justify-content: center;
2380
2874
  padding: 32px;
2381
- color: #8e8ea9;
2875
+ color: ${T.textMuted};
2382
2876
  font-size: 12px;
2383
- gap: 6px;
2877
+ gap: 8px;
2384
2878
  `;
2879
+ function useCountUp(target, duration = 850) {
2880
+ const [count, setCount] = react.useState(0);
2881
+ const prevRef = react.useRef(0);
2882
+ react.useEffect(() => {
2883
+ let raf;
2884
+ let start = 0;
2885
+ const from = prevRef.current;
2886
+ const diff = target - from;
2887
+ const step = (ts) => {
2888
+ if (!start) start = ts;
2889
+ const progress = Math.min((ts - start) / duration, 1);
2890
+ const eased = 1 - (1 - progress) ** 3;
2891
+ setCount(Math.round(from + eased * diff));
2892
+ if (progress < 1) raf = requestAnimationFrame(step);
2893
+ else prevRef.current = target;
2894
+ };
2895
+ raf = requestAnimationFrame(step);
2896
+ return () => cancelAnimationFrame(raf);
2897
+ }, [target, duration]);
2898
+ return count;
2899
+ }
2385
2900
  function Sparkline({
2386
2901
  data,
2387
2902
  color,
2388
2903
  id
2389
2904
  }) {
2390
2905
  const W = 300;
2391
- const H = 48;
2906
+ const H = 44;
2392
2907
  if (data.length < 2) return /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "100%", height: H });
2393
2908
  const min = Math.min(...data);
2394
2909
  const max = Math.max(...data, min + 1);
2395
- const range = max - min;
2396
2910
  const pts = data.map((v, i) => ({
2397
2911
  x: i / (data.length - 1) * W,
2398
- y: H - 4 - (v - min) / range * (H - 10)
2912
+ y: H - 3 - (v - min) / (max - min) * (H - 8)
2399
2913
  }));
2400
- const line = pts.reduce((acc, p, i) => {
2914
+ const line = pts.reduce((a, p, i) => {
2401
2915
  if (i === 0) return `M ${p.x} ${p.y}`;
2402
2916
  const pr = pts[i - 1];
2403
2917
  const cx = (pr.x + p.x) / 2;
2404
- return `${acc} C ${cx} ${pr.y} ${cx} ${p.y} ${p.x} ${p.y}`;
2918
+ return `${a} C ${cx} ${pr.y} ${cx} ${p.y} ${p.x} ${p.y}`;
2405
2919
  }, "");
2406
- const area = `${line} L ${pts[pts.length - 1].x} ${H} L 0 ${H} Z`;
2920
+ const area = `${line} L ${pts.at(-1).x} ${H} L 0 ${H} Z`;
2407
2921
  const gid = `spk-${id}`;
2408
2922
  return /* @__PURE__ */ jsxRuntime.jsxs(
2409
2923
  "svg",
@@ -2412,10 +2926,10 @@ function Sparkline({
2412
2926
  height: H,
2413
2927
  viewBox: `0 0 ${W} ${H}`,
2414
2928
  preserveAspectRatio: "none",
2415
- "aria-hidden": "true",
2929
+ "aria-hidden": true,
2416
2930
  children: [
2417
2931
  /* @__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" }),
2932
+ /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "0%", stopColor: color, stopOpacity: "0.32" }),
2419
2933
  /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "100%", stopColor: color, stopOpacity: "0" })
2420
2934
  ] }) }),
2421
2935
  /* @__PURE__ */ jsxRuntime.jsx("path", { d: area, fill: `url(#${gid})` }),
@@ -2424,57 +2938,58 @@ function Sparkline({
2424
2938
  {
2425
2939
  d: line,
2426
2940
  stroke: color,
2427
- strokeWidth: "2",
2941
+ strokeWidth: "1.6",
2428
2942
  fill: "none",
2429
- strokeLinecap: "round",
2430
- strokeLinejoin: "round"
2943
+ strokeLinecap: "round"
2431
2944
  }
2432
2945
  )
2433
2946
  ]
2434
2947
  }
2435
2948
  );
2436
2949
  }
2437
- const SERIES = [
2438
- { key: "totalUsers", color: "#4945FF", label: "Total" },
2439
- { key: "newUsers", color: "#5CB176", label: "New" },
2440
- { key: "activeUsers", color: "#E57553", label: "Active" }
2950
+ const ALL_SERIES = [
2951
+ { key: "totalUsers", color: T.accent, label: "Total" },
2952
+ { key: "newUsers", color: T.green, label: "New" },
2953
+ { key: "activeUsers", color: T.amber, label: "Active" }
2441
2954
  ];
2442
- function AreaChart({
2955
+ function GrowthChart({
2443
2956
  data,
2444
2957
  hovered,
2445
- onHover
2958
+ onHover,
2959
+ activeSeries
2446
2960
  }) {
2447
2961
  const W = 600;
2448
- const H = 220;
2449
- const PL = 44;
2962
+ const H = 200;
2963
+ const PL = 40;
2450
2964
  const PR = 12;
2451
2965
  const PT = 12;
2452
2966
  const PB = 28;
2453
2967
  const CW = W - PL - PR;
2454
2968
  const CH = H - PT - PB;
2455
- if (data.length === 0) {
2969
+ if (!data.length) {
2456
2970
  return /* @__PURE__ */ jsxRuntime.jsxs(Empty, { children: [
2457
2971
  /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 24 }, children: "📊" }),
2458
2972
  /* @__PURE__ */ jsxRuntime.jsx("div", { children: "No growth data for this period" })
2459
2973
  ] });
2460
2974
  }
2461
- const allVals = data.flatMap((d) => SERIES.map((s) => d[s.key]));
2975
+ const active = ALL_SERIES.filter((s) => activeSeries.has(s.key));
2976
+ const allVals = data.flatMap((d) => active.map((s) => d[s.key]));
2462
2977
  const maxV = Math.max(...allVals, 10);
2463
2978
  const yMax = Math.ceil(maxV * 1.15);
2464
2979
  const xp = (i) => PL + i / Math.max(data.length - 1, 1) * CW;
2465
2980
  const yp = (v) => PT + (1 - v / yMax) * CH;
2466
2981
  const smooth = (vals) => {
2467
2982
  const pts = vals.map((v, i) => ({ x: xp(i), y: yp(v) }));
2468
- const path = pts.reduce((acc, p, i) => {
2983
+ const path = pts.reduce((a, p, i) => {
2469
2984
  if (i === 0) return `M ${p.x} ${p.y}`;
2470
2985
  const pr = pts[i - 1];
2471
2986
  const cx = (pr.x + p.x) / 2;
2472
- return `${acc} C ${cx} ${pr.y} ${cx} ${p.y} ${p.x} ${p.y}`;
2987
+ return `${a} C ${cx} ${pr.y} ${cx} ${p.y} ${p.x} ${p.y}`;
2473
2988
  }, "");
2474
- const area = `${path} L ${pts[pts.length - 1].x} ${PT + CH} L ${pts[0].x} ${PT + CH} Z`;
2989
+ const area = `${path} L ${pts.at(-1).x} ${PT + CH} L ${pts[0].x} ${PT + CH} Z`;
2475
2990
  return { pts, path, area };
2476
2991
  };
2477
- const lines = SERIES.map((s) => ({
2992
+ const lines = active.map((s) => ({
2478
2993
  ...s,
2479
2994
  ...smooth(data.map((d) => d[s.key]))
2480
2995
  }));
@@ -2497,17 +3012,17 @@ function AreaChart({
2497
3012
  /* @__PURE__ */ jsxRuntime.jsx("defs", { children: lines.map((s) => /* @__PURE__ */ jsxRuntime.jsxs(
2498
3013
  "linearGradient",
2499
3014
  {
2500
- id: `ag-${s.color.replace("#", "")}`,
3015
+ id: `ag-${s.key}`,
2501
3016
  x1: "0",
2502
3017
  y1: "0",
2503
3018
  x2: "0",
2504
3019
  y2: "1",
2505
3020
  children: [
2506
- /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "0%", stopColor: s.color, stopOpacity: "0.13" }),
3021
+ /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "0%", stopColor: s.color, stopOpacity: "0.18" }),
2507
3022
  /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "100%", stopColor: s.color, stopOpacity: "0" })
2508
3023
  ]
2509
3024
  },
2510
- s.color
3025
+ s.key
2511
3026
  )) }),
2512
3027
  yTicks.map((t) => /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
2513
3028
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -2527,7 +3042,7 @@ function AreaChart({
2527
3042
  x: PL - 6,
2528
3043
  y: t.y + 4,
2529
3044
  textAnchor: "end",
2530
- fill: "#b8b8c7",
3045
+ fill: T.textMuted,
2531
3046
  fontSize: "9",
2532
3047
  children: t.v >= 1e3 ? `${(t.v / 1e3).toFixed(1)}k` : t.v
2533
3048
  }
@@ -2544,25 +3059,18 @@ function AreaChart({
2544
3059
  strokeWidth: "1"
2545
3060
  }
2546
3061
  ),
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
- )),
3062
+ lines.map((s) => /* @__PURE__ */ jsxRuntime.jsx("path", { d: s.area, fill: `url(#ag-${s.key})` }, `area-${s.key}`)),
2555
3063
  lines.map((s) => /* @__PURE__ */ jsxRuntime.jsx(
2556
3064
  "path",
2557
3065
  {
2558
3066
  d: s.path,
2559
3067
  stroke: s.color,
2560
- strokeWidth: "1.8",
3068
+ strokeWidth: "2",
2561
3069
  fill: "none",
2562
3070
  strokeLinecap: "round",
2563
3071
  strokeLinejoin: "round"
2564
3072
  },
2565
- `l-${s.color}`
3073
+ `line-${s.key}`
2566
3074
  )),
2567
3075
  data.map((d, i) => {
2568
3076
  if (i % step !== 0 && i !== data.length - 1) return null;
@@ -2572,7 +3080,7 @@ function AreaChart({
2572
3080
  x: xp(i),
2573
3081
  y: H - 6,
2574
3082
  textAnchor: "middle",
2575
- fill: "#b8b8c7",
3083
+ fill: T.textMuted,
2576
3084
  fontSize: "9",
2577
3085
  children: d.label
2578
3086
  },
@@ -2582,7 +3090,7 @@ function AreaChart({
2582
3090
  data.map((d, i) => {
2583
3091
  const isHov = hovered === i;
2584
3092
  return (
2585
- // biome-ignore lint/a11y/noStaticElementInteractions: SVG chart hover hit area
3093
+ // biome-ignore lint/a11y/noStaticElementInteractions: SVG chart hover
2586
3094
  /* @__PURE__ */ jsxRuntime.jsxs(
2587
3095
  "g",
2588
3096
  {
@@ -2607,10 +3115,9 @@ function AreaChart({
2607
3115
  y1: PT,
2608
3116
  x2: xp(i),
2609
3117
  y2: PT + CH,
2610
- stroke: "#32324d",
3118
+ stroke: "rgba(50,50,77,0.2)",
2611
3119
  strokeWidth: "1",
2612
- strokeDasharray: "3 3",
2613
- opacity: "0.35"
3120
+ strokeDasharray: "3 3"
2614
3121
  }
2615
3122
  ),
2616
3123
  lines.map((s) => /* @__PURE__ */ jsxRuntime.jsx(
@@ -2620,11 +3127,11 @@ function AreaChart({
2620
3127
  cy: yp(d[s.key]),
2621
3128
  r: isHov ? 4 : 2.5,
2622
3129
  fill: s.color,
2623
- stroke: "white",
3130
+ stroke: T.bg,
2624
3131
  strokeWidth: isHov ? 2 : 1,
2625
- opacity: isHov ? 1 : 0.45
3132
+ opacity: isHov ? 1 : 0.5
2626
3133
  },
2627
- s.color
3134
+ s.key
2628
3135
  ))
2629
3136
  ]
2630
3137
  },
@@ -2636,23 +3143,70 @@ function AreaChart({
2636
3143
  }
2637
3144
  );
2638
3145
  }
3146
+ function ProgressRing({
3147
+ value,
3148
+ max,
3149
+ color,
3150
+ size = 56
3151
+ }) {
3152
+ const r = (size - 9) / 2;
3153
+ const circ = 2 * Math.PI * r;
3154
+ const pct = Math.min(value / Math.max(max, 1), 1);
3155
+ const offset = circ * (1 - pct);
3156
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3157
+ "svg",
3158
+ {
3159
+ width: size,
3160
+ height: size,
3161
+ style: { flexShrink: 0 },
3162
+ "aria-hidden": true,
3163
+ children: [
3164
+ /* @__PURE__ */ jsxRuntime.jsx(
3165
+ "circle",
3166
+ {
3167
+ cx: size / 2,
3168
+ cy: size / 2,
3169
+ r,
3170
+ fill: "none",
3171
+ stroke: "#e5e7eb",
3172
+ strokeWidth: "7"
3173
+ }
3174
+ ),
3175
+ /* @__PURE__ */ jsxRuntime.jsx(
3176
+ "circle",
3177
+ {
3178
+ cx: size / 2,
3179
+ cy: size / 2,
3180
+ r,
3181
+ fill: "none",
3182
+ stroke: color,
3183
+ strokeWidth: "7",
3184
+ strokeDasharray: `${circ} ${circ}`,
3185
+ strokeDashoffset: offset,
3186
+ strokeLinecap: "round",
3187
+ transform: `rotate(-90 ${size / 2} ${size / 2})`,
3188
+ style: {
3189
+ transition: "stroke-dashoffset 0.9s cubic-bezier(0.4,0,0.2,1)"
3190
+ }
3191
+ }
3192
+ )
3193
+ ]
3194
+ }
3195
+ );
3196
+ }
2639
3197
  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`;
3198
+ const diff = Date.now() - new Date(date).getTime();
3199
+ const mins = Math.floor(diff / 6e4);
3200
+ if (mins < 1) return "just now";
3201
+ if (mins < 60) return `${mins}m`;
3202
+ const hrs = Math.floor(mins / 60);
3203
+ if (hrs < 24) return `${hrs}h`;
3204
+ return `${Math.floor(hrs / 24)}d`;
2647
3205
  }
2648
- function fmtDate(date) {
2649
- return new Date(date).toLocaleDateString(void 0, {
2650
- month: "short",
2651
- day: "numeric",
2652
- year: "numeric"
2653
- });
3206
+ function rateHue(r) {
3207
+ return r >= 70 ? 142 : r >= 40 ? 38 : 4;
2654
3208
  }
2655
- function StatCardItem({
3209
+ function StatItem({
2656
3210
  id,
2657
3211
  label,
2658
3212
  value,
@@ -2661,26 +3215,39 @@ function StatCardItem({
2661
3215
  color,
2662
3216
  delay = 0
2663
3217
  }) {
3218
+ const animated = useCountUp(value);
2664
3219
  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
- ] })
3220
+ return /* @__PURE__ */ jsxRuntime.jsxs(StatCard, { $delay: delay, $accent: color, children: [
3221
+ /* @__PURE__ */ jsxRuntime.jsx(StatLabel, { children: label }),
3222
+ /* @__PURE__ */ jsxRuntime.jsx(StatValue, { children: animated.toLocaleString() }),
3223
+ pct !== void 0 && /* @__PURE__ */ jsxRuntime.jsxs(TrendBadge, { $pos: isPos, children: [
3224
+ isPos ? "↑" : "↓",
3225
+ " ",
3226
+ Math.abs(pct).toFixed(1),
3227
+ "%"
2675
3228
  ] }),
2676
3229
  sparkline && sparkline.length > 1 && /* @__PURE__ */ jsxRuntime.jsx(SparkWrap, { children: /* @__PURE__ */ jsxRuntime.jsx(Sparkline, { data: sparkline, color, id }) })
2677
3230
  ] });
2678
3231
  }
2679
3232
  function OverviewPage() {
3233
+ const { get } = admin.useFetchClient();
2680
3234
  const [period, setPeriod] = react.useState(
2681
3235
  "weekly"
2682
3236
  );
3237
+ const [feedMode, setFeedMode] = react.useState("signups");
2683
3238
  const [hovIdx, setHovIdx] = react.useState(null);
3239
+ const [rtnHov, setRtnHov] = react.useState(null);
3240
+ const [activeSeries, setActiveSeries] = react.useState(
3241
+ () => /* @__PURE__ */ new Set(["totalUsers", "newUsers", "activeUsers"])
3242
+ );
3243
+ const toggleSeries = (key) => {
3244
+ setActiveSeries((prev) => {
3245
+ const next = new Set(prev);
3246
+ if (next.has(key) && next.size > 1) next.delete(key);
3247
+ else next.add(key);
3248
+ return next;
3249
+ });
3250
+ };
2684
3251
  const statsQuery = reactQuery.useQuery({
2685
3252
  queryKey: ["dash-user-stats"],
2686
3253
  queryFn: async () => {
@@ -2697,12 +3264,10 @@ function OverviewPage() {
2697
3264
  return r.data;
2698
3265
  }
2699
3266
  });
2700
- const sessionsQuery = reactQuery.useQuery({
2701
- queryKey: ["dash-recent-sessions"],
3267
+ const retentionQuery = reactQuery.useQuery({
3268
+ queryKey: ["dash-user-retention", period],
2702
3269
  queryFn: async () => {
2703
- const r = await client.dash.listAllSessions({
2704
- query: { page: 1, limit: 10 }
2705
- });
3270
+ const r = await client.dash.userRetentionData({ query: { period } });
2706
3271
  if (r.error) throw new Error(r.error.message ?? "Failed");
2707
3272
  return r.data;
2708
3273
  }
@@ -2711,12 +3276,22 @@ function OverviewPage() {
2711
3276
  queryKey: ["dash-recent-users"],
2712
3277
  queryFn: async () => {
2713
3278
  const r = await client.dash.listUsers({
2714
- query: { page: 1, limit: 8, search: "" }
3279
+ query: { limit: 12, offset: 0, sortBy: "createdAt", sortOrder: "desc" }
2715
3280
  });
2716
3281
  if (r.error) throw new Error(r.error.message ?? "Failed");
2717
3282
  return r.data;
2718
3283
  }
2719
3284
  });
3285
+ const sessionsQuery = reactQuery.useQuery({
3286
+ queryKey: ["dash-recent-sessions"],
3287
+ queryFn: async () => {
3288
+ const { data } = await get(
3289
+ "/better-auth-dashboard/db?uid=plugin::better-auth.session&pagination[pageSize]=12&sort[0]=createdAt:desc"
3290
+ );
3291
+ return data.results ?? [];
3292
+ },
3293
+ refetchInterval: 3e4
3294
+ });
2720
3295
  const orgsQuery = reactQuery.useQuery({
2721
3296
  queryKey: ["dash-orgs-count"],
2722
3297
  queryFn: async () => {
@@ -2727,595 +3302,385 @@ function OverviewPage() {
2727
3302
  return r.data;
2728
3303
  }
2729
3304
  });
3305
+ const sessionsRaw = sessionsQuery.data ?? [];
3306
+ const activeUserIds = [];
3307
+ const lastActiveByUserId = /* @__PURE__ */ new Map();
3308
+ for (const s of sessionsRaw) {
3309
+ if (!lastActiveByUserId.has(s.userId)) {
3310
+ lastActiveByUserId.set(s.userId, s.createdAt);
3311
+ activeUserIds.push(s.userId);
3312
+ }
3313
+ }
3314
+ const activeUsersQuery = reactQuery.useQuery({
3315
+ queryKey: ["dash-active-users", activeUserIds],
3316
+ queryFn: async () => {
3317
+ if (activeUserIds.length === 0) return [];
3318
+ const params = new URLSearchParams({ uid: "plugin::better-auth.user" });
3319
+ for (let i = 0; i < activeUserIds.length; i++) {
3320
+ params.set(`filters[id][$in][${i}]`, activeUserIds[i]);
3321
+ }
3322
+ params.set("pagination[pageSize]", "12");
3323
+ const { data } = await get(
3324
+ `/better-auth-dashboard/db?${params}`
3325
+ );
3326
+ return data.results ?? [];
3327
+ },
3328
+ enabled: feedMode === "active" && activeUserIds.length > 0
3329
+ });
3330
+ const chartRef = react.useRef(null);
3331
+ const [chartHeight, setChartHeight] = react.useState(0);
3332
+ react.useEffect(() => {
3333
+ const el = chartRef.current;
3334
+ if (!el) return;
3335
+ const ro = new ResizeObserver((entries) => {
3336
+ for (const entry of entries) setChartHeight(entry.contentRect.height);
3337
+ });
3338
+ ro.observe(el);
3339
+ return () => ro.disconnect();
3340
+ }, []);
2730
3341
  if (statsQuery.isLoading) {
2731
- return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", alignItems: "center", padding: 12, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading…" }) });
3342
+ return /* @__PURE__ */ jsxRuntime.jsx(
3343
+ designSystem.Flex,
3344
+ {
3345
+ justifyContent: "center",
3346
+ alignItems: "center",
3347
+ padding: 12,
3348
+ style: { background: T.bg, minHeight: "100%" },
3349
+ children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading…" })
3350
+ }
3351
+ );
2732
3352
  }
2733
3353
  const stats = statsQuery.data;
2734
3354
  if (!stats) return null;
2735
3355
  const graphData = graphQuery.data?.data ?? [];
2736
- const sessions = sessionsQuery.data?.sessions ?? [];
2737
- const recentUsers = usersQuery.data?.users ?? [];
3356
+ const rtnData = retentionQuery.data?.data ?? [];
3357
+ const users = usersQuery.data?.users ?? [];
2738
3358
  const orgCount = orgsQuery.data?.total ?? 0;
3359
+ const activeUserMap = /* @__PURE__ */ new Map();
3360
+ for (const u of activeUsersQuery.data ?? []) {
3361
+ activeUserMap.set(String(u.id), u);
3362
+ }
3363
+ const sortedActiveUsers = activeUserIds.map((uid) => activeUserMap.get(uid)).filter((u) => u !== void 0);
2739
3364
  const totalSpark = graphData.map((d) => d.totalUsers);
2740
3365
  const newSpark = graphData.map((d) => d.newUsers);
2741
3366
  const activeSpark = graphData.map((d) => d.activeUsers);
2742
3367
  const hovRow = hovIdx !== null ? graphData[hovIdx] : null;
2743
- return /* @__PURE__ */ jsxRuntime.jsxs(Wrap$2, { "data-testid": "overview-page", children: [
3368
+ const rtnHovRow = rtnHov !== null ? rtnData[rtnHov] : null;
3369
+ const activeMax = Math.max(
3370
+ stats.activeUsers.daily.active ?? 0,
3371
+ stats.activeUsers.weekly.active ?? 0,
3372
+ stats.activeUsers.monthly.active ?? 0,
3373
+ 1
3374
+ );
3375
+ return /* @__PURE__ */ jsxRuntime.jsxs(Wrap$1, { "data-testid": "overview-page", children: [
2744
3376
  /* @__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, {
3377
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
3378
+ /* @__PURE__ */ jsxRuntime.jsx(
3379
+ "div",
3380
+ {
3381
+ style: {
3382
+ fontSize: 22,
3383
+ fontWeight: 800,
3384
+ color: T.textPrimary,
3385
+ letterSpacing: "-0.03em"
3386
+ },
3387
+ children: "Overview"
3388
+ }
3389
+ ),
3390
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 11, color: T.textSecondary, marginTop: 4 }, children: (/* @__PURE__ */ new Date()).toLocaleDateString(void 0, {
2748
3391
  weekday: "long",
2749
3392
  year: "numeric",
2750
3393
  month: "long",
2751
3394
  day: "numeric"
2752
- }) }) })
3395
+ }) })
2753
3396
  ] }),
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
- ) })
3397
+ /* @__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
3398
  ] }),
2769
- /* @__PURE__ */ jsxRuntime.jsxs(Divider, { children: [
2770
- /* @__PURE__ */ jsxRuntime.jsx(DivLabel, { children: "Sign-ups" }),
3399
+ /* @__PURE__ */ jsxRuntime.jsxs(SectionDivider, { children: [
3400
+ /* @__PURE__ */ jsxRuntime.jsx(DivLabel, { children: "Metrics" }),
2771
3401
  /* @__PURE__ */ jsxRuntime.jsx(DivLine, {})
2772
3402
  ] }),
2773
- /* @__PURE__ */ jsxRuntime.jsxs(StatRow, { children: [
3403
+ /* @__PURE__ */ jsxRuntime.jsxs(StatGrid, { children: [
2774
3404
  /* @__PURE__ */ jsxRuntime.jsx(
2775
- StatCardItem,
3405
+ StatItem,
2776
3406
  {
2777
3407
  id: "total",
2778
3408
  label: "Total Users",
2779
3409
  value: stats.total ?? 0,
2780
3410
  sparkline: totalSpark,
2781
- color: "#4945FF",
3411
+ color: T.accent,
2782
3412
  delay: 0
2783
3413
  }
2784
3414
  ),
2785
3415
  /* @__PURE__ */ jsxRuntime.jsx(
2786
- StatCardItem,
3416
+ StatItem,
2787
3417
  {
2788
3418
  id: "d-sig",
2789
3419
  label: "Daily Sign-ups",
2790
3420
  value: stats.daily.signUps ?? 0,
2791
3421
  pct: stats.daily.percentage ?? void 0,
2792
3422
  sparkline: newSpark,
2793
- color: "#5CB176",
3423
+ color: T.green,
2794
3424
  delay: 1
2795
3425
  }
2796
3426
  ),
2797
3427
  /* @__PURE__ */ jsxRuntime.jsx(
2798
- StatCardItem,
3428
+ StatItem,
2799
3429
  {
2800
3430
  id: "w-sig",
2801
3431
  label: "Weekly Sign-ups",
2802
3432
  value: stats.weekly.signUps ?? 0,
2803
3433
  pct: stats.weekly.percentage ?? void 0,
2804
3434
  sparkline: newSpark,
2805
- color: "#5CB176",
3435
+ color: T.green,
2806
3436
  delay: 2
2807
3437
  }
2808
3438
  ),
2809
3439
  /* @__PURE__ */ jsxRuntime.jsx(
2810
- StatCardItem,
3440
+ StatItem,
2811
3441
  {
2812
3442
  id: "m-sig",
2813
3443
  label: "Monthly Sign-ups",
2814
3444
  value: stats.monthly.signUps ?? 0,
2815
3445
  pct: stats.monthly.percentage ?? void 0,
2816
3446
  sparkline: newSpark,
2817
- color: "#5CB176",
3447
+ color: T.green,
2818
3448
  delay: 3
2819
3449
  }
2820
3450
  ),
2821
3451
  /* @__PURE__ */ jsxRuntime.jsx(
2822
- StatCardItem,
3452
+ StatItem,
2823
3453
  {
2824
3454
  id: "orgs",
2825
3455
  label: "Organizations",
2826
3456
  value: orgCount,
2827
- color: "#9E6BF9",
3457
+ color: T.purple,
2828
3458
  delay: 4
2829
3459
  }
2830
3460
  )
2831
3461
  ] }),
2832
- /* @__PURE__ */ jsxRuntime.jsxs(MainRow, { children: [
2833
- /* @__PURE__ */ jsxRuntime.jsxs(ChartCard, { $delay: 5, children: [
3462
+ /* @__PURE__ */ jsxRuntime.jsxs(SectionDivider, { children: [
3463
+ /* @__PURE__ */ jsxRuntime.jsx(DivLabel, { children: "Growth" }),
3464
+ /* @__PURE__ */ jsxRuntime.jsx(DivLine, {})
3465
+ ] }),
3466
+ /* @__PURE__ */ jsxRuntime.jsxs(TwoPanel, { children: [
3467
+ /* @__PURE__ */ jsxRuntime.jsxs(ChartCard, { ref: chartRef, $delay: 5, children: [
2834
3468
  /* @__PURE__ */ jsxRuntime.jsxs(ChartHeader, { children: [
2835
3469
  /* @__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" }),
3470
+ /* @__PURE__ */ jsxRuntime.jsx(SeriesRow, { children: ALL_SERIES.map((s) => /* @__PURE__ */ jsxRuntime.jsxs(
3471
+ SeriesBtn,
3472
+ {
3473
+ $on: activeSeries.has(s.key),
3474
+ $c: s.color,
3475
+ onClick: () => toggleSeries(s.key),
3476
+ children: [
3477
+ /* @__PURE__ */ jsxRuntime.jsx(SDot, { $c: s.color }),
3478
+ s.label
3479
+ ]
3480
+ },
3481
+ s.key
3482
+ )) })
3483
+ ] }),
3484
+ /* @__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
3485
  graphQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(
2843
3486
  designSystem.Flex,
2844
3487
  {
2845
3488
  justifyContent: "center",
2846
3489
  alignItems: "center",
2847
- style: { flex: 1, minHeight: 180 },
3490
+ style: { flex: 1, minHeight: 170 },
2848
3491
  children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading…" })
2849
3492
  }
2850
- ) : /* @__PURE__ */ jsxRuntime.jsx(AreaChart, { data: graphData, hovered: hovIdx, onHover: setHovIdx })
3493
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
3494
+ GrowthChart,
3495
+ {
3496
+ data: graphData,
3497
+ hovered: hovIdx,
3498
+ onHover: setHovIdx,
3499
+ activeSeries
3500
+ }
3501
+ )
2851
3502
  ] }),
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,
3503
+ /* @__PURE__ */ jsxRuntime.jsxs(
3504
+ FeedCard,
2902
3505
  {
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
3506
+ $delay: 6,
3507
+ style: chartHeight > 0 ? { height: chartHeight } : void 0,
3508
+ children: [
3509
+ /* @__PURE__ */ jsxRuntime.jsxs(FeedHead, { children: [
3510
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: feedMode === "signups" ? "Recent Sign-ups" : "Recently Active" }),
3511
+ /* @__PURE__ */ jsxRuntime.jsxs(
3512
+ FeedSelect,
3513
+ {
3514
+ value: feedMode,
3515
+ onChange: (e) => setFeedMode(e.target.value),
3516
+ children: [
3517
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "signups", children: "Recent Sign-ups" }),
3518
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "active", children: "Recently Active" })
3519
+ ]
3520
+ }
3521
+ )
3522
+ ] }),
3523
+ /* @__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: [
3524
+ /* @__PURE__ */ jsxRuntime.jsxs(FeedTop, { children: [
3525
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { alignItems: "center", gap: 1, style: { minWidth: 0 }, children: [
3526
+ /* @__PURE__ */ jsxRuntime.jsx(Avatar, { name: u.name, src: u.image, size: 20 }),
3527
+ /* @__PURE__ */ jsxRuntime.jsx(FeedName, { title: u.name, children: u.name })
3528
+ ] }),
3529
+ /* @__PURE__ */ jsxRuntime.jsx(FeedMeta, { children: relTime(u.createdAt) })
3530
+ ] }),
3531
+ /* @__PURE__ */ jsxRuntime.jsx(FeedEmail, { title: u.email, children: u.email })
3532
+ ] }, u.id)) : activeUsersQuery.isLoading || sessionsQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading…" }) }) : sortedActiveUsers.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(Empty, { children: "No recent activity" }) : sortedActiveUsers.map((u) => /* @__PURE__ */ jsxRuntime.jsxs(FeedItem, { children: [
3533
+ /* @__PURE__ */ jsxRuntime.jsxs(FeedTop, { children: [
3534
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { alignItems: "center", gap: 1, style: { minWidth: 0 }, children: [
3535
+ /* @__PURE__ */ jsxRuntime.jsx(
3536
+ Avatar,
3537
+ {
3538
+ name: u.name,
3539
+ src: u.image ?? void 0,
3540
+ size: 20
3541
+ }
3542
+ ),
3543
+ /* @__PURE__ */ jsxRuntime.jsx(FeedName, { title: u.name, children: u.name })
3544
+ ] }),
3545
+ /* @__PURE__ */ jsxRuntime.jsx(FeedMeta, { children: relTime(
3546
+ lastActiveByUserId.get(String(u.id)) ?? u.createdAt
3547
+ ) })
3548
+ ] }),
3549
+ /* @__PURE__ */ jsxRuntime.jsx(FeedEmail, { title: u.email, children: u.email })
3550
+ ] }, u.documentId)) })
3551
+ ]
2910
3552
  }
2911
3553
  )
2912
3554
  ] }),
2913
- /* @__PURE__ */ jsxRuntime.jsxs(Divider, { children: [
2914
- /* @__PURE__ */ jsxRuntime.jsx(DivLabel, { children: "Recent Users" }),
3555
+ /* @__PURE__ */ jsxRuntime.jsxs(SectionDivider, { children: [
3556
+ /* @__PURE__ */ jsxRuntime.jsx(DivLabel, { children: "Retention & Activity" }),
2915
3557
  /* @__PURE__ */ jsxRuntime.jsx(DivLine, {})
2916
3558
  ] }),
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"
3559
+ /* @__PURE__ */ jsxRuntime.jsxs(TwoPanel, { children: [
3560
+ /* @__PURE__ */ jsxRuntime.jsxs(RtnCard, { $delay: 7, children: [
3561
+ /* @__PURE__ */ jsxRuntime.jsxs(ChartHeader, { children: [
3562
+ /* @__PURE__ */ jsxRuntime.jsx(ChartTitle, { children: "Cohort Retention" }),
3563
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { alignItems: "center", gap: 2, children: [
3564
+ { hue: 142, label: "≥70%" },
3565
+ { hue: 38, label: "40–70%" },
3566
+ { hue: 4, label: "<40%" }
3567
+ ].map(({ hue, label }) => /* @__PURE__ */ jsxRuntime.jsxs(
3568
+ designSystem.Flex,
3569
+ {
3570
+ alignItems: "center",
3571
+ gap: 1,
3572
+ style: { marginLeft: 8 },
3573
+ children: [
3574
+ /* @__PURE__ */ jsxRuntime.jsx(
3575
+ "span",
3576
+ {
3577
+ style: {
3578
+ width: 8,
3579
+ height: 8,
3580
+ borderRadius: "50%",
3581
+ background: `hsl(${hue},60%,50%)`,
3582
+ display: "inline-block",
3583
+ flexShrink: 0
3584
+ }
3585
+ }
3586
+ ),
3587
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: 9, color: T.textMuted }, children: label })
3588
+ ]
2953
3589
  },
2954
- children: fmtDate(u.createdAt)
2955
- }
2956
- ),
2957
- /* @__PURE__ */ jsxRuntime.jsx(TD$1, { children: /* @__PURE__ */ jsxRuntime.jsx(
2958
- StatusChip$1,
3590
+ hue
3591
+ )) })
3592
+ ] }),
3593
+ /* @__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" }),
3594
+ retentionQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(
3595
+ designSystem.Flex,
2959
3596
  {
2960
- $verified: u.emailVerified,
2961
- $banned: u.banned,
2962
- children: u.banned ? "Banned" : u.emailVerified ? "Verified" : "Unverified"
3597
+ justifyContent: "center",
3598
+ alignItems: "center",
3599
+ style: { minHeight: 80 },
3600
+ children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading…" })
2963
3601
  }
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" })
3602
+ ) : rtnData.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs(Empty, { children: [
3603
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 22 }, children: "📉" }),
3604
+ /* @__PURE__ */ jsxRuntime.jsx("div", { children: "No retention data for this period" })
3605
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", flexDirection: "column", gap: 5 }, children: rtnData.map((row, i) => {
3606
+ const hue = rateHue(row.retentionRate);
3607
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3608
+ RtnRow,
3609
+ {
3610
+ $hov: rtnHov === i,
3611
+ onMouseEnter: () => setRtnHov(i),
3612
+ onMouseLeave: () => setRtnHov(null),
3613
+ children: [
3614
+ /* @__PURE__ */ jsxRuntime.jsx(RtnLabel, { children: row.label }),
3615
+ /* @__PURE__ */ jsxRuntime.jsx(RtnSize, { children: row.cohortSize.toLocaleString() }),
3616
+ /* @__PURE__ */ jsxRuntime.jsx(RtnTrack, { children: /* @__PURE__ */ jsxRuntime.jsx(RtnBar, { $w: row.retentionRate, $hue: hue }) }),
3617
+ /* @__PURE__ */ jsxRuntime.jsxs(RtnPct, { $hue: hue, children: [
3618
+ row.retentionRate.toFixed(1),
3619
+ "%"
3620
+ ] })
3621
+ ]
3622
+ },
3623
+ row.n
3624
+ );
3625
+ }) })
3188
3626
  ] }),
3189
- someSelected && /* @__PURE__ */ jsxRuntime.jsxs(
3190
- designSystem.Button,
3627
+ /* @__PURE__ */ jsxRuntime.jsx(RingGrid, { children: [
3191
3628
  {
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
- ]
3629
+ label: "Daily Active",
3630
+ value: stats.activeUsers.daily.active,
3631
+ pct: stats.activeUsers.daily.percentage,
3632
+ color: T.amber,
3633
+ sparkline: activeSpark,
3634
+ delay: 8
3635
+ },
3636
+ {
3637
+ label: "Weekly Active",
3638
+ value: stats.activeUsers.weekly.active,
3639
+ pct: stats.activeUsers.weekly.percentage,
3640
+ color: T.green,
3641
+ sparkline: activeSpark,
3642
+ delay: 9
3643
+ },
3644
+ {
3645
+ label: "Monthly Active",
3646
+ value: stats.activeUsers.monthly.active,
3647
+ pct: stats.activeUsers.monthly.percentage,
3648
+ color: T.accent,
3649
+ sparkline: activeSpark,
3650
+ delay: 10
3202
3651
  }
3203
- )
3204
- ] }),
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,
3210
- {
3211
- checked: allSelected,
3212
- onCheckedChange: handleSelectAll,
3213
- "aria-label": "Select all users"
3214
- }
3215
- ),
3216
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: "User / Sessions" }),
3217
- /* @__PURE__ */ jsxRuntime.jsx(ColumnHeaderRight, { children: "Sessions" })
3218
- ] }),
3219
- usersWithSessions.map((userRow, i) => /* @__PURE__ */ jsxRuntime.jsxs(UserGroup, { $i: i, "data-testid": "session-user-row", children: [
3220
- /* @__PURE__ */ jsxRuntime.jsxs(UserRowHeader, { children: [
3652
+ ].map(({ label, value, pct, color, delay }) => {
3653
+ const isPos = pct == null || pct >= 0;
3654
+ return /* @__PURE__ */ jsxRuntime.jsxs(RingCard, { $delay: delay, children: [
3221
3655
  /* @__PURE__ */ jsxRuntime.jsx(
3222
- designSystem.Checkbox,
3656
+ ProgressRing,
3223
3657
  {
3224
- checked: selected.has(userRow.id),
3225
- onCheckedChange: () => toggleSelect(userRow.id),
3226
- "aria-label": `Select ${userRow.name}`
3658
+ value: value ?? 0,
3659
+ max: activeMax,
3660
+ color,
3661
+ size: 56
3227
3662
  }
3228
3663
  ),
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
- ] })
3239
- ] }),
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,
3664
+ /* @__PURE__ */ jsxRuntime.jsxs(RingInfo, { children: [
3665
+ /* @__PURE__ */ jsxRuntime.jsx(RingLabel, { children: label }),
3666
+ /* @__PURE__ */ jsxRuntime.jsx(RingVal, { children: (value ?? 0).toLocaleString() }),
3667
+ pct != null && /* @__PURE__ */ jsxRuntime.jsxs(
3668
+ TrendBadge,
3244
3669
  {
3245
- gap: 2,
3246
- alignItems: "center",
3247
- style: { flexWrap: "wrap" },
3670
+ $pos: isPos,
3671
+ style: { marginTop: 4, marginBottom: 0 },
3248
3672
  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
- ] })
3673
+ isPos ? "↑" : "↓",
3674
+ " ",
3675
+ Math.abs(pct).toFixed(1),
3676
+ "%"
3258
3677
  ]
3259
3678
  }
3260
- ),
3261
- session.userAgent && /* @__PURE__ */ jsxRuntime.jsx(AgentText, { children: session.userAgent })
3262
- ] }),
3263
- /* @__PURE__ */ jsxRuntime.jsx(
3264
- designSystem.IconButton,
3265
- {
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, {})
3271
- }
3272
- )
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,
3279
- {
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,
3289
- {
3290
- variant: "tertiary",
3291
- size: "S",
3292
- onClick: () => setPage((p) => p + 1),
3293
- children: "Next"
3294
- }
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
- )
3679
+ )
3680
+ ] })
3681
+ ] }, label);
3682
+ }) })
3683
+ ] })
3319
3684
  ] });
3320
3685
  }
3321
3686
  function useUsers(options = {}) {
@@ -3556,6 +3921,45 @@ const ReadOnlyCodeInput = styled__default.default.div`
3556
3921
  word-break: break-all;
3557
3922
  overflow-wrap: anywhere;
3558
3923
  `;
3924
+ const SessionCard = styled__default.default.div`
3925
+ display: flex;
3926
+ align-items: flex-start;
3927
+ justify-content: space-between;
3928
+ gap: 12px;
3929
+ padding: 10px 14px;
3930
+ background: white;
3931
+ border: 1px solid #eaeaef;
3932
+ border-radius: 8px;
3933
+ `;
3934
+ const SessionMeta = styled__default.default.div`
3935
+ display: flex;
3936
+ flex-direction: column;
3937
+ gap: 3px;
3938
+ flex: 1;
3939
+ min-width: 0;
3940
+ `;
3941
+ const IpChip = styled__default.default.span`
3942
+ display: inline-block;
3943
+ background: #f0f0ff;
3944
+ color: #4945ff;
3945
+ border-radius: 5px;
3946
+ padding: 2px 6px;
3947
+ font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
3948
+ font-size: 11px;
3949
+ font-weight: 600;
3950
+ `;
3951
+ const TimestampText = styled__default.default.span`
3952
+ font-size: 11px;
3953
+ color: #8e8ea9;
3954
+ `;
3955
+ const AgentText = styled__default.default.span`
3956
+ font-size: 11px;
3957
+ color: #b8b8c7;
3958
+ overflow: hidden;
3959
+ text-overflow: ellipsis;
3960
+ white-space: nowrap;
3961
+ display: block;
3962
+ `;
3559
3963
  const STANDARD_FIELDS = /* @__PURE__ */ new Set([
3560
3964
  "id",
3561
3965
  "name",
@@ -3577,6 +3981,7 @@ function UserDetailDrawer({
3577
3981
  }) {
3578
3982
  const qc = reactQuery.useQueryClient();
3579
3983
  const { toggleNotification } = admin.useNotification();
3984
+ const { get, put } = admin.useFetchClient();
3580
3985
  const schemaQuery = useModelSchema("user");
3581
3986
  const userQuery = reactQuery.useQuery({
3582
3987
  queryKey: ["dash-user", userId],
@@ -3598,6 +4003,27 @@ function UserDetailDrawer({
3598
4003
  return result.data?.organizations ?? [];
3599
4004
  }
3600
4005
  });
4006
+ const sessionsQuery = reactQuery.useQuery({
4007
+ queryKey: ["dash-user-sessions", userId],
4008
+ queryFn: async () => {
4009
+ const { data } = await get(
4010
+ `/better-auth-dashboard/db?uid=plugin::better-auth.session&filters[userId][$eq]=${userId}&sort[0]=createdAt:desc&pagination[pageSize]=50`
4011
+ );
4012
+ return data.results ?? [];
4013
+ }
4014
+ });
4015
+ const strapiUserQuery = reactQuery.useQuery({
4016
+ queryKey: ["dash-strapi-user", userId],
4017
+ enabled: !!schemaQuery.data,
4018
+ queryFn: async () => {
4019
+ const relationFields = Object.entries(schemaQuery.data).filter(([, attr]) => attr.type === "relation").map(([fieldName]) => fieldName);
4020
+ const populateParam = relationFields.length > 0 ? `&populate=${encodeURIComponent(relationFields.join(","))}` : "";
4021
+ const { data } = await get(
4022
+ `/better-auth-dashboard/db?uid=plugin::better-auth.user&filters[id][$eq]=${userId}&pagination[pageSize]=1${populateParam}`
4023
+ );
4024
+ return data.results?.[0] ?? null;
4025
+ }
4026
+ });
3601
4027
  const [activeTab, setActiveTab] = react.useState("profile");
3602
4028
  const [editName, setEditName] = react.useState(void 0);
3603
4029
  const [editEmail, setEditEmail] = react.useState(void 0);
@@ -3608,6 +4034,8 @@ function UserDetailDrawer({
3608
4034
  const [banReason, setBanReason] = react.useState("");
3609
4035
  const [banExpiresDays, setBanExpiresDays] = react.useState("");
3610
4036
  const [confirmRevokeAll, setConfirmRevokeAll] = react.useState(false);
4037
+ const [confirmRevokeSessionId, setConfirmRevokeSessionId] = react.useState(null);
4038
+ const [confirmBan, setConfirmBan] = react.useState(false);
3611
4039
  const [confirmUnban, setConfirmUnban] = react.useState(false);
3612
4040
  const [confirmUnlinkAccountId, setConfirmUnlinkAccountId] = react.useState(null);
3613
4041
  const [confirmDisable2FA, setConfirmDisable2FA] = react.useState(false);
@@ -3626,7 +4054,7 @@ function UserDetailDrawer({
3626
4054
  setEditExtra((prev) => ({ ...prev, [name]: value }));
3627
4055
  };
3628
4056
  const extraData = {
3629
- ...user,
4057
+ ...strapiUserQuery.data ?? {},
3630
4058
  ...editExtra
3631
4059
  };
3632
4060
  const updateMutation = reactQuery.useMutation({
@@ -3637,17 +4065,17 @@ function UserDetailDrawer({
3637
4065
  if (editEmailVerified !== void 0)
3638
4066
  body.emailVerified = editEmailVerified;
3639
4067
  if (editImage !== void 0) body.image = editImage;
3640
- const result = await client.dash.updateUser(
3641
- body,
3642
- withContext({ userId })
4068
+ const documentId = strapiUserQuery.data?.documentId;
4069
+ if (!documentId) throw new Error("Could not resolve documentId for user");
4070
+ await put(
4071
+ `/better-auth-dashboard/db/${documentId}?uid=plugin::better-auth.user`,
4072
+ body
3643
4073
  );
3644
- if (result.error)
3645
- throw new Error(result.error.message ?? "Update failed");
3646
- return result.data;
3647
4074
  },
3648
4075
  onSuccess: () => {
3649
4076
  qc.invalidateQueries({ queryKey: ["dash-user", userId] });
3650
4077
  qc.invalidateQueries({ queryKey: ["dash-users"] });
4078
+ qc.invalidateQueries({ queryKey: ["dash-strapi-user", userId] });
3651
4079
  setEditName(void 0);
3652
4080
  setEditEmail(void 0);
3653
4081
  setEditEmailVerified(void 0);
@@ -3666,6 +4094,27 @@ function UserDetailDrawer({
3666
4094
  });
3667
4095
  }
3668
4096
  });
4097
+ const revokeSessionMutation = reactQuery.useMutation({
4098
+ mutationFn: async (sessionId) => {
4099
+ const result = await client.dash.sessions.revoke(
4100
+ {},
4101
+ withContext({ sessionId })
4102
+ );
4103
+ if (result.error)
4104
+ throw new Error(result.error.message ?? "Revoke failed");
4105
+ },
4106
+ onSuccess: () => {
4107
+ setConfirmRevokeSessionId(null);
4108
+ qc.invalidateQueries({ queryKey: ["dash-user-sessions", userId] });
4109
+ toggleNotification({ type: "success", message: "Session revoked" });
4110
+ },
4111
+ onError: (err) => {
4112
+ toggleNotification({
4113
+ type: "danger",
4114
+ message: err.message ?? "Failed to revoke session"
4115
+ });
4116
+ }
4117
+ });
3669
4118
  const passwordMutation = reactQuery.useMutation({
3670
4119
  mutationFn: async () => {
3671
4120
  const result = await client.dash.setPassword(
@@ -3703,6 +4152,7 @@ function UserDetailDrawer({
3703
4152
  return result.data;
3704
4153
  },
3705
4154
  onSuccess: () => {
4155
+ setConfirmBan(false);
3706
4156
  qc.invalidateQueries({ queryKey: ["dash-user", userId] });
3707
4157
  qc.invalidateQueries({ queryKey: ["dash-users"] });
3708
4158
  setBanReason("");
@@ -4318,7 +4768,8 @@ function UserDetailDrawer({
4318
4768
  ] })
4319
4769
  ] })
4320
4770
  ] }) }),
4321
- banEnabled && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "ban", children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { direction: "column", gap: 4, paddingTop: 6, children: user?.banned ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
4771
+ banEnabled && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "ban", children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { direction: "column", gap: 5, paddingTop: 6, children: user?.banned ? /* @__PURE__ */ jsxRuntime.jsxs(FormSection, { children: [
4772
+ /* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { children: "Ban status" }),
4322
4773
  /* @__PURE__ */ jsxRuntime.jsxs(WarnCard, { children: [
4323
4774
  /* @__PURE__ */ jsxRuntime.jsx(
4324
4775
  designSystem.Typography,
@@ -4333,7 +4784,7 @@ function UserDetailDrawer({
4333
4784
  "Reason: ",
4334
4785
  user.banReason
4335
4786
  ] }),
4336
- user.banExpires && /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", textColor: "danger600", children: [
4787
+ user.banExpires ? /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", textColor: "danger600", children: [
4337
4788
  "Expires:",
4338
4789
  " ",
4339
4790
  new Date(user.banExpires).toLocaleDateString(
@@ -4345,8 +4796,7 @@ function UserDetailDrawer({
4345
4796
  day: "numeric"
4346
4797
  }
4347
4798
  )
4348
- ] }),
4349
- !user.banExpires && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "danger600", children: "Duration: Permanent" })
4799
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "danger600", children: "Duration: Permanent" })
4350
4800
  ] }),
4351
4801
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(
4352
4802
  designSystem.Button,
@@ -4356,27 +4806,28 @@ function UserDetailDrawer({
4356
4806
  children: "Lift ban"
4357
4807
  }
4358
4808
  ) })
4359
- ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
4809
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(FormSection, { children: [
4810
+ /* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { children: "Apply ban" }),
4811
+ /* @__PURE__ */ jsxRuntime.jsxs(
4812
+ designSystem.Field.Root,
4813
+ {
4814
+ hint: "Optional — will be shown to the user",
4815
+ style: { width: "100%" },
4816
+ children: [
4817
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Reason" }),
4818
+ /* @__PURE__ */ jsxRuntime.jsx(
4819
+ designSystem.TextInput,
4820
+ {
4821
+ value: banReason,
4822
+ onChange: (e) => setBanReason(e.target.value),
4823
+ placeholder: "e.g. Violated terms of service"
4824
+ }
4825
+ ),
4826
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {})
4827
+ ]
4828
+ }
4829
+ ),
4360
4830
  /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Grid.Root, { gap: 4, children: [
4361
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 12, children: /* @__PURE__ */ jsxRuntime.jsxs(
4362
- designSystem.Field.Root,
4363
- {
4364
- hint: "Optional — will be shown to the user",
4365
- style: { width: "100%" },
4366
- children: [
4367
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Reason" }),
4368
- /* @__PURE__ */ jsxRuntime.jsx(
4369
- designSystem.TextInput,
4370
- {
4371
- value: banReason,
4372
- onChange: (e) => setBanReason(e.target.value),
4373
- placeholder: "e.g. Violated terms of service"
4374
- }
4375
- ),
4376
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {})
4377
- ]
4378
- }
4379
- ) }),
4380
4831
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 6, children: /* @__PURE__ */ jsxRuntime.jsxs(
4381
4832
  designSystem.Field.Root,
4382
4833
  {
@@ -4397,24 +4848,13 @@ function UserDetailDrawer({
4397
4848
  ]
4398
4849
  }
4399
4850
  ) }),
4400
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 6, children: banExpiryPreview ? /* @__PURE__ */ jsxRuntime.jsx(
4401
- designSystem.Flex,
4402
- {
4403
- direction: "column",
4404
- justifyContent: "flex-end",
4405
- style: { height: "100%", paddingBottom: 4 },
4406
- children: /* @__PURE__ */ jsxRuntime.jsxs(PreviewPill, { children: [
4407
- "⏱ Expires ",
4408
- banExpiryPreview
4409
- ] })
4410
- }
4411
- ) : /* @__PURE__ */ jsxRuntime.jsx(
4851
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 6, children: /* @__PURE__ */ jsxRuntime.jsx(
4412
4852
  designSystem.Flex,
4413
4853
  {
4414
4854
  direction: "column",
4415
4855
  justifyContent: "flex-end",
4416
4856
  style: { height: "100%", paddingBottom: 4 },
4417
- children: /* @__PURE__ */ jsxRuntime.jsx(PreviewPill, { children: "♾ Permanent ban" })
4857
+ children: /* @__PURE__ */ jsxRuntime.jsx(PreviewPill, { children: banExpiryPreview ? `⏱ Expires ${banExpiryPreview}` : "♾ Permanent ban" })
4418
4858
  }
4419
4859
  ) })
4420
4860
  ] }),
@@ -4422,31 +4862,68 @@ function UserDetailDrawer({
4422
4862
  designSystem.Button,
4423
4863
  {
4424
4864
  variant: "danger",
4425
- loading: banMutation.isLoading,
4426
- onClick: () => banMutation.mutate(),
4865
+ onClick: () => setConfirmBan(true),
4427
4866
  children: "Ban user"
4428
4867
  }
4429
4868
  ) })
4430
4869
  ] }) }) }),
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
- )
4870
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "sessions", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 5, paddingTop: 6, children: [
4871
+ /* @__PURE__ */ jsxRuntime.jsxs(FormSection, { children: [
4872
+ /* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { children: "Session management" }),
4873
+ /* @__PURE__ */ jsxRuntime.jsxs(AccountRow, { children: [
4874
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 1, alignItems: "flex-start", children: [
4875
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", fontWeight: "semiBold", children: "Revoke all sessions" }),
4876
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: "Signs the user out immediately on all devices." })
4877
+ ] }),
4878
+ /* @__PURE__ */ jsxRuntime.jsx(
4879
+ designSystem.Button,
4880
+ {
4881
+ variant: "danger-light",
4882
+ size: "S",
4883
+ onClick: () => setConfirmRevokeAll(true),
4884
+ style: { flexShrink: 0 },
4885
+ children: "Revoke all"
4886
+ }
4887
+ )
4888
+ ] })
4889
+ ] }),
4890
+ /* @__PURE__ */ jsxRuntime.jsxs(FormSection, { children: [
4891
+ /* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { children: "Active sessions" }),
4892
+ 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: 2, alignItems: "stretch", children: (sessionsQuery.data ?? []).map((session) => /* @__PURE__ */ jsxRuntime.jsxs(SessionCard, { children: [
4893
+ /* @__PURE__ */ jsxRuntime.jsxs(SessionMeta, { children: [
4894
+ /* @__PURE__ */ jsxRuntime.jsxs(
4895
+ designSystem.Flex,
4896
+ {
4897
+ gap: 2,
4898
+ alignItems: "center",
4899
+ style: { flexWrap: "wrap" },
4900
+ children: [
4901
+ session.ipAddress && /* @__PURE__ */ jsxRuntime.jsx(IpChip, { children: session.ipAddress }),
4902
+ /* @__PURE__ */ jsxRuntime.jsxs(TimestampText, { children: [
4903
+ "Created",
4904
+ " ",
4905
+ new Date(session.createdAt).toLocaleString(),
4906
+ " · Expires",
4907
+ " ",
4908
+ new Date(session.expiresAt).toLocaleString()
4909
+ ] })
4910
+ ]
4911
+ }
4912
+ ),
4913
+ session.userAgent && /* @__PURE__ */ jsxRuntime.jsx(AgentText, { children: session.userAgent })
4914
+ ] }),
4915
+ /* @__PURE__ */ jsxRuntime.jsx(
4916
+ designSystem.IconButton,
4917
+ {
4918
+ label: "Revoke session",
4919
+ onClick: () => setConfirmRevokeSessionId(String(session.id)),
4920
+ style: { flexShrink: 0 },
4921
+ children: /* @__PURE__ */ jsxRuntime.jsx(icons.Trash, {})
4922
+ }
4923
+ )
4924
+ ] }, session.documentId)) })
4448
4925
  ] })
4449
- ] }) }) }),
4926
+ ] }) }),
4450
4927
  /* @__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
4928
  /* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { children: "Memberships" }),
4452
4929
  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 +4960,29 @@ function UserDetailDrawer({
4483
4960
  onCancel: () => setConfirmRevokeAll(false)
4484
4961
  }
4485
4962
  ),
4963
+ confirmRevokeSessionId && /* @__PURE__ */ jsxRuntime.jsx(
4964
+ ConfirmDialog,
4965
+ {
4966
+ title: "Revoke session",
4967
+ message: "Are you sure you want to revoke this session? The user will be signed out on this device.",
4968
+ confirmLabel: "Revoke",
4969
+ loading: revokeSessionMutation.isLoading,
4970
+ onConfirm: () => revokeSessionMutation.mutate(confirmRevokeSessionId),
4971
+ onCancel: () => setConfirmRevokeSessionId(null)
4972
+ }
4973
+ ),
4974
+ confirmBan && /* @__PURE__ */ jsxRuntime.jsx(
4975
+ ConfirmDialog,
4976
+ {
4977
+ title: "Ban user",
4978
+ message: banReason ? `Ban this user for the following reason: "${banReason}"? They will be prevented from signing in.` : "Are you sure you want to ban this user? They will be prevented from signing in.",
4979
+ confirmLabel: "Ban user",
4980
+ variant: "danger",
4981
+ loading: banMutation.isLoading,
4982
+ onConfirm: () => banMutation.mutate(),
4983
+ onCancel: () => setConfirmBan(false)
4984
+ }
4985
+ ),
4486
4986
  confirmUnban && /* @__PURE__ */ jsxRuntime.jsx(
4487
4987
  ConfirmDialog,
4488
4988
  {
@@ -4713,6 +5213,7 @@ const Toolbar = styled__default.default.div`
4713
5213
  `;
4714
5214
  function UsersPage({ config }) {
4715
5215
  const qc = reactQuery.useQueryClient();
5216
+ const { toggleNotification } = admin.useNotification();
4716
5217
  const banEnabled = hasPlugin(config, "admin");
4717
5218
  const emailVerificationEnabled = config.emailVerification.sendVerificationEmailEnabled;
4718
5219
  const twoFactorEnabled = hasPlugin(config, "two-factor");
@@ -4744,6 +5245,13 @@ function UsersPage({ config }) {
4744
5245
  setConfirmDelete(null);
4745
5246
  qc.invalidateQueries({ queryKey: ["dash-users"] });
4746
5247
  qc.invalidateQueries({ queryKey: ["dash-user-stats"] });
5248
+ toggleNotification({ type: "success", message: "User deleted" });
5249
+ },
5250
+ onError: (err) => {
5251
+ toggleNotification({
5252
+ type: "danger",
5253
+ message: err.message ?? "Failed to delete user"
5254
+ });
4747
5255
  }
4748
5256
  });
4749
5257
  const deleteManyMutation = reactQuery.useMutation({
@@ -4756,11 +5264,21 @@ function UsersPage({ config }) {
4756
5264
  throw new Error(result.error.message ?? "Delete failed");
4757
5265
  return result.data;
4758
5266
  },
4759
- onSuccess: () => {
5267
+ onSuccess: (_data, userIds) => {
4760
5268
  setConfirmDeleteMany(false);
4761
5269
  setSelected(/* @__PURE__ */ new Set());
4762
5270
  qc.invalidateQueries({ queryKey: ["dash-users"] });
4763
5271
  qc.invalidateQueries({ queryKey: ["dash-user-stats"] });
5272
+ toggleNotification({
5273
+ type: "success",
5274
+ message: `${userIds.length} user${userIds.length !== 1 ? "s" : ""} deleted`
5275
+ });
5276
+ },
5277
+ onError: (err) => {
5278
+ toggleNotification({
5279
+ type: "danger",
5280
+ message: err.message ?? "Failed to delete users"
5281
+ });
4764
5282
  }
4765
5283
  });
4766
5284
  const banManyMutation = reactQuery.useMutation({
@@ -4772,10 +5290,20 @@ function UsersPage({ config }) {
4772
5290
  if (result.error) throw new Error(result.error.message ?? "Ban failed");
4773
5291
  return result.data;
4774
5292
  },
4775
- onSuccess: () => {
5293
+ onSuccess: (_data, userIds) => {
4776
5294
  setConfirmBanMany(false);
4777
5295
  setSelected(/* @__PURE__ */ new Set());
4778
5296
  qc.invalidateQueries({ queryKey: ["dash-users"] });
5297
+ toggleNotification({
5298
+ type: "success",
5299
+ message: `${userIds.length} user${userIds.length !== 1 ? "s" : ""} banned`
5300
+ });
5301
+ },
5302
+ onError: (err) => {
5303
+ toggleNotification({
5304
+ type: "danger",
5305
+ message: err.message ?? "Failed to ban users"
5306
+ });
4779
5307
  }
4780
5308
  });
4781
5309
  const toggleSelect = (id) => {
@@ -4954,14 +5482,28 @@ function UsersPage({ config }) {
4954
5482
  user.id
4955
5483
  )) })
4956
5484
  ] }) }),
4957
- pageCount > 1 && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "flex-end", children: /* @__PURE__ */ jsxRuntime.jsx(
4958
- designSystem.Pagination,
4959
- {
4960
- activePage: page,
4961
- pageCount,
4962
- onChangePage: setPage
4963
- }
4964
- ) }),
5485
+ pageCount > 1 && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "flex-end", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, children: [
5486
+ /* @__PURE__ */ jsxRuntime.jsx(
5487
+ designSystem.Button,
5488
+ {
5489
+ variant: "tertiary",
5490
+ size: "S",
5491
+ disabled: page === 1,
5492
+ onClick: () => setPage((p) => p - 1),
5493
+ children: "Previous"
5494
+ }
5495
+ ),
5496
+ /* @__PURE__ */ jsxRuntime.jsx(
5497
+ designSystem.Button,
5498
+ {
5499
+ variant: "tertiary",
5500
+ size: "S",
5501
+ disabled: page >= pageCount,
5502
+ onClick: () => setPage((p) => p + 1),
5503
+ children: "Next"
5504
+ }
5505
+ )
5506
+ ] }) }),
4965
5507
  showCreate && /* @__PURE__ */ jsxRuntime.jsx(CreateUserDialog, { onClose: () => setShowCreate(false) }),
4966
5508
  detailUserId && /* @__PURE__ */ jsxRuntime.jsx(
4967
5509
  UserDetailDrawer,
@@ -5017,8 +5559,7 @@ const Accent = styled__default.default.div`
5017
5559
  const BrandIcon = styled__default.default.div`
5018
5560
  width: 30px;
5019
5561
  height: 30px;
5020
- border-radius: 7px;
5021
- background: linear-gradient(135deg, #4945ff 0%, #7b79ff 100%);
5562
+ background: black;
5022
5563
  display: flex;
5023
5564
  align-items: center;
5024
5565
  justify-content: center;
@@ -5094,7 +5635,7 @@ function App() {
5094
5635
  paddingBottom: 4,
5095
5636
  children: [
5096
5637
  /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "center", children: [
5097
- /* @__PURE__ */ jsxRuntime.jsx(BrandIcon, { children: "BA" }),
5638
+ /* @__PURE__ */ jsxRuntime.jsx(BrandIcon, { children: /* @__PURE__ */ jsxRuntime.jsx(index.PluginIcon, {}) }),
5098
5639
  /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { children: [
5099
5640
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "beta", textColor: "neutral800", children: "Better Auth" }),
5100
5641
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { paddingTop: "2px", children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: "Authentication Dashboard" }) })
@@ -5114,8 +5655,7 @@ function App() {
5114
5655
  "data-testid": "nav-organizations",
5115
5656
  children: "Organizations"
5116
5657
  }
5117
- ),
5118
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Trigger, { value: "sessions", "data-testid": "nav-sessions", children: "Sessions" })
5658
+ )
5119
5659
  ] })
5120
5660
  ]
5121
5661
  }
@@ -5125,8 +5665,7 @@ function App() {
5125
5665
  ),
5126
5666
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "overview", "data-testid": "tab-overview", children: /* @__PURE__ */ jsxRuntime.jsx(OverviewPage, {}) }),
5127
5667
  /* @__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, {}) })
5668
+ orgEnabled && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "organizations", "data-testid": "tab-organizations", children: /* @__PURE__ */ jsxRuntime.jsx(OrganizationsPage, { teamsEnabled }) })
5130
5669
  ] }) });
5131
5670
  }
5132
5671
  function Root() {