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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
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";
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-BpruO4vo.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;
@@ -930,6 +1076,14 @@ function OrganizationDetail({
930
1076
  return result.data ?? [];
931
1077
  }
932
1078
  });
1079
+ const invitationsQuery = useQuery({
1080
+ queryKey: ["dash-org-invitations", organizationId],
1081
+ queryFn: async () => {
1082
+ const result = await client.dash.organization[organizationId].invitations({}, withContext({ organizationId }));
1083
+ if (result.error) throw new Error(result.error.message ?? "Failed");
1084
+ return result.data ?? [];
1085
+ }
1086
+ });
933
1087
  const [activeTab, setActiveTab] = useState("details");
934
1088
  const [editName, setEditName] = useState(void 0);
935
1089
  const [editSlug, setEditSlug] = useState(void 0);
@@ -942,6 +1096,9 @@ function OrganizationDetail({
942
1096
  const [confirmDeleteSsoId, setConfirmDeleteSsoId] = useState(
943
1097
  null
944
1098
  );
1099
+ const [inviteEmail, setInviteEmail] = useState("");
1100
+ const [inviteRole, setInviteRole] = useState("member");
1101
+ const [confirmCancelInvitationId, setConfirmCancelInvitationId] = useState(null);
945
1102
  const handleExtraChange = (name, value) => {
946
1103
  setEditExtra((prev) => ({ ...prev, [name]: value }));
947
1104
  };
@@ -1112,9 +1269,74 @@ function OrganizationDetail({
1112
1269
  });
1113
1270
  }
1114
1271
  });
1272
+ const inviteMemberMutation = useMutation({
1273
+ mutationFn: async () => {
1274
+ const result = await client.dash.organization.inviteMember(
1275
+ { email: inviteEmail, role: inviteRole, invitedBy: "" },
1276
+ withContext({ organizationId })
1277
+ );
1278
+ if (result.error) throw new Error(result.error.message ?? "Failed");
1279
+ return result.data;
1280
+ },
1281
+ onSuccess: () => {
1282
+ qc.invalidateQueries({
1283
+ queryKey: ["dash-org-invitations", organizationId]
1284
+ });
1285
+ setInviteEmail("");
1286
+ setInviteRole("member");
1287
+ toggleNotification({ type: "success", message: "Invitation sent" });
1288
+ },
1289
+ onError: (err) => {
1290
+ toggleNotification({
1291
+ type: "danger",
1292
+ message: err.message ?? "Failed to invite"
1293
+ });
1294
+ }
1295
+ });
1296
+ const cancelInvitationMutation = useMutation({
1297
+ mutationFn: async (invitationId) => {
1298
+ const result = await client.dash.organization.cancelInvitation(
1299
+ { invitationId },
1300
+ withContext({ organizationId })
1301
+ );
1302
+ if (result.error) throw new Error(result.error.message ?? "Failed");
1303
+ },
1304
+ onSuccess: () => {
1305
+ setConfirmCancelInvitationId(null);
1306
+ qc.invalidateQueries({
1307
+ queryKey: ["dash-org-invitations", organizationId]
1308
+ });
1309
+ toggleNotification({ type: "success", message: "Invitation cancelled" });
1310
+ },
1311
+ onError: (err) => {
1312
+ toggleNotification({
1313
+ type: "danger",
1314
+ message: err.message ?? "Failed to cancel"
1315
+ });
1316
+ }
1317
+ });
1318
+ const resendInvitationMutation = useMutation({
1319
+ mutationFn: async (invitationId) => {
1320
+ const result = await client.dash.organization.resendInvitation(
1321
+ { invitationId },
1322
+ withContext({ organizationId })
1323
+ );
1324
+ if (result.error) throw new Error(result.error.message ?? "Failed");
1325
+ },
1326
+ onSuccess: () => {
1327
+ toggleNotification({ type: "success", message: "Invitation resent" });
1328
+ },
1329
+ onError: (err) => {
1330
+ toggleNotification({
1331
+ type: "danger",
1332
+ message: err.message ?? "Failed to resend"
1333
+ });
1334
+ }
1335
+ });
1115
1336
  const members = membersQuery.data ?? [];
1116
1337
  const teams = teamsQuery.data ?? [];
1117
1338
  const ssoProviders = ssoQuery.data ?? [];
1339
+ const invitations = invitationsQuery.data ?? [];
1118
1340
  const hasOrgEdits = editName !== void 0 || editSlug !== void 0 || editLogo !== void 0 || Object.keys(editExtra).length > 0;
1119
1341
  const customFields = Object.entries(schemaQuery.data ?? {}).filter(([name]) => !STANDARD_ORG_FIELDS.has(name)).map(([name, attribute]) => ({ name, attribute }));
1120
1342
  const detailsFooter = activeTab === "details" ? /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -1171,6 +1393,11 @@ function OrganizationDetail({
1171
1393
  teams.length,
1172
1394
  ")"
1173
1395
  ] }),
1396
+ /* @__PURE__ */ jsxs(Tabs.Trigger, { value: "invitations", children: [
1397
+ "Invitations (",
1398
+ invitations.length,
1399
+ ")"
1400
+ ] }),
1174
1401
  /* @__PURE__ */ jsxs(Tabs.Trigger, { value: "sso", children: [
1175
1402
  "SSO (",
1176
1403
  ssoProviders.length,
@@ -1426,7 +1653,127 @@ function OrganizationDetail({
1426
1653
  },
1427
1654
  provider.id
1428
1655
  )) })
1429
- ] }) }) })
1656
+ ] }) }) }),
1657
+ /* @__PURE__ */ jsx(Tabs.Content, { value: "invitations", children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 5, paddingTop: 6, children: [
1658
+ /* @__PURE__ */ jsxs(FormSection, { children: [
1659
+ /* @__PURE__ */ jsx(SectionLabel, { children: "Invite member by email" }),
1660
+ /* @__PURE__ */ jsxs(Grid.Root, { gap: 3, children: [
1661
+ /* @__PURE__ */ jsx(Grid.Item, { col: 8, children: /* @__PURE__ */ jsxs(Field.Root, { children: [
1662
+ /* @__PURE__ */ jsx(Field.Label, { children: "Email address" }),
1663
+ /* @__PURE__ */ jsx(
1664
+ TextInput,
1665
+ {
1666
+ type: "email",
1667
+ value: inviteEmail,
1668
+ onChange: (e) => setInviteEmail(e.target.value),
1669
+ placeholder: "user@example.com"
1670
+ }
1671
+ )
1672
+ ] }) }),
1673
+ /* @__PURE__ */ jsx(Grid.Item, { col: 4, children: /* @__PURE__ */ jsxs(Field.Root, { children: [
1674
+ /* @__PURE__ */ jsx(Field.Label, { children: "Role" }),
1675
+ /* @__PURE__ */ jsxs(
1676
+ SingleSelect,
1677
+ {
1678
+ value: inviteRole,
1679
+ onChange: (v) => setInviteRole(String(v)),
1680
+ "aria-label": "Invite role",
1681
+ children: [
1682
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "member", children: "Member" }),
1683
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "admin", children: "Admin" }),
1684
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "owner", children: "Owner" })
1685
+ ]
1686
+ }
1687
+ )
1688
+ ] }) })
1689
+ ] }),
1690
+ /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(
1691
+ Button,
1692
+ {
1693
+ size: "S",
1694
+ startIcon: /* @__PURE__ */ jsx(Plus, {}),
1695
+ disabled: !inviteEmail,
1696
+ loading: inviteMemberMutation.isLoading,
1697
+ onClick: () => inviteMemberMutation.mutate(),
1698
+ children: "Send invitation"
1699
+ }
1700
+ ) })
1701
+ ] }),
1702
+ /* @__PURE__ */ jsxs(FormSection, { children: [
1703
+ /* @__PURE__ */ jsxs(SectionLabel, { children: [
1704
+ "Invitations (",
1705
+ invitations.length,
1706
+ ")"
1707
+ ] }),
1708
+ 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) => {
1709
+ const statusColor = {
1710
+ pending: "#f59e0b",
1711
+ accepted: "#5cb176",
1712
+ rejected: "#d02b20",
1713
+ canceled: "#8e8ea9"
1714
+ };
1715
+ const color = statusColor[inv.status] ?? "#8e8ea9";
1716
+ const isPending = inv.status === "pending";
1717
+ return /* @__PURE__ */ jsxs(AccountRow, { children: [
1718
+ /* @__PURE__ */ jsxs(
1719
+ Flex,
1720
+ {
1721
+ direction: "column",
1722
+ gap: 1,
1723
+ alignItems: "flex-start",
1724
+ style: { flex: 1 },
1725
+ children: [
1726
+ /* @__PURE__ */ jsx(Typography, { variant: "omega", fontWeight: "semiBold", children: inv.user?.name ?? inv.email }),
1727
+ /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "neutral500", children: [
1728
+ inv.email,
1729
+ " · role: ",
1730
+ inv.role
1731
+ ] }),
1732
+ /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "neutral500", children: [
1733
+ "Expires",
1734
+ " ",
1735
+ new Date(inv.expiresAt).toLocaleDateString()
1736
+ ] })
1737
+ ]
1738
+ }
1739
+ ),
1740
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
1741
+ /* @__PURE__ */ jsx(
1742
+ MonoChip,
1743
+ {
1744
+ style: {
1745
+ color,
1746
+ borderColor: color,
1747
+ background: `${color}18`
1748
+ },
1749
+ children: inv.status
1750
+ }
1751
+ ),
1752
+ isPending && /* @__PURE__ */ jsxs(Fragment, { children: [
1753
+ /* @__PURE__ */ jsx(
1754
+ Button,
1755
+ {
1756
+ size: "S",
1757
+ variant: "secondary",
1758
+ loading: resendInvitationMutation.isLoading,
1759
+ onClick: () => resendInvitationMutation.mutate(inv.id),
1760
+ children: "Resend"
1761
+ }
1762
+ ),
1763
+ /* @__PURE__ */ jsx(
1764
+ IconButton,
1765
+ {
1766
+ label: "Cancel invitation",
1767
+ onClick: () => setConfirmCancelInvitationId(inv.id),
1768
+ children: /* @__PURE__ */ jsx(Trash, {})
1769
+ }
1770
+ )
1771
+ ] })
1772
+ ] })
1773
+ ] }, inv.id);
1774
+ }) })
1775
+ ] })
1776
+ ] }) })
1430
1777
  ] }),
1431
1778
  confirmRemoveMemberId && /* @__PURE__ */ jsx(
1432
1779
  ConfirmDialog,
@@ -1460,6 +1807,17 @@ function OrganizationDetail({
1460
1807
  onConfirm: () => deleteSsoMutation.mutate(confirmDeleteSsoId),
1461
1808
  onCancel: () => setConfirmDeleteSsoId(null)
1462
1809
  }
1810
+ ),
1811
+ confirmCancelInvitationId && /* @__PURE__ */ jsx(
1812
+ ConfirmDialog,
1813
+ {
1814
+ title: "Cancel invitation",
1815
+ message: "Are you sure you want to cancel this invitation? The invite link will no longer work.",
1816
+ confirmLabel: "Cancel invitation",
1817
+ loading: cancelInvitationMutation.isLoading,
1818
+ onConfirm: () => cancelInvitationMutation.mutate(confirmCancelInvitationId),
1819
+ onCancel: () => setConfirmCancelInvitationId(null)
1820
+ }
1463
1821
  )
1464
1822
  ]
1465
1823
  }
@@ -1605,12 +1963,12 @@ function TeamRow({
1605
1963
  )
1606
1964
  ] });
1607
1965
  }
1608
- const PAGE_SIZE$2 = 25;
1609
- const fadeUp$3 = keyframes`
1966
+ const PAGE_SIZE$1 = 25;
1967
+ const fadeUp$2 = keyframes`
1610
1968
  from { opacity: 0; transform: translateY(6px); }
1611
1969
  to { opacity: 1; transform: translateY(0); }
