@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.
@@ -1,13 +1,13 @@
1
1
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
- import { useEffect, useState, useMemo } from "react";
2
+ import { useEffect, useState, useRef, useMemo } from "react";
3
3
  import { useQuery, useQueryClient, useMutation, QueryClient, QueryClientProvider } from "react-query";
4
- import { Modal, Typography, Button, Portal, Flex, IconButton, Box, Field, TextInput, Combobox, ComboboxOption, Checkbox, SingleSelect, SingleSelectOption, NumberInput, JSONInput, Textarea, Alert, Tabs, Grid, SearchForm, Searchbar, Loader, Badge, Pagination } from "@strapi/design-system";
4
+ import { Modal, Typography, Button, Portal, Flex, IconButton, Box, Field, TextInput, Combobox, ComboboxOption, Checkbox, SingleSelect, SingleSelectOption, NumberInput, JSONInput, Textarea, Alert, Tabs, Grid, SearchForm, Searchbar, Loader, Badge } from "@strapi/design-system";
5
5
  import styled, { keyframes } from "styled-components";
6
6
  import { dashClient } from "@better-auth/infra/client";
7
7
  import { createAuthClient, InferPlugin } from "better-auth/client";
8
- import { Cross, Images, Plus, Trash, Pencil } from "@strapi/icons";
8
+ import { Cross, Images, Trash, Plus, Pencil } from "@strapi/icons";
9
9
  import { useNotification, useFetchClient } from "@strapi/strapi/admin";
10
- import { g as getMediaLibraryComponent } from "./index-Cvcysa5M.mjs";
10
+ import { g as getMediaLibraryComponent, P as PluginIcon } from "./index-QVlTR8eL.mjs";
11
11
  const dashPathMethods = () => ({
12
12
  id: "dash-path-methods",
13
13
  pathMethods: {
@@ -454,7 +454,7 @@ function UserCombobox({
454
454
  ]) : void 0;
455
455
  const result = await client.dash.listUsers({
456
456
  query: {
457
- limit: 20,
457
+ limit: search ? 100 : 20,
458
458
  offset: 0,
459
459
  sortBy: "createdAt",
460
460
  sortOrder: "desc",
@@ -629,6 +629,159 @@ function CreateOrganizationDialog({ teamsEnabled, onClose }) {
629
629
  function slugify(str) {
630
630
  return str.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
631
631
  }
632
+ const EntryRow = styled.div`
633
+ display: flex;
634
+ align-items: center;
635
+ justify-content: space-between;
636
+ gap: 8px;
637
+ padding: 8px 12px;
638
+ width: 100%;
639
+ box-sizing: border-box;
640
+ background: #ffffff;
641
+ border: 1px solid ${({ theme }) => theme.colors?.neutral200 ?? "#dcdce4"};
642
+ border-radius: 4px;
643
+ min-width: 0;
644
+ `;
645
+ function getDisplayLabel(doc) {
646
+ for (const key of ["name", "title", "email", "username", "label", "slug"]) {
647
+ const v = doc[key];
648
+ if (typeof v === "string" && v.trim()) return v;
649
+ }
650
+ return String(doc.documentId ?? doc.id ?? "");
651
+ }
652
+ function normalizeValue(val, cache) {
653
+ if (!val) return [];
654
+ if (typeof val === "object" && !Array.isArray(val) && val !== null && "set" in val) {
655
+ const items2 = val.set ?? [];
656
+ return items2.filter((item) => item?.documentId).map((item) => ({
657
+ documentId: item.documentId,
658
+ label: cache.get(item.documentId) ?? item.documentId
659
+ }));
660
+ }
661
+ const items = Array.isArray(val) ? val : [val];
662
+ return items.filter(
663
+ (item) => typeof item === "object" && item !== null && "documentId" in item
664
+ ).map((item) => ({
665
+ documentId: String(item.documentId),
666
+ label: getDisplayLabel(item)
667
+ }));
668
+ }
669
+ function RelationField({
670
+ name,
671
+ label,
672
+ attribute,
673
+ value,
674
+ onChange,
675
+ readOnly = false
676
+ }) {
677
+ const { get } = useFetchClient();
678
+ const cache = useRef(/* @__PURE__ */ new Map());
679
+ const [search, setSearch] = useState("");
680
+ const [addKey, setAddKey] = useState(0);
681
+ const target = attribute.target ?? "";
682
+ const relationType = attribute.relation ?? "manyToOne";
683
+ const isMulti = relationType === "oneToMany" || relationType === "manyToMany";
684
+ const currentDocs = normalizeValue(value, cache.current);
685
+ const selectedIds = new Set(currentDocs.map((d) => d.documentId));
686
+ const { data: searchResults = [], isLoading } = useQuery({
687
+ queryKey: ["relation-search", target, search],
688
+ queryFn: async () => {
689
+ const params = new URLSearchParams({
690
+ uid: target,
691
+ "pagination[pageSize]": "50"
692
+ });
693
+ if (search) params.set("filters[name][$containsi]", search);
694
+ const { data } = await get(
695
+ `/better-auth-dashboard/db?${params}`
696
+ );
697
+ const docs = data.results ?? [];
698
+ for (const doc of docs) {
699
+ if (doc.documentId) {
700
+ cache.current.set(String(doc.documentId), getDisplayLabel(doc));
701
+ }
702
+ }
703
+ return docs.map((doc) => ({
704
+ documentId: String(doc.documentId ?? ""),
705
+ label: getDisplayLabel(doc)
706
+ })).filter((d) => d.documentId);
707
+ },
708
+ keepPreviousData: true,
709
+ enabled: !readOnly
710
+ });
711
+ const availableOptions = searchResults.filter(
712
+ (r) => !selectedIds.has(r.documentId)
713
+ );
714
+ const commit = (ids) => {
715
+ onChange(name, { set: ids.map((id) => ({ documentId: id })) });
716
+ };
717
+ const handleSelect = (val) => {
718
+ if (!val) return;
719
+ const opt = searchResults.find((r) => r.documentId === val);
720
+ if (opt) cache.current.set(val, opt.label);
721
+ if (isMulti) {
722
+ if (selectedIds.has(val)) return;
723
+ commit([...currentDocs.map((d) => d.documentId), val]);
724
+ } else {
725
+ commit([val]);
726
+ }
727
+ setSearch("");
728
+ setAddKey((k) => k + 1);
729
+ };
730
+ const handleRemove = (docId) => {
731
+ commit(
732
+ currentDocs.filter((d) => d.documentId !== docId).map((d) => d.documentId)
733
+ );
734
+ };
735
+ return /* @__PURE__ */ jsxs(Field.Root, { style: { width: "100%" }, children: [
736
+ /* @__PURE__ */ jsx(Field.Label, { children: label }),
737
+ !readOnly && /* @__PURE__ */ jsx(
738
+ Combobox,
739
+ {
740
+ value: "",
741
+ onChange: (val) => handleSelect(val),
742
+ onInputChange: (e) => setSearch(e.target.value),
743
+ loading: isLoading,
744
+ placeholder: "Search…",
745
+ children: availableOptions.map((opt) => /* @__PURE__ */ jsx(ComboboxOption, { value: opt.documentId, children: opt.label }, opt.documentId))
746
+ },
747
+ addKey
748
+ ),
749
+ currentDocs.length > 0 && /* @__PURE__ */ jsx(
750
+ Flex,
751
+ {
752
+ direction: "column",
753
+ gap: 1,
754
+ style: { marginTop: 8, width: "100%" },
755
+ children: currentDocs.map((doc) => /* @__PURE__ */ jsxs(EntryRow, { children: [
756
+ /* @__PURE__ */ jsx(
757
+ Typography,
758
+ {
759
+ variant: "omega",
760
+ textColor: "neutral800",
761
+ style: {
762
+ overflow: "hidden",
763
+ textOverflow: "ellipsis",
764
+ whiteSpace: "nowrap",
765
+ flex: 1,
766
+ minWidth: 0
767
+ },
768
+ children: doc.label
769
+ }
770
+ ),
771
+ !readOnly && /* @__PURE__ */ jsx(
772
+ IconButton,
773
+ {
774
+ label: "Remove",
775
+ size: "S",
776
+ onClick: () => handleRemove(doc.documentId),
777
+ children: /* @__PURE__ */ jsx(Trash, {})
778
+ }
779
+ )
780
+ ] }, doc.documentId))
781
+ }
782
+ )
783
+ ] });
784
+ }
632
785
  function makeLabel(name) {
633
786
  return name.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()).trim();
634
787
  }
@@ -752,24 +905,15 @@ function DynamicField({
752
905
  ] });
753
906
  }
754
907
  if (type === "relation") {
755
- return /* @__PURE__ */ jsxs(
756
- Field.Root,
908
+ return /* @__PURE__ */ jsx(
909
+ RelationField,
757
910
  {
758
- style: fieldStyle,
759
- hint: `Relation → ${attribute.target ?? "unknown"}`,
760
- children: [
761
- /* @__PURE__ */ jsx(Field.Label, { children: label }),
762
- /* @__PURE__ */ jsx(
763
- TextInput,
764
- {
765
- value: value != null ? String(value) : "",
766
- onChange: (e) => onChange(name, e.target.value),
767
- disabled: readOnly,
768
- placeholder: "ID…"
769
- }
770
- ),
771
- /* @__PURE__ */ jsx(Field.Hint, {})
772
- ]
911
+ name,
912
+ label,
913
+ attribute,
914
+ value,
915
+ onChange,
916
+ readOnly
773
917
  }
774
918
  );
775
919
  }
@@ -840,6 +984,8 @@ function useModelSchema(model) {
840
984
  if (SYSTEM_FIELDS.has(name)) continue;
841
985
  if (attr.type === "relation" && typeof attr.target === "string" && (attr.target.startsWith("plugin::users-permissions") || attr.target.startsWith("admin::")))
842
986
  continue;
987
+ const baOptions = attr.pluginOptions?.["better-auth"];
988
+ if (baOptions?.managed === true) continue;
843
989
  attributes[name] = attr;
844
990
  }
845
991
  return attributes;
@@ -892,6 +1038,7 @@ function OrganizationDetail({
892
1038
  const schemaQuery = useModelSchema("organization");
893
1039
  const qc = useQueryClient();
894
1040
  const { toggleNotification } = useNotification();
1041
+ const { get, put } = useFetchClient();
895
1042
  const orgQuery = useQuery({
896
1043
  queryKey: ["dash-org", organizationId],
897
1044
  queryFn: async () => {
@@ -930,6 +1077,24 @@ function OrganizationDetail({
930
1077
  return result.data ?? [];
931
1078
  }
932
1079
  });
1080
+ const invitationsQuery = useQuery({
1081
+ queryKey: ["dash-org-invitations", organizationId],
1082
+ queryFn: async () => {
1083
+ const result = await client.dash.organization[organizationId].invitations({}, withContext({ organizationId }));
1084
+ if (result.error) throw new Error(result.error.message ?? "Failed");
1085
+ return result.data ?? [];
1086
+ }
1087
+ });
1088
+ const strapiOrgQuery = useQuery({
1089
+ queryKey: ["dash-strapi-org", organizationId],
1090
+ enabled: !!orgQuery.data,
1091
+ queryFn: async () => {
1092
+ const { data } = await get(
1093
+ `/better-auth-dashboard/db?uid=plugin::better-auth.organization&filters[id][$eq]=${organizationId}&pagination[pageSize]=1`
1094
+ );
1095
+ return data.results?.[0] ?? null;
1096
+ }
1097
+ });
933
1098
  const [activeTab, setActiveTab] = useState("details");
934
1099
  const [editName, setEditName] = useState(void 0);
935
1100
  const [editSlug, setEditSlug] = useState(void 0);
@@ -942,6 +1107,9 @@ function OrganizationDetail({
942
1107
  const [confirmDeleteSsoId, setConfirmDeleteSsoId] = useState(
943
1108
  null
944
1109
  );
1110
+ const [inviteEmail, setInviteEmail] = useState("");
1111
+ const [inviteRole, setInviteRole] = useState("member");
1112
+ const [confirmCancelInvitationId, setConfirmCancelInvitationId] = useState(null);
945
1113
  const handleExtraChange = (name, value) => {
946
1114
  setEditExtra((prev) => ({ ...prev, [name]: value }));
947
1115
  };
@@ -956,13 +1124,13 @@ function OrganizationDetail({
956
1124
  if (editName !== void 0) body.name = editName;
957
1125
  if (editSlug !== void 0) body.slug = editSlug;
958
1126
  if (editLogo !== void 0) body.logo = editLogo;
959
- const result = await client.dash.organization.update(
960
- body,
961
- withContext({ organizationId })
1127
+ const documentId = strapiOrgQuery.data?.documentId;
1128
+ if (!documentId)
1129
+ throw new Error("Could not resolve documentId for organization");
1130
+ await put(
1131
+ `/better-auth-dashboard/db/${documentId}?uid=plugin::better-auth.organization`,
1132
+ body
962
1133
  );
963
- if (result.error)
964
- throw new Error(result.error.message ?? "Update failed");
965
- return result.data;
966
1134
  },
967
1135
  onSuccess: () => {
968
1136
  qc.invalidateQueries({ queryKey: ["dash-org", organizationId] });
@@ -1112,9 +1280,74 @@ function OrganizationDetail({
1112
1280
  });
1113
1281
  }
1114
1282
  });
1283
+ const inviteMemberMutation = useMutation({
1284
+ mutationFn: async () => {
1285
+ const result = await client.dash.organization.inviteMember(
1286
+ { email: inviteEmail, role: inviteRole, invitedBy: "" },
1287
+ withContext({ organizationId })
1288
+ );
1289
+ if (result.error) throw new Error(result.error.message ?? "Failed");
1290
+ return result.data;
1291
+ },
1292
+ onSuccess: () => {
1293
+ qc.invalidateQueries({
1294
+ queryKey: ["dash-org-invitations", organizationId]
1295
+ });
1296
+ setInviteEmail("");
1297
+ setInviteRole("member");
1298
+ toggleNotification({ type: "success", message: "Invitation sent" });
1299
+ },
1300
+ onError: (err) => {
1301
+ toggleNotification({
1302
+ type: "danger",
1303
+ message: err.message ?? "Failed to invite"
1304
+ });
1305
+ }
1306
+ });
1307
+ const cancelInvitationMutation = useMutation({
1308
+ mutationFn: async (invitationId) => {
1309
+ const result = await client.dash.organization.cancelInvitation(
1310
+ { invitationId },
1311
+ withContext({ organizationId })
1312
+ );
1313
+ if (result.error) throw new Error(result.error.message ?? "Failed");
1314
+ },
1315
+ onSuccess: () => {
1316
+ setConfirmCancelInvitationId(null);
1317
+ qc.invalidateQueries({
1318
+ queryKey: ["dash-org-invitations", organizationId]
1319
+ });
1320
+ toggleNotification({ type: "success", message: "Invitation cancelled" });
1321
+ },
1322
+ onError: (err) => {
1323
+ toggleNotification({
1324
+ type: "danger",
1325
+ message: err.message ?? "Failed to cancel"
1326
+ });
1327
+ }
1328
+ });
1329
+ const resendInvitationMutation = useMutation({
1330
+ mutationFn: async (invitationId) => {
1331
+ const result = await client.dash.organization.resendInvitation(
1332
+ { invitationId },
1333
+ withContext({ organizationId })
1334
+ );
1335
+ if (result.error) throw new Error(result.error.message ?? "Failed");
1336
+ },
1337
+ onSuccess: () => {
1338
+ toggleNotification({ type: "success", message: "Invitation resent" });
1339
+ },
1340
+ onError: (err) => {
1341
+ toggleNotification({
1342
+ type: "danger",
1343
+ message: err.message ?? "Failed to resend"
1344
+ });
1345
+ }
1346
+ });
1115
1347
  const members = membersQuery.data ?? [];
1116
1348
  const teams = teamsQuery.data ?? [];
1117
1349
  const ssoProviders = ssoQuery.data ?? [];
1350
+ const invitations = invitationsQuery.data ?? [];
1118
1351
  const hasOrgEdits = editName !== void 0 || editSlug !== void 0 || editLogo !== void 0 || Object.keys(editExtra).length > 0;
1119
1352
  const customFields = Object.entries(schemaQuery.data ?? {}).filter(([name]) => !STANDARD_ORG_FIELDS.has(name)).map(([name, attribute]) => ({ name, attribute }));
1120
1353
  const detailsFooter = activeTab === "details" ? /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -1171,6 +1404,11 @@ function OrganizationDetail({
1171
1404
  teams.length,
1172
1405
  ")"
1173
1406
  ] }),
1407
+ /* @__PURE__ */ jsxs(Tabs.Trigger, { value: "invitations", children: [
1408
+ "Invitations (",
1409
+ invitations.length,
1410
+ ")"
1411
+ ] }),
1174
1412
  /* @__PURE__ */ jsxs(Tabs.Trigger, { value: "sso", children: [
1175
1413
  "SSO (",
1176
1414
  ssoProviders.length,
@@ -1426,7 +1664,127 @@ function OrganizationDetail({
1426
1664
  },
1427
1665
  provider.id
1428
1666
  )) })
1429
- ] }) }) })
1667
+ ] }) }) }),
1668
+ /* @__PURE__ */ jsx(Tabs.Content, { value: "invitations", children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 5, paddingTop: 6, children: [
1669
+ /* @__PURE__ */ jsxs(FormSection, { children: [
1670
+ /* @__PURE__ */ jsx(SectionLabel, { children: "Invite member by email" }),
1671
+ /* @__PURE__ */ jsxs(Grid.Root, { gap: 3, children: [
1672
+ /* @__PURE__ */ jsx(Grid.Item, { col: 8, children: /* @__PURE__ */ jsxs(Field.Root, { children: [
1673
+ /* @__PURE__ */ jsx(Field.Label, { children: "Email address" }),
1674
+ /* @__PURE__ */ jsx(
1675
+ TextInput,
1676
+ {
1677
+ type: "email",
1678
+ value: inviteEmail,
1679
+ onChange: (e) => setInviteEmail(e.target.value),
1680
+ placeholder: "user@example.com"
1681
+ }
1682
+ )
1683
+ ] }) }),
1684
+ /* @__PURE__ */ jsx(Grid.Item, { col: 4, children: /* @__PURE__ */ jsxs(Field.Root, { children: [
1685
+ /* @__PURE__ */ jsx(Field.Label, { children: "Role" }),
1686
+ /* @__PURE__ */ jsxs(
1687
+ SingleSelect,
1688
+ {
1689
+ value: inviteRole,
1690
+ onChange: (v) => setInviteRole(String(v)),
1691
+ "aria-label": "Invite role",
1692
+ children: [
1693
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "member", children: "Member" }),
1694
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "admin", children: "Admin" }),
1695
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "owner", children: "Owner" })
1696
+ ]
1697
+ }
1698
+ )
1699
+ ] }) })
1700
+ ] }),
1701
+ /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(
1702
+ Button,
1703
+ {
1704
+ size: "S",
1705
+ startIcon: /* @__PURE__ */ jsx(Plus, {}),
1706
+ disabled: !inviteEmail,
1707
+ loading: inviteMemberMutation.isLoading,
1708
+ onClick: () => inviteMemberMutation.mutate(),
1709
+ children: "Send invitation"
1710
+ }
1711
+ ) })
1712
+ ] }),
1713
+ /* @__PURE__ */ jsxs(FormSection, { children: [
1714
+ /* @__PURE__ */ jsxs(SectionLabel, { children: [
1715
+ "Invitations (",
1716
+ invitations.length,
1717
+ ")"
1718
+ ] }),
1719
+ invitationsQuery.isLoading ? /* @__PURE__ */ jsx(Typography, { textColor: "neutral500", children: "Loading…" }) : invitations.length === 0 ? /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: "No invitations yet." }) : /* @__PURE__ */ jsx(Flex, { direction: "column", gap: 2, children: invitations.map((inv) => {
1720
+ const statusColor = {
1721
+ pending: "#f59e0b",
1722
+ accepted: "#5cb176",
1723
+ rejected: "#d02b20",
1724
+ canceled: "#8e8ea9"
1725
+ };
1726
+ const color = statusColor[inv.status] ?? "#8e8ea9";
1727
+ const isPending = inv.status === "pending";
1728
+ return /* @__PURE__ */ jsxs(AccountRow, { children: [
1729
+ /* @__PURE__ */ jsxs(
1730
+ Flex,
1731
+ {
1732
+ direction: "column",
1733
+ gap: 1,
1734
+ alignItems: "flex-start",
1735
+ style: { flex: 1 },
1736
+ children: [
1737
+ /* @__PURE__ */ jsx(Typography, { variant: "omega", fontWeight: "semiBold", children: inv.user?.name ?? inv.email }),
1738
+ /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "neutral500", children: [
1739
+ inv.email,
1740
+ " · role: ",
1741
+ inv.role
1742
+ ] }),
1743
+ /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "neutral500", children: [
1744
+ "Expires",
1745
+ " ",
1746
+ new Date(inv.expiresAt).toLocaleDateString()
1747
+ ] })
1748
+ ]
1749
+ }
1750
+ ),
1751
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
1752
+ /* @__PURE__ */ jsx(
1753
+ MonoChip,
1754
+ {
1755
+ style: {
1756
+ color,
1757
+ borderColor: color,
1758
+ background: `${color}18`
1759
+ },
1760
+ children: inv.status
1761
+ }
1762
+ ),
1763
+ isPending && /* @__PURE__ */ jsxs(Fragment, { children: [
1764
+ /* @__PURE__ */ jsx(
1765
+ Button,
1766
+ {
1767
+ size: "S",
1768
+ variant: "secondary",
1769
+ loading: resendInvitationMutation.isLoading,
1770
+ onClick: () => resendInvitationMutation.mutate(inv.id),
1771
+ children: "Resend"
1772
+ }
1773
+ ),
1774
+ /* @__PURE__ */ jsx(
1775
+ IconButton,
1776
+ {
1777
+ label: "Cancel invitation",
1778
+ onClick: () => setConfirmCancelInvitationId(inv.id),
1779
+ children: /* @__PURE__ */ jsx(Trash, {})
1780
+ }
1781
+ )
1782
+ ] })
1783
+ ] })
1784
+ ] }, inv.id);
1785
+ }) })
1786
+ ] })
1787
+ ] }) })
1430
1788
  ] }),