1612
1970
  `;
1613
- const Wrap$3 = styled.div`
1971
+ const Wrap$2 = styled.div`
1614
1972
  padding: 28px 32px;
1615
1973
  background: #f6f6f9;
1616
1974
  min-height: 100%;
@@ -1618,39 +1976,39 @@ const Wrap$3 = styled.div`
1618
1976
  flex-direction: column;
1619
1977
  gap: 20px;
1620
1978
  `;
1621
- const PageHeader$2 = styled.div`
1979
+ const PageHeader$1 = styled.div`
1622
1980
  display: flex;
1623
1981
  justify-content: space-between;
1624
1982
  align-items: flex-start;
1625
1983
  `;
1626
- const TitleBlock$2 = styled.div`
1984
+ const TitleBlock$1 = styled.div`
1627
1985
  display: flex;
1628
1986
  flex-direction: column;
1629
1987
  gap: 4px;
1630
1988
  `;
1631
- const PageTitle$2 = styled.h1`
1989
+ const PageTitle$1 = styled.h1`
1632
1990
  margin: 0;
1633
1991
  font-size: 22px;
1634
1992
  font-weight: 800;
1635
1993
  color: #32324d;
1636
1994
  letter-spacing: -0.03em;
1637
1995
  `;
1638
- const PageSubtitle$2 = styled.p`
1996
+ const PageSubtitle$1 = styled.p`
1639
1997
  margin: 0;
1640
1998
  font-size: 12px;
1641
1999
  color: #8e8ea9;
1642
2000
  `;
1643
- const TableCard$2 = styled.div`
2001
+ const TableCard$1 = styled.div`
1644
2002
  background: #ffffff;
1645
2003
  border: 1px solid #eaeaef;
1646
2004
  border-radius: 10px;
1647
2005
  overflow: hidden;
1648
2006
  `;
1649
- const Table$2 = styled.table`
2007
+ const Table$1 = styled.table`
1650
2008
  width: 100%;
1651
2009
  border-collapse: collapse;
1652
2010
  `;
1653
- const TH$2 = styled.th`
2011
+ const TH$1 = styled.th`
1654
2012
  text-align: left;
1655
2013
  padding: 10px 14px;
1656
2014
  font-size: 10px;
@@ -1664,13 +2022,13 @@ const TH$2 = styled.th`
1664
2022
  &:first-child { padding-left: 20px; }
1665
2023
  &:last-child { padding-right: 20px; }
1666
2024
  `;
1667
- const THCheck$1 = styled(TH$2)`
2025
+ const THCheck$1 = styled(TH$1)`
1668
2026
  width: 44px;
1669
2027
  `;
1670
- const THActions$1 = styled(TH$2)`
2028
+ const THActions$1 = styled(TH$1)`
1671
2029
  width: 80px;
1672
2030
  `;
1673
- const TD$2 = styled.td`
2031
+ const TD$1 = styled.td`
1674
2032
  padding: 11px 14px;
1675
2033
  font-size: 12px;
1676
2034
  color: #32324d;
@@ -1679,14 +2037,14 @@ const TD$2 = styled.td`
1679
2037
  &:first-child { padding-left: 20px; }
1680
2038
  &:last-child { padding-right: 20px; }
1681
2039
  `;
1682
- const TDCheck$1 = styled(TD$2)`
2040
+ const TDCheck$1 = styled(TD$1)`
1683
2041
  width: 44px;
1684
2042
  `;
1685
- const TDActions$1 = styled(TD$2)`
2043
+ const TDActions$1 = styled(TD$1)`
1686
2044
  width: 80px;
1687
2045
  `;
1688
- const TR$2 = styled.tr`
1689
- animation: ${fadeUp$3} 280ms ease both;
2046
+ const TR$1 = styled.tr`
2047
+ animation: ${fadeUp$2} 280ms ease both;
1690
2048
  animation-delay: ${(p) => (p.$i ?? 0) * 25}ms;
1691
2049
  background: ${(p) => p.$selected ? "#f0f0ff" : "transparent"};
1692
2050
  transition: background 120ms ease;
@@ -1782,13 +2140,13 @@ function OrganizationsPage({ teamsEnabled }) {
1782
2140
  const [detailOrgId, setDetailOrgId] = useState(null);
1783
2141
  const [confirmDelete, setConfirmDelete] = useState(null);
1784
2142
  const [confirmDeleteMany, setConfirmDeleteMany] = useState(false);
1785
- const offset = (page - 1) * PAGE_SIZE$2;
2143
+ const offset = (page - 1) * PAGE_SIZE$1;
1786
2144
  const orgsQuery = useQuery({
1787
2145
  queryKey: ["dash-organizations", page, search],
1788
2146
  queryFn: async () => {
1789
2147
  const result = await client.dash.listOrganizations({
1790
2148
  query: {
1791
- limit: PAGE_SIZE$2,
2149
+ limit: PAGE_SIZE$1,
1792
2150
  offset,
1793
2151
  sortBy: "createdAt",
1794
2152
  sortOrder: "desc",
@@ -1833,7 +2191,7 @@ function OrganizationsPage({ teamsEnabled }) {
1833
2191
  });
1834
2192
  const orgs = orgsQuery.data && "organizations" in orgsQuery.data ? orgsQuery.data.organizations : [];
1835
2193
  const total = orgsQuery.data && "total" in orgsQuery.data ? orgsQuery.data.total : 0;
1836
- const pageCount = Math.ceil(total / PAGE_SIZE$2);
2194
+ const pageCount = Math.ceil(total / PAGE_SIZE$1);
1837
2195
  const allSelected = orgs.length > 0 && orgs.every((o) => selected.has(o.id));
1838
2196
  const someSelected = selected.size > 0;
1839
2197
  const toggleSelect = (id) => {
@@ -1853,11 +2211,11 @@ function OrganizationsPage({ teamsEnabled }) {
1853
2211
  setSearch(searchInput);
1854
2212
  setPage(1);
1855
2213
  };
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: [
2214
+ return /* @__PURE__ */ jsxs(Wrap$2, { "data-testid": "organizations-page", children: [
2215
+ /* @__PURE__ */ jsxs(PageHeader$1, { children: [
2216
+ /* @__PURE__ */ jsxs(TitleBlock$1, { children: [
2217
+ /* @__PURE__ */ jsx(PageTitle$1, { children: "Organizations" }),
2218
+ /* @__PURE__ */ jsxs(PageSubtitle$1, { children: [
1861
2219
  total.toLocaleString(),
1862
2220
  " total"
1863
2221
  ] })
@@ -1905,7 +2263,7 @@ function OrganizationsPage({ teamsEnabled }) {
1905
2263
  )
1906
2264
  ] }),
1907
2265
  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: [
2266
+ /* @__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
2267
  /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
1910
2268
  /* @__PURE__ */ jsx(THCheck$1, { children: /* @__PURE__ */ jsx(
1911
2269
  Checkbox,
@@ -1915,14 +2273,14 @@ function OrganizationsPage({ teamsEnabled }) {
1915
2273
  "aria-label": "Select all"
1916
2274
  }
1917
2275
  ) }),
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" }),
2276
+ /* @__PURE__ */ jsx(TH$1, { children: "Name" }),
2277
+ /* @__PURE__ */ jsx(TH$1, { children: "Slug" }),
2278
+ /* @__PURE__ */ jsx(TH$1, { children: "Members" }),
2279
+ /* @__PURE__ */ jsx(TH$1, { children: "Created" }),
1922
2280
  /* @__PURE__ */ jsx(THActions$1, {})
1923
2281
  ] }) }),
1924
2282
  /* @__PURE__ */ jsx("tbody", { children: orgs.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx(
1925
- TD$2,
2283
+ TD$1,
1926
2284
  {
1927
2285
  colSpan: 6,
1928
2286
  style: {
@@ -1934,7 +2292,7 @@ function OrganizationsPage({ teamsEnabled }) {
1934
2292
  children: search ? `No organizations matching "${search}"` : "No organizations found"
1935
2293
  }
1936
2294
  ) }) : orgs.map((org, i) => /* @__PURE__ */ jsxs(
1937
- TR$2,
2295
+ TR$1,
1938
2296
  {
1939
2297
  $selected: selected.has(org.id),
1940
2298
  $i: i,
@@ -1948,13 +2306,13 @@ function OrganizationsPage({ teamsEnabled }) {
1948
2306
  "aria-label": `Select ${org.name}`
1949
2307
  }
1950
2308
  ) }),
1951
- /* @__PURE__ */ jsx(TD$2, { children: /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 2, children: [
2309
+ /* @__PURE__ */ jsx(TD$1, { children: /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 2, children: [
1952
2310
  /* @__PURE__ */ jsx(OrgAvatar, { name: org.name, logo: org.logo }),
1953
2311
  /* @__PURE__ */ jsx(OrgName, { children: org.name })
1954
2312
  ] }) }),
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() }) }),
2313
+ /* @__PURE__ */ jsx(TD$1, { children: /* @__PURE__ */ jsx(SlugChip, { children: org.slug }) }),
2314
+ /* @__PURE__ */ jsx(TD$1, { children: /* @__PURE__ */ jsx(CountChip, { children: org.memberCount }) }),
2315
+ /* @__PURE__ */ jsx(TD$1, { children: /* @__PURE__ */ jsx(DateText$1, { children: new Date(org.createdAt).toLocaleDateString() }) }),
1958
2316
  /* @__PURE__ */ jsx(TDActions$1, { children: /* @__PURE__ */ jsxs(Flex, { gap: 1, justifyContent: "flex-end", children: [
1959
2317
  /* @__PURE__ */ jsx(
1960
2318
  IconButton,
@@ -2101,95 +2459,143 @@ function Avatar({
2101
2459
  const color = getColor(name || "?");
2102
2460
  return /* @__PURE__ */ jsx(Circle, { $bg: color.bg, $fg: color.fg, $size: size, children: getInitials(name || "?") });
2103
2461
  }
2104
- const fadeUp$2 = keyframes`
2105
- from { opacity: 0; transform: translateY(8px); }
2462
+ const T = {
2463
+ bg: "#f6f6f9",
2464
+ bgCard: "#ffffff",
2465
+ border: "#eaeaef",
2466
+ borderHover: "rgba(73,69,255,0.35)",
2467
+ accent: "#4945ff",
2468
+ green: "#5cb176",
2469
+ greenDim: "rgba(92,177,118,0.12)",
2470
+ amber: "#d9822f",
2471
+ red: "#d02b20",
2472
+ redDim: "rgba(208,43,32,0.1)",
2473
+ purple: "#8460b8",
2474
+ textPrimary: "#32324d",
2475
+ textSecondary: "#666687",
2476
+ textMuted: "#b8b8c7",
2477
+ mono: `'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace`
2478
+ };
2479
+ const fadeUp$1 = keyframes`
2480
+ from { opacity: 0; transform: translateY(10px); }
2106
2481
  to { opacity: 1; transform: translateY(0); }
2107
2482
  `;
2108
- const Wrap$2 = styled.div`
2109
- padding: 28px 32px;
2110
- background: #f6f6f9;
2483
+ const Wrap$1 = styled.div`
2484
+ padding: 28px 32px 48px;
2485
+ background: ${T.bg};
2111
2486
  min-height: 100%;
2112
2487
  display: flex;
2113
2488
  flex-direction: column;
2114
- gap: 20px;
2489
+ gap: 22px;
2115
2490
  `;
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); }
2491
+ const SectionDivider = styled.div`
2492
+ display: flex;
2493
+ align-items: center;
2494
+ gap: 10px;
2495
+ margin-bottom: -6px;
2122
2496
  `;
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; }
2497
+ const DivLabel = styled.span`
2498
+ font-size: 9px;
2499
+ font-weight: 700;
2500
+ letter-spacing: 0.14em;
2501
+ text-transform: uppercase;
2502
+ color: ${T.textMuted};
2503
+ white-space: nowrap;
2128
2504
  `;
2129
- const ActiveRow = styled.div`
2130
- display: grid;
2131
- grid-template-columns: repeat(3, 1fr);
2132
- gap: 12px;
2505
+ const DivLine = styled.div`
2506
+ flex: 1;
2507
+ height: 1px;
2508
+ background: ${T.border};
2133
2509
  `;
2134
2510
  const Card = styled.div`
2135
- background: #ffffff;
2136
- border: 1px solid #eaeaef;
2137
- border-radius: 10px;
2511
+ background: ${T.bgCard};
2512
+ border: 1px solid ${T.border};
2513
+ border-radius: 12px;
2138
2514
  overflow: hidden;
2139
2515
  position: relative;
2140
- animation: ${fadeUp$2} 360ms ease both;
2516
+ animation: ${fadeUp$1} 380ms ease both;
2141
2517
  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
-
2518
+ transition: border-color 200ms, box-shadow 200ms;
2519
+ &:hover {
2520
+ border-color: ${T.borderHover};
2521
+ box-shadow: 0 0 0 1px ${T.borderHover}, 0 8px 32px rgba(124,109,250,0.07);
2522
+ }
2523
+ `;
2524
+ const PillGroup = styled.div`
2525
+ display: flex;
2526
+ background: rgba(0,0,0,0.03);
2527
+ border: 1px solid ${T.border};
2528
+ border-radius: 9px;
2529
+ padding: 3px;
2530
+ gap: 2px;
2531
+ `;
2532
+ const Pill = styled.button`
2533
+ appearance: none;
2534
+ border: none;
2535
+ cursor: pointer;
2536
+ padding: 5px 16px;
2537
+ border-radius: 6px;
2538
+ font-size: 11px;
2539
+ font-weight: 600;
2540
+ letter-spacing: 0.03em;
2541
+ transition: background 180ms, color 180ms;
2542
+ background: ${(p) => p.$active ? T.accent : "transparent"};
2543
+ color: ${(p) => p.$active ? "#fff" : T.textSecondary};
2155
2544
  &:hover {
2156
- border-color: #c0bfff;
2157
- box-shadow: 0 2px 16px rgba(73,69,255,0.08);
2545
+ background: ${(p) => p.$active ? T.accent : "rgba(0,0,0,0.05)"};
2546
+ color: ${(p) => p.$active ? "#fff" : T.textPrimary};
2158
2547
  }
2159
2548
  `;
2160
- const StatTop = styled.div`
2161
- padding: 18px 18px 10px;
2549
+ const StatGrid = styled.div`
2550
+ display: grid;
2551
+ grid-template-columns: repeat(5, 1fr);
2552
+ gap: 10px;
2553
+ @media (max-width: 1200px) { grid-template-columns: repeat(3, 1fr); }
2554
+ `;
2555
+ const StatCard = styled(Card)`
2556
+ padding: 18px 20px 0;
2557
+ &::after {
2558
+ content: '';
2559
+ position: absolute;
2560
+ top: 0; left: 0; right: 0;
2561
+ height: 2px;
2562
+ background: ${(p) => p.$accent};
2563
+ opacity: 0.75;
2564
+ }
2162
2565
  `;
2163
2566
  const StatLabel = styled.div`
2164
- font-size: 10px;
2567
+ font-size: 9px;
2165
2568
  font-weight: 700;
2166
- letter-spacing: 0.08em;
2569
+ letter-spacing: 0.1em;
2167
2570
  text-transform: uppercase;
2168
- color: #8e8ea9;
2169
- margin-bottom: 8px;
2571
+ color: ${T.textMuted};
2572
+ margin-bottom: 10px;
2170
2573
  `;
2171
2574
  const StatValue = styled.div`
2172
2575
  font-size: 30px;
2173
2576
  font-weight: 800;
2174
- color: #32324d;
2577
+ color: ${T.textPrimary};
2175
2578
  line-height: 1;
2176
- letter-spacing: -0.05em;
2579
+ letter-spacing: -0.045em;
2177
2580
  font-variant-numeric: tabular-nums;
2178
2581
  margin-bottom: 10px;
2179
2582
  `;
2180
2583
  const TrendBadge = styled.span`
2181
2584
  display: inline-flex;
2182
2585
  align-items: center;
2183
- gap: 2px;
2184
- padding: 2px 7px;
2586
+ gap: 3px;
2587
+ padding: 2px 8px;
2185
2588
  border-radius: 20px;
2186
- font-size: 10px;
2589
+ font-size: 9px;
2187
2590
  font-weight: 700;
2188
- background: ${(p) => p.$pos ? "#eafbe7" : "#fcecea"};
2189
- color: ${(p) => p.$pos ? "#5cb176" : "#d02b20"};
2591
+ letter-spacing: 0.03em;
2592
+ background: ${(p) => p.$pos ? T.greenDim : T.redDim};
2593
+ color: ${(p) => p.$pos ? T.green : T.red};
2594
+ margin-bottom: 12px;
2190
2595
  `;
2191
2596
  const SparkWrap = styled.div`
2192
- height: 48px;
2597
+ height: 44px;
2598
+ margin: 0 -20px;
2193
2599
  `;
2194
2600
  const ChartCard = styled(Card)`
2195
2601
  padding: 20px 20px 12px;
@@ -2201,31 +2607,42 @@ const ChartHeader = styled.div`
2201
2607
  display: flex;
2202
2608
  align-items: center;
2203
2609
  justify-content: space-between;
2610
+ gap: 12px;
2204
2611
  flex-shrink: 0;
2205
2612
  `;
2206
2613
  const ChartTitle = styled.div`
2207
- font-size: 13px;
2614
+ font-size: 12px;
2208
2615
  font-weight: 700;
2209
- color: #32324d;
2616
+ color: ${T.textPrimary};
2617
+ letter-spacing: 0.01em;
2210
2618
  `;
2211
- const LegendRow = styled.div`
2619
+ const SeriesRow = styled.div`
2212
2620
  display: flex;
2213
2621
  align-items: center;
2214
- gap: 14px;
2622
+ gap: 6px;
2215
2623
  `;
2216
- const LegendItem = styled.div`
2624
+ const SeriesBtn = styled.button`
2625
+ appearance: none;
2626
+ border: 1px solid ${(p) => p.$on ? `${p.$c}55` : T.border};
2627
+ background: ${(p) => p.$on ? `${p.$c}18` : "transparent"};
2628
+ color: ${(p) => p.$on ? p.$c : T.textMuted};
2629
+ font-size: 10px;
2630
+ font-weight: 600;
2631
+ padding: 3px 10px 3px 8px;
2632
+ border-radius: 6px;
2633
+ cursor: pointer;
2217
2634
  display: flex;
2218
2635
  align-items: center;
2219
2636
  gap: 5px;
2220
- font-size: 10px;
2221
- color: #8e8ea9;
2222
- font-weight: 600;
2223
- text-transform: uppercase;
2224
- letter-spacing: 0.04em;
2637
+ transition: all 160ms;
2638
+ &:hover {
2639
+ border-color: ${(p) => `${p.$c}88`};
2640
+ color: ${(p) => p.$c};
2641
+ background: ${(p) => `${p.$c}22`};
2642
+ }
2225
2643
  `;
2226
- const LegendDot = styled.span`
2227
- width: 8px;
2228
- height: 8px;
2644
+ const SDot = styled.span`
2645
+ width: 6px; height: 6px;
2229
2646
  border-radius: 50%;
2230
2647
  background: ${(p) => p.$c};
2231
2648
  display: inline-block;
@@ -2233,140 +2650,188 @@ const LegendDot = styled.span`
2233
2650
  `;
2234
2651
  const HoverInfo = styled.div`
2235
2652
  font-size: 11px;
2236
- color: #8e8ea9;
2653
+ color: ${T.textSecondary};
2237
2654
  font-variant-numeric: tabular-nums;
2238
- min-height: 16px;
2655
+ font-family: ${T.mono};
2656
+ min-height: 15px;
2657
+ `;
2658
+ const TwoPanel = styled.div`
2659
+ display: grid;
2660
+ grid-template-columns: ${(p) => p.$ratio ?? "1fr 288px"};
2661
+ gap: 10px;
2662
+ align-items: stretch;
2663
+ @media (max-width: 900px) { grid-template-columns: 1fr; }
2239
2664
  `;
2240
2665
  const FeedCard = styled(Card)`
2241
2666
  display: flex;
2242
2667
  flex-direction: column;
2243
- max-height: 380px;
2668
+ overflow: hidden;
2244
2669
  `;
2245
- const FeedHeader = styled.div`
2246
- padding: 16px 18px 12px;
2247
- border-bottom: 1px solid #f0f0f7;
2248
- flex-shrink: 0;
2249
- font-size: 10px;
2670
+ const FeedHead = styled.div`
2671
+ padding: 13px 16px 9px;
2672
+ border-bottom: 1px solid ${T.border};
2673
+ font-size: 11px;
2250
2674
  font-weight: 700;
2251
- letter-spacing: 0.08em;
2252
- text-transform: uppercase;
2253
- color: #8e8ea9;
2675
+ color: ${T.textPrimary};
2676
+ flex-shrink: 0;
2677
+ display: flex;
2678
+ justify-content: space-between;
2679
+ align-items: center;
2254
2680
  `;
2255
- const FeedList = styled.div`
2681
+ const FeedSelect = styled.select`
2682
+ appearance: none;
2683
+ border: 1px solid ${T.border};
2684
+ border-radius: 6px;
2685
+ background: ${T.bgCard};
2686
+ color: ${T.textSecondary};
2687
+ font-size: 10px;
2688
+ font-weight: 600;
2689
+ padding: 3px 22px 3px 8px;
2690
+ cursor: pointer;
2691
+ outline: none;
2692
+ 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");
2693
+ background-repeat: no-repeat;
2694
+ background-position: right 7px center;
2695
+ transition: border-color 160ms;
2696
+ &:hover, &:focus { border-color: ${T.accent}; color: ${T.textPrimary}; }
2697
+ `;
2698
+ const FeedScroll = styled.div`
2256
2699
  flex: 1;
2257
2700
  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; }
2701
+ min-height: 0;
2702
+ &::-webkit-scrollbar { width: 4px; }
2703
+ &::-webkit-scrollbar-thumb { background: ${T.textMuted}; border-radius: 2px; }
2262
2704
  `;
2263
2705
  const FeedItem = styled.div`
2706
+ padding: 9px 14px;
2707
+ border-bottom: 1px solid #f0f0f5;
2264
2708
  display: flex;
2265
- align-items: flex-start;
2266
- gap: 10px;
2267
- padding: 9px 18px;
2268
- &:hover { background: #fafafe; }
2709
+ flex-direction: column;
2710
+ gap: 3px;
2711
+ transition: background 130ms;
2712
+ &:hover { background: #fafaff; }
2713
+ &:last-child { border-bottom: none; }
2269
2714
  `;
2270
- const FeedContent = styled.div`
2271
- flex: 1;
2272
- min-width: 0;
2715
+ const FeedTop = styled.div`
2716
+ display: flex;
2717
+ align-items: center;
2718
+ justify-content: space-between;
2719
+ gap: 6px;
2273
2720
  `;
2274
- const FeedName = styled.div`
2275
- font-size: 12px;
2721
+ const FeedName = styled.span`
2722
+ font-size: 11px;
2276
2723
  font-weight: 600;
2277
- color: #32324d;
2278
- white-space: nowrap;
2724
+ color: ${T.textPrimary};
2279
2725
  overflow: hidden;
2280
2726
  text-overflow: ellipsis;
2727
+ white-space: nowrap;
2281
2728
  `;
2282
- const FeedSub = styled.div`
2729
+ const FeedEmail = styled.span`
2283
2730
  font-size: 10px;
2284
- color: #8e8ea9;
2285
- white-space: nowrap;
2731
+ color: ${T.textSecondary};
2286
2732
  overflow: hidden;
2287
2733
  text-overflow: ellipsis;