1431
1789
  confirmRemoveMemberId && /* @__PURE__ */ jsx(
1432
1790
  ConfirmDialog,
@@ -1460,6 +1818,17 @@ function OrganizationDetail({
1460
1818
  onConfirm: () => deleteSsoMutation.mutate(confirmDeleteSsoId),
1461
1819
  onCancel: () => setConfirmDeleteSsoId(null)
1462
1820
  }
1821
+ ),
1822
+ confirmCancelInvitationId && /* @__PURE__ */ jsx(
1823
+ ConfirmDialog,
1824
+ {
1825
+ title: "Cancel invitation",
1826
+ message: "Are you sure you want to cancel this invitation? The invite link will no longer work.",
1827
+ confirmLabel: "Cancel invitation",
1828
+ loading: cancelInvitationMutation.isLoading,
1829
+ onConfirm: () => cancelInvitationMutation.mutate(confirmCancelInvitationId),
1830
+ onCancel: () => setConfirmCancelInvitationId(null)
1831
+ }
1463
1832
  )
1464
1833
  ]
1465
1834
  }
@@ -1605,12 +1974,12 @@ function TeamRow({
1605
1974
  )
1606
1975
  ] });
1607
1976
  }
1608
- const PAGE_SIZE$2 = 25;
1609
- const fadeUp$3 = keyframes`
1977
+ const PAGE_SIZE$1 = 25;
1978
+ const fadeUp$2 = keyframes`
1610
1979
  from { opacity: 0; transform: translateY(6px); }
1611
1980
  to { opacity: 1; transform: translateY(0); }
1612
1981
  `;
1613
- const Wrap$3 = styled.div`
1982
+ const Wrap$2 = styled.div`
1614
1983
  padding: 28px 32px;
1615
1984
  background: #f6f6f9;
1616
1985
  min-height: 100%;
@@ -1618,39 +1987,39 @@ const Wrap$3 = styled.div`
1618
1987
  flex-direction: column;
1619
1988
  gap: 20px;
1620
1989
  `;
1621
- const PageHeader$2 = styled.div`
1990
+ const PageHeader$1 = styled.div`
1622
1991
  display: flex;
1623
1992
  justify-content: space-between;
1624
1993
  align-items: flex-start;
1625
1994
  `;
1626
- const TitleBlock$2 = styled.div`
1995
+ const TitleBlock$1 = styled.div`
1627
1996
  display: flex;
1628
1997
  flex-direction: column;
1629
1998
  gap: 4px;
1630
1999
  `;
1631
- const PageTitle$2 = styled.h1`
2000
+ const PageTitle$1 = styled.h1`
1632
2001
  margin: 0;
1633
2002
  font-size: 22px;
1634
2003
  font-weight: 800;
1635
2004
  color: #32324d;
1636
2005
  letter-spacing: -0.03em;
1637
2006
  `;
1638
- const PageSubtitle$2 = styled.p`
2007
+ const PageSubtitle$1 = styled.p`
1639
2008
  margin: 0;
1640
2009
  font-size: 12px;
1641
2010
  color: #8e8ea9;
1642
2011
  `;
1643
- const TableCard$2 = styled.div`
2012
+ const TableCard$1 = styled.div`
1644
2013
  background: #ffffff;
1645
2014
  border: 1px solid #eaeaef;
1646
2015
  border-radius: 10px;
1647
2016
  overflow: hidden;
1648
2017
  `;
1649
- const Table$2 = styled.table`
2018
+ const Table$1 = styled.table`
1650
2019
  width: 100%;
1651
2020
  border-collapse: collapse;
1652
2021
  `;
1653
- const TH$2 = styled.th`
2022
+ const TH$1 = styled.th`
1654
2023
  text-align: left;
1655
2024
  padding: 10px 14px;
1656
2025
  font-size: 10px;
@@ -1664,13 +2033,13 @@ const TH$2 = styled.th`
1664
2033
  &:first-child { padding-left: 20px; }
1665
2034
  &:last-child { padding-right: 20px; }
1666
2035
  `;
1667
- const THCheck$1 = styled(TH$2)`
2036
+ const THCheck$1 = styled(TH$1)`
1668
2037
  width: 44px;
1669
2038
  `;
1670
- const THActions$1 = styled(TH$2)`
2039
+ const THActions$1 = styled(TH$1)`
1671
2040
  width: 80px;
1672
2041
  `;
1673
- const TD$2 = styled.td`
2042
+ const TD$1 = styled.td`
1674
2043
  padding: 11px 14px;
1675
2044
  font-size: 12px;
1676
2045
  color: #32324d;
@@ -1679,14 +2048,14 @@ const TD$2 = styled.td`
1679
2048
  &:first-child { padding-left: 20px; }
1680
2049
  &:last-child { padding-right: 20px; }
1681
2050
  `;
1682
- const TDCheck$1 = styled(TD$2)`
2051
+ const TDCheck$1 = styled(TD$1)`
1683
2052
  width: 44px;
1684
2053
  `;
1685
- const TDActions$1 = styled(TD$2)`
2054
+ const TDActions$1 = styled(TD$1)`
1686
2055
  width: 80px;
1687
2056
  `;
1688
- const TR$2 = styled.tr`
1689
- animation: ${fadeUp$3} 280ms ease both;
2057
+ const TR$1 = styled.tr`
2058
+ animation: ${fadeUp$2} 280ms ease both;
1690
2059
  animation-delay: ${(p) => (p.$i ?? 0) * 25}ms;
1691
2060
  background: ${(p) => p.$selected ? "#f0f0ff" : "transparent"};
1692
2061
  transition: background 120ms ease;
@@ -1774,6 +2143,7 @@ function OrgAvatar({ name, logo }) {
1774
2143
  }
1775
2144
  function OrganizationsPage({ teamsEnabled }) {
1776
2145
  const qc = useQueryClient();
2146
+ const { toggleNotification } = useNotification();
1777
2147
  const [page, setPage] = useState(1);
1778
2148
  const [search, setSearch] = useState("");
1779
2149
  const [searchInput, setSearchInput] = useState("");
@@ -1782,13 +2152,13 @@ function OrganizationsPage({ teamsEnabled }) {
1782
2152
  const [detailOrgId, setDetailOrgId] = useState(null);
1783
2153
  const [confirmDelete, setConfirmDelete] = useState(null);
1784
2154
  const [confirmDeleteMany, setConfirmDeleteMany] = useState(false);
1785
- const offset = (page - 1) * PAGE_SIZE$2;
2155
+ const offset = (page - 1) * PAGE_SIZE$1;
1786
2156
  const orgsQuery = useQuery({
1787
2157
  queryKey: ["dash-organizations", page, search],
1788
2158
  queryFn: async () => {
1789
2159
  const result = await client.dash.listOrganizations({
1790
2160
  query: {
1791
- limit: PAGE_SIZE$2,
2161
+ limit: PAGE_SIZE$1,
1792
2162
  offset,
1793
2163
  sortBy: "createdAt",
1794
2164
  sortOrder: "desc",
@@ -1813,6 +2183,13 @@ function OrganizationsPage({ teamsEnabled }) {
1813
2183
  onSuccess: () => {
1814
2184
  setConfirmDelete(null);
1815
2185
  qc.invalidateQueries({ queryKey: ["dash-organizations"] });
2186
+ toggleNotification({ type: "success", message: "Organization deleted" });
2187
+ },
2188
+ onError: (err) => {
2189
+ toggleNotification({
2190
+ type: "danger",
2191
+ message: err.message ?? "Failed to delete organization"
2192
+ });
1816
2193
  }
1817
2194
  });
1818
2195
  const deleteManyMutation = useMutation({
@@ -1825,15 +2202,25 @@ function OrganizationsPage({ teamsEnabled }) {
1825
2202
  throw new Error(result.error.message ?? "Delete failed");
1826
2203
  return result.data;
1827
2204
  },
1828
- onSuccess: () => {
2205
+ onSuccess: (_data, organizationIds) => {
1829
2206
  setConfirmDeleteMany(false);
1830
2207
  setSelected(/* @__PURE__ */ new Set());
1831
2208
  qc.invalidateQueries({ queryKey: ["dash-organizations"] });
2209
+ toggleNotification({
2210
+ type: "success",
2211
+ message: `${organizationIds.length} organization${organizationIds.length !== 1 ? "s" : ""} deleted`
2212
+ });
2213
+ },
2214
+ onError: (err) => {
2215
+ toggleNotification({
2216
+ type: "danger",
2217
+ message: err.message ?? "Failed to delete organizations"
2218
+ });
1832
2219
  }
1833
2220
  });
1834
2221
  const orgs = orgsQuery.data && "organizations" in orgsQuery.data ? orgsQuery.data.organizations : [];
1835
2222
  const total = orgsQuery.data && "total" in orgsQuery.data ? orgsQuery.data.total : 0;
1836
- const pageCount = Math.ceil(total / PAGE_SIZE$2);
2223
+ const pageCount = Math.ceil(total / PAGE_SIZE$1);
1837
2224
  const allSelected = orgs.length > 0 && orgs.every((o) => selected.has(o.id));
1838
2225
  const someSelected = selected.size > 0;
1839
2226
  const toggleSelect = (id) => {
@@ -1853,11 +2240,11 @@ function OrganizationsPage({ teamsEnabled }) {
1853
2240
  setSearch(searchInput);
1854
2241
  setPage(1);
1855
2242
  };
1856
- return /* @__PURE__ */ jsxs(Wrap$3, { "data-testid": "organizations-page", children: [
1857
- /* @__PURE__ */ jsxs(PageHeader$2, { children: [
1858
- /* @__PURE__ */ jsxs(TitleBlock$2, { children: [
1859
- /* @__PURE__ */ jsx(PageTitle$2, { children: "Organizations" }),
1860
- /* @__PURE__ */ jsxs(PageSubtitle$2, { children: [
2243
+ return /* @__PURE__ */ jsxs(Wrap$2, { "data-testid": "organizations-page", children: [
2244
+ /* @__PURE__ */ jsxs(PageHeader$1, { children: [
2245
+ /* @__PURE__ */ jsxs(TitleBlock$1, { children: [
2246
+ /* @__PURE__ */ jsx(PageTitle$1, { children: "Organizations" }),
2247
+ /* @__PURE__ */ jsxs(PageSubtitle$1, { children: [
1861
2248
  total.toLocaleString(),
1862
2249
  " total"
1863
2250
  ] })
@@ -1905,7 +2292,7 @@ function OrganizationsPage({ teamsEnabled }) {
1905
2292
  )
1906
2293
  ] }),
1907
2294
  orgsQuery.isError && /* @__PURE__ */ jsx("div", { style: { color: "#d02b20", fontSize: 12, padding: "8px 0" }, children: orgsQuery.error instanceof Error ? orgsQuery.error.message : "An error occurred" }),
1908
- /* @__PURE__ */ jsx(TableCard$2, { children: orgsQuery.isLoading ? /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 8, children: /* @__PURE__ */ jsx(Loader, { children: "Loading organizations…" }) }) : /* @__PURE__ */ jsxs(Table$2, { children: [
2295
+ /* @__PURE__ */ jsx(TableCard$1, { children: orgsQuery.isLoading ? /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 8, children: /* @__PURE__ */ jsx(Loader, { children: "Loading organizations…" }) }) : /* @__PURE__ */ jsxs(Table$1, { children: [
1909
2296
  /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
1910
2297
  /* @__PURE__ */ jsx(THCheck$1, { children: /* @__PURE__ */ jsx(
1911
2298
  Checkbox,
@@ -1915,14 +2302,14 @@ function OrganizationsPage({ teamsEnabled }) {
1915
2302
  "aria-label": "Select all"
1916
2303
  }
1917
2304
  ) }),
1918
- /* @__PURE__ */ jsx(TH$2, { children: "Name" }),
1919
- /* @__PURE__ */ jsx(TH$2, { children: "Slug" }),
1920
- /* @__PURE__ */ jsx(TH$2, { children: "Members" }),
1921
- /* @__PURE__ */ jsx(TH$2, { children: "Created" }),
2305
+ /* @__PURE__ */ jsx(TH$1, { children: "Name" }),
2306
+ /* @__PURE__ */ jsx(TH$1, { children: "Slug" }),
2307
+ /* @__PURE__ */ jsx(TH$1, { children: "Members" }),
2308
+ /* @__PURE__ */ jsx(TH$1, { children: "Created" }),
1922
2309
  /* @__PURE__ */ jsx(THActions$1, {})
1923
2310
  ] }) }),
1924
2311
  /* @__PURE__ */ jsx("tbody", { children: orgs.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx(
1925
- TD$2,
2312
+ TD$1,
1926
2313
  {
1927
2314
  colSpan: 6,
1928
2315
  style: {
@@ -1934,7 +2321,7 @@ function OrganizationsPage({ teamsEnabled }) {
1934
2321
  children: search ? `No organizations matching "${search}"` : "No organizations found"
1935
2322
  }
1936
2323
  ) }) : orgs.map((org, i) => /* @__PURE__ */ jsxs(
1937
- TR$2,
2324
+ TR$1,
1938
2325
  {
1939
2326
  $selected: selected.has(org.id),
1940
2327
  $i: i,
@@ -1948,13 +2335,13 @@ function OrganizationsPage({ teamsEnabled }) {
1948
2335
  "aria-label": `Select ${org.name}`
1949
2336
  }
1950
2337
  ) }),
1951
- /* @__PURE__ */ jsx(TD$2, { children: /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 2, children: [
2338
+ /* @__PURE__ */ jsx(TD$1, { children: /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 2, children: [
1952
2339
  /* @__PURE__ */ jsx(OrgAvatar, { name: org.name, logo: org.logo }),
1953
2340
  /* @__PURE__ */ jsx(OrgName, { children: org.name })
1954
2341
  ] }) }),
1955
- /* @__PURE__ */ jsx(TD$2, { children: /* @__PURE__ */ jsx(SlugChip, { children: org.slug }) }),
1956
- /* @__PURE__ */ jsx(TD$2, { children: /* @__PURE__ */ jsx(CountChip, { children: org.memberCount }) }),
1957
- /* @__PURE__ */ jsx(TD$2, { children: /* @__PURE__ */ jsx(DateText$1, { children: new Date(org.createdAt).toLocaleDateString() }) }),
2342
+ /* @__PURE__ */ jsx(TD$1, { children: /* @__PURE__ */ jsx(SlugChip, { children: org.slug }) }),
2343
+ /* @__PURE__ */ jsx(TD$1, { children: /* @__PURE__ */ jsx(CountChip, { children: org.memberCount }) }),
2344
+ /* @__PURE__ */ jsx(TD$1, { children: /* @__PURE__ */ jsx(DateText$1, { children: new Date(org.createdAt).toLocaleDateString() }) }),
1958
2345
  /* @__PURE__ */ jsx(TDActions$1, { children: /* @__PURE__ */ jsxs(Flex, { gap: 1, justifyContent: "flex-end", children: [
1959
2346
  /* @__PURE__ */ jsx(
1960
2347
  IconButton,
@@ -2101,95 +2488,143 @@ function Avatar({
2101
2488
  const color = getColor(name || "?");
2102
2489
  return /* @__PURE__ */ jsx(Circle, { $bg: color.bg, $fg: color.fg, $size: size, children: getInitials(name || "?") });
2103
2490
  }
2104
- const fadeUp$2 = keyframes`
2105
- from { opacity: 0; transform: translateY(8px); }
2491
+ const T = {
2492
+ bg: "#f6f6f9",
2493
+ bgCard: "#ffffff",
2494
+ border: "#eaeaef",
2495
+ borderHover: "rgba(73,69,255,0.35)",
2496
+ accent: "#4945ff",
2497
+ green: "#5cb176",
2498
+ greenDim: "rgba(92,177,118,0.12)",
2499
+ amber: "#d9822f",
2500
+ red: "#d02b20",
2501
+ redDim: "rgba(208,43,32,0.1)",
2502
+ purple: "#8460b8",
2503
+ textPrimary: "#32324d",
2504
+ textSecondary: "#666687",
2505
+ textMuted: "#b8b8c7",
2506
+ mono: `'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace`
2507
+ };
2508
+ const fadeUp$1 = keyframes`
2509
+ from { opacity: 0; transform: translateY(10px); }
2106
2510
  to { opacity: 1; transform: translateY(0); }
2107
2511
  `;
2108
- const Wrap$2 = styled.div`
2109
- padding: 28px 32px;
2110
- background: #f6f6f9;
2512
+ const Wrap$1 = styled.div`
2513
+ padding: 28px 32px 48px;
2514
+ background: ${T.bg};
2111
2515
  min-height: 100%;
2112
2516
  display: flex;
2113
2517
  flex-direction: column;
2114
- gap: 20px;
2518
+ gap: 22px;
2115
2519
  `;
2116
- const StatRow = styled.div`
2117
- display: grid;
2118
- grid-template-columns: repeat(5, 1fr);
2119
- gap: 12px;
2120
- @media (max-width: 1200px) { grid-template-columns: repeat(3, 1fr); }
2121
- @media (max-width: 768px) { grid-template-columns: repeat(2, 1fr); }
2520
+ const SectionDivider = styled.div`
2521
+ display: flex;
2522
+ align-items: center;
2523
+ gap: 10px;
2524
+ margin-bottom: -6px;
2122
2525
  `;
2123
- const MainRow = styled.div`
2124
- display: grid;
2125
- grid-template-columns: 1fr 300px;
2126
- gap: 12px;
2127
- @media (max-width: 960px) { grid-template-columns: 1fr; }
2526
+ const DivLabel = styled.span`
2527
+ font-size: 9px;
2528
+ font-weight: 700;
2529
+ letter-spacing: 0.14em;
2530
+ text-transform: uppercase;
2531
+ color: ${T.textMuted};
2532
+ white-space: nowrap;
2128
2533
  `;
2129
- const ActiveRow = styled.div`
2130
- display: grid;
2131
- grid-template-columns: repeat(3, 1fr);
2132
- gap: 12px;
2534
+ const DivLine = styled.div`
2535
+ flex: 1;
2536
+ height: 1px;
2537
+ background: ${T.border};
2133
2538
  `;
2134
2539
  const Card = styled.div`
2135
- background: #ffffff;
2136
- border: 1px solid #eaeaef;
2137
- border-radius: 10px;
2540
+ background: ${T.bgCard};
2541
+ border: 1px solid ${T.border};
2542
+ border-radius: 12px;
2138
2543
  overflow: hidden;
2139
2544
  position: relative;
2140
- animation: ${fadeUp$2} 360ms ease both;
2545
+ animation: ${fadeUp$1} 380ms ease both;
2141
2546
  animation-delay: ${(p) => (p.$delay ?? 0) * 55}ms;
2142
- transition: border-color 180ms ease, box-shadow 180ms ease;
2143
-
2144
- ${(p) => p.$accent && `
2145
- &::before {
2146
- content: '';
2147
- position: absolute;
2148
- top: 0; left: 0; right: 0;
2149
- height: 3px;
2150
- background: ${p.$accent};
2151
- z-index: 1;
2152
- }
2153
- `}
2154
-
2547
+ transition: border-color 200ms, box-shadow 200ms;
2155
2548
  &:hover {
2156
- border-color: #c0bfff;
2157
- box-shadow: 0 2px 16px rgba(73,69,255,0.08);
2549
+ border-color: ${T.borderHover};
2550
+ box-shadow: 0 0 0 1px ${T.borderHover}, 0 8px 32px rgba(124,109,250,0.07);
2158
2551
  }
2159
2552
  `;
2160
- const StatTop = styled.div`
2161
- padding: 18px 18px 10px;
2553
+ const PillGroup = styled.div`
2554
+ display: flex;
2555
+ background: rgba(0,0,0,0.03);
2556
+ border: 1px solid ${T.border};
2557
+ border-radius: 9px;
2558
+ padding: 3px;
2559
+ gap: 2px;
2162
2560
  `;
2163
- const StatLabel = styled.div`
2164
- font-size: 10px;
2165
- font-weight: 700;
2166
- letter-spacing: 0.08em;
2561
+ const Pill = styled.button`
2562
+ appearance: none;
2563
+ border: none;
2564
+ cursor: pointer;
2565
+ padding: 5px 16px;
2566
+ border-radius: 6px;
2567
+ font-size: 11px;
2568
+ font-weight: 600;
2569
+ letter-spacing: 0.03em;
2570
+ transition: background 180ms, color 180ms;
2571
+ background: ${(p) => p.$active ? T.accent : "transparent"};
2572
+ color: ${(p) => p.$active ? "#fff" : T.textSecondary};
2573
+ &:hover {
2574
+ background: ${(p) => p.$active ? T.accent : "rgba(0,0,0,0.05)"};
2575
+ color: ${(p) => p.$active ? "#fff" : T.textPrimary};
2576
+ }
2577
+ `;
2578
+ const StatGrid = styled.div`
2579
+ display: grid;
2580
+ grid-template-columns: repeat(5, 1fr);
2581
+ gap: 10px;
2582
+ @media (max-width: 1200px) { grid-template-columns: repeat(3, 1fr); }
2583
+ `;
2584
+ const StatCard = styled(Card)`
2585
+ padding: 18px 20px 0;
2586
+ &::after {
2587
+ content: '';
2588
+ position: absolute;
2589
+ top: 0; left: 0; right: 0;
2590
+ height: 2px;
2591
+ background: ${(p) => p.$accent};
2592
+ opacity: 0.75;
2593
+ }
2594
+ `;
2595
+ const StatLabel = styled.div`
2596
+ font-size: 9px;
2597
+ font-weight: 700;
2598
+ letter-spacing: 0.1em;
2167
2599
  text-transform: uppercase;
2168
- color: #8e8ea9;
2169
- margin-bottom: 8px;
2600
+ color: ${T.textMuted};
2601
+ margin-bottom: 10px;
2170
2602
  `;
2171
2603
  const StatValue = styled.div`
2172
2604
  font-size: 30px;
2173
2605
  font-weight: 800;
2174
- color: #32324d;
2606
+ color: ${T.textPrimary};
2175
2607
  line-height: 1;
2176
- letter-spacing: -0.05em;
2608
+ letter-spacing: -0.045em;
2177
2609
  font-variant-numeric: tabular-nums;
2178
2610
  margin-bottom: 10px;
2179
2611
  `;
2180
2612
  const TrendBadge = styled.span`
2181
2613
  display: inline-flex;
2182
2614
  align-items: center;
2183
- gap: 2px;
2184
- padding: 2px 7px;
2615
+ gap: 3px;
2616
+ padding: 2px 8px;
2185
2617
  border-radius: 20px;
2186
- font-size: 10px;
2618
+ font-size: 9px;
2187
2619
  font-weight: 700;
2188
- background: ${(p) => p.$pos ? "#eafbe7" : "#fcecea"};
2189
- color: ${(p) => p.$pos ? "#5cb176" : "#d02b20"};
2620
+ letter-spacing: 0.03em;
2621
+ background: ${(p) => p.$pos ? T.greenDim : T.redDim};
2622
+ color: ${(p) => p.$pos ? T.green : T.red};
2623
+ margin-bottom: 12px;
2190
2624
  `;
2191
2625
  const SparkWrap = styled.div`
2192
- height: 48px;
2626
+ height: 44px;
2627
+ margin: 0 -20px;
2193
2628
  `;
2194
2629
  const ChartCard = styled(Card)`
2195
2630
  padding: 20px 20px 12px;
@@ -2201,31 +2636,42 @@ const ChartHeader = styled.div`
2201
2636
  display: flex;
2202
2637
  align-items: center;
2203
2638
  justify-content: space-between;
2639
+ gap: 12px;
2204
2640
  flex-shrink: 0;
2205
2641
  `;
2206
2642
  const ChartTitle = styled.div`
2207
- font-size: 13px;
2643
+ font-size: 12px;
2208
2644
  font-weight: 700;
2209
- color: #32324d;
2645
+ color: ${T.textPrimary};
2646
+ letter-spacing: 0.01em;
2210
2647
  `;
2211
- const LegendRow = styled.div`
2648
+ const SeriesRow = styled.div`
2212
2649
  display: flex;
2213
2650
  align-items: center;
2214
- gap: 14px;
2651
+ gap: 6px;
2215
2652
  `;
2216
- const LegendItem = styled.div`
2653
+ const SeriesBtn = styled.button`
2654
+ appearance: none;
2655
+ border: 1px solid ${(p) => p.$on ? `${p.$c}55` : T.border};
2656
+ background: ${(p) => p.$on ? `${p.$c}18` : "transparent"};
2657
+ color: ${(p) => p.$on ? p.$c : T.textMuted};
2658
+ font-size: 10px;
2659
+ font-weight: 600;
2660
+ padding: 3px 10px 3px 8px;
2661
+ border-radius: 6px;
2662
+ cursor: pointer;
2217
2663
  display: flex;
2218
2664
  align-items: center;
2219
2665
  gap: 5px;
2220
- font-size: 10px;
2221
- color: #8e8ea9;
2222
- font-weight: 600;
2223
- text-transform: uppercase;
2224
- letter-spacing: 0.04em;
2666
+ transition: all 160ms;
2667
+ &:hover {
2668
+ border-color: ${(p) => `${p.$c}88`};
2669
+ color: ${(p) => p.$c};
2670
+ background: ${(p) => `${p.$c}22`};
2671
+ }
2225
2672
  `;
2226
- const LegendDot = styled.span`
2227
- width: 8px;
2228
- height: 8px;
2673
+ const SDot = styled.span`
2674
+ width: 6px; height: 6px;
2229
2675
  border-radius: 50%;
2230
2676
  background: ${(p) => p.$c};
2231
2677
  display: inline-block;
@@ -2233,140 +2679,188 @@ const LegendDot = styled.span`
2233
2679
  `;
2234
2680
  const HoverInfo = styled.div`
2235
2681
  font-size: 11px;
2236
- color: #8e8ea9;
2682
+ color: ${T.textSecondary};
2237
2683
  font-variant-numeric: tabular-nums;
2238
- min-height: 16px;
2684
+ font-family: ${T.mono};
2685
+ min-height: 15px;
2686
+ `;
2687
+ const TwoPanel = styled.div`
2688
+ display: grid;
2689
+ grid-template-columns: ${(p) => p.$ratio ?? "1fr 288px"};
2690
+ gap: 10px;
2691
+ align-items: stretch;
2692
+ @media (max-width: 900px) { grid-template-columns: 1fr; }
2239
2693
  `;
2240
2694
  const FeedCard = styled(Card)`
2241
2695
  display: flex;
2242
2696
  flex-direction: column;
2243
- max-height: 380px;
2697
+ overflow: hidden;
2244
2698
  `;
2245
- const FeedHeader = styled.div`
2246
- padding: 16px 18px 12px;
2247
- border-bottom: 1px solid #f0f0f7;
2248
- flex-shrink: 0;
2249
- font-size: 10px;
2699
+ const FeedHead = styled.div`
2700
+ padding: 13px 16px 9px;
2701
+ border-bottom: 1px solid ${T.border};
2702
+ font-size: 11px;
2250
2703
  font-weight: 700;
2251
- letter-spacing: 0.08em;
2252
- text-transform: uppercase;
2253
- color: #8e8ea9;
2704
+ color: ${T.textPrimary};
2705
+ flex-shrink: 0;
2706
+ display: flex;
2707
+ justify-content: space-between;
2708
+ align-items: center;
2254
2709
  `;
2255
- const FeedList = styled.div`
2710
+ const FeedSelect = styled.select`
2711
+ appearance: none;
2712
+ border: 1px solid ${T.border};
2713
+ border-radius: 6px;
2714
+ background: ${T.bgCard};
2715
+ color: ${T.textSecondary};
2716
+ font-size: 10px;
2717
+ font-weight: 600;
2718
+ padding: 3px 22px 3px 8px;
2719
+ cursor: pointer;
2720
+ outline: none;
2721
+ 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");
2722
+ background-repeat: no-repeat;
2723
+ background-position: right 7px center;
2724
+ transition: border-color 160ms;
2725
+ &:hover, &:focus { border-color: ${T.accent}; color: ${T.textPrimary}; }
2726
+ `;
2727
+ const FeedScroll = styled.div`
2256
2728
  flex: 1;
2257
2729
  overflow-y: auto;