2288
- margin-top: 1px;
2734
+ white-space: nowrap;
2735
+ display: block;
2289
2736
  `;
2290
- const FeedTime = styled.div`
2291
- font-size: 10px;
2292
- color: #b8b8c7;
2737
+ const FeedMeta = styled.span`
2738
+ font-size: 9px;
2739
+ color: ${T.textMuted};
2293
2740
  white-space: nowrap;
2294
2741
  font-variant-numeric: tabular-nums;
2295
- margin-top: 1px;
2296
2742
  `;
2297
- const UsersCard = styled(Card)``;
2298
- const TableWrap = styled.div`
2299
- overflow-x: auto;
2743
+ const RtnCard = styled(Card)`
2744
+ padding: 20px;
2745
+ display: flex;
2746
+ flex-direction: column;
2747
+ gap: 12px;
2300
2748
  `;
2301
- const Table$1 = styled.table`
2302
- width: 100%;
2303
- border-collapse: collapse;
2749
+ const RtnRow = styled.div`
2750
+ display: flex;
2751
+ align-items: center;
2752
+ gap: 10px;
2753
+ padding: 4px 6px;
2754
+ border-radius: 7px;
2755
+ transition: background 130ms;
2756
+ background: ${(p) => p.$hov ? "#f0f0f6" : "transparent"};
2757
+ cursor: default;
2304
2758
  `;
2305
- const TH$1 = styled.th`
2306
- text-align: left;
2307
- padding: 10px 14px;
2759
+ const RtnLabel = styled.span`
2308
2760
  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;
2761
+ color: ${T.textSecondary};
2762
+ min-width: 80px;
2763
+ text-align: right;
2315
2764
  white-space: nowrap;
2316
- &:first-child { padding-left: 20px; }
2317
- &:last-child { padding-right: 20px; }
2318
2765
  `;
2319
- const TR$1 = styled.tr`
2320
- &:hover td { background: #fafafe; }
2321
- &:last-child td { border-bottom: none; }
2766
+ const RtnSize = styled.span`
2767
+ font-size: 9px;
2768
+ color: ${T.textMuted};
2769
+ min-width: 52px;
2770
+ text-align: right;
2771
+ white-space: nowrap;
2772
+ font-variant-numeric: tabular-nums;
2322
2773
  `;
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; }
2774
+ const RtnTrack = styled.div`
2775
+ flex: 1;
2776
+ height: 10px;
2777
+ background: #eaeaef;
2778
+ border-radius: 4px;
2779
+ overflow: hidden;
2331
2780
  `;
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;
2781
+ const RtnBar = styled.div`
2782
+ width: ${(p) => Math.max(p.$w, 0.5)}%;
2783
+ height: 100%;
2784
+ background: hsl(${(p) => p.$hue}, 60%, 50%);
2785
+ border-radius: 4px;
2786
+ transition: width 0.55s cubic-bezier(0.25, 0.46, 0.45, 0.94);
2787
+ `;
2788
+ const RtnPct = styled.span`
2338
2789
  font-size: 10px;
2339
2790
  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
- }
2791
+ color: hsl(${(p) => p.$hue}, 60%, 60%);
2792
+ min-width: 38px;
2793
+ text-align: right;
2794
+ font-variant-numeric: tabular-nums;
2795
+ font-family: ${T.mono};
2796
+ `;
2797
+ const RtnTip = styled.div`
2798
+ font-size: 10px;
2799
+ color: ${T.textSecondary};
2800
+ min-height: 14px;
2801
+ font-variant-numeric: tabular-nums;
2802
+ padding: 0 6px;
2351
2803
  `;
2352
- const Divider = styled.div`
2804
+ const RingGrid = styled.div`
2353
2805
  display: flex;
2354
- align-items: center;
2806
+ flex-direction: column;
2355
2807
  gap: 10px;
2356
- margin-bottom: -8px;
2357
2808
  `;
2358
- const DivLabel = styled.span`
2359
- font-size: 10px;
2809
+ const RingCard = styled(Card)`
2810
+ padding: 18px 20px;
2811
+ display: flex;
2812
+ align-items: center;
2813
+ gap: 16px;
2814
+ flex: 1;
2815
+ `;
2816
+ const RingInfo = styled.div`
2817
+ display: flex;
2818
+ flex-direction: column;
2819
+ gap: 4px;
2820
+ `;
2821
+ const RingVal = styled.div`
2822
+ font-size: 24px;
2823
+ font-weight: 800;
2824
+ color: ${T.textPrimary};
2825
+ letter-spacing: -0.04em;
2826
+ font-variant-numeric: tabular-nums;
2827
+ line-height: 1;
2828
+ `;
2829
+ const RingLabel = styled.div`
2830
+ font-size: 9px;
2360
2831
  font-weight: 700;
2361
2832
  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;
2833
+ letter-spacing: 0.1em;
2834
+ color: ${T.textMuted};
2370
2835
  `;
2371
2836
  const Empty = styled.div`
2372
2837
  display: flex;
@@ -2374,32 +2839,52 @@ const Empty = styled.div`
2374
2839
  align-items: center;
2375
2840
  justify-content: center;
2376
2841
  padding: 32px;
2377
- color: #8e8ea9;
2842
+ color: ${T.textMuted};
2378
2843
  font-size: 12px;
2379
- gap: 6px;
2844
+ gap: 8px;
2380
2845
  `;
2846
+ function useCountUp(target, duration = 850) {
2847
+ const [count, setCount] = useState(0);
2848
+ const prevRef = useRef(0);
2849
+ useEffect(() => {
2850
+ let raf;
2851
+ let start = 0;
2852
+ const from = prevRef.current;
2853
+ const diff = target - from;
2854
+ const step = (ts) => {
2855
+ if (!start) start = ts;
2856
+ const progress = Math.min((ts - start) / duration, 1);
2857
+ const eased = 1 - (1 - progress) ** 3;
2858
+ setCount(Math.round(from + eased * diff));
2859
+ if (progress < 1) raf = requestAnimationFrame(step);
2860
+ else prevRef.current = target;
2861
+ };
2862
+ raf = requestAnimationFrame(step);
2863
+ return () => cancelAnimationFrame(raf);
2864
+ }, [target, duration]);
2865
+ return count;
2866
+ }
2381
2867
  function Sparkline({
2382
2868
  data,
2383
2869
  color,
2384
2870
  id
2385
2871
  }) {
2386
2872
  const W = 300;
2387
- const H = 48;
2873
+ const H = 44;
2388
2874
  if (data.length < 2) return /* @__PURE__ */ jsx("svg", { width: "100%", height: H });
2389
2875
  const min = Math.min(...data);
2390
2876
  const max = Math.max(...data, min + 1);
2391
- const range = max - min;
2392
2877
  const pts = data.map((v, i) => ({
2393
2878
  x: i / (data.length - 1) * W,
2394
- y: H - 4 - (v - min) / range * (H - 10)
2879
+ y: H - 3 - (v - min) / (max - min) * (H - 8)
2395
2880
  }));
2396
- const line = pts.reduce((acc, p, i) => {
2881
+ const line = pts.reduce((a, p, i) => {
2397
2882
  if (i === 0) return `M ${p.x} ${p.y}`;
2398
2883
  const pr = pts[i - 1];
2399
2884
  const cx = (pr.x + p.x) / 2;
2400
- return `${acc} C ${cx} ${pr.y} ${cx} ${p.y} ${p.x} ${p.y}`;
2885
+ return `${a} C ${cx} ${pr.y} ${cx} ${p.y} ${p.x} ${p.y}`;
2401
2886
  }, "");
2402
- const area = `${line} L ${pts[pts.length - 1].x} ${H} L 0 ${H} Z`;
2887
+ const area = `${line} L ${pts.at(-1).x} ${H} L 0 ${H} Z`;
2403
2888
  const gid = `spk-${id}`;
2404
2889
  return /* @__PURE__ */ jsxs(
2405
2890
  "svg",
@@ -2408,10 +2893,10 @@ function Sparkline({
2408
2893
  height: H,
2409
2894
  viewBox: `0 0 ${W} ${H}`,
2410
2895
  preserveAspectRatio: "none",
2411
- "aria-hidden": "true",
2896
+ "aria-hidden": true,
2412
2897
  children: [
2413
2898
  /* @__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" }),
2899
+ /* @__PURE__ */ jsx("stop", { offset: "0%", stopColor: color, stopOpacity: "0.32" }),
2415
2900
  /* @__PURE__ */ jsx("stop", { offset: "100%", stopColor: color, stopOpacity: "0" })
2416
2901
  ] }) }),
2417
2902
  /* @__PURE__ */ jsx("path", { d: area, fill: `url(#${gid})` }),
@@ -2420,57 +2905,58 @@ function Sparkline({
2420
2905
  {
2421
2906
  d: line,
2422
2907
  stroke: color,
2423
- strokeWidth: "2",
2908
+ strokeWidth: "1.6",
2424
2909
  fill: "none",
2425
- strokeLinecap: "round",
2426
- strokeLinejoin: "round"
2910
+ strokeLinecap: "round"
2427
2911
  }
2428
2912
  )
2429
2913
  ]
2430
2914
  }
2431
2915
  );
2432
2916
  }