2258
- padding: 6px 0;
2259
- &::-webkit-scrollbar { width: 3px; }
2260
- &::-webkit-scrollbar-track { background: transparent; }
2261
- &::-webkit-scrollbar-thumb { background: #d9d8ff; border-radius: 2px; }
2730
+ min-height: 0;
2731
+ &::-webkit-scrollbar { width: 4px; }
2732
+ &::-webkit-scrollbar-thumb { background: ${T.textMuted}; border-radius: 2px; }
2262
2733
  `;
2263
2734
  const FeedItem = styled.div`
2735
+ padding: 9px 14px;
2736
+ border-bottom: 1px solid #f0f0f5;
2264
2737
  display: flex;
2265
- align-items: flex-start;
2266
- gap: 10px;
2267
- padding: 9px 18px;
2268
- &:hover { background: #fafafe; }
2738
+ flex-direction: column;
2739
+ gap: 3px;
2740
+ transition: background 130ms;
2741
+ &:hover { background: #fafaff; }
2742
+ &:last-child { border-bottom: none; }
2269
2743
  `;
2270
- const FeedContent = styled.div`
2271
- flex: 1;
2272
- min-width: 0;
2744
+ const FeedTop = styled.div`
2745
+ display: flex;
2746
+ align-items: center;
2747
+ justify-content: space-between;
2748
+ gap: 6px;
2273
2749
  `;
2274
- const FeedName = styled.div`
2275
- font-size: 12px;
2750
+ const FeedName = styled.span`
2751
+ font-size: 11px;
2276
2752
  font-weight: 600;
2277
- color: #32324d;
2278
- white-space: nowrap;
2753
+ color: ${T.textPrimary};
2279
2754
  overflow: hidden;
2280
2755
  text-overflow: ellipsis;
2756
+ white-space: nowrap;
2281
2757
  `;
2282
- const FeedSub = styled.div`
2758
+ const FeedEmail = styled.span`
2283
2759
  font-size: 10px;
2284
- color: #8e8ea9;
2285
- white-space: nowrap;
2760
+ color: ${T.textSecondary};
2286
2761
  overflow: hidden;
2287
2762
  text-overflow: ellipsis;
2288
- margin-top: 1px;
2763
+ white-space: nowrap;
2764
+ display: block;
2289
2765
  `;
2290
- const FeedTime = styled.div`
2291
- font-size: 10px;
2292
- color: #b8b8c7;
2766
+ const FeedMeta = styled.span`
2767
+ font-size: 9px;
2768
+ color: ${T.textMuted};
2293
2769
  white-space: nowrap;
2294
2770
  font-variant-numeric: tabular-nums;
2295
- margin-top: 1px;
2296
2771
  `;
2297
- const UsersCard = styled(Card)``;
2298
- const TableWrap = styled.div`
2299
- overflow-x: auto;
2772
+ const RtnCard = styled(Card)`
2773
+ padding: 20px;
2774
+ display: flex;
2775
+ flex-direction: column;
2776
+ gap: 12px;
2300
2777
  `;
2301
- const Table$1 = styled.table`
2302
- width: 100%;
2303
- border-collapse: collapse;
2778
+ const RtnRow = styled.div`
2779
+ display: flex;
2780
+ align-items: center;
2781
+ gap: 10px;
2782
+ padding: 4px 6px;
2783
+ border-radius: 7px;
2784
+ transition: background 130ms;
2785
+ background: ${(p) => p.$hov ? "#f0f0f6" : "transparent"};
2786
+ cursor: default;
2304
2787
  `;
2305
- const TH$1 = styled.th`
2306
- text-align: left;
2307
- padding: 10px 14px;
2788
+ const RtnLabel = styled.span`
2308
2789
  font-size: 10px;
2309
- font-weight: 700;
2310
- color: #8e8ea9;
2311
- text-transform: uppercase;
2312
- letter-spacing: 0.06em;
2313
- border-bottom: 1px solid #eaeaef;
2314
- background: #fafafa;
2790
+ color: ${T.textSecondary};
2791
+ min-width: 80px;
2792
+ text-align: right;
2315
2793
  white-space: nowrap;
2316
- &:first-child { padding-left: 20px; }
2317
- &:last-child { padding-right: 20px; }
2318
2794
  `;
2319
- const TR$1 = styled.tr`
2320
- &:hover td { background: #fafafe; }
2321
- &:last-child td { border-bottom: none; }
2795
+ const RtnSize = styled.span`
2796
+ font-size: 9px;
2797
+ color: ${T.textMuted};
2798
+ min-width: 52px;
2799
+ text-align: right;
2800
+ white-space: nowrap;
2801
+ font-variant-numeric: tabular-nums;
2322
2802
  `;
2323
- const TD$1 = styled.td`
2324
- padding: 10px 14px;
2325
- font-size: 12px;
2326
- color: #32324d;
2327
- border-bottom: 1px solid #f5f5f9;
2328
- vertical-align: middle;
2329
- &:first-child { padding-left: 20px; }
2330
- &:last-child { padding-right: 20px; }
2803
+ const RtnTrack = styled.div`
2804
+ flex: 1;
2805
+ height: 10px;
2806
+ background: #eaeaef;
2807
+ border-radius: 4px;
2808
+ overflow: hidden;
2331
2809
  `;
2332
- const StatusChip$1 = styled.span`
2333
- display: inline-flex;
2334
- align-items: center;
2335
- gap: 4px;
2336
- padding: 2px 8px;
2337
- border-radius: 20px;
2810
+ const RtnBar = styled.div`
2811
+ width: ${(p) => Math.max(p.$w, 0.5)}%;
2812
+ height: 100%;
2813
+ background: hsl(${(p) => p.$hue}, 60%, 50%);
2814
+ border-radius: 4px;
2815
+ transition: width 0.55s cubic-bezier(0.25, 0.46, 0.45, 0.94);
2816
+ `;
2817
+ const RtnPct = styled.span`
2338
2818
  font-size: 10px;
2339
2819
  font-weight: 700;
2340
- background: ${(p) => p.$banned ? "#fcecea" : p.$verified ? "#eafbe7" : "#f0f0ff"};
2341
- color: ${(p) => p.$banned ? "#d02b20" : p.$verified ? "#5cb176" : "#8e8ea9"};
2342
-
2343
- &::before {
2344
- content: '';
2345
- display: inline-block;
2346
- width: 5px;
2347
- height: 5px;
2348
- border-radius: 50%;
2349
- background: ${(p) => p.$banned ? "#d02b20" : p.$verified ? "#5cb176" : "#b8b8c7"};
2350
- }
2820
+ color: hsl(${(p) => p.$hue}, 60%, 60%);
2821
+ min-width: 38px;
2822
+ text-align: right;
2823
+ font-variant-numeric: tabular-nums;
2824
+ font-family: ${T.mono};
2825
+ `;
2826
+ const RtnTip = styled.div`
2827
+ font-size: 10px;
2828
+ color: ${T.textSecondary};
2829
+ min-height: 14px;
2830
+ font-variant-numeric: tabular-nums;
2831
+ padding: 0 6px;
2351
2832
  `;
2352
- const Divider = styled.div`
2833
+ const RingGrid = styled.div`
2353
2834
  display: flex;
2354
- align-items: center;
2835
+ flex-direction: column;
2355
2836
  gap: 10px;
2356
- margin-bottom: -8px;
2357
2837
  `;
2358
- const DivLabel = styled.span`
2359
- font-size: 10px;
2838
+ const RingCard = styled(Card)`
2839
+ padding: 18px 20px;
2840
+ display: flex;
2841
+ align-items: center;
2842
+ gap: 16px;
2843
+ flex: 1;
2844
+ `;
2845
+ const RingInfo = styled.div`
2846
+ display: flex;
2847
+ flex-direction: column;
2848
+ gap: 4px;
2849
+ `;
2850
+ const RingVal = styled.div`
2851
+ font-size: 24px;
2852
+ font-weight: 800;
2853
+ color: ${T.textPrimary};
2854
+ letter-spacing: -0.04em;
2855
+ font-variant-numeric: tabular-nums;
2856
+ line-height: 1;
2857
+ `;
2858
+ const RingLabel = styled.div`
2859
+ font-size: 9px;
2360
2860
  font-weight: 700;
2361
2861
  text-transform: uppercase;
2362
- letter-spacing: 0.08em;
2363
- color: #b8b8c7;
2364
- white-space: nowrap;
2365
- `;
2366
- const DivLine = styled.div`
2367
- flex: 1;
2368
- height: 1px;
2369
- background: #eaeaef;
2862
+ letter-spacing: 0.1em;
2863
+ color: ${T.textMuted};
2370
2864
  `;
2371
2865
  const Empty = styled.div`
2372
2866
  display: flex;
@@ -2374,32 +2868,52 @@ const Empty = styled.div`
2374
2868
  align-items: center;
2375
2869
  justify-content: center;
2376
2870
  padding: 32px;
2377
- color: #8e8ea9;
2871
+ color: ${T.textMuted};
2378
2872
  font-size: 12px;
2379
- gap: 6px;
2873
+ gap: 8px;
2380
2874
  `;
2875
+ function useCountUp(target, duration = 850) {
2876
+ const [count, setCount] = useState(0);
2877
+ const prevRef = useRef(0);
2878
+ useEffect(() => {
2879
+ let raf;
2880
+ let start = 0;
2881
+ const from = prevRef.current;
2882
+ const diff = target - from;
2883
+ const step = (ts) => {
2884
+ if (!start) start = ts;
2885
+ const progress = Math.min((ts - start) / duration, 1);
2886
+ const eased = 1 - (1 - progress) ** 3;
2887
+ setCount(Math.round(from + eased * diff));
2888
+ if (progress < 1) raf = requestAnimationFrame(step);
2889
+ else prevRef.current = target;
2890
+ };
2891
+ raf = requestAnimationFrame(step);
2892
+ return () => cancelAnimationFrame(raf);
2893
+ }, [target, duration]);
2894
+ return count;
2895
+ }
2381
2896
  function Sparkline({
2382
2897
  data,
2383
2898
  color,
2384
2899
  id
2385
2900
  }) {
2386
2901
  const W = 300;
2387
- const H = 48;
2902
+ const H = 44;
2388
2903
  if (data.length < 2) return /* @__PURE__ */ jsx("svg", { width: "100%", height: H });
2389
2904
  const min = Math.min(...data);
2390
2905
  const max = Math.max(...data, min + 1);
2391
- const range = max - min;
2392
2906
  const pts = data.map((v, i) => ({
2393
2907
  x: i / (data.length - 1) * W,
2394
- y: H - 4 - (v - min) / range * (H - 10)
2908
+ y: H - 3 - (v - min) / (max - min) * (H - 8)
2395
2909
  }));
2396
- const line = pts.reduce((acc, p, i) => {
2910
+ const line = pts.reduce((a, p, i) => {
2397
2911
  if (i === 0) return `M ${p.x} ${p.y}`;
2398
2912
  const pr = pts[i - 1];
2399
2913
  const cx = (pr.x + p.x) / 2;
2400
- return `${acc} C ${cx} ${pr.y} ${cx} ${p.y} ${p.x} ${p.y}`;
2914
+ return `${a} C ${cx} ${pr.y} ${cx} ${p.y} ${p.x} ${p.y}`;
2401
2915
  }, "");
2402
- const area = `${line} L ${pts[pts.length - 1].x} ${H} L 0 ${H} Z`;
2916
+ const area = `${line} L ${pts.at(-1).x} ${H} L 0 ${H} Z`;
2403
2917
  const gid = `spk-${id}`;
2404
2918
  return /* @__PURE__ */ jsxs(
2405
2919
  "svg",
@@ -2408,10 +2922,10 @@ function Sparkline({
2408
2922
  height: H,
2409
2923
  viewBox: `0 0 ${W} ${H}`,
2410
2924
  preserveAspectRatio: "none",
2411
- "aria-hidden": "true",
2925
+ "aria-hidden": true,
2412
2926
  children: [
2413
2927
  /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsxs("linearGradient", { id: gid, x1: "0", y1: "0", x2: "0", y2: "1", children: [
2414
- /* @__PURE__ */ jsx("stop", { offset: "0%", stopColor: color, stopOpacity: "0.25" }),
2928
+ /* @__PURE__ */ jsx("stop", { offset: "0%", stopColor: color, stopOpacity: "0.32" }),
2415
2929
  /* @__PURE__ */ jsx("stop", { offset: "100%", stopColor: color, stopOpacity: "0" })
2416
2930
  ] }) }),
2417
2931
  /* @__PURE__ */ jsx("path", { d: area, fill: `url(#${gid})` }),
@@ -2420,57 +2934,58 @@ function Sparkline({
2420
2934
  {
2421
2935
  d: line,
2422
2936
  stroke: color,
2423
- strokeWidth: "2",
2937
+ strokeWidth: "1.6",
2424
2938
  fill: "none",
2425
- strokeLinecap: "round",
2426
- strokeLinejoin: "round"
2939
+ strokeLinecap: "round"
2427
2940
  }
2428
2941
  )
2429
2942
  ]
2430
2943
  }
2431
2944
  );
2432
2945
  }
2433
- const SERIES = [
2434
- { key: "totalUsers", color: "#4945FF", label: "Total" },
2435
- { key: "newUsers", color: "#5CB176", label: "New" },
2436
- { key: "activeUsers", color: "#E57553", label: "Active" }
2946
+ const ALL_SERIES = [
2947
+ { key: "totalUsers", color: T.accent, label: "Total" },
2948
+ { key: "newUsers", color: T.green, label: "New" },
2949
+ { key: "activeUsers", color: T.amber, label: "Active" }
2437
2950
  ];
2438
- function AreaChart({
2951
+ function GrowthChart({
2439
2952
  data,
2440
2953
  hovered,
2441
- onHover
2954
+ onHover,
2955
+ activeSeries
2442
2956
  }) {
2443
2957
  const W = 600;
2444
- const H = 220;
2445
- const PL = 44;
2958
+ const H = 200;
2959
+ const PL = 40;
2446
2960
  const PR = 12;
2447
2961
  const PT = 12;
2448
2962
  const PB = 28;
2449
2963
  const CW = W - PL - PR;
2450
2964
  const CH = H - PT - PB;
2451
- if (data.length === 0) {
2965
+ if (!data.length) {
2452
2966
  return /* @__PURE__ */ jsxs(Empty, { children: [
2453
2967
  /* @__PURE__ */ jsx("div", { style: { fontSize: 24 }, children: "📊" }),
2454
2968
  /* @__PURE__ */ jsx("div", { children: "No growth data for this period" })
2455
2969
  ] });
2456
2970
  }
2457
- const allVals = data.flatMap((d) => SERIES.map((s) => d[s.key]));
2971
+ const active = ALL_SERIES.filter((s) => activeSeries.has(s.key));
2972
+ const allVals = data.flatMap((d) => active.map((s) => d[s.key]));
2458
2973
  const maxV = Math.max(...allVals, 10);
2459
2974
  const yMax = Math.ceil(maxV * 1.15);
2460
2975
  const xp = (i) => PL + i / Math.max(data.length - 1, 1) * CW;
2461
2976
  const yp = (v) => PT + (1 - v / yMax) * CH;
2462
2977
  const smooth = (vals) => {
2463
2978
  const pts = vals.map((v, i) => ({ x: xp(i), y: yp(v) }));
2464
- const path = pts.reduce((acc, p, i) => {
2979
+ const path = pts.reduce((a, p, i) => {
2465
2980
  if (i === 0) return `M ${p.x} ${p.y}`;
2466
2981
  const pr = pts[i - 1];
2467
2982
  const cx = (pr.x + p.x) / 2;
2468
- return `${acc} C ${cx} ${pr.y} ${cx} ${p.y} ${p.x} ${p.y}`;
2983
+ return `${a} C ${cx} ${pr.y} ${cx} ${p.y} ${p.x} ${p.y}`;
2469
2984
  }, "");
2470
- const area = `${path} L ${pts[pts.length - 1].x} ${PT + CH} L ${pts[0].x} ${PT + CH} Z`;
2985
+ const area = `${path} L ${pts.at(-1).x} ${PT + CH} L ${pts[0].x} ${PT + CH} Z`;
2471
2986
  return { pts, path, area };
2472
2987
  };
2473
- const lines = SERIES.map((s) => ({
2988
+ const lines = active.map((s) => ({
2474
2989
  ...s,
2475
2990
  ...smooth(data.map((d) => d[s.key]))
2476
2991
  }));
@@ -2493,17 +3008,17 @@ function AreaChart({
2493
3008
  /* @__PURE__ */ jsx("defs", { children: lines.map((s) => /* @__PURE__ */ jsxs(
2494
3009
  "linearGradient",
2495
3010
  {
2496
- id: `ag-${s.color.replace("#", "")}`,
3011
+ id: `ag-${s.key}`,
2497
3012
  x1: "0",
2498
3013
  y1: "0",
2499
3014
  x2: "0",
2500
3015
  y2: "1",
2501
3016
  children: [
2502
- /* @__PURE__ */ jsx("stop", { offset: "0%", stopColor: s.color, stopOpacity: "0.13" }),
3017
+ /* @__PURE__ */ jsx("stop", { offset: "0%", stopColor: s.color, stopOpacity: "0.18" }),
2503
3018
  /* @__PURE__ */ jsx("stop", { offset: "100%", stopColor: s.color, stopOpacity: "0" })
2504
3019
  ]
2505
3020
  },
2506
- s.color
3021
+ s.key
2507
3022
  )) }),
2508
3023
  yTicks.map((t) => /* @__PURE__ */ jsxs("g", { children: [
2509
3024
  /* @__PURE__ */ jsx(
@@ -2523,7 +3038,7 @@ function AreaChart({
2523
3038
  x: PL - 6,
2524
3039
  y: t.y + 4,
2525
3040
  textAnchor: "end",
2526
- fill: "#b8b8c7",
3041
+ fill: T.textMuted,
2527
3042
  fontSize: "9",
2528
3043
  children: t.v >= 1e3 ? `${(t.v / 1e3).toFixed(1)}k` : t.v
2529
3044
  }
@@ -2540,25 +3055,18 @@ function AreaChart({
2540
3055
  strokeWidth: "1"
2541
3056
  }
2542
3057
  ),
2543
- lines.map((s) => /* @__PURE__ */ jsx(
2544
- "path",
2545
- {
2546
- d: s.area,
2547
- fill: `url(#ag-${s.color.replace("#", "")})`
2548
- },
2549
- `a-${s.color}`
2550
- )),
3058
+ lines.map((s) => /* @__PURE__ */ jsx("path", { d: s.area, fill: `url(#ag-${s.key})` }, `area-${s.key}`)),
2551
3059
  lines.map((s) => /* @__PURE__ */ jsx(
2552
3060
  "path",
2553
3061
  {
2554
3062
  d: s.path,
2555
3063
  stroke: s.color,
2556
- strokeWidth: "1.8",
3064
+ strokeWidth: "2",
2557
3065
  fill: "none",
2558
3066
  strokeLinecap: "round",
2559
3067
  strokeLinejoin: "round"
2560
3068
  },
2561
- `l-${s.color}`
3069
+ `line-${s.key}`
2562
3070
  )),
2563
3071
  data.map((d, i) => {
2564
3072
  if (i % step !== 0 && i !== data.length - 1) return null;
@@ -2568,7 +3076,7 @@ function AreaChart({
2568
3076
  x: xp(i),
2569
3077
  y: H - 6,
2570
3078
  textAnchor: "middle",
2571
- fill: "#b8b8c7",
3079
+ fill: T.textMuted,
2572
3080
  fontSize: "9",
2573
3081
  children: d.label
2574
3082
  },
@@ -2578,7 +3086,7 @@ function AreaChart({
2578
3086
  data.map((d, i) => {
2579
3087
  const isHov = hovered === i;
2580
3088
  return (
2581
- // biome-ignore lint/a11y/noStaticElementInteractions: SVG chart hover hit area
3089
+ // biome-ignore lint/a11y/noStaticElementInteractions: SVG chart hover
2582
3090
  /* @__PURE__ */ jsxs(
2583
3091
  "g",
2584
3092
  {
@@ -2603,10 +3111,9 @@ function AreaChart({
2603
3111
  y1: PT,
2604
3112
  x2: xp(i),
2605
3113
  y2: PT + CH,
2606
- stroke: "#32324d",
3114
+ stroke: "rgba(50,50,77,0.2)",
2607
3115
  strokeWidth: "1",
2608
- strokeDasharray: "3 3",
2609
- opacity: "0.35"
3116
+ strokeDasharray: "3 3"
2610
3117
  }
2611
3118
  ),
2612
3119
  lines.map((s) => /* @__PURE__ */ jsx(
@@ -2616,11 +3123,11 @@ function AreaChart({
2616
3123
  cy: yp(d[s.key]),
2617
3124
  r: isHov ? 4 : 2.5,
2618
3125
  fill: s.color,
2619
- stroke: "white",
3126
+ stroke: T.bg,
2620
3127
  strokeWidth: isHov ? 2 : 1,
2621
- opacity: isHov ? 1 : 0.45
3128
+ opacity: isHov ? 1 : 0.5
2622
3129
  },
2623
- s.color
3130
+ s.key
2624
3131
  ))
2625
3132
  ]
2626
3133
  },
@@ -2632,23 +3139,70 @@ function AreaChart({
2632
3139
  }
2633
3140
  );
2634
3141
  }
3142
+ function ProgressRing({
3143
+ value,
3144
+ max,
3145
+ color,
3146
+ size = 56
3147
+ }) {
3148
+ const r = (size - 9) / 2;
3149
+ const circ = 2 * Math.PI * r;
3150
+ const pct = Math.min(value / Math.max(max, 1), 1);
3151
+ const offset = circ * (1 - pct);
3152
+ return /* @__PURE__ */ jsxs(
3153
+ "svg",
3154
+ {
3155
+ width: size,
3156
+ height: size,
3157
+ style: { flexShrink: 0 },
3158
+ "aria-hidden": true,
3159
+ children: [
3160
+ /* @__PURE__ */ jsx(
3161
+ "circle",
3162
+ {
3163
+ cx: size / 2,
3164
+ cy: size / 2,
3165
+ r,
3166
+ fill: "none",
3167
+ stroke: "#e5e7eb",
3168
+ strokeWidth: "7"
3169
+ }
3170
+ ),
3171
+ /* @__PURE__ */ jsx(
3172
+ "circle",
3173
+ {
3174
+ cx: size / 2,
3175
+ cy: size / 2,
3176
+ r,
3177
+ fill: "none",
3178
+ stroke: color,
3179
+ strokeWidth: "7",
3180
+ strokeDasharray: `${circ} ${circ}`,
3181
+ strokeDashoffset: offset,
3182
+ strokeLinecap: "round",
3183
+ transform: `rotate(-90 ${size / 2} ${size / 2})`,
3184
+ style: {
3185
+ transition: "stroke-dashoffset 0.9s cubic-bezier(0.4,0,0.2,1)"
3186
+ }
3187
+ }
3188
+ )
3189
+ ]
3190
+ }
3191
+ );
3192
+ }
2635
3193
  function relTime(date) {
2636
- const d = typeof date === "string" ? new Date(date) : date;
2637
- const m = Math.floor((Date.now() - d.getTime()) / 6e4);
2638
- if (m < 1) return "just now";
2639
- if (m < 60) return `${m}m ago`;
2640
- const h = Math.floor(m / 60);
2641
- if (h < 24) return `${h}h ago`;
2642
- return `${Math.floor(h / 24)}d ago`;
3194
+ const diff = Date.now() - new Date(date).getTime();
3195
+ const mins = Math.floor(diff / 6e4);
3196
+ if (mins < 1) return "just now";
3197
+ if (mins < 60) return `${mins}m`;
3198
+ const hrs = Math.floor(mins / 60);
3199
+ if (hrs < 24) return `${hrs}h`;
3200
+ return `${Math.floor(hrs / 24)}d`;
2643
3201
  }
2644
- function fmtDate(date) {
2645
- return new Date(date).toLocaleDateString(void 0, {
2646
- month: "short",
2647
- day: "numeric",
2648
- year: "numeric"
2649
- });
3202
+ function rateHue(r) {
3203
+ return r >= 70 ? 142 : r >= 40 ? 38 : 4;
2650
3204
  }
2651
- function StatCardItem({
3205
+ function StatItem({
2652
3206
  id,
2653
3207
  label,
2654
3208
  value,
@@ -2657,26 +3211,39 @@ function StatCardItem({
2657
3211
  color,
2658
3212
  delay = 0
2659
3213
  }) {
3214
+ const animated = useCountUp(value);
2660
3215
  const isPos = pct === void 0 || pct >= 0;
2661
- return /* @__PURE__ */ jsxs(Card, { $delay: delay, $accent: color, children: [
2662
- /* @__PURE__ */ jsxs(StatTop, { children: [
2663
- /* @__PURE__ */ jsx(StatLabel, { children: label }),
2664
- /* @__PURE__ */ jsx(StatValue, { children: typeof value === "number" ? value.toLocaleString() : value }),
2665
- pct !== void 0 && /* @__PURE__ */ jsxs(TrendBadge, { $pos: isPos, children: [
2666
- isPos ? "↑" : "↓",
2667
- " ",
2668
- Math.abs(pct).toFixed(1),
2669
- "%"
2670
- ] })
3216
+ return /* @__PURE__ */ jsxs(StatCard, { $delay: delay, $accent: color, children: [
3217
+ /* @__PURE__ */ jsx(StatLabel, { children: label }),
3218
+ /* @__PURE__ */ jsx(StatValue, { children: animated.toLocaleString() }),
3219
+ pct !== void 0 && /* @__PURE__ */ jsxs(TrendBadge, { $pos: isPos, children: [
3220
+ isPos ? "↑" : "↓",
3221
+ " ",
3222
+ Math.abs(pct).toFixed(1),
3223
+ "%"
2671
3224
  ] }),
2672
3225
  sparkline && sparkline.length > 1 && /* @__PURE__ */ jsx(SparkWrap, { children: /* @__PURE__ */ jsx(Sparkline, { data: sparkline, color, id }) })
2673
3226
  ] });
2674
3227
  }
2675
3228
  function OverviewPage() {
3229
+ const { get } = useFetchClient();
2676
3230
  const [period, setPeriod] = useState(
2677
3231
  "weekly"
2678
3232
  );
3233
+ const [feedMode, setFeedMode] = useState("signups");
2679
3234
  const [hovIdx, setHovIdx] = useState(null);
3235
+ const [rtnHov, setRtnHov] = useState(null);
3236
+ const [activeSeries, setActiveSeries] = useState(
3237
+ () => /* @__PURE__ */ new Set(["totalUsers", "newUsers", "activeUsers"])
3238
+ );
3239
+ const toggleSeries = (key) => {
3240
+ setActiveSeries((prev) => {
3241
+ const next = new Set(prev);
3242
+ if (next.has(key) && next.size > 1) next.delete(key);
3243
+ else next.add(key);
3244
+ return next;
3245
+ });
3246
+ };
2680
3247
  const statsQuery = useQuery({
2681
3248
  queryKey: ["dash-user-stats"],
2682
3249
  queryFn: async () => {
@@ -2693,12 +3260,10 @@ function OverviewPage() {
2693
3260
  return r.data;
2694
3261
  }
2695
3262
  });
2696
- const sessionsQuery = useQuery({
2697
- queryKey: ["dash-recent-sessions"],
3263
+ const retentionQuery = useQuery({
3264
+ queryKey: ["dash-user-retention", period],
2698
3265
  queryFn: async () => {
2699
- const r = await client.dash.listAllSessions({
2700
- query: { page: 1, limit: 10 }
2701
- });
3266
+ const r = await client.dash.userRetentionData({ query: { period } });
2702
3267
  if (r.error) throw new Error(r.error.message ?? "Failed");
2703
3268
  return r.data;
2704
3269
  }
@@ -2707,12 +3272,22 @@ function OverviewPage() {
2707
3272
  queryKey: ["dash-recent-users"],
2708
3273
  queryFn: async () => {
2709
3274
  const r = await client.dash.listUsers({
2710
- query: { page: 1, limit: 8, search: "" }
3275
+ query: { limit: 12, offset: 0, sortBy: "createdAt", sortOrder: "desc" }
2711
3276
  });
2712
3277
  if (r.error) throw new Error(r.error.message ?? "Failed");
2713
3278
  return r.data;
2714
3279
  }
2715
3280
  });
3281
+ const sessionsQuery = useQuery({
3282
+ queryKey: ["dash-recent-sessions"],
3283
+ queryFn: async () => {
3284
+ const { data } = await get(
3285
+ "/better-auth-dashboard/db?uid=plugin::better-auth.session&pagination[pageSize]=12&sort[0]=createdAt:desc"
3286
+ );
3287
+ return data.results ?? [];
3288
+ },
3289
+ refetchInterval: 3e4
3290
+ });
2716
3291
  const orgsQuery = useQuery({
2717
3292
  queryKey: ["dash-orgs-count"],
2718
3293
  queryFn: async () => {
@@ -2723,595 +3298,385 @@ function OverviewPage() {
2723
3298
  return r.data;
2724
3299
  }
2725
3300
  });
3301
+ const sessionsRaw = sessionsQuery.data ?? [];
3302
+ const activeUserIds = [];
3303
+ const lastActiveByUserId = /* @__PURE__ */ new Map();
3304
+ for (const s of sessionsRaw) {
3305
+ if (!lastActiveByUserId.has(s.userId)) {
3306
+ lastActiveByUserId.set(s.userId, s.createdAt);
3307
+ activeUserIds.push(s.userId);
3308
+ }
3309
+ }
3310
+ const activeUsersQuery = useQuery({
3311
+ queryKey: ["dash-active-users", activeUserIds],
3312
+ queryFn: async () => {
3313
+ if (activeUserIds.length === 0) return [];
3314
+ const params = new URLSearchParams({ uid: "plugin::better-auth.user" });
3315
+ for (let i = 0; i < activeUserIds.length; i++) {
3316
+ params.set(`filters[id][$in][${i}]`, activeUserIds[i]);
3317
+ }
3318
+ params.set("pagination[pageSize]", "12");
3319
+ const { data } = await get(
3320
+ `/better-auth-dashboard/db?${params}`
3321
+ );
3322
+ return data.results ?? [];
3323
+ },
3324
+ enabled: feedMode === "active" && activeUserIds.length > 0
3325
+ });
3326
+ const chartRef = useRef(null);
3327
+ const [chartHeight, setChartHeight] = useState(0);
3328
+ useEffect(() => {
3329
+ const el = chartRef.current;
3330
+ if (!el) return;
3331
+ const ro = new ResizeObserver((entries) => {
3332
+ for (const entry of entries) setChartHeight(entry.contentRect.height);
3333
+ });
3334
+ ro.observe(el);
3335
+ return () => ro.disconnect();
3336
+ }, []);
2726
3337
  if (statsQuery.isLoading) {
2727
- return /* @__PURE__ */ jsx(Flex, { justifyContent: "center", alignItems: "center", padding: 12, children: /* @__PURE__ */ jsx(Loader, { children: "Loading…" }) });
3338
+ return /* @__PURE__ */ jsx(
3339
+ Flex,
3340
+ {
3341
+ justifyContent: "center",
3342
+ alignItems: "center",
3343
+ padding: 12,
3344
+ style: { background: T.bg, minHeight: "100%" },
3345
+ children: /* @__PURE__ */ jsx(Loader, { children: "Loading…" })
3346
+ }
3347
+ );
2728
3348
  }
2729
3349
  const stats = statsQuery.data;
2730
3350
  if (!stats) return null;
2731
3351
  const graphData = graphQuery.data?.data ?? [];
2732
- const sessions = sessionsQuery.data?.sessions ?? [];
2733
- const recentUsers = usersQuery.data?.users ?? [];
3352
+ const rtnData = retentionQuery.data?.data ?? [];
3353
+ const users = usersQuery.data?.users ?? [];
2734
3354
  const orgCount = orgsQuery.data?.total ?? 0;
3355
+ const activeUserMap = /* @__PURE__ */ new Map();
3356
+ for (const u of activeUsersQuery.data ?? []) {
3357
+ activeUserMap.set(String(u.id), u);
3358
+ }
3359
+ const sortedActiveUsers = activeUserIds.map((uid) => activeUserMap.get(uid)).filter((u) => u !== void 0);
2735
3360
  const totalSpark = graphData.map((d) => d.totalUsers);
2736
3361
  const newSpark = graphData.map((d) => d.newUsers);
2737
3362
  const activeSpark = graphData.map((d) => d.activeUsers);
2738
3363
  const hovRow = hovIdx !== null ? graphData[hovIdx] : null;
2739
- return /* @__PURE__ */ jsxs(Wrap$2, { "data-testid": "overview-page", children: [
3364
+ const rtnHovRow = rtnHov !== null ? rtnData[rtnHov] : null;
3365
+ const activeMax = Math.max(
3366
+ stats.activeUsers.daily.active ?? 0,
3367
+ stats.activeUsers.weekly.active ?? 0,
3368
+ stats.activeUsers.monthly.active ?? 0,
3369
+ 1
3370
+ );
3371
+ return /* @__PURE__ */ jsxs(Wrap$1, { "data-testid": "overview-page", children: [
2740
3372
  /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "flex-end", children: [
2741
- /* @__PURE__ */ jsxs(Box, { children: [
2742
- /* @__PURE__ */ jsx(Typography, { variant: "alpha", textColor: "neutral800", children: "Overview" }),
2743
- /* @__PURE__ */ jsx(Box, { paddingTop: 1, children: /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: (/* @__PURE__ */ new Date()).toLocaleDateString(void 0, {
3373
+ /* @__PURE__ */ jsxs("div", { children: [
3374
+ /* @__PURE__ */ jsx(
3375
+ "div",
3376
+ {
3377
+ style: {
3378
+ fontSize: 22,
3379
+ fontWeight: 800,
3380
+ color: T.textPrimary,
3381
+ letterSpacing: "-0.03em"
3382
+ },
3383
+ children: "Overview"
3384
+ }
3385
+ ),
3386
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 11, color: T.textSecondary, marginTop: 4 }, children: (/* @__PURE__ */ new Date()).toLocaleDateString(void 0, {
2744
3387
  weekday: "long",
2745
3388
  year: "numeric",
2746
3389
  month: "long",
2747
3390
  day: "numeric"
2748
- }) }) })
3391
+ }) })
2749
3392
  ] }),
2750
- /* @__PURE__ */ jsx(Box, { width: "150px", children: /* @__PURE__ */ jsxs(
2751
- SingleSelect,
2752
- {
2753
- value: period,
2754
- onChange: (v) => setPeriod(v),
2755
- size: "S",
2756
- "aria-label": "Select period",
2757
- children: [
2758
- /* @__PURE__ */ jsx(SingleSelectOption, { value: "daily", children: "Daily" }),
2759
- /* @__PURE__ */ jsx(SingleSelectOption, { value: "weekly", children: "Weekly" }),
2760
- /* @__PURE__ */ jsx(SingleSelectOption, { value: "monthly", children: "Monthly" })
2761
- ]
2762
- }
2763
- ) })
3393
+ /* @__PURE__ */ jsx(PillGroup, { children: ["daily", "weekly", "monthly"].map((p) => /* @__PURE__ */ jsx(Pill, { $active: period === p, onClick: () => setPeriod(p), children: p[0].toUpperCase() + p.slice(1) }, p)) })
2764
3394
  ] }),
2765
- /* @__PURE__ */ jsxs(Divider, { children: [
2766
- /* @__PURE__ */ jsx(DivLabel, { children: "Sign-ups" }),
3395
+ /* @__PURE__ */ jsxs(SectionDivider, { children: [
3396
+ /* @__PURE__ */ jsx(DivLabel, { children: "Metrics" }),
2767
3397
  /* @__PURE__ */ jsx(DivLine, {})
2768
3398
  ] }),
2769
- /* @__PURE__ */ jsxs(StatRow, { children: [
3399
+ /* @__PURE__ */ jsxs(StatGrid, { children: [
2770
3400
  /* @__PURE__ */ jsx(
2771
- StatCardItem,
3401
+ StatItem,
2772
3402
  {
2773
3403
  id: "total",
2774
3404
  label: "Total Users",
2775
3405
  value: stats.total ?? 0,
2776
3406
  sparkline: totalSpark,
2777
- color: "#4945FF",
3407
+ color: T.accent,
2778
3408
  delay: 0
2779
3409
  }
2780
3410
  ),
2781
3411
  /* @__PURE__ */ jsx(
2782
- StatCardItem,
3412
+ StatItem,
2783
3413
  {
2784
3414
  id: "d-sig",
2785
3415
  label: "Daily Sign-ups",
2786
3416
  value: stats.daily.signUps ?? 0,
2787
3417
  pct: stats.daily.percentage ?? void 0,
2788
3418
  sparkline: newSpark,
2789
- color: "#5CB176",
3419
+ color: T.green,
2790
3420
  delay: 1
2791
3421
  }
2792
3422
  ),
2793
3423
  /* @__PURE__ */ jsx(
2794
- StatCardItem,
3424
+ StatItem,
2795
3425
  {
2796
3426
  id: "w-sig",
2797
3427
  label: "Weekly Sign-ups",
2798
3428
  value: stats.weekly.signUps ?? 0,
2799
3429
  pct: stats.weekly.percentage ?? void 0,
2800
3430
  sparkline: newSpark,
2801
- color: "#5CB176",
3431
+ color: T.green,
2802
3432
  delay: 2
2803
3433
  }
2804
3434
  ),
2805
3435
  /* @__PURE__ */ jsx(
2806
- StatCardItem,
3436
+ StatItem,
2807
3437
  {
2808
3438
  id: "m-sig",
2809
3439
  label: "Monthly Sign-ups",
2810
3440
  value: stats.monthly.signUps ?? 0,
2811
3441
  pct: stats.monthly.percentage ?? void 0,
2812
3442
  sparkline: newSpark,
2813
- color: "#5CB176",
3443
+ color: T.green,
2814
3444
  delay: 3
2815
3445
  }
2816
3446
  ),
2817
3447
  /* @__PURE__ */ jsx(
2818
- StatCardItem,
3448
+ StatItem,
2819
3449
  {
2820
3450
  id: "orgs",
2821
3451
  label: "Organizations",
2822
3452
  value: orgCount,
2823
- color: "#9E6BF9",
3453
+ color: T.purple,
2824
3454
  delay: 4
2825
3455
  }
2826
3456
  )
2827
3457
  ] }),
2828
- /* @__PURE__ */ jsxs(MainRow, { children: [
2829
- /* @__PURE__ */ jsxs(ChartCard, { $delay: 5, children: [
3458
+ /* @__PURE__ */ jsxs(SectionDivider, { children: [
3459
+ /* @__PURE__ */ jsx(DivLabel, { children: "Growth" }),
3460
+ /* @__PURE__ */ jsx(DivLine, {})
3461
+ ] }),
3462
+ /* @__PURE__ */ jsxs(TwoPanel, { children: [
3463
+ /* @__PURE__ */ jsxs(ChartCard, { ref: chartRef, $delay: 5, children: [
2830
3464
  /* @__PURE__ */ jsxs(ChartHeader, { children: [
2831
3465
  /* @__PURE__ */ jsx(ChartTitle, { children: "User Growth" }),
2832
- /* @__PURE__ */ jsx(LegendRow, { children: SERIES.map((s) => /* @__PURE__ */ jsxs(LegendItem, { children: [
2833
- /* @__PURE__ */ jsx(LegendDot, { $c: s.color }),
2834
- s.label
2835
- ] }, s.color)) })
2836
- ] }),
2837
- /* @__PURE__ */ jsx(HoverInfo, { children: hovRow ? `${hovRow.label} · ${hovRow.totalUsers.toLocaleString()} total · +${hovRow.newUsers.toLocaleString()} new · ${hovRow.activeUsers.toLocaleString()} active` : "Hover the chart to inspect a data point" }),
3466
+ /* @__PURE__ */ jsx(SeriesRow, { children: ALL_SERIES.map((s) => /* @__PURE__ */ jsxs(
3467
+ SeriesBtn,
3468
+ {
3469
+ $on: activeSeries.has(s.key),
3470
+ $c: s.color,
3471
+ onClick: () => toggleSeries(s.key),
3472
+ children: [
3473
+ /* @__PURE__ */ jsx(SDot, { $c: s.color }),
3474
+ s.label
3475
+ ]
3476
+ },
3477
+ s.key
3478
+ )) })
3479
+ ] }),
3480
+ /* @__PURE__ */ jsx(HoverInfo, { children: hovRow ? `${hovRow.label} · ${hovRow.totalUsers.toLocaleString()} total · +${hovRow.newUsers.toLocaleString()} new · ${hovRow.activeUsers.toLocaleString()} active` : "Hover the chart to inspect a data point" }),
2838
3481
  graphQuery.isLoading ? /* @__PURE__ */ jsx(
2839
3482
  Flex,
2840
3483
  {
2841
3484
  justifyContent: "center",
2842
3485
  alignItems: "center",
2843
- style: { flex: 1, minHeight: 180 },
3486
+ style: { flex: 1, minHeight: 170 },
2844
3487
  children: /* @__PURE__ */ jsx(Loader, { children: "Loading…" })
2845
3488
  }
2846
- ) : /* @__PURE__ */ jsx(AreaChart, { data: graphData, hovered: hovIdx, onHover: setHovIdx })
3489
+ ) : /* @__PURE__ */ jsx(
3490
+ GrowthChart,
3491
+ {
3492
+ data: graphData,
3493
+ hovered: hovIdx,
3494
+ onHover: setHovIdx,
3495
+ activeSeries
3496
+ }
3497
+ )
2847
3498
  ] }),
2848
- /* @__PURE__ */ jsxs(FeedCard, { $delay: 6, children: [
2849
- /* @__PURE__ */ jsx(FeedHeader, { children: "Recent Sessions" }),
2850
- /* @__PURE__ */ jsx(FeedList, { children: sessionsQuery.isLoading ? /* @__PURE__ */ jsx(Empty, { children: /* @__PURE__ */ jsx(Loader, { children: "Loading…" }) }) : sessions.length === 0 ? /* @__PURE__ */ jsx(Empty, { children: "No recent sessions" }) : sessions.map((s) => /* @__PURE__ */ jsxs(FeedItem, { children: [
2851
- /* @__PURE__ */ jsx(
2852
- Avatar,
2853
- {
2854
- name: s.user?.name ?? "?",
2855
- src: s.user?.image,
2856
- size: 28
2857
- }
2858
- ),
2859
- /* @__PURE__ */ jsxs(FeedContent, { children: [
2860
- /* @__PURE__ */ jsx(FeedName, { children: s.user?.name ?? "Unknown" }),
2861
- /* @__PURE__ */ jsx(FeedSub, { children: s.user?.email ?? "" }),
2862
- /* @__PURE__ */ jsx(FeedTime, { children: relTime(s.createdAt) })
2863
- ] })
2864
- ] }, s.id)) })
2865
- ] })
2866
- ] }),
2867
- /* @__PURE__ */ jsxs(Divider, { children: [
2868
- /* @__PURE__ */ jsx(DivLabel, { children: "Active Users" }),
2869
- /* @__PURE__ */ jsx(DivLine, {})
2870
- ] }),
2871
- /* @__PURE__ */ jsxs(ActiveRow, { children: [
2872
- /* @__PURE__ */ jsx(
2873
- StatCardItem,
2874
- {
2875
- id: "d-act",
2876
- label: "Daily Active",
2877
- value: stats.activeUsers.daily.active ?? 0,
2878
- pct: stats.activeUsers.daily.percentage ?? void 0,
2879
- sparkline: activeSpark,
2880
- color: "#E57553",
2881
- delay: 7
2882
- }
2883
- ),
2884
- /* @__PURE__ */ jsx(
2885
- StatCardItem,
2886
- {
2887
- id: "w-act",
2888
- label: "Weekly Active",
2889
- value: stats.activeUsers.weekly.active ?? 0,
2890
- pct: stats.activeUsers.weekly.percentage ?? void 0,
2891
- sparkline: activeSpark,
2892
- color: "#E57553",
2893
- delay: 8
2894
- }
2895
- ),
2896
- /* @__PURE__ */ jsx(
2897
- StatCardItem,
3499
+ /* @__PURE__ */ jsxs(
3500
+ FeedCard,
2898
3501
  {
2899
- id: "m-act",
2900
- label: "Monthly Active",
2901
- value: stats.activeUsers.monthly.active ?? 0,
2902
- pct: stats.activeUsers.monthly.percentage ?? void 0,
2903
- sparkline: activeSpark,
2904
- color: "#E57553",
2905
- delay: 9
3502
+ $delay: 6,
3503
+ style: chartHeight > 0 ? { height: chartHeight } : void 0,
3504
+ children: [
3505
+ /* @__PURE__ */ jsxs(FeedHead, { children: [
3506
+ /* @__PURE__ */ jsx("span", { children: feedMode === "signups" ? "Recent Sign-ups" : "Recently Active" }),
3507
+ /* @__PURE__ */ jsxs(
3508
+ FeedSelect,
3509
+ {
3510
+ value: feedMode,
3511
+ onChange: (e) => setFeedMode(e.target.value),
3512
+ children: [
3513
+ /* @__PURE__ */ jsx("option", { value: "signups", children: "Recent Sign-ups" }),
3514
+ /* @__PURE__ */ jsx("option", { value: "active", children: "Recently Active" })
3515
+ ]
3516
+ }
3517
+ )
3518
+ ] }),
3519
+ /* @__PURE__ */ jsx(FeedScroll, { children: feedMode === "signups" ? usersQuery.isLoading ? /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ jsx(Loader, { children: "Loading…" }) }) : users.length === 0 ? /* @__PURE__ */ jsx(Empty, { children: "No users yet" }) : users.map((u) => /* @__PURE__ */ jsxs(FeedItem, { children: [
3520
+ /* @__PURE__ */ jsxs(FeedTop, { children: [
3521
+ /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 1, style: { minWidth: 0 }, children: [
3522
+ /* @__PURE__ */ jsx(Avatar, { name: u.name, src: u.image, size: 20 }),
3523
+ /* @__PURE__ */ jsx(FeedName, { title: u.name, children: u.name })
3524
+ ] }),
3525
+ /* @__PURE__ */ jsx(FeedMeta, { children: relTime(u.createdAt) })
3526
+ ] }),
3527
+ /* @__PURE__ */ jsx(FeedEmail, { title: u.email, children: u.email })
3528
+ ] }, u.id)) : activeUsersQuery.isLoading || sessionsQuery.isLoading ? /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ jsx(Loader, { children: "Loading…" }) }) : sortedActiveUsers.length === 0 ? /* @__PURE__ */ jsx(Empty, { children: "No recent activity" }) : sortedActiveUsers.map((u) => /* @__PURE__ */ jsxs(FeedItem, { children: [
3529
+ /* @__PURE__ */ jsxs(FeedTop, { children: [
3530
+ /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 1, style: { minWidth: 0 }, children: [
3531
+ /* @__PURE__ */ jsx(
3532
+ Avatar,
3533
+ {
3534
+ name: u.name,
3535
+ src: u.image ?? void 0,
3536
+ size: 20
3537
+ }
3538
+ ),
3539
+ /* @__PURE__ */ jsx(FeedName, { title: u.name, children: u.name })
3540
+ ] }),
3541
+ /* @__PURE__ */ jsx(FeedMeta, { children: relTime(
3542
+ lastActiveByUserId.get(String(u.id)) ?? u.createdAt
3543
+ ) })
3544
+ ] }),
3545
+ /* @__PURE__ */ jsx(FeedEmail, { title: u.email, children: u.email })
3546
+ ] }, u.documentId)) })
3547
+ ]
2906
3548
  }
2907
3549
  )
2908
3550
  ] }),
2909
- /* @__PURE__ */ jsxs(Divider, { children: [
2910
- /* @__PURE__ */ jsx(DivLabel, { children: "Recent Users" }),
3551
+ /* @__PURE__ */ jsxs(SectionDivider, { children: [
3552
+ /* @__PURE__ */ jsx(DivLabel, { children: "Retention & Activity" }),
2911
3553
  /* @__PURE__ */ jsx(DivLine, {})
2912
3554
  ] }),
2913
- /* @__PURE__ */ jsx(UsersCard, { $delay: 10, children: /* @__PURE__ */ jsx(TableWrap, { children: /* @__PURE__ */ jsxs(Table$1, { children: [
2914
- /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { children: ["User", "Email", "Joined", "Status"].map((h) => /* @__PURE__ */ jsx(TH$1, { children: h }, h)) }) }),
2915
- /* @__PURE__ */ jsx("tbody", { children: usersQuery.isLoading ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx(TD$1, { colSpan: 4, style: { textAlign: "center", padding: 28 }, children: /* @__PURE__ */ jsx(Loader, { children: "Loading users…" }) }) }) : recentUsers.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx(
2916
- TD$1,
2917
- {
2918
- colSpan: 4,
2919
- style: {
2920
- textAlign: "center",
2921
- padding: 28,
2922
- color: "#8e8ea9"
2923
- },
2924
- children: "No users yet"
2925
- }
2926
- ) }) : recentUsers.map((u) => /* @__PURE__ */ jsxs(TR$1, { children: [
2927
- /* @__PURE__ */ jsx(TD$1, { children: /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 2, children: [
2928
- /* @__PURE__ */ jsx(Avatar, { name: u.name, src: u.image, size: 26 }),
2929
- /* @__PURE__ */ jsx("span", { style: { fontWeight: 600, fontSize: 12 }, children: u.name })
2930
- ] }) }),
2931
- /* @__PURE__ */ jsx(
2932
- TD$1,
2933
- {
2934
- style: {
2935
- color: "#8e8ea9",
2936
- fontFamily: "monospace",
2937
- fontSize: 11
2938
- },
2939
- children: u.email
2940
- }
2941
- ),
2942
- /* @__PURE__ */ jsx(
2943
- TD$1,
2944
- {
2945
- style: {
2946
- color: "#8e8ea9",
2947
- fontSize: 11,
2948
- whiteSpace: "nowrap"
3555
+ /* @__PURE__ */ jsxs(TwoPanel, { children: [
3556
+ /* @__PURE__ */ jsxs(RtnCard, { $delay: 7, children: [
3557
+ /* @__PURE__ */ jsxs(ChartHeader, { children: [
3558
+ /* @__PURE__ */ jsx(ChartTitle, { children: "Cohort Retention" }),
3559
+ /* @__PURE__ */ jsx(Flex, { alignItems: "center", gap: 2, children: [
3560
+ { hue: 142, label: "≥70%" },
3561
+ { hue: 38, label: "40–70%" },
3562
+ { hue: 4, label: "<40%" }
3563
+ ].map(({ hue, label }) => /* @__PURE__ */ jsxs(
3564
+ Flex,
3565
+ {
3566
+ alignItems: "center",
3567
+ gap: 1,
3568
+ style: { marginLeft: 8 },
3569
+ children: [
3570
+ /* @__PURE__ */ jsx(
3571
+ "span",
3572
+ {
3573
+ style: {
3574
+ width: 8,
3575
+ height: 8,
3576
+ borderRadius: "50%",
3577
+ background: `hsl(${hue},60%,50%)`,
3578
+ display: "inline-block",
3579
+ flexShrink: 0
3580
+ }
3581
+ }
3582
+ ),
3583
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 9, color: T.textMuted }, children: label })
3584
+ ]
2949
3585
  },
2950
- children: fmtDate(u.createdAt)
2951
- }
2952
- ),
2953
- /* @__PURE__ */ jsx(TD$1, { children: /* @__PURE__ */ jsx(
2954
- StatusChip$1,
3586
+ hue
3587
+ )) })
3588
+ ] }),
3589
+ /* @__PURE__ */ jsx(RtnTip, { children: rtnHovRow ? `Cohort ${rtnHovRow.label} · ${rtnHovRow.cohortSize.toLocaleString()} users · active ${rtnHovRow.activeStart} – ${rtnHovRow.activeEnd}` : "Hover a row to see cohort details" }),
3590
+ retentionQuery.isLoading ? /* @__PURE__ */ jsx(
3591
+ Flex,
2955
3592
  {
2956
- $verified: u.emailVerified,
2957
- $banned: u.banned,
2958
- children: u.banned ? "Banned" : u.emailVerified ? "Verified" : "Unverified"
3593
+ justifyContent: "center",
3594
+ alignItems: "center",
3595
+ style: { minHeight: 80 },
3596
+ children: /* @__PURE__ */ jsx(Loader, { children: "Loading…" })
2959
3597
  }
2960
- ) })
2961
- ] }, u.id)) })
2962
- ] }) }) })
2963
- ] });
2964
- }
2965
- const PAGE_SIZE$1 = 25;
2966
- const fadeUp$1 = keyframes`
2967
- from { opacity: 0; transform: translateY(6px); }
2968
- to { opacity: 1; transform: translateY(0); }
2969
- `;
2970
- const Wrap$1 = styled.div`
2971
- padding: 28px 32px;
2972
- background: #f6f6f9;
2973
- min-height: 100%;
2974
- display: flex;
2975
- flex-direction: column;
2976
- gap: 20px;
2977
- `;
2978
- const PageHeader$1 = styled.div`
2979
- display: flex;
2980
- justify-content: space-between;
2981
- align-items: flex-start;
2982
- `;
2983
- const TitleBlock$1 = styled.div`
2984
- display: flex;
2985
- flex-direction: column;
2986
- gap: 4px;
2987
- `;
2988
- const PageTitle$1 = styled.h1`
2989
- margin: 0;
2990
- font-size: 22px;
2991
- font-weight: 800;
2992
- color: #32324d;
2993
- letter-spacing: -0.03em;
2994
- `;
2995
- const PageSubtitle$1 = styled.p`
2996
- margin: 0;
2997
- font-size: 12px;
2998
- color: #8e8ea9;
2999
- `;
3000
- const TableCard$1 = styled.div`
3001
- background: #ffffff;
3002
- border: 1px solid #eaeaef;
3003
- border-radius: 10px;
3004
- overflow: hidden;
3005
- `;
3006
- const ColumnHeader = styled.div`
3007
- padding: 10px 20px;
3008
- display: flex;
3009
- align-items: center;
3010
- gap: 12px;
3011
- background: #fafafa;
3012
- border-bottom: 1px solid #eaeaef;
3013
- font-size: 10px;
3014
- font-weight: 700;
3015
- text-transform: uppercase;
3016
- letter-spacing: 0.08em;
3017
- color: #8e8ea9;
3018
- `;
3019
- const ColumnHeaderRight = styled.div`
3020
- margin-left: auto;
3021
- font-size: 10px;
3022
- font-weight: 700;
3023
- text-transform: uppercase;
3024
- letter-spacing: 0.08em;
3025
- color: #8e8ea9;
3026
- `;
3027
- const UserGroup = styled.div`
3028
- border-bottom: 1px solid #eaeaef;
3029
- animation: ${fadeUp$1} 280ms ease both;
3030
- animation-delay: ${(p) => (p.$i ?? 0) * 30}ms;
3031
- &:last-child { border-bottom: none; }
3032
- `;
3033
- const UserRowHeader = styled.div`
3034
- padding: 14px 20px;
3035
- display: flex;
3036
- align-items: center;
3037
- gap: 12px;
3038
- border-bottom: 1px solid #f5f5f9;
3039
- cursor: default;
3040
- &:hover { background: #fafafe; }
3041
- `;
3042
- const UserInfo = styled.div`
3043
- flex: 1;
3044
- display: flex;
3045
- flex-direction: column;
3046
- gap: 2px;
3047
- `;
3048
- const UserName$1 = styled.span`
3049
- font-size: 12px;
3050
- font-weight: 600;
3051
- color: #32324d;
3052
- `;
3053
- const UserEmail = styled.span`
3054
- font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
3055
- font-size: 11px;
3056
- color: #8e8ea9;
3057
- `;
3058
- const SessionCountChip = styled.span`
3059
- display: inline-flex;
3060
- align-items: center;
3061
- padding: 2px 8px;
3062
- border-radius: 20px;
3063
- font-size: 10px;
3064
- font-weight: 700;
3065
- background: #f0f0ff;
3066
- color: #4945ff;
3067
- `;
3068
- const SessionCard = styled.div`
3069
- padding: 10px 20px 10px 64px;
3070
- border-top: 1px solid #f5f5f9;
3071
- background: #fafafa;
3072
- display: flex;
3073
- align-items: flex-start;
3074
- justify-content: space-between;
3075
- gap: 16px;
3076
- &:hover { background: #f5f5ff; }
3077
- `;
3078
- const SessionMeta = styled.div`
3079
- display: flex;
3080
- flex-direction: column;
3081
- gap: 3px;
3082
- flex: 1;
3083
- min-width: 0;
3084
- `;
3085
- const IpChip = styled.span`
3086
- display: inline-block;
3087
- background: #f0f0ff;
3088
- color: #4945ff;
3089
- border-radius: 5px;
3090
- padding: 2px 6px;
3091
- font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
3092
- font-size: 11px;
3093
- font-weight: 600;
3094
- `;
3095
- const TimestampText = styled.span`
3096
- font-size: 11px;
3097
- color: #8e8ea9;
3098
- font-variant-numeric: tabular-nums;
3099
- `;
3100
- const AgentText = styled.span`
3101
- font-size: 11px;
3102
- color: #b8b8c7;
3103
- overflow: hidden;
3104
- text-overflow: ellipsis;
3105
- white-space: nowrap;
3106
- display: block;
3107
- max-width: 400px;
3108
- `;
3109
- const EmptyState = styled.div`
3110
- display: flex;
3111
- justify-content: center;
3112
- align-items: center;
3113
- padding: 48px;
3114
- font-size: 12px;
3115
- color: #8e8ea9;
3116
- `;
3117
- function SessionsPage() {
3118
- const qc = useQueryClient();
3119
- const [page, setPage] = useState(1);
3120
- const [selected, setSelected] = useState(/* @__PURE__ */ new Set());
3121
- const [confirmRevokeSessionId, setConfirmRevokeSessionId] = useState(null);
3122
- const [confirmRevokeMany, setConfirmRevokeMany] = useState(false);
3123
- const sessionsQuery = useQuery({
3124
- queryKey: ["dash-all-sessions", page],
3125
- queryFn: async () => {
3126
- const result = await client.dash.listAllSessions({});
3127
- if (result.error)
3128
- throw new Error(result.error.message ?? "Failed to load sessions");
3129
- return result.data ?? [];
3130
- },
3131
- keepPreviousData: true
3132
- });
3133
- const revokeSessionMutation = useMutation({
3134
- mutationFn: async (sessionId) => {
3135
- const result = await client.dash.sessions.revoke(
3136
- {},
3137
- withContext({ sessionId })
3138
- );
3139
- if (result.error)
3140
- throw new Error(result.error.message ?? "Revoke failed");
3141
- },
3142
- onSuccess: () => {
3143
- setConfirmRevokeSessionId(null);
3144
- qc.invalidateQueries({ queryKey: ["dash-all-sessions"] });
3145
- }
3146
- });
3147
- const revokeManyMutation = useMutation({
3148
- mutationFn: async (userIds) => {
3149
- const result = await client.dash.sessions.revokeMany(
3150
- {},
3151
- withContext({ userIds })
3152
- );
3153
- if (result.error)
3154
- throw new Error(result.error.message ?? "Revoke failed");
3155
- return result.data;
3156
- },
3157
- onSuccess: () => {
3158
- setConfirmRevokeMany(false);
3159
- setSelected(/* @__PURE__ */ new Set());
3160
- qc.invalidateQueries({ queryKey: ["dash-all-sessions"] });
3161
- }
3162
- });
3163
- const usersWithSessions = sessionsQuery.data ?? [];
3164
- const allUserIds = usersWithSessions.map((u) => u.id);
3165
- const allSelected = allUserIds.length > 0 && allUserIds.every((id) => selected.has(id));
3166
- const someSelected = selected.size > 0;
3167
- const toggleSelect = (id) => {
3168
- setSelected((prev) => {
3169
- const next = new Set(prev);
3170
- if (next.has(id)) next.delete(id);
3171
- else next.add(id);
3172
- return next;
3173
- });
3174
- };
3175
- const handleSelectAll = () => {
3176
- if (allSelected) setSelected(/* @__PURE__ */ new Set());
3177
- else setSelected(new Set(allUserIds));
3178
- };
3179
- return /* @__PURE__ */ jsxs(Wrap$1, { "data-testid": "sessions-page", children: [
3180
- /* @__PURE__ */ jsxs(PageHeader$1, { children: [
3181
- /* @__PURE__ */ jsxs(TitleBlock$1, { children: [
3182
- /* @__PURE__ */ jsx(PageTitle$1, { children: "Sessions" }),
3183
- /* @__PURE__ */ jsx(PageSubtitle$1, { children: usersWithSessions.length > 0 ? `${usersWithSessions.length} user${usersWithSessions.length !== 1 ? "s" : ""} with active sessions` : "Active sessions across all users" })
3598
+ ) : rtnData.length === 0 ? /* @__PURE__ */ jsxs(Empty, { children: [
3599
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 22 }, children: "📉" }),
3600
+ /* @__PURE__ */ jsx("div", { children: "No retention data for this period" })
3601
+ ] }) : /* @__PURE__ */ jsx("div", { style: { display: "flex", flexDirection: "column", gap: 5 }, children: rtnData.map((row, i) => {
3602
+ const hue = rateHue(row.retentionRate);
3603
+ return /* @__PURE__ */ jsxs(
3604
+ RtnRow,
3605
+ {
3606
+ $hov: rtnHov === i,
3607
+ onMouseEnter: () => setRtnHov(i),
3608
+ onMouseLeave: () => setRtnHov(null),
3609
+ children: [
3610
+ /* @__PURE__ */ jsx(RtnLabel, { children: row.label }),
3611
+ /* @__PURE__ */ jsx(RtnSize, { children: row.cohortSize.toLocaleString() }),
3612
+ /* @__PURE__ */ jsx(RtnTrack, { children: /* @__PURE__ */ jsx(RtnBar, { $w: row.retentionRate, $hue: hue }) }),
3613
+ /* @__PURE__ */ jsxs(RtnPct, { $hue: hue, children: [
3614
+ row.retentionRate.toFixed(1),
3615
+ "%"
3616
+ ] })
3617
+ ]
3618
+ },
3619
+ row.n
3620
+ );
3621
+ }) })
3184
3622
  ] }),
3185
- someSelected && /* @__PURE__ */ jsxs(
3186
- Button,
3623
+ /* @__PURE__ */ jsx(RingGrid, { children: [
3187
3624
  {
3188
- variant: "danger-light",
3189
- size: "S",
3190
- onClick: () => setConfirmRevokeMany(true),
3191
- "data-testid": "revoke-selected-btn",
3192
- children: [
3193
- "Revoke sessions for ",
3194
- selected.size,
3195
- " user",
3196
- selected.size !== 1 ? "s" : ""
3197
- ]
3625
+ label: "Daily Active",
3626
+ value: stats.activeUsers.daily.active,
3627
+ pct: stats.activeUsers.daily.percentage,
3628
+ color: T.amber,
3629
+ sparkline: activeSpark,
3630
+ delay: 8
3631
+ },
3632
+ {
3633
+ label: "Weekly Active",
3634
+ value: stats.activeUsers.weekly.active,
3635
+ pct: stats.activeUsers.weekly.percentage,
3636
+ color: T.green,
3637
+ sparkline: activeSpark,
3638
+ delay: 9
3639
+ },
3640
+ {
3641
+ label: "Monthly Active",
3642
+ value: stats.activeUsers.monthly.active,
3643
+ pct: stats.activeUsers.monthly.percentage,
3644
+ color: T.accent,
3645
+ sparkline: activeSpark,
3646
+ delay: 10
3198
3647
  }
3199
- )
3200
- ] }),
3201
- sessionsQuery.isError && /* @__PURE__ */ jsx("div", { style: { color: "#d02b20", fontSize: 12, padding: "8px 0" }, children: sessionsQuery.error instanceof Error ? sessionsQuery.error.message : "An error occurred" }),
3202
- /* @__PURE__ */ jsx(TableCard$1, { children: sessionsQuery.isLoading ? /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 8, children: /* @__PURE__ */ jsx(Loader, { children: "Loading sessions…" }) }) : usersWithSessions.length === 0 ? /* @__PURE__ */ jsx(EmptyState, { children: "No active sessions" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
3203
- /* @__PURE__ */ jsxs(ColumnHeader, { children: [
3204
- /* @__PURE__ */ jsx(
3205
- Checkbox,
3206
- {
3207
- checked: allSelected,
3208
- onCheckedChange: handleSelectAll,
3209
- "aria-label": "Select all users"
3210
- }
3211
- ),
3212
- /* @__PURE__ */ jsx("span", { children: "User / Sessions" }),
3213
- /* @__PURE__ */ jsx(ColumnHeaderRight, { children: "Sessions" })
3214
- ] }),
3215
- usersWithSessions.map((userRow, i) => /* @__PURE__ */ jsxs(UserGroup, { $i: i, "data-testid": "session-user-row", children: [
3216
- /* @__PURE__ */ jsxs(UserRowHeader, { children: [
3648
+ ].map(({ label, value, pct, color, delay }) => {
3649
+ const isPos = pct == null || pct >= 0;
3650
+ return /* @__PURE__ */ jsxs(RingCard, { $delay: delay, children: [
3217
3651
  /* @__PURE__ */ jsx(
3218
- Checkbox,
3652
+ ProgressRing,
3219
3653
  {
3220
- checked: selected.has(userRow.id),
3221
- onCheckedChange: () => toggleSelect(userRow.id),
3222
- "aria-label": `Select ${userRow.name}`
3654
+ value: value ?? 0,
3655
+ max: activeMax,
3656
+ color,
3657
+ size: 56
3223
3658
  }
3224
3659
  ),
3225
- /* @__PURE__ */ jsx(Avatar, { name: userRow.name ?? "", src: null, size: 30 }),
3226
- /* @__PURE__ */ jsxs(UserInfo, { children: [
3227
- /* @__PURE__ */ jsx(UserName$1, { children: userRow.name }),
3228
- /* @__PURE__ */ jsx(UserEmail, { children: userRow.email })
3229
- ] }),
3230
- /* @__PURE__ */ jsxs(SessionCountChip, { children: [
3231
- userRow.sessions.length,
3232
- " session",
3233
- userRow.sessions.length !== 1 ? "s" : ""
3234
- ] })
3235
- ] }),
3236
- userRow.sessions.map((session) => /* @__PURE__ */ jsxs(SessionCard, { "data-testid": "session-row", children: [
3237
- /* @__PURE__ */ jsxs(SessionMeta, { children: [
3238
- /* @__PURE__ */ jsxs(
3239
- Flex,
3660
+ /* @__PURE__ */ jsxs(RingInfo, { children: [
3661
+ /* @__PURE__ */ jsx(RingLabel, { children: label }),
3662
+ /* @__PURE__ */ jsx(RingVal, { children: (value ?? 0).toLocaleString() }),
3663
+ pct != null && /* @__PURE__ */ jsxs(
3664
+ TrendBadge,
3240
3665
  {
3241
- gap: 2,
3242
- alignItems: "center",
3243
- style: { flexWrap: "wrap" },
3666
+ $pos: isPos,
3667
+ style: { marginTop: 4, marginBottom: 0 },
3244
3668
  children: [
3245
- session.ipAddress && /* @__PURE__ */ jsx(IpChip, { children: session.ipAddress }),
3246
- /* @__PURE__ */ jsxs(TimestampText, { children: [
3247
- "Created ",
3248
- new Date(session.createdAt).toLocaleString(),
3249
- " ",
3250
- "· Expires",
3251
- " ",
3252
- new Date(session.expiresAt).toLocaleString()
3253
- ] })
3669
+ isPos ? "↑" : "↓",
3670
+ " ",
3671
+ Math.abs(pct).toFixed(1),
3672
+ "%"
3254
3673
  ]
3255
3674
  }
3256
- ),
3257
- session.userAgent && /* @__PURE__ */ jsx(AgentText, { children: session.userAgent })
3258
- ] }),
3259
- /* @__PURE__ */ jsx(
3260
- IconButton,
3261
- {
3262
- label: "Revoke session",
3263
- onClick: () => setConfirmRevokeSessionId(session.id),
3264
- style: { flexShrink: 0 },
3265
- "data-testid": "revoke-session-btn",
3266
- children: /* @__PURE__ */ jsx(Trash, {})
3267
- }
3268
- )
3269
- ] }, session.id))
3270
- ] }, userRow.id))
3271
- ] }) }),
3272
- usersWithSessions.length === PAGE_SIZE$1 && /* @__PURE__ */ jsx(Flex, { justifyContent: "flex-end", children: /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
3273
- /* @__PURE__ */ jsx(
3274
- Button,
3275
- {
3276
- variant: "tertiary",
3277
- size: "S",
3278
- disabled: page === 1,
3279
- onClick: () => setPage((p) => p - 1),
3280
- children: "Previous"
3281
- }
3282
- ),
3283
- /* @__PURE__ */ jsx(
3284
- Button,
3285
- {
3286
- variant: "tertiary",
3287
- size: "S",
3288
- onClick: () => setPage((p) => p + 1),
3289
- children: "Next"
3290
- }
3291
- )
3292
- ] }) }),
3293
- confirmRevokeSessionId && /* @__PURE__ */ jsx(
3294
- ConfirmDialog,
3295
- {
3296
- title: "Revoke session",
3297
- message: "Are you sure you want to revoke this session? The user will be signed out on this device.",
3298
- confirmLabel: "Revoke",
3299
- loading: revokeSessionMutation.isLoading,
3300
- onConfirm: () => revokeSessionMutation.mutate(confirmRevokeSessionId),
3301
- onCancel: () => setConfirmRevokeSessionId(null)
3302
- }
3303
- ),
3304
- confirmRevokeMany && /* @__PURE__ */ jsx(
3305
- ConfirmDialog,
3306
- {
3307
- title: `Revoke sessions for ${selected.size} user${selected.size !== 1 ? "s" : ""}`,
3308
- 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.`,
3309
- confirmLabel: "Revoke all",
3310
- loading: revokeManyMutation.isLoading,
3311
- onConfirm: () => revokeManyMutation.mutate([...selected]),
3312
- onCancel: () => setConfirmRevokeMany(false)
3313
- }
3314
- )
3675
+ )
3676
+ ] })
3677
+ ] }, label);
3678
+ }) })
3679
+ ] })
3315
3680
  ] });
3316
3681
  }
3317
3682
  function useUsers(options = {}) {
@@ -3552,6 +3917,45 @@ const ReadOnlyCodeInput = styled.div`
3552
3917
  word-break: break-all;
3553
3918
  overflow-wrap: anywhere;
3554
3919
  `;
3920
+ const SessionCard = styled.div`
3921
+ display: flex;
3922
+ align-items: flex-start;
3923
+ justify-content: space-between;
3924
+ gap: 12px;
3925
+ padding: 10px 14px;
3926
+ background: white;
3927
+ border: 1px solid #eaeaef;
3928
+ border-radius: 8px;
3929
+ `;
3930
+ const SessionMeta = styled.div`
3931
+ display: flex;
3932
+ flex-direction: column;
3933
+ gap: 3px;
3934
+ flex: 1;
3935
+ min-width: 0;
3936
+ `;
3937
+ const IpChip = styled.span`
3938
+ display: inline-block;
3939
+ background: #f0f0ff;
3940
+ color: #4945ff;
3941
+ border-radius: 5px;
3942
+ padding: 2px 6px;
3943
+ font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
3944
+ font-size: 11px;
3945
+ font-weight: 600;
3946
+ `;
3947
+ const TimestampText = styled.span`
3948
+ font-size: 11px;
3949
+ color: #8e8ea9;
3950
+ `;
3951
+ const AgentText = styled.span`
3952
+ font-size: 11px;
3953
+ color: #b8b8c7;
3954
+ overflow: hidden;
3955
+ text-overflow: ellipsis;
3956
+ white-space: nowrap;
3957
+ display: block;
3958
+ `;
3555
3959
  const STANDARD_FIELDS = /* @__PURE__ */ new Set([
3556
3960
  "id",
3557
3961
  "name",
@@ -3573,6 +3977,7 @@ function UserDetailDrawer({
3573
3977
  }) {
3574
3978
  const qc = useQueryClient();
3575
3979
  const { toggleNotification } = useNotification();
3980
+ const { get, put } = useFetchClient();
3576
3981
  const schemaQuery = useModelSchema("user");
3577
3982
  const userQuery = useQuery({
3578
3983
  queryKey: ["dash-user", userId],
@@ -3594,6 +3999,27 @@ function UserDetailDrawer({
3594
3999
  return result.data?.organizations ?? [];
3595
4000
  }
3596
4001
  });
4002
+ const sessionsQuery = useQuery({
4003
+ queryKey: ["dash-user-sessions", userId],
4004
+ queryFn: async () => {
4005
+ const { data } = await get(
4006
+ `/better-auth-dashboard/db?uid=plugin::better-auth.session&filters[userId][$eq]=${userId}&sort[0]=createdAt:desc&pagination[pageSize]=50`
4007
+ );
4008
+ return data.results ?? [];
4009
+ }
4010
+ });
4011
+ const strapiUserQuery = useQuery({
4012
+ queryKey: ["dash-strapi-user", userId],
4013
+ enabled: !!schemaQuery.data,
4014
+ queryFn: async () => {
4015
+ const relationFields = Object.entries(schemaQuery.data).filter(([, attr]) => attr.type === "relation").map(([fieldName]) => fieldName);
4016
+ const populateParam = relationFields.length > 0 ? `&populate=${encodeURIComponent(relationFields.join(","))}` : "";
4017
+ const { data } = await get(
4018
+ `/better-auth-dashboard/db?uid=plugin::better-auth.user&filters[id][$eq]=${userId}&pagination[pageSize]=1${populateParam}`
4019
+ );
4020
+ return data.results?.[0] ?? null;
4021
+ }
4022
+ });
3597
4023
  const [activeTab, setActiveTab] = useState("profile");
3598
4024
  const [editName, setEditName] = useState(void 0);
3599
4025
  const [editEmail, setEditEmail] = useState(void 0);
@@ -3604,6 +4030,8 @@ function UserDetailDrawer({
3604
4030
  const [banReason, setBanReason] = useState("");
3605
4031
  const [banExpiresDays, setBanExpiresDays] = useState("");
3606
4032
  const [confirmRevokeAll, setConfirmRevokeAll] = useState(false);
4033
+ const [confirmRevokeSessionId, setConfirmRevokeSessionId] = useState(null);
4034
+ const [confirmBan, setConfirmBan] = useState(false);
3607
4035
  const [confirmUnban, setConfirmUnban] = useState(false);
3608
4036
  const [confirmUnlinkAccountId, setConfirmUnlinkAccountId] = useState(null);
3609
4037
  const [confirmDisable2FA, setConfirmDisable2FA] = useState(false);
@@ -3622,7 +4050,7 @@ function UserDetailDrawer({
3622
4050
  setEditExtra((prev) => ({ ...prev, [name]: value }));
3623
4051
  };
3624
4052
  const extraData = {
3625
- ...user,
4053
+ ...strapiUserQuery.data ?? {},
3626
4054
  ...editExtra
3627
4055
  };
3628
4056
  const updateMutation = useMutation({
@@ -3633,17 +4061,17 @@ function UserDetailDrawer({
3633
4061
  if (editEmailVerified !== void 0)
3634
4062
  body.emailVerified = editEmailVerified;
3635
4063
  if (editImage !== void 0) body.image = editImage;
3636
- const result = await client.dash.updateUser(
3637
- body,
3638
- withContext({ userId })
4064
+ const documentId = strapiUserQuery.data?.documentId;
4065
+ if (!documentId) throw new Error("Could not resolve documentId for user");
4066
+ await put(
4067
+ `/better-auth-dashboard/db/${documentId}?uid=plugin::better-auth.user`,
4068
+ body
3639
4069
  );
3640
- if (result.error)
3641
- throw new Error(result.error.message ?? "Update failed");
3642
- return result.data;
3643
4070
  },
3644
4071
  onSuccess: () => {
3645
4072
  qc.invalidateQueries({ queryKey: ["dash-user", userId] });
3646
4073
  qc.invalidateQueries({ queryKey: ["dash-users"] });
4074
+ qc.invalidateQueries({ queryKey: ["dash-strapi-user", userId] });
3647
4075
  setEditName(void 0);
3648
4076
  setEditEmail(void 0);
3649
4077
  setEditEmailVerified(void 0);
@@ -3662,6 +4090,27 @@ function UserDetailDrawer({
3662
4090
  });
3663
4091
  }
3664
4092
  });
4093
+ const revokeSessionMutation = useMutation({
4094
+ mutationFn: async (sessionId) => {
4095
+ const result = await client.dash.sessions.revoke(
4096
+ {},
4097
+ withContext({ sessionId })
4098
+ );
4099
+ if (result.error)
4100
+ throw new Error(result.error.message ?? "Revoke failed");
4101
+ },
4102
+ onSuccess: () => {
4103
+ setConfirmRevokeSessionId(null);
4104
+ qc.invalidateQueries({ queryKey: ["dash-user-sessions", userId] });
4105
+ toggleNotification({ type: "success", message: "Session revoked" });
4106
+ },
4107
+ onError: (err) => {
4108
+ toggleNotification({
4109
+ type: "danger",
4110
+ message: err.message ?? "Failed to revoke session"
4111
+ });
4112
+ }
4113
+ });
3665
4114
  const passwordMutation = useMutation({
3666
4115
  mutationFn: async () => {
3667
4116
  const result = await client.dash.setPassword(
@@ -3699,6 +4148,7 @@ function UserDetailDrawer({
3699
4148
  return result.data;
3700
4149
  },
3701
4150
  onSuccess: () => {
4151
+ setConfirmBan(false);
3702
4152
  qc.invalidateQueries({ queryKey: ["dash-user", userId] });
3703
4153
  qc.invalidateQueries({ queryKey: ["dash-users"] });
3704
4154
  setBanReason("");
@@ -4314,7 +4764,8 @@ function UserDetailDrawer({
4314
4764
  ] })
4315
4765
  ] })
4316
4766
  ] }) }),
4317
- banEnabled && /* @__PURE__ */ jsx(Tabs.Content, { value: "ban", children: /* @__PURE__ */ jsx(Flex, { direction: "column", gap: 4, paddingTop: 6, children: user?.banned ? /* @__PURE__ */ jsxs(Fragment, { children: [
4767
+ banEnabled && /* @__PURE__ */ jsx(Tabs.Content, { value: "ban", children: /* @__PURE__ */ jsx(Flex, { direction: "column", gap: 5, paddingTop: 6, children: user?.banned ? /* @__PURE__ */ jsxs(FormSection, { children: [
4768
+ /* @__PURE__ */ jsx(SectionLabel, { children: "Ban status" }),
4318
4769
  /* @__PURE__ */ jsxs(WarnCard, { children: [
4319
4770
  /* @__PURE__ */ jsx(
4320
4771
  Typography,
@@ -4329,7 +4780,7 @@ function UserDetailDrawer({
4329
4780
  "Reason: ",
4330
4781
  user.banReason
4331
4782
  ] }),
4332
- user.banExpires && /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "danger600", children: [
4783
+ user.banExpires ? /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "danger600", children: [
4333
4784
  "Expires:",
4334
4785
  " ",
4335
4786
  new Date(user.banExpires).toLocaleDateString(
@@ -4341,8 +4792,7 @@ function UserDetailDrawer({
4341
4792
  day: "numeric"
4342
4793
  }
4343
4794
  )
4344
- ] }),
4345
- !user.banExpires && /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "danger600", children: "Duration: Permanent" })
4795
+ ] }) : /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "danger600", children: "Duration: Permanent" })
4346
4796
  ] }),
4347
4797
  /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(
4348
4798
  Button,
@@ -4352,27 +4802,28 @@ function UserDetailDrawer({
4352
4802
  children: "Lift ban"
4353
4803
  }
4354
4804
  ) })
4355
- ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
4805
+ ] }) : /* @__PURE__ */ jsxs(FormSection, { children: [
4806
+ /* @__PURE__ */ jsx(SectionLabel, { children: "Apply ban" }),
4807
+ /* @__PURE__ */ jsxs(
4808
+ Field.Root,
4809
+ {
4810
+ hint: "Optional — will be shown to the user",
4811
+ style: { width: "100%" },
4812
+ children: [
4813
+ /* @__PURE__ */ jsx(Field.Label, { children: "Reason" }),
4814
+ /* @__PURE__ */ jsx(
4815
+ TextInput,
4816
+ {
4817
+ value: banReason,
4818
+ onChange: (e) => setBanReason(e.target.value),
4819
+ placeholder: "e.g. Violated terms of service"
4820
+ }
4821
+ ),
4822
+ /* @__PURE__ */ jsx(Field.Hint, {})
4823
+ ]
4824
+ }
4825
+ ),
4356
4826
  /* @__PURE__ */ jsxs(Grid.Root, { gap: 4, children: [
4357
- /* @__PURE__ */ jsx(Grid.Item, { col: 12, children: /* @__PURE__ */ jsxs(
4358
- Field.Root,
4359
- {
4360
- hint: "Optional — will be shown to the user",
4361
- style: { width: "100%" },
4362
- children: [
4363
- /* @__PURE__ */ jsx(Field.Label, { children: "Reason" }),
4364
- /* @__PURE__ */ jsx(
4365
- TextInput,
4366
- {
4367
- value: banReason,
4368
- onChange: (e) => setBanReason(e.target.value),
4369
- placeholder: "e.g. Violated terms of service"
4370
- }
4371
- ),
4372
- /* @__PURE__ */ jsx(Field.Hint, {})
4373
- ]
4374
- }
4375
- ) }),
4376
4827
  /* @__PURE__ */ jsx(Grid.Item, { col: 6, children: /* @__PURE__ */ jsxs(
4377
4828
  Field.Root,
4378
4829
  {
@@ -4393,24 +4844,13 @@ function UserDetailDrawer({
4393
4844
  ]
4394
4845
  }
4395
4846
  ) }),
4396
- /* @__PURE__ */ jsx(Grid.Item, { col: 6, children: banExpiryPreview ? /* @__PURE__ */ jsx(
4397
- Flex,
4398
- {
4399
- direction: "column",
4400
- justifyContent: "flex-end",
4401
- style: { height: "100%", paddingBottom: 4 },
4402
- children: /* @__PURE__ */ jsxs(PreviewPill, { children: [
4403
- "⏱ Expires ",
4404
- banExpiryPreview
4405
- ] })
4406
- }
4407
- ) : /* @__PURE__ */ jsx(
4847
+ /* @__PURE__ */ jsx(Grid.Item, { col: 6, children: /* @__PURE__ */ jsx(
4408
4848
  Flex,
4409
4849
  {
4410
4850
  direction: "column",
4411
4851
  justifyContent: "flex-end",
4412
4852
  style: { height: "100%", paddingBottom: 4 },
4413
- children: /* @__PURE__ */ jsx(PreviewPill, { children: "♾ Permanent ban" })
4853
+ children: /* @__PURE__ */ jsx(PreviewPill, { children: banExpiryPreview ? `⏱ Expires ${banExpiryPreview}` : "♾ Permanent ban" })
4414
4854
  }
4415
4855
  ) })
4416
4856
  ] }),
@@ -4418,31 +4858,68 @@ function UserDetailDrawer({
4418
4858
  Button,
4419
4859
  {
4420
4860
  variant: "danger",
4421
- loading: banMutation.isLoading,
4422
- onClick: () => banMutation.mutate(),
4861
+ onClick: () => setConfirmBan(true),
4423
4862
  children: "Ban user"
4424
4863
  }
4425
4864
  ) })
4426
4865
  ] }) }) }),
4427
- /* @__PURE__ */ jsx(Tabs.Content, { value: "sessions", children: /* @__PURE__ */ jsx(Flex, { direction: "column", gap: 5, paddingTop: 6, children: /* @__PURE__ */ jsxs(FormSection, { children: [
4428
- /* @__PURE__ */ jsx(SectionLabel, { children: "Session management" }),
4429
- /* @__PURE__ */ jsxs(AccountRow, { children: [
4430
- /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 1, alignItems: "flex-start", children: [
4431
- /* @__PURE__ */ jsx(Typography, { variant: "omega", fontWeight: "semiBold", children: "Revoke all sessions" }),
4432
- /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: "Signs the user out immediately on all devices." })
4433
- ] }),
4434
- /* @__PURE__ */ jsx(
4435
- Button,
4436
- {
4437
- variant: "danger-light",
4438
- size: "S",
4439
- onClick: () => setConfirmRevokeAll(true),
4440
- style: { flexShrink: 0 },
4441
- children: "Revoke all"
4442
- }
4443
- )
4866
+ /* @__PURE__ */ jsx(Tabs.Content, { value: "sessions", children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 5, paddingTop: 6, children: [
4867
+ /* @__PURE__ */ jsxs(FormSection, { children: [
4868
+ /* @__PURE__ */ jsx(SectionLabel, { children: "Session management" }),
4869
+ /* @__PURE__ */ jsxs(AccountRow, { children: [
4870
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 1, alignItems: "flex-start", children: [
4871
+ /* @__PURE__ */ jsx(Typography, { variant: "omega", fontWeight: "semiBold", children: "Revoke all sessions" }),
4872
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: "Signs the user out immediately on all devices." })
4873
+ ] }),
4874
+ /* @__PURE__ */ jsx(
4875
+ Button,
4876
+ {
4877
+ variant: "danger-light",
4878
+ size: "S",
4879
+ onClick: () => setConfirmRevokeAll(true),
4880
+ style: { flexShrink: 0 },
4881
+ children: "Revoke all"
4882
+ }
4883
+ )
4884
+ ] })
4885
+ ] }),
4886
+ /* @__PURE__ */ jsxs(FormSection, { children: [
4887
+ /* @__PURE__ */ jsx(SectionLabel, { children: "Active sessions" }),
4888
+ sessionsQuery.isLoading ? /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ jsx(Loader, { children: "Loading sessions…" }) }) : (sessionsQuery.data ?? []).length === 0 ? /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: "No active sessions." }) : /* @__PURE__ */ jsx(Flex, { direction: "column", gap: 2, alignItems: "stretch", children: (sessionsQuery.data ?? []).map((session) => /* @__PURE__ */ jsxs(SessionCard, { children: [
4889
+ /* @__PURE__ */ jsxs(SessionMeta, { children: [
4890
+ /* @__PURE__ */ jsxs(
4891
+ Flex,
4892
+ {
4893
+ gap: 2,
4894
+ alignItems: "center",
4895
+ style: { flexWrap: "wrap" },
4896
+ children: [
4897
+ session.ipAddress && /* @__PURE__ */ jsx(IpChip, { children: session.ipAddress }),
4898
+ /* @__PURE__ */ jsxs(TimestampText, { children: [
4899
+ "Created",
4900
+ " ",
4901
+ new Date(session.createdAt).toLocaleString(),
4902
+ " · Expires",
4903
+ " ",
4904
+ new Date(session.expiresAt).toLocaleString()
4905
+ ] })
4906
+ ]
4907
+ }
4908
+ ),
4909
+ session.userAgent && /* @__PURE__ */ jsx(AgentText, { children: session.userAgent })
4910
+ ] }),
4911
+ /* @__PURE__ */ jsx(
4912
+ IconButton,
4913
+ {
4914
+ label: "Revoke session",
4915
+ onClick: () => setConfirmRevokeSessionId(String(session.id)),
4916
+ style: { flexShrink: 0 },
4917
+ children: /* @__PURE__ */ jsx(Trash, {})
4918
+ }
4919
+ )
4920
+ ] }, session.documentId)) })
4444
4921
  ] })
4445
- ] }) }) }),
4922
+ ] }) }),
4446
4923
  /* @__PURE__ */ jsx(Tabs.Content, { value: "organizations", children: /* @__PURE__ */ jsx(Flex, { direction: "column", gap: 5, paddingTop: 6, children: /* @__PURE__ */ jsxs(FormSection, { children: [
4447
4924
  /* @__PURE__ */ jsx(SectionLabel, { children: "Memberships" }),
4448
4925
  orgsQuery.isLoading ? /* @__PURE__ */ jsx(Typography, { textColor: "neutral500", children: "Loading…" }) : (orgsQuery.data ?? []).length === 0 ? /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: "Not a member of any organizations." }) : /* @__PURE__ */ jsx(Flex, { direction: "column", gap: 2, children: (orgsQuery.data ?? []).map(
@@ -4479,6 +4956,29 @@ function UserDetailDrawer({
4479
4956
  onCancel: () => setConfirmRevokeAll(false)
4480
4957
  }
4481
4958
  ),
4959
+ confirmRevokeSessionId && /* @__PURE__ */ jsx(
4960
+ ConfirmDialog,
4961
+ {
4962
+ title: "Revoke session",
4963
+ message: "Are you sure you want to revoke this session? The user will be signed out on this device.",
4964
+ confirmLabel: "Revoke",
4965
+ loading: revokeSessionMutation.isLoading,
4966
+ onConfirm: () => revokeSessionMutation.mutate(confirmRevokeSessionId),
4967
+ onCancel: () => setConfirmRevokeSessionId(null)
4968
+ }
4969
+ ),
4970
+ confirmBan && /* @__PURE__ */ jsx(
4971
+ ConfirmDialog,
4972
+ {
4973
+ title: "Ban user",
4974
+ 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.",
4975
+ confirmLabel: "Ban user",
4976
+ variant: "danger",
4977
+ loading: banMutation.isLoading,
4978
+ onConfirm: () => banMutation.mutate(),
4979
+ onCancel: () => setConfirmBan(false)
4980
+ }
4981
+ ),
4482
4982
  confirmUnban && /* @__PURE__ */ jsx(
4483
4983
  ConfirmDialog,
4484
4984
  {
@@ -4709,6 +5209,7 @@ const Toolbar = styled.div`
4709
5209
  `;
4710
5210
  function UsersPage({ config }) {
4711
5211
  const qc = useQueryClient();
5212
+ const { toggleNotification } = useNotification();
4712
5213
  const banEnabled = hasPlugin(config, "admin");
4713
5214
  const emailVerificationEnabled = config.emailVerification.sendVerificationEmailEnabled;
4714
5215
  const twoFactorEnabled = hasPlugin(config, "two-factor");
@@ -4740,6 +5241,13 @@ function UsersPage({ config }) {
4740
5241
  setConfirmDelete(null);
4741
5242
  qc.invalidateQueries({ queryKey: ["dash-users"] });
4742
5243
  qc.invalidateQueries({ queryKey: ["dash-user-stats"] });
5244
+ toggleNotification({ type: "success", message: "User deleted" });
5245
+ },
5246
+ onError: (err) => {
5247
+ toggleNotification({
5248
+ type: "danger",
5249
+ message: err.message ?? "Failed to delete user"
5250
+ });
4743
5251
  }
4744
5252
  });
4745
5253
  const deleteManyMutation = useMutation({
@@ -4752,11 +5260,21 @@ function UsersPage({ config }) {
4752
5260
  throw new Error(result.error.message ?? "Delete failed");
4753
5261
  return result.data;
4754
5262
  },
4755
- onSuccess: () => {
5263
+ onSuccess: (_data, userIds) => {
4756
5264
  setConfirmDeleteMany(false);
4757
5265
  setSelected(/* @__PURE__ */ new Set());
4758
5266
  qc.invalidateQueries({ queryKey: ["dash-users"] });
4759
5267
  qc.invalidateQueries({ queryKey: ["dash-user-stats"] });
5268
+ toggleNotification({
5269
+ type: "success",
5270
+ message: `${userIds.length} user${userIds.length !== 1 ? "s" : ""} deleted`
5271
+ });
5272
+ },
5273
+ onError: (err) => {
5274
+ toggleNotification({
5275
+ type: "danger",
5276
+ message: err.message ?? "Failed to delete users"
5277
+ });
4760
5278
  }
4761
5279
  });
4762
5280
  const banManyMutation = useMutation({
@@ -4768,10 +5286,20 @@ function UsersPage({ config }) {
4768
5286
  if (result.error) throw new Error(result.error.message ?? "Ban failed");
4769
5287
  return result.data;
4770
5288
  },
4771
- onSuccess: () => {
5289
+ onSuccess: (_data, userIds) => {
4772
5290
  setConfirmBanMany(false);
4773
5291
  setSelected(/* @__PURE__ */ new Set());
4774
5292
  qc.invalidateQueries({ queryKey: ["dash-users"] });
5293
+ toggleNotification({
5294
+ type: "success",
5295
+ message: `${userIds.length} user${userIds.length !== 1 ? "s" : ""} banned`
5296
+ });
5297
+ },
5298
+ onError: (err) => {
5299
+ toggleNotification({
5300
+ type: "danger",
5301
+ message: err.message ?? "Failed to ban users"
5302
+ });
4775
5303
  }
4776
5304
  });
4777
5305
  const toggleSelect = (id) => {
@@ -4950,14 +5478,28 @@ function UsersPage({ config }) {
4950
5478
  user.id
4951
5479
  )) })
4952
5480
  ] }) }),
4953
- pageCount > 1 && /* @__PURE__ */ jsx(Flex, { justifyContent: "flex-end", children: /* @__PURE__ */ jsx(
4954
- Pagination,
4955
- {
4956
- activePage: page,
4957
- pageCount,
4958
- onChangePage: setPage
4959
- }
4960
- ) }),
5481
+ pageCount > 1 && /* @__PURE__ */ jsx(Flex, { justifyContent: "flex-end", children: /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
5482
+ /* @__PURE__ */ jsx(
5483
+ Button,
5484
+ {
5485
+ variant: "tertiary",
5486
+ size: "S",
5487
+ disabled: page === 1,
5488
+ onClick: () => setPage((p) => p - 1),
5489
+ children: "Previous"
5490
+ }
5491
+ ),
5492
+ /* @__PURE__ */ jsx(
5493
+ Button,
5494
+ {
5495
+ variant: "tertiary",
5496
+ size: "S",
5497
+ disabled: page >= pageCount,
5498
+ onClick: () => setPage((p) => p + 1),
5499
+ children: "Next"
5500
+ }
5501
+ )
5502
+ ] }) }),
4961
5503
  showCreate && /* @__PURE__ */ jsx(CreateUserDialog, { onClose: () => setShowCreate(false) }),
4962
5504
  detailUserId && /* @__PURE__ */ jsx(
4963
5505
  UserDetailDrawer,
@@ -5013,8 +5555,7 @@ const Accent = styled.div`
5013
5555
  const BrandIcon = styled.div`
5014
5556
  width: 30px;
5015
5557
  height: 30px;
5016
- border-radius: 7px;
5017
- background: linear-gradient(135deg, #4945ff 0%, #7b79ff 100%);
5558
+ background: black;
5018
5559
  display: flex;
5019
5560
  align-items: center;
5020
5561
  justify-content: center;
@@ -5090,7 +5631,7 @@ function App() {
5090
5631
  paddingBottom: 4,
5091
5632
  children: [
5092
5633
  /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
5093
- /* @__PURE__ */ jsx(BrandIcon, { children: "BA" }),
5634
+ /* @__PURE__ */ jsx(BrandIcon, { children: /* @__PURE__ */ jsx(PluginIcon, {}) }),
5094
5635
  /* @__PURE__ */ jsxs(Box, { children: [
5095
5636
  /* @__PURE__ */ jsx(Typography, { variant: "beta", textColor: "neutral800", children: "Better Auth" }),
5096
5637
  /* @__PURE__ */ jsx(Box, { paddingTop: "2px", children: /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: "Authentication Dashboard" }) })
@@ -5110,8 +5651,7 @@ function App() {
5110
5651
  "data-testid": "nav-organizations",
5111
5652
  children: "Organizations"
5112
5653
  }
5113
- ),
5114
- /* @__PURE__ */ jsx(Tabs.Trigger, { value: "sessions", "data-testid": "nav-sessions", children: "Sessions" })
5654
+ )
5115
5655
  ] })
5116
5656
  ]
5117
5657
  }
@@ -5121,8 +5661,7 @@ function App() {
5121
5661
  ),
5122
5662
  /* @__PURE__ */ jsx(Tabs.Content, { value: "overview", "data-testid": "tab-overview", children: /* @__PURE__ */ jsx(OverviewPage, {}) }),
5123
5663
  /* @__PURE__ */ jsx(Tabs.Content, { value: "users", "data-testid": "tab-users", children: /* @__PURE__ */ jsx(UsersPage, { config }) }),
5124
- orgEnabled && /* @__PURE__ */ jsx(Tabs.Content, { value: "organizations", "data-testid": "tab-organizations", children: /* @__PURE__ */ jsx(OrganizationsPage, { teamsEnabled }) }),
5125
- /* @__PURE__ */ jsx(Tabs.Content, { value: "sessions", "data-testid": "tab-sessions", children: /* @__PURE__ */ jsx(SessionsPage, {}) })
5664
+ orgEnabled && /* @__PURE__ */ jsx(Tabs.Content, { value: "organizations", "data-testid": "tab-organizations", children: /* @__PURE__ */ jsx(OrganizationsPage, { teamsEnabled }) })
5126
5665
  ] }) });
5127
5666
  }
5128
5667
  function Root() {