2433
- const SERIES = [
2434
- { key: "totalUsers", color: "#4945FF", label: "Total" },
2435
- { key: "newUsers", color: "#5CB176", label: "New" },
2436
- { key: "activeUsers", color: "#E57553", label: "Active" }
2917
+ const ALL_SERIES = [
2918
+ { key: "totalUsers", color: T.accent, label: "Total" },
2919
+ { key: "newUsers", color: T.green, label: "New" },
2920
+ { key: "activeUsers", color: T.amber, label: "Active" }
2437
2921
  ];
2438
- function AreaChart({
2922
+ function GrowthChart({
2439
2923
  data,
2440
2924
  hovered,
2441
- onHover
2925
+ onHover,
2926
+ activeSeries
2442
2927
  }) {
2443
2928
  const W = 600;
2444
- const H = 220;
2445
- const PL = 44;
2929
+ const H = 200;
2930
+ const PL = 40;
2446
2931
  const PR = 12;
2447
2932
  const PT = 12;
2448
2933
  const PB = 28;
2449
2934
  const CW = W - PL - PR;
2450
2935
  const CH = H - PT - PB;
2451
- if (data.length === 0) {
2936
+ if (!data.length) {
2452
2937
  return /* @__PURE__ */ jsxs(Empty, { children: [
2453
2938
  /* @__PURE__ */ jsx("div", { style: { fontSize: 24 }, children: "📊" }),
2454
2939
  /* @__PURE__ */ jsx("div", { children: "No growth data for this period" })
2455
2940
  ] });
2456
2941
  }
2457
- const allVals = data.flatMap((d) => SERIES.map((s) => d[s.key]));
2942
+ const active = ALL_SERIES.filter((s) => activeSeries.has(s.key));
2943
+ const allVals = data.flatMap((d) => active.map((s) => d[s.key]));
2458
2944
  const maxV = Math.max(...allVals, 10);
2459
2945
  const yMax = Math.ceil(maxV * 1.15);
2460
2946
  const xp = (i) => PL + i / Math.max(data.length - 1, 1) * CW;
2461
2947
  const yp = (v) => PT + (1 - v / yMax) * CH;
2462
2948
  const smooth = (vals) => {
2463
2949
  const pts = vals.map((v, i) => ({ x: xp(i), y: yp(v) }));
2464
- const path = pts.reduce((acc, p, i) => {
2950
+ const path = pts.reduce((a, p, i) => {
2465
2951
  if (i === 0) return `M ${p.x} ${p.y}`;
2466
2952
  const pr = pts[i - 1];
2467
2953
  const cx = (pr.x + p.x) / 2;
2468
- return `${acc} C ${cx} ${pr.y} ${cx} ${p.y} ${p.x} ${p.y}`;
2954
+ return `${a} C ${cx} ${pr.y} ${cx} ${p.y} ${p.x} ${p.y}`;
2469
2955
  }, "");
2470
- const area = `${path} L ${pts[pts.length - 1].x} ${PT + CH} L ${pts[0].x} ${PT + CH} Z`;
2956
+ const area = `${path} L ${pts.at(-1).x} ${PT + CH} L ${pts[0].x} ${PT + CH} Z`;
2471
2957
  return { pts, path, area };
2472
2958
  };
2473
- const lines = SERIES.map((s) => ({
2959
+ const lines = active.map((s) => ({
2474
2960
  ...s,
2475
2961
  ...smooth(data.map((d) => d[s.key]))
2476
2962
  }));
@@ -2493,17 +2979,17 @@ function AreaChart({
2493
2979
  /* @__PURE__ */ jsx("defs", { children: lines.map((s) => /* @__PURE__ */ jsxs(
2494
2980
  "linearGradient",
2495
2981
  {
2496
- id: `ag-${s.color.replace("#", "")}`,
2982
+ id: `ag-${s.key}`,
2497
2983
  x1: "0",
2498
2984
  y1: "0",
2499
2985
  x2: "0",
2500
2986
  y2: "1",
2501
2987
  children: [
2502
- /* @__PURE__ */ jsx("stop", { offset: "0%", stopColor: s.color, stopOpacity: "0.13" }),
2988
+ /* @__PURE__ */ jsx("stop", { offset: "0%", stopColor: s.color, stopOpacity: "0.18" }),
2503
2989
  /* @__PURE__ */ jsx("stop", { offset: "100%", stopColor: s.color, stopOpacity: "0" })
2504
2990
  ]
2505
2991
  },
2506
- s.color
2992
+ s.key
2507
2993
  )) }),
2508
2994
  yTicks.map((t) => /* @__PURE__ */ jsxs("g", { children: [
2509
2995
  /* @__PURE__ */ jsx(
@@ -2523,7 +3009,7 @@ function AreaChart({
2523
3009
  x: PL - 6,
2524
3010
  y: t.y + 4,
2525
3011
  textAnchor: "end",
2526
- fill: "#b8b8c7",
3012
+ fill: T.textMuted,
2527
3013
  fontSize: "9",
2528
3014
  children: t.v >= 1e3 ? `${(t.v / 1e3).toFixed(1)}k` : t.v
2529
3015
  }
@@ -2540,25 +3026,18 @@ function AreaChart({
2540
3026
  strokeWidth: "1"
2541
3027
  }
2542
3028
  ),
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
- )),
3029
+ lines.map((s) => /* @__PURE__ */ jsx("path", { d: s.area, fill: `url(#ag-${s.key})` }, `area-${s.key}`)),
2551
3030
  lines.map((s) => /* @__PURE__ */ jsx(
2552
3031
  "path",
2553
3032
  {
2554
3033
  d: s.path,
2555
3034
  stroke: s.color,
2556
- strokeWidth: "1.8",
3035
+ strokeWidth: "2",
2557
3036
  fill: "none",
2558
3037
  strokeLinecap: "round",
2559
3038
  strokeLinejoin: "round"
2560
3039
  },
2561
- `l-${s.color}`
3040
+ `line-${s.key}`
2562
3041
  )),
2563
3042
  data.map((d, i) => {
2564
3043
  if (i % step !== 0 && i !== data.length - 1) return null;
@@ -2568,7 +3047,7 @@ function AreaChart({
2568
3047
  x: xp(i),
2569
3048
  y: H - 6,
2570
3049
  textAnchor: "middle",
2571
- fill: "#b8b8c7",
3050
+ fill: T.textMuted,
2572
3051
  fontSize: "9",
2573
3052
  children: d.label
2574
3053
  },
@@ -2578,7 +3057,7 @@ function AreaChart({
2578
3057
  data.map((d, i) => {
2579
3058
  const isHov = hovered === i;
2580
3059
  return (
2581
- // biome-ignore lint/a11y/noStaticElementInteractions: SVG chart hover hit area
3060
+ // biome-ignore lint/a11y/noStaticElementInteractions: SVG chart hover
2582
3061
  /* @__PURE__ */ jsxs(
2583
3062
  "g",
2584
3063
  {
@@ -2603,10 +3082,9 @@ function AreaChart({
2603
3082
  y1: PT,
2604
3083
  x2: xp(i),
2605
3084
  y2: PT + CH,
2606
- stroke: "#32324d",
3085
+ stroke: "rgba(50,50,77,0.2)",
2607
3086
  strokeWidth: "1",
2608
- strokeDasharray: "3 3",
2609
- opacity: "0.35"
3087
+ strokeDasharray: "3 3"
2610
3088
  }
2611
3089
  ),
2612
3090
  lines.map((s) => /* @__PURE__ */ jsx(
@@ -2616,11 +3094,11 @@ function AreaChart({
2616
3094
  cy: yp(d[s.key]),
2617
3095
  r: isHov ? 4 : 2.5,
2618
3096
  fill: s.color,
2619
- stroke: "white",
3097
+ stroke: T.bg,
2620
3098
  strokeWidth: isHov ? 2 : 1,
2621
- opacity: isHov ? 1 : 0.45
3099
+ opacity: isHov ? 1 : 0.5
2622
3100
  },
2623
- s.color
3101
+ s.key
2624
3102
  ))
2625
3103
  ]
2626
3104
  },
@@ -2632,23 +3110,70 @@ function AreaChart({
2632
3110
  }
2633
3111
  );
2634
3112
  }
3113
+ function ProgressRing({
3114
+ value,
3115
+ max,
3116
+ color,
3117
+ size = 56
3118
+ }) {
3119
+ const r = (size - 9) / 2;
3120
+ const circ = 2 * Math.PI * r;
3121
+ const pct = Math.min(value / Math.max(max, 1), 1);
3122
+ const offset = circ * (1 - pct);
3123
+ return /* @__PURE__ */ jsxs(
3124
+ "svg",
3125
+ {
3126
+ width: size,
3127
+ height: size,
3128
+ style: { flexShrink: 0 },
3129
+ "aria-hidden": true,
3130
+ children: [
3131
+ /* @__PURE__ */ jsx(
3132
+ "circle",
3133
+ {
3134
+ cx: size / 2,
3135
+ cy: size / 2,
3136
+ r,
3137
+ fill: "none",
3138
+ stroke: "#e5e7eb",
3139
+ strokeWidth: "7"
3140
+ }
3141
+ ),
3142
+ /* @__PURE__ */ jsx(
3143
+ "circle",
3144
+ {
3145
+ cx: size / 2,
3146
+ cy: size / 2,
3147
+ r,
3148
+ fill: "none",
3149
+ stroke: color,
3150
+ strokeWidth: "7",
3151
+ strokeDasharray: `${circ} ${circ}`,
3152
+ strokeDashoffset: offset,
3153
+ strokeLinecap: "round",
3154
+ transform: `rotate(-90 ${size / 2} ${size / 2})`,
3155
+ style: {
3156
+ transition: "stroke-dashoffset 0.9s cubic-bezier(0.4,0,0.2,1)"
3157
+ }
3158
+ }
3159
+ )
3160
+ ]
3161
+ }
3162
+ );
3163
+ }
2635
3164
  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`;
3165
+ const diff = Date.now() - new Date(date).getTime();
3166
+ const mins = Math.floor(diff / 6e4);
3167
+ if (mins < 1) return "just now";
3168
+ if (mins < 60) return `${mins}m`;
3169
+ const hrs = Math.floor(mins / 60);
3170
+ if (hrs < 24) return `${hrs}h`;
3171
+ return `${Math.floor(hrs / 24)}d`;
2643
3172
  }
2644
- function fmtDate(date) {
2645
- return new Date(date).toLocaleDateString(void 0, {
2646
- month: "short",
2647
- day: "numeric",
2648
- year: "numeric"
2649
- });
3173
+ function rateHue(r) {
3174
+ return r >= 70 ? 142 : r >= 40 ? 38 : 4;
2650
3175
  }
2651
- function StatCardItem({
3176
+ function StatItem({
2652
3177
  id,
2653
3178
  label,
2654
3179
  value,
@@ -2657,26 +3182,39 @@ function StatCardItem({
2657
3182
  color,
2658
3183
  delay = 0
2659
3184
  }) {
3185
+ const animated = useCountUp(value);
2660
3186
  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
- ] })
3187
+ return /* @__PURE__ */ jsxs(StatCard, { $delay: delay, $accent: color, children: [
3188
+ /* @__PURE__ */ jsx(StatLabel, { children: label }),
3189
+ /* @__PURE__ */ jsx(StatValue, { children: animated.toLocaleString() }),
3190
+ pct !== void 0 && /* @__PURE__ */ jsxs(TrendBadge, { $pos: isPos, children: [
3191
+ isPos ? "↑" : "↓",
3192
+ " ",
3193
+ Math.abs(pct).toFixed(1),
3194
+ "%"
2671
3195
  ] }),
2672
3196
  sparkline && sparkline.length > 1 && /* @__PURE__ */ jsx(SparkWrap, { children: /* @__PURE__ */ jsx(Sparkline, { data: sparkline, color, id }) })
2673
3197
  ] });
2674
3198
  }
2675
3199
  function OverviewPage() {
3200
+ const { get } = useFetchClient();
2676
3201
  const [period, setPeriod] = useState(
2677
3202
  "weekly"
2678
3203
  );
3204
+ const [feedMode, setFeedMode] = useState("signups");
2679
3205
  const [hovIdx, setHovIdx] = useState(null);
3206
+ const [rtnHov, setRtnHov] = useState(null);
3207
+ const [activeSeries, setActiveSeries] = useState(
3208
+ () => /* @__PURE__ */ new Set(["totalUsers", "newUsers", "activeUsers"])
3209
+ );
3210
+ const toggleSeries = (key) => {
3211
+ setActiveSeries((prev) => {
3212
+ const next = new Set(prev);
3213
+ if (next.has(key) && next.size > 1) next.delete(key);
3214
+ else next.add(key);
3215
+ return next;
3216
+ });
3217
+ };
2680
3218
  const statsQuery = useQuery({
2681
3219
  queryKey: ["dash-user-stats"],
2682
3220
  queryFn: async () => {
@@ -2693,12 +3231,10 @@ function OverviewPage() {
2693
3231
  return r.data;
2694
3232
  }
2695
3233
  });
2696
- const sessionsQuery = useQuery({
2697
- queryKey: ["dash-recent-sessions"],
3234
+ const retentionQuery = useQuery({
3235
+ queryKey: ["dash-user-retention", period],
2698
3236
  queryFn: async () => {
2699
- const r = await client.dash.listAllSessions({
2700
- query: { page: 1, limit: 10 }
2701
- });
3237
+ const r = await client.dash.userRetentionData({ query: { period } });
2702
3238
  if (r.error) throw new Error(r.error.message ?? "Failed");
2703
3239
  return r.data;
2704
3240
  }
@@ -2707,12 +3243,22 @@ function OverviewPage() {
2707
3243
  queryKey: ["dash-recent-users"],
2708
3244
  queryFn: async () => {
2709
3245
  const r = await client.dash.listUsers({
2710
- query: { page: 1, limit: 8, search: "" }
3246
+ query: { limit: 12, offset: 0, sortBy: "createdAt", sortOrder: "desc" }
2711
3247
  });
2712
3248
  if (r.error) throw new Error(r.error.message ?? "Failed");
2713
3249
  return r.data;
2714
3250
  }
2715
3251
  });
3252
+ const sessionsQuery = useQuery({
3253
+ queryKey: ["dash-recent-sessions"],
3254
+ queryFn: async () => {
3255
+ const { data } = await get(
3256
+ "/better-auth-dashboard/db?uid=plugin::better-auth.session&pagination[pageSize]=12&sort[0]=createdAt:desc"
3257
+ );
3258
+ return data.results ?? [];
3259
+ },
3260
+ refetchInterval: 3e4
3261
+ });
2716
3262
  const orgsQuery = useQuery({
2717
3263
  queryKey: ["dash-orgs-count"],
2718
3264
  queryFn: async () => {
@@ -2724,594 +3270,344 @@ function OverviewPage() {
2724
3270
  }
2725
3271
  });
2726
3272
  if (statsQuery.isLoading) {
2727
- return /* @__PURE__ */ jsx(Flex, { justifyContent: "center", alignItems: "center", padding: 12, children: /* @__PURE__ */ jsx(Loader, { children: "Loading…" }) });
3273
+ return /* @__PURE__ */ jsx(
3274
+ Flex,
3275
+ {
3276
+ justifyContent: "center",
3277
+ alignItems: "center",
3278
+ padding: 12,
3279
+ style: { background: T.bg, minHeight: "100%" },
3280
+ children: /* @__PURE__ */ jsx(Loader, { children: "Loading…" })
3281
+ }
3282
+ );
2728
3283
  }
2729
3284
  const stats = statsQuery.data;
2730
3285
  if (!stats) return null;
2731
3286
  const graphData = graphQuery.data?.data ?? [];
2732
- const sessions = sessionsQuery.data?.sessions ?? [];
2733
- const recentUsers = usersQuery.data?.users ?? [];
3287
+ const rtnData = retentionQuery.data?.data ?? [];
3288
+ const users = usersQuery.data?.users ?? [];
3289
+ const sessions = sessionsQuery.data ?? [];
2734
3290
  const orgCount = orgsQuery.data?.total ?? 0;
2735
3291
  const totalSpark = graphData.map((d) => d.totalUsers);
2736
3292
  const newSpark = graphData.map((d) => d.newUsers);
2737
3293
  const activeSpark = graphData.map((d) => d.activeUsers);
2738
3294
  const hovRow = hovIdx !== null ? graphData[hovIdx] : null;
2739
- return /* @__PURE__ */ jsxs(Wrap$2, { "data-testid": "overview-page", children: [
3295
+ const rtnHovRow = rtnHov !== null ? rtnData[rtnHov] : null;
3296
+ const activeMax = Math.max(
3297
+ stats.activeUsers.daily.active ?? 0,
3298
+ stats.activeUsers.weekly.active ?? 0,
3299
+ stats.activeUsers.monthly.active ?? 0,
3300
+ 1
3301
+ );
3302
+ return /* @__PURE__ */ jsxs(Wrap$1, { "data-testid": "overview-page", children: [
2740
3303
  /* @__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, {
3304
+ /* @__PURE__ */ jsxs("div", { children: [
3305
+ /* @__PURE__ */ jsx(
3306
+ "div",
3307
+ {
3308
+ style: {
3309
+ fontSize: 22,
3310
+ fontWeight: 800,
3311
+ color: T.textPrimary,
3312
+ letterSpacing: "-0.03em"
3313
+ },
3314
+ children: "Overview"
3315
+ }
3316
+ ),
3317
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 11, color: T.textSecondary, marginTop: 4 }, children: (/* @__PURE__ */ new Date()).toLocaleDateString(void 0, {
2744
3318
  weekday: "long",
2745
3319
  year: "numeric",
2746
3320
  month: "long",
2747
3321
  day: "numeric"
2748
- }) }) })
3322
+ }) })
2749
3323
  ] }),
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
- ) })
3324
+ /* @__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
3325
  ] }),
2765
- /* @__PURE__ */ jsxs(Divider, { children: [
2766
- /* @__PURE__ */ jsx(DivLabel, { children: "Sign-ups" }),
3326
+ /* @__PURE__ */ jsxs(SectionDivider, { children: [
3327
+ /* @__PURE__ */ jsx(DivLabel, { children: "Metrics" }),
2767
3328
  /* @__PURE__ */ jsx(DivLine, {})
2768
3329
  ] }),
2769
- /* @__PURE__ */ jsxs(StatRow, { children: [
3330
+ /* @__PURE__ */ jsxs(StatGrid, { children: [
2770
3331
  /* @__PURE__ */ jsx(
2771
- StatCardItem,
3332
+ StatItem,
2772
3333
  {
2773
3334
  id: "total",
2774
3335
  label: "Total Users",
2775
3336
  value: stats.total ?? 0,
2776
3337
  sparkline: totalSpark,
2777
- color: "#4945FF",
3338
+ color: T.accent,
2778
3339
  delay: 0
2779
3340
  }
2780
3341
  ),
2781
3342
  /* @__PURE__ */ jsx(
2782
- StatCardItem,
3343
+ StatItem,
2783
3344
  {
2784
3345
  id: "d-sig",
2785
3346
  label: "Daily Sign-ups",
2786
3347
  value: stats.daily.signUps ?? 0,
2787
3348
  pct: stats.daily.percentage ?? void 0,
2788
3349
  sparkline: newSpark,
2789
- color: "#5CB176",
3350
+ color: T.green,
2790
3351
  delay: 1
2791
3352
  }
2792
3353
  ),
2793
3354
  /* @__PURE__ */ jsx(
2794
- StatCardItem,
3355
+ StatItem,
2795
3356
  {
2796
3357
  id: "w-sig",
2797
3358
  label: "Weekly Sign-ups",
2798
3359
  value: stats.weekly.signUps ?? 0,
2799
3360
  pct: stats.weekly.percentage ?? void 0,
2800
3361
  sparkline: newSpark,
2801
- color: "#5CB176",
3362
+ color: T.green,
2802
3363
  delay: 2
2803
3364
  }
2804
- ),
2805
- /* @__PURE__ */ jsx(
2806
- StatCardItem,
2807
- {
2808
- id: "m-sig",
2809
- label: "Monthly Sign-ups",
2810
- value: stats.monthly.signUps ?? 0,
2811
- pct: stats.monthly.percentage ?? void 0,
2812
- sparkline: newSpark,
2813
- color: "#5CB176",
2814
- delay: 3
2815
- }
2816
- ),
2817
- /* @__PURE__ */ jsx(
2818
- StatCardItem,
2819
- {
2820
- id: "orgs",
2821
- label: "Organizations",
2822
- value: orgCount,
2823
- color: "#9E6BF9",
2824
- delay: 4
2825
- }
2826
- )
2827
- ] }),
2828
- /* @__PURE__ */ jsxs(MainRow, { children: [
2829
- /* @__PURE__ */ jsxs(ChartCard, { $delay: 5, children: [
2830
- /* @__PURE__ */ jsxs(ChartHeader, { children: [
2831
- /* @__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" }),
2838
- graphQuery.isLoading ? /* @__PURE__ */ jsx(
2839
- Flex,
2840
- {
2841
- justifyContent: "center",
2842
- alignItems: "center",
2843
- style: { flex: 1, minHeight: 180 },
2844
- children: /* @__PURE__ */ jsx(Loader, { children: "Loading…" })
2845
- }
2846
- ) : /* @__PURE__ */ jsx(AreaChart, { data: graphData, hovered: hovIdx, onHover: setHovIdx })
2847
- ] }),
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,
2898
- {
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
2906
- }
2907
- )
2908
- ] }),
2909
- /* @__PURE__ */ jsxs(Divider, { children: [
2910
- /* @__PURE__ */ jsx(DivLabel, { children: "Recent Users" }),
2911
- /* @__PURE__ */ jsx(DivLine, {})
2912
- ] }),
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"
2949
- },
2950
- children: fmtDate(u.createdAt)
2951
- }
2952
- ),
2953
- /* @__PURE__ */ jsx(TD$1, { children: /* @__PURE__ */ jsx(
2954
- StatusChip$1,
2955
- {
2956
- $verified: u.emailVerified,
2957
- $banned: u.banned,
2958
- children: u.banned ? "Banned" : u.emailVerified ? "Verified" : "Unverified"
2959
- }
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" })
3184
- ] }),
3185
- someSelected && /* @__PURE__ */ jsxs(
3186
- Button,
3365
+ ),
3366
+ /* @__PURE__ */ jsx(
3367
+ StatItem,
3187
3368
  {
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
- ]
3369
+ id: "m-sig",
3370
+ label: "Monthly Sign-ups",
3371
+ value: stats.monthly.signUps ?? 0,
3372
+ pct: stats.monthly.percentage ?? void 0,
3373
+ sparkline: newSpark,
3374
+ color: T.green,
3375
+ delay: 3
3376
+ }
3377
+ ),
3378
+ /* @__PURE__ */ jsx(
3379
+ StatItem,
3380
+ {
3381
+ id: "orgs",
3382
+ label: "Organizations",
3383
+ value: orgCount,
3384
+ color: T.purple,
3385
+ delay: 4
3198
3386
  }
3199
3387
  )
3200
3388
  ] }),
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,
3389
+ /* @__PURE__ */ jsxs(SectionDivider, { children: [
3390
+ /* @__PURE__ */ jsx(DivLabel, { children: "Growth" }),
3391
+ /* @__PURE__ */ jsx(DivLine, {})
3392
+ ] }),
3393
+ /* @__PURE__ */ jsxs(TwoPanel, { children: [
3394
+ /* @__PURE__ */ jsxs(ChartCard, { $delay: 5, children: [
3395
+ /* @__PURE__ */ jsxs(ChartHeader, { children: [
3396
+ /* @__PURE__ */ jsx(ChartTitle, { children: "User Growth" }),
3397
+ /* @__PURE__ */ jsx(SeriesRow, { children: ALL_SERIES.map((s) => /* @__PURE__ */ jsxs(
3398
+ SeriesBtn,
3399
+ {
3400
+ $on: activeSeries.has(s.key),
3401
+ $c: s.color,
3402
+ onClick: () => toggleSeries(s.key),
3403
+ children: [
3404
+ /* @__PURE__ */ jsx(SDot, { $c: s.color }),
3405
+ s.label
3406
+ ]
3407
+ },
3408
+ s.key
3409
+ )) })
3410
+ ] }),
3411
+ /* @__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" }),
3412
+ graphQuery.isLoading ? /* @__PURE__ */ jsx(
3413
+ Flex,
3206
3414
  {
3207
- checked: allSelected,
3208
- onCheckedChange: handleSelectAll,
3209
- "aria-label": "Select all users"
3415
+ justifyContent: "center",
3416
+ alignItems: "center",
3417
+ style: { flex: 1, minHeight: 170 },
3418
+ children: /* @__PURE__ */ jsx(Loader, { children: "Loading…" })
3210
3419
  }
3211
- ),
3212
- /* @__PURE__ */ jsx("span", { children: "User / Sessions" }),
3213
- /* @__PURE__ */ jsx(ColumnHeaderRight, { children: "Sessions" })
3420
+ ) : /* @__PURE__ */ jsx(
3421
+ GrowthChart,
3422
+ {
3423
+ data: graphData,
3424
+ hovered: hovIdx,
3425
+ onHover: setHovIdx,
3426
+ activeSeries
3427
+ }
3428
+ )
3214
3429
  ] }),
3215
- usersWithSessions.map((userRow, i) => /* @__PURE__ */ jsxs(UserGroup, { $i: i, "data-testid": "session-user-row", children: [
3216
- /* @__PURE__ */ jsxs(UserRowHeader, { children: [
3217
- /* @__PURE__ */ jsx(
3218
- Checkbox,
3430
+ /* @__PURE__ */ jsxs(FeedCard, { $delay: 6, children: [
3431
+ /* @__PURE__ */ jsxs(FeedHead, { children: [
3432
+ /* @__PURE__ */ jsx("span", { children: feedMode === "signups" ? "Recent Sign-ups" : "Recent Active" }),
3433
+ /* @__PURE__ */ jsxs(
3434
+ FeedSelect,
3219
3435
  {
3220
- checked: selected.has(userRow.id),
3221
- onCheckedChange: () => toggleSelect(userRow.id),
3222
- "aria-label": `Select ${userRow.name}`
3436
+ value: feedMode,
3437
+ onChange: (e) => setFeedMode(e.target.value),
3438
+ children: [
3439
+ /* @__PURE__ */ jsx("option", { value: "signups", children: "Recent Sign-ups" }),
3440
+ /* @__PURE__ */ jsx("option", { value: "active", children: "Recent Active" })
3441
+ ]
3223
3442
  }
3224
- ),
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
- ] })
3443
+ )
3235
3444
  ] }),
3236
- userRow.sessions.map((session) => /* @__PURE__ */ jsxs(SessionCard, { "data-testid": "session-row", children: [
3237
- /* @__PURE__ */ jsxs(SessionMeta, { children: [
3238
- /* @__PURE__ */ jsxs(
3239
- Flex,
3445
+ /* @__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: [
3446
+ /* @__PURE__ */ jsxs(FeedTop, { children: [
3447
+ /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 1, style: { minWidth: 0 }, children: [
3448
+ /* @__PURE__ */ jsx(Avatar, { name: u.name, src: u.image, size: 20 }),
3449
+ /* @__PURE__ */ jsx(FeedName, { title: u.name, children: u.name })
3450
+ ] }),
3451
+ /* @__PURE__ */ jsx(FeedMeta, { children: relTime(u.createdAt) })
3452
+ ] }),
3453
+ /* @__PURE__ */ jsx(FeedEmail, { title: u.email, children: u.email })
3454
+ ] }, u.id)) : sessionsQuery.isLoading ? /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ jsx(Loader, { children: "Loading…" }) }) : sessions.length === 0 ? /* @__PURE__ */ jsx(Empty, { children: "No sessions yet" }) : sessions.map((s) => /* @__PURE__ */ jsxs(FeedItem, { children: [
3455
+ /* @__PURE__ */ jsxs(FeedTop, { children: [
3456
+ /* @__PURE__ */ jsx(
3457
+ FeedName,
3240
3458
  {
3241
- gap: 2,
3242
- alignItems: "center",
3243
- style: { flexWrap: "wrap" },
3244
- 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
- ] })
3254
- ]
3459
+ title: s.userAgent ?? void 0,
3460
+ style: { fontFamily: T.mono, fontSize: 10 },
3461
+ children: s.ipAddress ?? ""
3255
3462
  }
3256
3463
  ),
3257
- session.userAgent && /* @__PURE__ */ jsx(AgentText, { children: session.userAgent })
3464
+ /* @__PURE__ */ jsx(FeedMeta, { children: relTime(s.createdAt) })
3258
3465
  ] }),
3259
3466
  /* @__PURE__ */ jsx(
3260
- IconButton,
3467
+ FeedEmail,
3261
3468
  {
3262
- label: "Revoke session",
3263
- onClick: () => setConfirmRevokeSessionId(session.id),
3264
- style: { flexShrink: 0 },
3265
- "data-testid": "revoke-session-btn",
3266
- children: /* @__PURE__ */ jsx(Trash, {})
3469
+ title: s.userAgent ?? void 0,
3470
+ style: {
3471
+ fontSize: 9,
3472
+ overflow: "hidden",
3473
+ textOverflow: "ellipsis",
3474
+ whiteSpace: "nowrap"
3475
+ },
3476
+ children: s.userAgent ?? "Unknown agent"
3267
3477
  }
3268
3478
  )
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,
3479
+ ] }, s.documentId)) })
3480
+ ] })
3481
+ ] }),
3482
+ /* @__PURE__ */ jsxs(SectionDivider, { children: [
3483
+ /* @__PURE__ */ jsx(DivLabel, { children: "Retention & Activity" }),
3484
+ /* @__PURE__ */ jsx(DivLine, {})
3485
+ ] }),
3486
+ /* @__PURE__ */ jsxs(TwoPanel, { children: [
3487
+ /* @__PURE__ */ jsxs(RtnCard, { $delay: 7, children: [
3488
+ /* @__PURE__ */ jsxs(ChartHeader, { children: [
3489
+ /* @__PURE__ */ jsx(ChartTitle, { children: "Cohort Retention" }),
3490
+ /* @__PURE__ */ jsx(Flex, { alignItems: "center", gap: 2, children: [
3491
+ { hue: 142, label: "≥70%" },
3492
+ { hue: 38, label: "40–70%" },
3493
+ { hue: 4, label: "<40%" }
3494
+ ].map(({ hue, label }) => /* @__PURE__ */ jsxs(
3495
+ Flex,
3496
+ {
3497
+ alignItems: "center",
3498
+ gap: 1,
3499
+ style: { marginLeft: 8 },
3500
+ children: [
3501
+ /* @__PURE__ */ jsx(
3502
+ "span",
3503
+ {
3504
+ style: {
3505
+ width: 8,
3506
+ height: 8,
3507
+ borderRadius: "50%",
3508
+ background: `hsl(${hue},60%,50%)`,
3509
+ display: "inline-block",
3510
+ flexShrink: 0
3511
+ }
3512
+ }
3513
+ ),
3514
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 9, color: T.textMuted }, children: label })
3515
+ ]
3516
+ },
3517
+ hue
3518
+ )) })
3519
+ ] }),
3520
+ /* @__PURE__ */ jsx(RtnTip, { children: rtnHovRow ? `Cohort ${rtnHovRow.label} · ${rtnHovRow.cohortSize.toLocaleString()} users · active ${rtnHovRow.activeStart} – ${rtnHovRow.activeEnd}` : "Hover a row to see cohort details" }),
3521
+ retentionQuery.isLoading ? /* @__PURE__ */ jsx(
3522
+ Flex,
3523
+ {
3524
+ justifyContent: "center",
3525
+ alignItems: "center",
3526
+ style: { minHeight: 80 },
3527
+ children: /* @__PURE__ */ jsx(Loader, { children: "Loading…" })
3528
+ }
3529
+ ) : rtnData.length === 0 ? /* @__PURE__ */ jsxs(Empty, { children: [
3530
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 22 }, children: "📉" }),
3531
+ /* @__PURE__ */ jsx("div", { children: "No retention data for this period" })
3532
+ ] }) : /* @__PURE__ */ jsx("div", { style: { display: "flex", flexDirection: "column", gap: 5 }, children: rtnData.map((row, i) => {
3533
+ const hue = rateHue(row.retentionRate);
3534
+ return /* @__PURE__ */ jsxs(
3535
+ RtnRow,
3536
+ {
3537
+ $hov: rtnHov === i,
3538
+ onMouseEnter: () => setRtnHov(i),
3539
+ onMouseLeave: () => setRtnHov(null),
3540
+ children: [
3541
+ /* @__PURE__ */ jsx(RtnLabel, { children: row.label }),
3542
+ /* @__PURE__ */ jsx(RtnSize, { children: row.cohortSize.toLocaleString() }),
3543
+ /* @__PURE__ */ jsx(RtnTrack, { children: /* @__PURE__ */ jsx(RtnBar, { $w: row.retentionRate, $hue: hue }) }),
3544
+ /* @__PURE__ */ jsxs(RtnPct, { $hue: hue, children: [
3545
+ row.retentionRate.toFixed(1),
3546
+ "%"
3547
+ ] })
3548
+ ]
3549
+ },
3550
+ row.n
3551
+ );
3552
+ }) })
3553
+ ] }),
3554
+ /* @__PURE__ */ jsx(RingGrid, { children: [
3275
3555
  {
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,
3556
+ label: "Daily Active",
3557
+ value: stats.activeUsers.daily.active,
3558
+ pct: stats.activeUsers.daily.percentage,
3559
+ color: T.amber,
3560
+ sparkline: activeSpark,
3561
+ delay: 8
3562
+ },
3285
3563
  {
3286
- variant: "tertiary",
3287
- size: "S",
3288
- onClick: () => setPage((p) => p + 1),
3289
- children: "Next"
3564
+ label: "Weekly Active",
3565
+ value: stats.activeUsers.weekly.active,
3566
+ pct: stats.activeUsers.weekly.percentage,
3567
+ color: T.green,
3568
+ sparkline: activeSpark,
3569
+ delay: 9
3570
+ },
3571
+ {
3572
+ label: "Monthly Active",
3573
+ value: stats.activeUsers.monthly.active,
3574
+ pct: stats.activeUsers.monthly.percentage,
3575
+ color: T.accent,
3576
+ sparkline: activeSpark,
3577
+ delay: 10
3290
3578
  }
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
- )
3579
+ ].map(({ label, value, pct, color, delay }) => {
3580
+ const isPos = pct == null || pct >= 0;
3581
+ return /* @__PURE__ */ jsxs(RingCard, { $delay: delay, children: [
3582
+ /* @__PURE__ */ jsx(
3583
+ ProgressRing,
3584
+ {
3585
+ value: value ?? 0,
3586
+ max: activeMax,
3587
+ color,
3588
+ size: 56
3589
+ }
3590
+ ),
3591
+ /* @__PURE__ */ jsxs(RingInfo, { children: [
3592
+ /* @__PURE__ */ jsx(RingLabel, { children: label }),
3593
+ /* @__PURE__ */ jsx(RingVal, { children: (value ?? 0).toLocaleString() }),
3594
+ pct != null && /* @__PURE__ */ jsxs(
3595
+ TrendBadge,
3596
+ {
3597
+ $pos: isPos,
3598
+ style: { marginTop: 4, marginBottom: 0 },
3599
+ children: [
3600
+ isPos ? "↑" : "↓",
3601
+ " ",
3602
+ Math.abs(pct).toFixed(1),
3603
+ "%"
3604
+ ]
3605
+ }
3606
+ )
3607
+ ] })
3608
+ ] }, label);
3609
+ }) })
3610
+ ] })
3315
3611
  ] });
3316
3612
  }
3317
3613
  function useUsers(options = {}) {
@@ -3552,6 +3848,44 @@ const ReadOnlyCodeInput = styled.div`
3552
3848
  word-break: break-all;
3553
3849
  overflow-wrap: anywhere;
3554
3850
  `;
3851
+ const SessionCard = styled.div`
3852
+ display: flex;
3853
+ align-items: flex-start;
3854
+ justify-content: space-between;
3855
+ gap: 12px;
3856
+ padding: 10px 0;
3857
+ border-bottom: 1px solid #f5f5f9;
3858
+ &:last-child { border-bottom: none; }
3859
+ `;
3860
+ const SessionMeta = styled.div`
3861
+ display: flex;
3862
+ flex-direction: column;
3863
+ gap: 3px;
3864
+ flex: 1;
3865
+ min-width: 0;
3866
+ `;
3867
+ const IpChip = styled.span`
3868
+ display: inline-block;
3869
+ background: #f0f0ff;
3870
+ color: #4945ff;
3871
+ border-radius: 5px;
3872
+ padding: 2px 6px;
3873
+ font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
3874
+ font-size: 11px;
3875
+ font-weight: 600;
3876
+ `;
3877
+ const TimestampText = styled.span`
3878
+ font-size: 11px;
3879
+ color: #8e8ea9;
3880
+ `;
3881
+ const AgentText = styled.span`
3882
+ font-size: 11px;
3883
+ color: #b8b8c7;
3884
+ overflow: hidden;
3885
+ text-overflow: ellipsis;
3886
+ white-space: nowrap;
3887
+ display: block;
3888
+ `;
3555
3889
  const STANDARD_FIELDS = /* @__PURE__ */ new Set([
3556
3890
  "id",
3557
3891
  "name",
@@ -3573,6 +3907,7 @@ function UserDetailDrawer({
3573
3907
  }) {
3574
3908
  const qc = useQueryClient();
3575
3909
  const { toggleNotification } = useNotification();
3910
+ const { get, put } = useFetchClient();
3576
3911
  const schemaQuery = useModelSchema("user");
3577
3912
  const userQuery = useQuery({
3578
3913
  queryKey: ["dash-user", userId],
@@ -3594,6 +3929,27 @@ function UserDetailDrawer({
3594
3929
  return result.data?.organizations ?? [];
3595
3930
  }
3596
3931
  });
3932
+ const sessionsQuery = useQuery({
3933
+ queryKey: ["dash-user-sessions", userId],
3934
+ queryFn: async () => {
3935
+ const { data } = await get(
3936
+ `/better-auth-dashboard/db?uid=plugin::better-auth.session&filters[userId][$eq]=${userId}&sort[0]=createdAt:desc&pagination[pageSize]=50`
3937
+ );
3938
+ return data.results ?? [];
3939
+ }
3940
+ });
3941
+ const strapiUserQuery = useQuery({
3942
+ queryKey: ["dash-strapi-user", userId],
3943
+ enabled: !!schemaQuery.data,
3944
+ queryFn: async () => {
3945
+ const relationFields = Object.entries(schemaQuery.data).filter(([, attr]) => attr.type === "relation").map(([fieldName]) => fieldName);
3946
+ const populateParam = relationFields.length > 0 ? `&populate=${encodeURIComponent(relationFields.join(","))}` : "";
3947
+ const { data } = await get(
3948
+ `/better-auth-dashboard/db?uid=plugin::better-auth.user&filters[id][$eq]=${userId}&pagination[pageSize]=1${populateParam}`
3949
+ );
3950
+ return data.results?.[0] ?? null;
3951
+ }
3952
+ });
3597
3953
  const [activeTab, setActiveTab] = useState("profile");
3598
3954
  const [editName, setEditName] = useState(void 0);
3599
3955
  const [editEmail, setEditEmail] = useState(void 0);
@@ -3604,6 +3960,7 @@ function UserDetailDrawer({
3604
3960
  const [banReason, setBanReason] = useState("");
3605
3961
  const [banExpiresDays, setBanExpiresDays] = useState("");
3606
3962
  const [confirmRevokeAll, setConfirmRevokeAll] = useState(false);
3963
+ const [confirmRevokeSessionId, setConfirmRevokeSessionId] = useState(null);
3607
3964
  const [confirmUnban, setConfirmUnban] = useState(false);
3608
3965
  const [confirmUnlinkAccountId, setConfirmUnlinkAccountId] = useState(null);
3609
3966
  const [confirmDisable2FA, setConfirmDisable2FA] = useState(false);
@@ -3622,28 +3979,43 @@ function UserDetailDrawer({
3622
3979
  setEditExtra((prev) => ({ ...prev, [name]: value }));
3623
3980
  };
3624
3981
  const extraData = {
3625
- ...user,
3982
+ ...strapiUserQuery.data ?? {},
3626
3983
  ...editExtra
3627
3984
  };
3628
3985
  const updateMutation = useMutation({
3629
3986
  mutationFn: async () => {
3630
- const body = { ...editExtra };
3631
- if (editName !== void 0) body.name = editName;
3632
- if (editEmail !== void 0) body.email = editEmail;
3987
+ const baBody = {};
3988
+ if (editName !== void 0) baBody.name = editName;
3989
+ if (editEmail !== void 0) baBody.email = editEmail;
3633
3990
  if (editEmailVerified !== void 0)
3634
- body.emailVerified = editEmailVerified;
3635
- if (editImage !== void 0) body.image = editImage;
3636
- const result = await client.dash.updateUser(
3637
- body,
3638
- withContext({ userId })
3639
- );
3640
- if (result.error)
3641
- throw new Error(result.error.message ?? "Update failed");
3642
- return result.data;
3991
+ baBody.emailVerified = editEmailVerified;
3992
+ if (editImage !== void 0) baBody.image = editImage;
3993
+ const ops = [];
3994
+ if (Object.keys(baBody).length > 0) {
3995
+ ops.push(
3996
+ client.dash.updateUser(baBody, withContext({ userId })).then((result) => {
3997
+ if (result.error)
3998
+ throw new Error(result.error.message ?? "Update failed");
3999
+ })
4000
+ );
4001
+ }
4002
+ if (Object.keys(editExtra).length > 0) {
4003
+ const documentId = strapiUserQuery.data?.documentId;
4004
+ if (!documentId)
4005
+ throw new Error("Could not resolve documentId for user");
4006
+ ops.push(
4007
+ put(
4008
+ `/better-auth-dashboard/db/${documentId}?uid=plugin::better-auth.user`,
4009
+ editExtra
4010
+ )
4011
+ );
4012
+ }
4013
+ await Promise.all(ops);
3643
4014
  },
3644
4015
  onSuccess: () => {
3645
4016
  qc.invalidateQueries({ queryKey: ["dash-user", userId] });
3646
4017
  qc.invalidateQueries({ queryKey: ["dash-users"] });
4018
+ qc.invalidateQueries({ queryKey: ["dash-strapi-user", userId] });
3647
4019
  setEditName(void 0);
3648
4020
  setEditEmail(void 0);
3649
4021
  setEditEmailVerified(void 0);
@@ -3662,6 +4034,27 @@ function UserDetailDrawer({
3662
4034
  });
3663
4035
  }
3664
4036
  });
4037
+ const revokeSessionMutation = useMutation({
4038
+ mutationFn: async (sessionId) => {
4039
+ const result = await client.dash.sessions.revoke(
4040
+ {},
4041
+ withContext({ sessionId })
4042
+ );
4043
+ if (result.error)
4044
+ throw new Error(result.error.message ?? "Revoke failed");
4045
+ },
4046
+ onSuccess: () => {
4047
+ setConfirmRevokeSessionId(null);
4048
+ qc.invalidateQueries({ queryKey: ["dash-user-sessions", userId] });
4049
+ toggleNotification({ type: "success", message: "Session revoked" });
4050
+ },
4051
+ onError: (err) => {
4052
+ toggleNotification({
4053
+ type: "danger",
4054
+ message: err.message ?? "Failed to revoke session"
4055
+ });
4056
+ }
4057
+ });
3665
4058
  const passwordMutation = useMutation({
3666
4059
  mutationFn: async () => {
3667
4060
  const result = await client.dash.setPassword(
@@ -4424,25 +4817,55 @@ function UserDetailDrawer({
4424
4817
  }
4425
4818
  ) })
4426
4819
  ] }) }) }),
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
- )
4820
+ /* @__PURE__ */ jsx(Tabs.Content, { value: "sessions", children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 5, paddingTop: 6, children: [
4821
+ /* @__PURE__ */ jsxs(FormSection, { children: [
4822
+ /* @__PURE__ */ jsx(SectionLabel, { children: "Session management" }),
4823
+ /* @__PURE__ */ jsxs(AccountRow, { children: [
4824
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 1, alignItems: "flex-start", children: [
4825
+ /* @__PURE__ */ jsx(Typography, { variant: "omega", fontWeight: "semiBold", children: "Revoke all sessions" }),
4826
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: "Signs the user out immediately on all devices." })
4827
+ ] }),
4828
+ /* @__PURE__ */ jsx(
4829
+ Button,
4830
+ {
4831
+ variant: "danger-light",
4832
+ size: "S",
4833
+ onClick: () => setConfirmRevokeAll(true),
4834
+ style: { flexShrink: 0 },
4835
+ children: "Revoke all"
4836
+ }
4837
+ )
4838
+ ] })
4839
+ ] }),
4840
+ /* @__PURE__ */ jsxs(FormSection, { children: [
4841
+ /* @__PURE__ */ jsx(SectionLabel, { children: "Active sessions" }),
4842
+ 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: 0, alignItems: "stretch", children: (sessionsQuery.data ?? []).map((session) => /* @__PURE__ */ jsxs(SessionCard, { children: [
4843
+ /* @__PURE__ */ jsxs(SessionMeta, { children: [
4844
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", style: { flexWrap: "wrap" }, children: [
4845
+ session.ipAddress && /* @__PURE__ */ jsx(IpChip, { children: session.ipAddress }),
4846
+ /* @__PURE__ */ jsxs(TimestampText, { children: [
4847
+ "Created",
4848
+ " ",
4849
+ new Date(session.createdAt).toLocaleString(),
4850
+ " · Expires",
4851
+ " ",
4852
+ new Date(session.expiresAt).toLocaleString()
4853
+ ] })
4854
+ ] }),
4855
+ session.userAgent && /* @__PURE__ */ jsx(AgentText, { children: session.userAgent })
4856
+ ] }),
4857
+ /* @__PURE__ */ jsx(
4858
+ IconButton,
4859
+ {
4860
+ label: "Revoke session",
4861
+ onClick: () => setConfirmRevokeSessionId(String(session.id)),
4862
+ style: { flexShrink: 0 },
4863
+ children: /* @__PURE__ */ jsx(Trash, {})
4864
+ }
4865
+ )
4866
+ ] }, session.documentId)) })
4444
4867
  ] })
4445
- ] }) }) }),
4868
+ ] }) }),
4446
4869
  /* @__PURE__ */ jsx(Tabs.Content, { value: "organizations", children: /* @__PURE__ */ jsx(Flex, { direction: "column", gap: 5, paddingTop: 6, children: /* @__PURE__ */ jsxs(FormSection, { children: [
4447
4870
  /* @__PURE__ */ jsx(SectionLabel, { children: "Memberships" }),
4448
4871
  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 +4902,17 @@ function UserDetailDrawer({
4479
4902
  onCancel: () => setConfirmRevokeAll(false)
4480
4903
  }
4481
4904
  ),
4905
+ confirmRevokeSessionId && /* @__PURE__ */ jsx(
4906
+ ConfirmDialog,
4907
+ {
4908
+ title: "Revoke session",
4909
+ message: "Are you sure you want to revoke this session? The user will be signed out on this device.",
4910
+ confirmLabel: "Revoke",
4911
+ loading: revokeSessionMutation.isLoading,
4912
+ onConfirm: () => revokeSessionMutation.mutate(confirmRevokeSessionId),
4913
+ onCancel: () => setConfirmRevokeSessionId(null)
4914
+ }
4915
+ ),
4482
4916
  confirmUnban && /* @__PURE__ */ jsx(
4483
4917
  ConfirmDialog,
4484
4918
  {
@@ -5013,8 +5447,7 @@ const Accent = styled.div`
5013
5447
  const BrandIcon = styled.div`
5014
5448
  width: 30px;
5015
5449
  height: 30px;
5016
- border-radius: 7px;
5017
- background: linear-gradient(135deg, #4945ff 0%, #7b79ff 100%);
5450
+ background: black;
5018
5451
  display: flex;
5019
5452
  align-items: center;
5020
5453
  justify-content: center;
@@ -5090,7 +5523,7 @@ function App() {
5090
5523
  paddingBottom: 4,
5091
5524
  children: [
5092
5525
  /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
5093
- /* @__PURE__ */ jsx(BrandIcon, { children: "BA" }),
5526
+ /* @__PURE__ */ jsx(BrandIcon, { children: /* @__PURE__ */ jsx(PluginIcon, {}) }),
5094
5527
  /* @__PURE__ */ jsxs(Box, { children: [
5095
5528
  /* @__PURE__ */ jsx(Typography, { variant: "beta", textColor: "neutral800", children: "Better Auth" }),
5096
5529
  /* @__PURE__ */ jsx(Box, { paddingTop: "2px", children: /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: "Authentication Dashboard" }) })
@@ -5110,8 +5543,7 @@ function App() {
5110
5543
  "data-testid": "nav-organizations",
5111
5544
  children: "Organizations"
5112
5545
  }
5113
- ),
5114
- /* @__PURE__ */ jsx(Tabs.Trigger, { value: "sessions", "data-testid": "nav-sessions", children: "Sessions" })
5546
+ )
5115
5547
  ] })
5116
5548
  ]
5117
5549
  }
@@ -5121,8 +5553,7 @@ function App() {
5121
5553
  ),
5122
5554
  /* @__PURE__ */ jsx(Tabs.Content, { value: "overview", "data-testid": "tab-overview", children: /* @__PURE__ */ jsx(OverviewPage, {}) }),
5123
5555
  /* @__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, {}) })
5556
+ orgEnabled && /* @__PURE__ */ jsx(Tabs.Content, { value: "organizations", "data-testid": "tab-organizations", children: /* @__PURE__ */ jsx(OrganizationsPage, { teamsEnabled }) })
5126
5557
  ] }) });
5127
5558
  }
5128
5559
  function Root() {