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

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.
@@ -3,13 +3,15 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const jsxRuntime = require("react/jsx-runtime");
4
4
  const react = require("react");
5
5
  const reactQuery = require("react-query");
6
+ const reactRouterDom = require("react-router-dom");
6
7
  const designSystem = require("@strapi/design-system");
7
- const styled = require("styled-components");
8
+ const admin = require("@strapi/strapi/admin");
8
9
  const client$2 = require("@better-auth/infra/client");
9
10
  const client$1 = require("better-auth/client");
11
+ const reactIntl = require("react-intl");
12
+ const index = require("./index-WTRykUoM.js");
10
13
  const icons = require("@strapi/icons");
11
- const admin = require("@strapi/strapi/admin");
12
- const index = require("./index-A9PUvldu.js");
14
+ const styled = require("styled-components");
13
15
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
14
16
  const styled__default = /* @__PURE__ */ _interopDefault(styled);
15
17
  const dashPathMethods = () => ({
@@ -103,6 +105,315 @@ const client = client$1.createAuthClient({
103
105
  headers: getAuthHeaders()
104
106
  }
105
107
  });
108
+ const getTranslation = (id) => `${index.PLUGIN_ID}.${id}`;
109
+ const HEIGHT_TOP_NAVIGATION = "6.4rem";
110
+ const HEIGHT_TOP_NAVIGATION_MEDIUM = "5.6rem";
111
+ const WIDTH_SIDE_NAVIGATION = "23.2rem";
112
+ const MainSubNav = styled__default.default(designSystem.SubNav)`
113
+ width: 100%;
114
+ height: 100%;
115
+ overflow: hidden;
116
+ background-color: ${({ theme }) => theme.colors.neutral0};
117
+ display: flex;
118
+ flex-direction: column;
119
+ border-right: 0;
120
+ box-shadow: none;
121
+ position: relative;
122
+
123
+ ${({ theme }) => theme.breakpoints.medium} {
124
+ width: ${WIDTH_SIDE_NAVIGATION};
125
+ position: sticky;
126
+ top: 0;
127
+ border-right: 1px solid ${({ theme }) => theme.colors.neutral150};
128
+ overscroll-behavior: contain;
129
+ }
130
+ `;
131
+ const Main = ({ children, ...props }) => /* @__PURE__ */ jsxRuntime.jsx(MainSubNav, { ...props, children });
132
+ const StyledLink = styled__default.default(reactRouterDom.NavLink)`
133
+ display: flex;
134
+ align-items: center;
135
+ justify-content: space-between;
136
+ text-decoration: none;
137
+ height: 32px;
138
+ color: ${({ theme }) => theme.colors.neutral800};
139
+
140
+ &.active > div {
141
+ background-color: ${({ theme }) => theme.colors.primary100};
142
+ color: ${({ theme }) => theme.colors.primary700};
143
+ font-weight: 500;
144
+ }
145
+
146
+ &:hover.active > div {
147
+ background-color: ${({ theme }) => theme.colors.primary100};
148
+ color: ${({ theme }) => theme.colors.primary700};
149
+ font-weight: 500;
150
+ }
151
+
152
+ &:hover > div {
153
+ background-color: ${({ theme }) => theme.colors.neutral100};
154
+ }
155
+
156
+ &:focus-visible {
157
+ outline-offset: -2px;
158
+ }
159
+ `;
160
+ const Link = (props) => {
161
+ const { to, label, endAction, handleClick, ...rest } = props;
162
+ return /* @__PURE__ */ jsxRuntime.jsx(StyledLink, { to, ...rest, onClick: handleClick, children: /* @__PURE__ */ jsxRuntime.jsx(
163
+ designSystem.Box,
164
+ {
165
+ width: "100%",
166
+ paddingLeft: 3,
167
+ paddingRight: 3,
168
+ paddingTop: { initial: 1, large: 0 },
169
+ paddingBottom: { initial: 1, large: 0 },
170
+ borderRadius: 1,
171
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
172
+ designSystem.Flex,
173
+ {
174
+ justifyContent: "space-between",
175
+ width: "100%",
176
+ gap: { initial: 2, large: 1 },
177
+ children: [
178
+ /* @__PURE__ */ jsxRuntime.jsx(
179
+ designSystem.Typography,
180
+ {
181
+ tag: "div",
182
+ lineHeight: "32px",
183
+ width: { initial: "80dvw", medium: "100%" },
184
+ overflow: "hidden",
185
+ style: { textOverflow: "ellipsis", whiteSpace: "nowrap" },
186
+ children: label
187
+ }
188
+ ),
189
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { gap: 2, children: endAction })
190
+ ]
191
+ }
192
+ )
193
+ }
194
+ ) });
195
+ };
196
+ const StyledHeader = styled__default.default(designSystem.Flex)`
197
+ flex: 0 0 ${HEIGHT_TOP_NAVIGATION};
198
+ height: ${HEIGHT_TOP_NAVIGATION};
199
+
200
+ ${({ theme }) => theme.breakpoints.medium} {
201
+ flex: 0 0 ${HEIGHT_TOP_NAVIGATION_MEDIUM};
202
+ height: ${HEIGHT_TOP_NAVIGATION_MEDIUM};
203
+ }
204
+ `;
205
+ const Header = ({ label }) => {
206
+ return /* @__PURE__ */ jsxRuntime.jsx(
207
+ StyledHeader,
208
+ {
209
+ justifyContent: "space-between",
210
+ paddingLeft: 5,
211
+ paddingRight: 5,
212
+ children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "beta", tag: "h2", children: label })
213
+ }
214
+ );
215
+ };
216
+ const Sections = ({
217
+ children,
218
+ ...props
219
+ }) => {
220
+ return /* @__PURE__ */ jsxRuntime.jsx(
221
+ designSystem.Box,
222
+ {
223
+ paddingTop: { initial: 5, large: 4 },
224
+ paddingBottom: { initial: 5, large: 4 },
225
+ paddingLeft: { initial: 3, large: 0 },
226
+ paddingRight: { initial: 3, large: 0 },
227
+ maxWidth: { initial: "100%", medium: WIDTH_SIDE_NAVIGATION },
228
+ children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { tag: "ul", gap: 6, direction: "column", alignItems: "stretch", ...props, children: children.map((child, index2) => {
229
+ return /* @__PURE__ */ jsxRuntime.jsx("li", { children: child }, index2);
230
+ }) })
231
+ }
232
+ );
233
+ };
234
+ const Section = ({
235
+ label,
236
+ children,
237
+ badgeLabel
238
+ }) => {
239
+ const listId = react.useId();
240
+ return /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", alignItems: "stretch", gap: 2, children: [
241
+ /* @__PURE__ */ jsxRuntime.jsx(
242
+ designSystem.Box,
243
+ {
244
+ paddingLeft: {
245
+ initial: 3,
246
+ large: 5
247
+ },
248
+ paddingRight: {
249
+ initial: 3,
250
+ large: 5
251
+ },
252
+ children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { position: "relative", justifyContent: "space-between", gap: 2, children: [
253
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", textColor: "neutral600", children: label }) }) }),
254
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { gap: 1, children: badgeLabel && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { backgroundColor: "neutral150", textColor: "neutral600", children: badgeLabel }) })
255
+ ] })
256
+ }
257
+ ),
258
+ /* @__PURE__ */ jsxRuntime.jsx(
259
+ designSystem.Flex,
260
+ {
261
+ tag: "ol",
262
+ id: listId,
263
+ direction: "column",
264
+ gap: { initial: 2, large: "2px" },
265
+ alignItems: "stretch",
266
+ marginLeft: {
267
+ initial: 0,
268
+ large: 2
269
+ },
270
+ marginRight: {
271
+ initial: 0,
272
+ large: 2
273
+ },
274
+ children: children.map((child, index2) => {
275
+ return /* @__PURE__ */ jsxRuntime.jsx("li", { children: child }, index2);
276
+ })
277
+ }
278
+ )
279
+ ] });
280
+ };
281
+ styled__default.default.button`
282
+ cursor: pointer;
283
+ width: 100%;
284
+ border: none;
285
+ padding: 0;
286
+ background: transparent;
287
+ display: flex;
288
+ align-items: center;
289
+ border-radius: ${({ theme }) => theme.borderRadius};
290
+ padding-left: ${({ theme }) => theme.spaces[3]};
291
+ padding-right: ${({ theme }) => theme.spaces[3]};
292
+ padding-top: ${({ theme }) => theme.spaces[2]};
293
+ padding-bottom: ${({ theme }) => theme.spaces[2]};
294
+
295
+ &:hover {
296
+ background-color: ${({ theme }) => theme.colors.neutral100};
297
+ }
298
+ `;
299
+ styled__default.default.li`
300
+ ${StyledLink} > div {
301
+ padding-left: 36px;
302
+ }
303
+ `;
304
+ styled__default.default(designSystem.Box)`
305
+ width: 100%;
306
+
307
+ ${MainSubNav} {
308
+ background-color: transparent;
309
+ border-right: none;
310
+ }
311
+
312
+ ${({ theme }) => theme.breakpoints.medium} {
313
+ overflow: hidden;
314
+
315
+ ${MainSubNav} {
316
+ top: 0;
317
+ }
318
+ }
319
+ `;
320
+ const Content = ({ children }) => {
321
+ return /* @__PURE__ */ jsxRuntime.jsx(designSystem.ScrollArea, { children });
322
+ };
323
+ const SubNav = {
324
+ Main,
325
+ Content,
326
+ Header,
327
+ Link,
328
+ Sections,
329
+ Section
330
+ };
331
+ const sections = [
332
+ {
333
+ id: "api-roles",
334
+ intlLabel: {
335
+ id: getTranslation("settings.general"),
336
+ defaultMessage: "General"
337
+ },
338
+ links: [
339
+ {
340
+ id: "overview",
341
+ intlLabel: {
342
+ id: getTranslation("settings.overview"),
343
+ defaultMessage: "Overview"
344
+ },
345
+ to: "/plugins/better-auth-dashboard/overview",
346
+ testid: "nav-overview"
347
+ },
348
+ {
349
+ id: "users",
350
+ intlLabel: {
351
+ id: getTranslation("settings.users"),
352
+ defaultMessage: "User Management"
353
+ },
354
+ to: "/plugins/better-auth-dashboard/users",
355
+ testid: "nav-users"
356
+ },
357
+ {
358
+ id: "organizations",
359
+ intlLabel: {
360
+ id: getTranslation("settings.organizations"),
361
+ defaultMessage: "Organization Management"
362
+ },
363
+ to: "/plugins/better-auth-dashboard/organizations",
364
+ testid: "nav-organizations"
365
+ }
366
+ ]
367
+ }
368
+ ];
369
+ const Nav = ({ isFullPage = false, orgEnabled = false }) => {
370
+ const { formatMessage } = reactIntl.useIntl();
371
+ const overviewRBAC = admin.useRBAC(index.PERMISSIONS.overview);
372
+ const userRBAC = admin.useRBAC(index.PERMISSIONS.user);
373
+ const organizationRBAC = admin.useRBAC(index.PERMISSIONS.organization);
374
+ return /* @__PURE__ */ jsxRuntime.jsxs(SubNav.Main, { children: [
375
+ !isFullPage && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
376
+ /* @__PURE__ */ jsxRuntime.jsx(SubNav.Header, { label: "Authentication" }),
377
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Divider, {})
378
+ ] }),
379
+ /* @__PURE__ */ jsxRuntime.jsxs(SubNav.Content, { children: [
380
+ isFullPage && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
381
+ /* @__PURE__ */ jsxRuntime.jsx(SubNav.Header, { label: "Authentication" }),
382
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Divider, {})
383
+ ] }),
384
+ /* @__PURE__ */ jsxRuntime.jsx(SubNav.Sections, { "data-testid": "main-nav", children: sections.map((section) => /* @__PURE__ */ jsxRuntime.jsx(
385
+ SubNav.Section,
386
+ {
387
+ label: formatMessage(section.intlLabel),
388
+ children: section.links.filter((link) => orgEnabled || link.id !== "organizations").filter((link) => {
389
+ if (link.id === "overview") {
390
+ return overviewRBAC.allowedActions.canRead;
391
+ }
392
+ if (link.id === "users") {
393
+ return userRBAC.allowedActions.canRead;
394
+ }
395
+ if (link.id === "organizations") {
396
+ return organizationRBAC.allowedActions.canRead;
397
+ }
398
+ return true;
399
+ }).map((link) => {
400
+ return /* @__PURE__ */ jsxRuntime.jsx(
401
+ SubNav.Link,
402
+ {
403
+ to: link.to,
404
+ label: formatMessage(link.intlLabel),
405
+ "data-testid": link.testid,
406
+ children: formatMessage(link.intlLabel)
407
+ },
408
+ link.id
409
+ );
410
+ })
411
+ },
412
+ section.id
413
+ )) })
414
+ ] })
415
+ ] });
416
+ };
106
417
  function useDashConfig() {
107
418
  return reactQuery.useQuery({
108
419
  queryKey: ["dash-config"],
@@ -119,6 +430,74 @@ function useDashConfig() {
119
430
  function hasPlugin(config, id) {
120
431
  return config?.plugins.some((p) => p.id === id) ?? false;
121
432
  }
433
+ const RESPONSIVE_DEFAULT_SPACING = {
434
+ initial: 4,
435
+ medium: 6,
436
+ large: 10
437
+ };
438
+ const App = () => {
439
+ const { data: config, isLoading, isError, error } = useDashConfig();
440
+ const orgEnabled = hasPlugin(config, "organization");
441
+ const orgOptionsQuery = reactQuery.useQuery({
442
+ queryKey: ["dash-org-options"],
443
+ queryFn: async () => {
444
+ const result = await client.dash.organization.options();
445
+ if (result.error) return { teamsEnabled: false };
446
+ return result.data ?? { teamsEnabled: false };
447
+ },
448
+ enabled: orgEnabled
449
+ });
450
+ const teamsEnabled = orgEnabled ? orgOptionsQuery.data?.teamsEnabled ?? false : false;
451
+ if (isLoading) {
452
+ return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", alignItems: "center", padding: 12, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading Better Auth dashboard…" }) });
453
+ }
454
+ if (isError) {
455
+ return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { padding: 6, children: /* @__PURE__ */ jsxRuntime.jsx(
456
+ designSystem.Alert,
457
+ {
458
+ closeLabel: "Close",
459
+ title: "Error loading configuration",
460
+ variant: "danger",
461
+ children: error?.message
462
+ }
463
+ ) });
464
+ }
465
+ if (!config) return null;
466
+ return /* @__PURE__ */ jsxRuntime.jsx(
467
+ "div",
468
+ {
469
+ "data-testid": "dashboard-root",
470
+ style: { display: "flex", flex: "1 1 auto", minHeight: "100vh" },
471
+ children: /* @__PURE__ */ jsxRuntime.jsxs(admin.Layouts.Root, { sideNav: /* @__PURE__ */ jsxRuntime.jsx(Nav, { orgEnabled }), children: [
472
+ /* @__PURE__ */ jsxRuntime.jsx(admin.Page.Title, { children: "Authentication" }),
473
+ /* @__PURE__ */ jsxRuntime.jsx(
474
+ designSystem.Box,
475
+ {
476
+ display: { initial: "block", medium: "none" },
477
+ paddingLeft: RESPONSIVE_DEFAULT_SPACING,
478
+ paddingRight: RESPONSIVE_DEFAULT_SPACING,
479
+ paddingTop: RESPONSIVE_DEFAULT_SPACING,
480
+ children: /* @__PURE__ */ jsxRuntime.jsx(admin.BackButton, { fallback: "/plugins/better-auth-dashboard" })
481
+ }
482
+ ),
483
+ /* @__PURE__ */ jsxRuntime.jsx(reactRouterDom.Outlet, { context: { config, teamsEnabled } })
484
+ ] })
485
+ }
486
+ );
487
+ };
488
+ const ProtectedApp = () => {
489
+ return /* @__PURE__ */ jsxRuntime.jsx(
490
+ admin.Page.Protect,
491
+ {
492
+ permissions: [
493
+ ...index.PERMISSIONS.overview,
494
+ ...index.PERMISSIONS.user,
495
+ ...index.PERMISSIONS.organization
496
+ ],
497
+ children: /* @__PURE__ */ jsxRuntime.jsx(App, {})
498
+ }
499
+ );
500
+ };
122
501
  function ConfirmDialog({
123
502
  title,
124
503
  message,
@@ -145,8 +524,9 @@ function ConfirmDialog({
145
524
  );
146
525
  }
147
526
  const DASH_CONTEXT_HEADER = "X-Dash-Context";
148
- const withContext = (ctx) => ({
527
+ const withContext = (ctx, headers) => ({
149
528
  headers: {
529
+ ...headers,
150
530
  [DASH_CONTEXT_HEADER]: btoa(JSON.stringify(ctx))
151
531
  }
152
532
  });
@@ -454,11 +834,22 @@ function UserCombobox({
454
834
  queryKey: ["user-combobox-search", search],
455
835
  queryFn: async () => {
456
836
  const whereClause = search ? JSON.stringify([
457
- { field: "email", operator: "contains", value: search }
837
+ {
838
+ field: "email",
839
+ operator: "contains",
840
+ value: search,
841
+ connector: "OR"
842
+ },
843
+ {
844
+ field: "name",
845
+ operator: "contains",
846
+ value: search,
847
+ connector: "OR"
848
+ }
458
849
  ]) : void 0;
459
850
  const result = await client.dash.listUsers({
460
851
  query: {
461
- limit: 20,
852
+ limit: search ? 100 : 20,
462
853
  offset: 0,
463
854
  sortBy: "createdAt",
464
855
  sortOrder: "desc",
@@ -510,7 +901,7 @@ function CreateOrganizationDialog({ teamsEnabled, onClose }) {
510
901
  ...logo ? { logo } : {},
511
902
  ...teamsEnabled && !skipDefaultTeam && defaultTeamName ? { defaultTeamName } : {}
512
903
  },
513
- withContext({ userId: ownerId, skipDefaultTeam })
904
+ withContext({ userId: ownerId, skipDefaultTeam }, getAuthHeaders())
514
905
  );
515
906
  if (result.error)
516
907
  throw new Error(result.error.message ?? "Create failed");
@@ -546,6 +937,7 @@ function CreateOrganizationDialog({ teamsEnabled, onClose }) {
546
937
  loading: createMutation.isLoading,
547
938
  disabled: !name || !slug || !ownerId,
548
939
  onClick: () => createMutation.mutate(),
940
+ "data-testid": "create-org-submit",
549
941
  children: "Create organization"
550
942
  }
551
943
  )
@@ -633,6 +1025,159 @@ function CreateOrganizationDialog({ teamsEnabled, onClose }) {
633
1025
  function slugify(str) {
634
1026
  return str.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
635
1027
  }
1028
+ const EntryRow = styled__default.default.div`
1029
+ display: flex;
1030
+ align-items: center;
1031
+ justify-content: space-between;
1032
+ gap: 8px;
1033
+ padding: 8px 12px;
1034
+ width: 100%;
1035
+ box-sizing: border-box;
1036
+ background: #ffffff;
1037
+ border: 1px solid ${({ theme }) => theme.colors?.neutral200 ?? "#dcdce4"};
1038
+ border-radius: 4px;
1039
+ min-width: 0;
1040
+ `;
1041
+ function getDisplayLabel(doc) {
1042
+ for (const key of ["name", "title", "email", "username", "label", "slug"]) {
1043
+ const v = doc[key];
1044
+ if (typeof v === "string" && v.trim()) return v;
1045
+ }
1046
+ return String(doc.documentId ?? doc.id ?? "");
1047
+ }
1048
+ function normalizeValue(val, cache) {
1049
+ if (!val) return [];
1050
+ if (typeof val === "object" && !Array.isArray(val) && val !== null && "set" in val) {
1051
+ const items2 = val.set ?? [];
1052
+ return items2.filter((item) => item?.documentId).map((item) => ({
1053
+ documentId: item.documentId,
1054
+ label: cache.get(item.documentId) ?? item.documentId
1055
+ }));
1056
+ }
1057
+ const items = Array.isArray(val) ? val : [val];
1058
+ return items.filter(
1059
+ (item) => typeof item === "object" && item !== null && "documentId" in item
1060
+ ).map((item) => ({
1061
+ documentId: String(item.documentId),
1062
+ label: getDisplayLabel(item)
1063
+ }));
1064
+ }
1065
+ function RelationField({
1066
+ name,
1067
+ label,
1068
+ attribute,
1069
+ value,
1070
+ onChange,
1071
+ readOnly = false
1072
+ }) {
1073
+ const { get } = admin.useFetchClient();
1074
+ const cache = react.useRef(/* @__PURE__ */ new Map());
1075
+ const [search, setSearch] = react.useState("");
1076
+ const [addKey, setAddKey] = react.useState(0);
1077
+ const target = attribute.target ?? "";
1078
+ const relationType = attribute.relation ?? "manyToOne";
1079
+ const isMulti = relationType === "oneToMany" || relationType === "manyToMany";
1080
+ const currentDocs = normalizeValue(value, cache.current);
1081
+ const selectedIds = new Set(currentDocs.map((d) => d.documentId));
1082
+ const { data: searchResults = [], isLoading } = reactQuery.useQuery({
1083
+ queryKey: ["relation-search", target, search],
1084
+ queryFn: async () => {
1085
+ const params = new URLSearchParams({
1086
+ uid: target,
1087
+ "pagination[pageSize]": "50"
1088
+ });
1089
+ if (search) params.set("filters[name][$containsi]", search);
1090
+ const { data } = await get(
1091
+ `/better-auth-dashboard/db?${params}`
1092
+ );
1093
+ const docs = data.results ?? [];
1094
+ for (const doc of docs) {
1095
+ if (doc.documentId) {
1096
+ cache.current.set(String(doc.documentId), getDisplayLabel(doc));
1097
+ }
1098
+ }
1099
+ return docs.map((doc) => ({
1100
+ documentId: String(doc.documentId ?? ""),
1101
+ label: getDisplayLabel(doc)
1102
+ })).filter((d) => d.documentId);
1103
+ },
1104
+ keepPreviousData: true,
1105
+ enabled: !readOnly
1106
+ });
1107
+ const availableOptions = searchResults.filter(
1108
+ (r) => !selectedIds.has(r.documentId)
1109
+ );
1110
+ const commit = (ids) => {
1111
+ onChange(name, { set: ids.map((id) => ({ documentId: id })) });
1112
+ };
1113
+ const handleSelect = (val) => {
1114
+ if (!val) return;
1115
+ const opt = searchResults.find((r) => r.documentId === val);
1116
+ if (opt) cache.current.set(val, opt.label);
1117
+ if (isMulti) {
1118
+ if (selectedIds.has(val)) return;
1119
+ commit([...currentDocs.map((d) => d.documentId), val]);
1120
+ } else {
1121
+ commit([val]);
1122
+ }
1123
+ setSearch("");
1124
+ setAddKey((k) => k + 1);
1125
+ };
1126
+ const handleRemove = (docId) => {
1127
+ commit(
1128
+ currentDocs.filter((d) => d.documentId !== docId).map((d) => d.documentId)
1129
+ );
1130
+ };
1131
+ return /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { style: { width: "100%" }, children: [
1132
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: label }),
1133
+ !readOnly && /* @__PURE__ */ jsxRuntime.jsx(
1134
+ designSystem.Combobox,
1135
+ {
1136
+ value: "",
1137
+ onChange: (val) => handleSelect(val),
1138
+ onInputChange: (e) => setSearch(e.target.value),
1139
+ loading: isLoading,
1140
+ placeholder: "Search…",
1141
+ children: availableOptions.map((opt) => /* @__PURE__ */ jsxRuntime.jsx(designSystem.ComboboxOption, { value: opt.documentId, children: opt.label }, opt.documentId))
1142
+ },
1143
+ addKey
1144
+ ),
1145
+ currentDocs.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
1146
+ designSystem.Flex,
1147
+ {
1148
+ direction: "column",
1149
+ gap: 1,
1150
+ style: { marginTop: 8, width: "100%" },
1151
+ children: currentDocs.map((doc) => /* @__PURE__ */ jsxRuntime.jsxs(EntryRow, { children: [
1152
+ /* @__PURE__ */ jsxRuntime.jsx(
1153
+ designSystem.Typography,
1154
+ {
1155
+ variant: "omega",
1156
+ textColor: "neutral800",
1157
+ style: {
1158
+ overflow: "hidden",
1159
+ textOverflow: "ellipsis",
1160
+ whiteSpace: "nowrap",
1161
+ flex: 1,
1162
+ minWidth: 0
1163
+ },
1164
+ children: doc.label
1165
+ }
1166
+ ),
1167
+ !readOnly && /* @__PURE__ */ jsxRuntime.jsx(
1168
+ designSystem.IconButton,
1169
+ {
1170
+ label: "Remove",
1171
+ size: "S",
1172
+ onClick: () => handleRemove(doc.documentId),
1173
+ children: /* @__PURE__ */ jsxRuntime.jsx(icons.Trash, {})
1174
+ }
1175
+ )
1176
+ ] }, doc.documentId))
1177
+ }
1178
+ )
1179
+ ] });
1180
+ }
636
1181
  function makeLabel(name) {
637
1182
  return name.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()).trim();
638
1183
  }
@@ -756,24 +1301,15 @@ function DynamicField({
756
1301
  ] });
757
1302
  }
758
1303
  if (type === "relation") {
759
- return /* @__PURE__ */ jsxRuntime.jsxs(
760
- designSystem.Field.Root,
1304
+ return /* @__PURE__ */ jsxRuntime.jsx(
1305
+ RelationField,
761
1306
  {
762
- style: fieldStyle,
763
- hint: `Relation → ${attribute.target ?? "unknown"}`,
764
- children: [
765
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: label }),
766
- /* @__PURE__ */ jsxRuntime.jsx(
767
- designSystem.TextInput,
768
- {
769
- value: value != null ? String(value) : "",
770
- onChange: (e) => onChange(name, e.target.value),
771
- disabled: readOnly,
772
- placeholder: "ID…"
773
- }
774
- ),
775
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {})
776
- ]
1307
+ name,
1308
+ label,
1309
+ attribute,
1310
+ value,
1311
+ onChange,
1312
+ readOnly
777
1313
  }
778
1314
  );
779
1315
  }
@@ -813,6 +1349,21 @@ function CustomFieldsSection({
813
1349
  )) })
814
1350
  ] });
815
1351
  }
1352
+ function EditViewSidePanels({ model, documentId, document: document2 }) {
1353
+ const panels = index.getEditViewSidePanels(model);
1354
+ if (panels.length === 0) return null;
1355
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: panels.map(({ id, title, Component }) => /* @__PURE__ */ jsxRuntime.jsxs(FormSection, { children: [
1356
+ /* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { children: title }),
1357
+ /* @__PURE__ */ jsxRuntime.jsx(
1358
+ Component,
1359
+ {
1360
+ model,
1361
+ documentId,
1362
+ document: document2
1363
+ }
1364
+ )
1365
+ ] }, id)) });
1366
+ }
816
1367
  const SYSTEM_FIELDS = /* @__PURE__ */ new Set([
817
1368
  "id",
818
1369
  "documentId",
@@ -844,6 +1395,8 @@ function useModelSchema(model) {
844
1395
  if (SYSTEM_FIELDS.has(name)) continue;
845
1396
  if (attr.type === "relation" && typeof attr.target === "string" && (attr.target.startsWith("plugin::users-permissions") || attr.target.startsWith("admin::")))
846
1397
  continue;
1398
+ const baOptions = attr.pluginOptions?.["better-auth"];
1399
+ if (baOptions?.managed === true) continue;
847
1400
  attributes[name] = attr;
848
1401
  }
849
1402
  return attributes;
@@ -896,12 +1449,13 @@ function OrganizationDetail({
896
1449
  const schemaQuery = useModelSchema("organization");
897
1450
  const qc = reactQuery.useQueryClient();
898
1451
  const { toggleNotification } = admin.useNotification();
1452
+ const { get, put } = admin.useFetchClient();
899
1453
  const orgQuery = reactQuery.useQuery({
900
1454
  queryKey: ["dash-org", organizationId],
901
1455
  queryFn: async () => {
902
1456
  const result = await client.dash.organization[organizationId](
903
1457
  {},
904
- withContext({ organizationId })
1458
+ withContext({ organizationId }, getAuthHeaders())
905
1459
  );
906
1460
  if (result.error)
907
1461
  throw new Error(result.error.message ?? "Failed to load org");
@@ -911,7 +1465,7 @@ function OrganizationDetail({
911
1465
  const membersQuery = reactQuery.useQuery({
912
1466
  queryKey: ["dash-org-members", organizationId],
913
1467
  queryFn: async () => {
914
- const result = await client.dash.organization[organizationId].members({}, withContext({ organizationId }));
1468
+ const result = await client.dash.organization[organizationId].members({}, withContext({ organizationId }, getAuthHeaders()));
915
1469
  if (result.error) throw new Error(result.error.message ?? "Failed");
916
1470
  return result.data ?? [];
917
1471
  }
@@ -920,7 +1474,7 @@ function OrganizationDetail({
920
1474
  queryKey: ["dash-org-teams", organizationId],
921
1475
  queryFn: async () => {
922
1476
  if (!teamsEnabled) return [];
923
- const result = await client.dash.organization[organizationId].teams({}, withContext({ organizationId }));
1477
+ const result = await client.dash.organization[organizationId].teams({}, withContext({ organizationId }, getAuthHeaders()));
924
1478
  if (result.error) throw new Error(result.error.message ?? "Failed");
925
1479
  return result.data ?? [];
926
1480
  },
@@ -929,11 +1483,29 @@ function OrganizationDetail({
929
1483
  const ssoQuery = reactQuery.useQuery({
930
1484
  queryKey: ["dash-org-sso", organizationId],
931
1485
  queryFn: async () => {
932
- const result = await client.dash.organization[organizationId].ssoProviders({}, withContext({ organizationId }));
1486
+ const result = await client.dash.organization[organizationId].ssoProviders({}, withContext({ organizationId }, getAuthHeaders()));
1487
+ if (result.error) throw new Error(result.error.message ?? "Failed");
1488
+ return result.data ?? [];
1489
+ }
1490
+ });
1491
+ const invitationsQuery = reactQuery.useQuery({
1492
+ queryKey: ["dash-org-invitations", organizationId],
1493
+ queryFn: async () => {
1494
+ const result = await client.dash.organization[organizationId].invitations({}, withContext({ organizationId }, getAuthHeaders()));
933
1495
  if (result.error) throw new Error(result.error.message ?? "Failed");
934
1496
  return result.data ?? [];
935
1497
  }
936
1498
  });
1499
+ const strapiOrgQuery = reactQuery.useQuery({
1500
+ queryKey: ["dash-strapi-org", organizationId],
1501
+ enabled: !!orgQuery.data,
1502
+ queryFn: async () => {
1503
+ const { data } = await get(
1504
+ `/better-auth-dashboard/db?uid=plugin::better-auth.organization&filters[id][$eq]=${organizationId}&pagination[pageSize]=1`
1505
+ );
1506
+ return data.results?.[0] ?? null;
1507
+ }
1508
+ });
937
1509
  const [activeTab, setActiveTab] = react.useState("details");
938
1510
  const [editName, setEditName] = react.useState(void 0);
939
1511
  const [editSlug, setEditSlug] = react.useState(void 0);
@@ -946,12 +1518,15 @@ function OrganizationDetail({
946
1518
  const [confirmDeleteSsoId, setConfirmDeleteSsoId] = react.useState(
947
1519
  null
948
1520
  );
1521
+ const [inviteEmail, setInviteEmail] = react.useState("");
1522
+ const [inviteRole, setInviteRole] = react.useState("member");
1523
+ const [confirmCancelInvitationId, setConfirmCancelInvitationId] = react.useState(null);
949
1524
  const handleExtraChange = (name, value) => {
950
1525
  setEditExtra((prev) => ({ ...prev, [name]: value }));
951
1526
  };
952
1527
  const org = orgQuery.data;
953
1528
  const extraData = {
954
- ...org,
1529
+ ...strapiOrgQuery.data ?? {},
955
1530
  ...editExtra
956
1531
  };
957
1532
  const updateOrgMutation = reactQuery.useMutation({
@@ -960,17 +1535,21 @@ function OrganizationDetail({
960
1535
  if (editName !== void 0) body.name = editName;
961
1536
  if (editSlug !== void 0) body.slug = editSlug;
962
1537
  if (editLogo !== void 0) body.logo = editLogo;
963
- const result = await client.dash.organization.update(
964
- body,
965
- withContext({ organizationId })
1538
+ const documentId = strapiOrgQuery.data?.documentId;
1539
+ if (!documentId)
1540
+ throw new Error("Could not resolve documentId for organization");
1541
+ await put(
1542
+ `/better-auth-dashboard/db/${documentId}?uid=plugin::better-auth.organization`,
1543
+ body
966
1544
  );
967
- if (result.error)
968
- throw new Error(result.error.message ?? "Update failed");
969
- return result.data;
970
1545
  },
971
- onSuccess: () => {
1546
+ onSuccess: async () => {
1547
+ await qc.invalidateQueries({
1548
+ queryKey: ["dash-strapi-org", organizationId]
1549
+ });
972
1550
  qc.invalidateQueries({ queryKey: ["dash-org", organizationId] });
973
1551
  qc.invalidateQueries({ queryKey: ["dash-organizations"] });
1552
+ qc.invalidateQueries({ queryKey: ["dash-strapi-org", organizationId] });
974
1553
  setEditName(void 0);
975
1554
  setEditSlug(void 0);
976
1555
  setEditLogo(void 0);
@@ -994,7 +1573,7 @@ function OrganizationDetail({
994
1573
  mutationFn: async () => {
995
1574
  const result = await client.dash.organization.addMember(
996
1575
  { userId: addUserId, role: addRole },
997
- withContext({ organizationId })
1576
+ withContext({ organizationId }, getAuthHeaders())
998
1577
  );
999
1578
  if (result.error)
1000
1579
  throw new Error(result.error.message ?? "Add member failed");
@@ -1017,7 +1596,7 @@ function OrganizationDetail({
1017
1596
  mutationFn: async (memberId) => {
1018
1597
  const result = await client.dash.organization.removeMember(
1019
1598
  { memberId },
1020
- withContext({ organizationId })
1599
+ withContext({ organizationId }, getAuthHeaders())
1021
1600
  );
1022
1601
  if (result.error) throw new Error(result.error.message ?? "Failed");
1023
1602
  },
@@ -1040,7 +1619,7 @@ function OrganizationDetail({
1040
1619
  }) => {
1041
1620
  const result = await client.dash.organization.updateMemberRole(
1042
1621
  { memberId, role },
1043
- withContext({ organizationId })
1622
+ withContext({ organizationId }, getAuthHeaders())
1044
1623
  );
1045
1624
  if (result.error) throw new Error(result.error.message ?? "Failed");
1046
1625
  return result.data;
@@ -1061,7 +1640,7 @@ function OrganizationDetail({
1061
1640
  mutationFn: async () => {
1062
1641
  const result = await client.dash.organization.createTeam(
1063
1642
  { name: newTeamName },
1064
- withContext({ organizationId })
1643
+ withContext({ organizationId }, getAuthHeaders())
1065
1644
  );
1066
1645
  if (result.error)
1067
1646
  throw new Error(result.error.message ?? "Create team failed");
@@ -1083,7 +1662,7 @@ function OrganizationDetail({
1083
1662
  mutationFn: async (teamId) => {
1084
1663
  const result = await client.dash.organization.deleteTeam(
1085
1664
  { teamId },
1086
- withContext({ organizationId })
1665
+ withContext({ organizationId }, getAuthHeaders())
1087
1666
  );
1088
1667
  if (result.error) throw new Error(result.error.message ?? "Failed");
1089
1668
  },
@@ -1101,7 +1680,10 @@ function OrganizationDetail({
1101
1680
  });
1102
1681
  const deleteSsoMutation = reactQuery.useMutation({
1103
1682
  mutationFn: async (providerId) => {
1104
- const result = await client.dash.organization[organizationId].ssoProvider.delete({ providerId }, withContext({ organizationId }));
1683
+ const result = await client.dash.organization[organizationId].ssoProvider.delete(
1684
+ { providerId },
1685
+ withContext({ organizationId }, getAuthHeaders())
1686
+ );
1105
1687
  if (result.error) throw new Error(result.error.message ?? "Failed");
1106
1688
  },
1107
1689
  onSuccess: () => {
@@ -1116,9 +1698,74 @@ function OrganizationDetail({
1116
1698
  });
1117
1699
  }
1118
1700
  });
1701
+ const inviteMemberMutation = reactQuery.useMutation({
1702
+ mutationFn: async () => {
1703
+ const result = await client.dash.organization.inviteMember(
1704
+ { email: inviteEmail, role: inviteRole, invitedBy: "" },
1705
+ withContext({ organizationId }, getAuthHeaders())
1706
+ );
1707
+ if (result.error) throw new Error(result.error.message ?? "Failed");
1708
+ return result.data;
1709
+ },
1710
+ onSuccess: () => {
1711
+ qc.invalidateQueries({
1712
+ queryKey: ["dash-org-invitations", organizationId]
1713
+ });
1714
+ setInviteEmail("");
1715
+ setInviteRole("member");
1716
+ toggleNotification({ type: "success", message: "Invitation sent" });
1717
+ },
1718
+ onError: (err) => {
1719
+ toggleNotification({
1720
+ type: "danger",
1721
+ message: err.message ?? "Failed to invite"
1722
+ });
1723
+ }
1724
+ });
1725
+ const cancelInvitationMutation = reactQuery.useMutation({
1726
+ mutationFn: async (invitationId) => {
1727
+ const result = await client.dash.organization.cancelInvitation(
1728
+ { invitationId },
1729
+ withContext({ organizationId }, getAuthHeaders())
1730
+ );
1731
+ if (result.error) throw new Error(result.error.message ?? "Failed");
1732
+ },
1733
+ onSuccess: () => {
1734
+ setConfirmCancelInvitationId(null);
1735
+ qc.invalidateQueries({
1736
+ queryKey: ["dash-org-invitations", organizationId]
1737
+ });
1738
+ toggleNotification({ type: "success", message: "Invitation cancelled" });
1739
+ },
1740
+ onError: (err) => {
1741
+ toggleNotification({
1742
+ type: "danger",
1743
+ message: err.message ?? "Failed to cancel"
1744
+ });
1745
+ }
1746
+ });
1747
+ const resendInvitationMutation = reactQuery.useMutation({
1748
+ mutationFn: async (invitationId) => {
1749
+ const result = await client.dash.organization.resendInvitation(
1750
+ { invitationId },
1751
+ withContext({ organizationId }, getAuthHeaders())
1752
+ );
1753
+ if (result.error) throw new Error(result.error.message ?? "Failed");
1754
+ },
1755
+ onSuccess: () => {
1756
+ toggleNotification({ type: "success", message: "Invitation resent" });
1757
+ },
1758
+ onError: (err) => {
1759
+ toggleNotification({
1760
+ type: "danger",
1761
+ message: err.message ?? "Failed to resend"
1762
+ });
1763
+ }
1764
+ });
1119
1765
  const members = membersQuery.data ?? [];
1120
1766
  const teams = teamsQuery.data ?? [];
1121
1767
  const ssoProviders = ssoQuery.data ?? [];
1768
+ const invitations = invitationsQuery.data ?? [];
1122
1769
  const hasOrgEdits = editName !== void 0 || editSlug !== void 0 || editLogo !== void 0 || Object.keys(editExtra).length > 0;
1123
1770
  const customFields = Object.entries(schemaQuery.data ?? {}).filter(([name]) => !STANDARD_ORG_FIELDS.has(name)).map(([name, attribute]) => ({ name, attribute }));
1124
1771
  const detailsFooter = activeTab === "details" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
@@ -1175,6 +1822,11 @@ function OrganizationDetail({
1175
1822
  teams.length,
1176
1823
  ")"
1177
1824
  ] }),
1825
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Tabs.Trigger, { value: "invitations", children: [
1826
+ "Invitations (",
1827
+ invitations.length,
1828
+ ")"
1829
+ ] }),
1178
1830
  /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Tabs.Trigger, { value: "sso", children: [
1179
1831
  "SSO (",
1180
1832
  ssoProviders.length,
@@ -1229,33 +1881,43 @@ function OrganizationDetail({
1229
1881
  }
1230
1882
  )
1231
1883
  ] }),
1232
- /* @__PURE__ */ jsxRuntime.jsx(EditSidebar, { children: /* @__PURE__ */ jsxRuntime.jsxs(FormSection, { children: [
1233
- /* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { children: "Details" }),
1234
- /* @__PURE__ */ jsxRuntime.jsxs(
1235
- "div",
1884
+ /* @__PURE__ */ jsxRuntime.jsxs(EditSidebar, { children: [
1885
+ /* @__PURE__ */ jsxRuntime.jsxs(FormSection, { children: [
1886
+ /* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { children: "Details" }),
1887
+ /* @__PURE__ */ jsxRuntime.jsxs(
1888
+ "div",
1889
+ {
1890
+ style: {
1891
+ display: "grid",
1892
+ gridTemplateColumns: "1fr 1fr",
1893
+ gap: 12
1894
+ },
1895
+ children: [
1896
+ /* @__PURE__ */ jsxRuntime.jsxs(MetaItem, { style: { gridColumn: "1 / -1" }, children: [
1897
+ /* @__PURE__ */ jsxRuntime.jsx(MetaKey, { children: "Organization ID" }),
1898
+ /* @__PURE__ */ jsxRuntime.jsx(MonoChip, { children: org?.id })
1899
+ ] }),
1900
+ /* @__PURE__ */ jsxRuntime.jsxs(MetaItem, { children: [
1901
+ /* @__PURE__ */ jsxRuntime.jsx(MetaKey, { children: "Members" }),
1902
+ /* @__PURE__ */ jsxRuntime.jsx(MetaVal, { children: org?.memberCount ?? 0 })
1903
+ ] }),
1904
+ /* @__PURE__ */ jsxRuntime.jsxs(MetaItem, { children: [
1905
+ /* @__PURE__ */ jsxRuntime.jsx(MetaKey, { children: "Created" }),
1906
+ /* @__PURE__ */ jsxRuntime.jsx(MetaVal, { children: org?.createdAt ? new Date(org.createdAt).toLocaleDateString() : "—" })
1907
+ ] })
1908
+ ]
1909
+ }
1910
+ )
1911
+ ] }),
1912
+ /* @__PURE__ */ jsxRuntime.jsx(
1913
+ EditViewSidePanels,
1236
1914
  {
1237
- style: {
1238
- display: "grid",
1239
- gridTemplateColumns: "1fr 1fr",
1240
- gap: 12
1241
- },
1242
- children: [
1243
- /* @__PURE__ */ jsxRuntime.jsxs(MetaItem, { style: { gridColumn: "1 / -1" }, children: [
1244
- /* @__PURE__ */ jsxRuntime.jsx(MetaKey, { children: "Organization ID" }),
1245
- /* @__PURE__ */ jsxRuntime.jsx(MonoChip, { children: org?.id })
1246
- ] }),
1247
- /* @__PURE__ */ jsxRuntime.jsxs(MetaItem, { children: [
1248
- /* @__PURE__ */ jsxRuntime.jsx(MetaKey, { children: "Members" }),
1249
- /* @__PURE__ */ jsxRuntime.jsx(MetaVal, { children: org?.memberCount ?? 0 })
1250
- ] }),
1251
- /* @__PURE__ */ jsxRuntime.jsxs(MetaItem, { children: [
1252
- /* @__PURE__ */ jsxRuntime.jsx(MetaKey, { children: "Created" }),
1253
- /* @__PURE__ */ jsxRuntime.jsx(MetaVal, { children: org?.createdAt ? new Date(org.createdAt).toLocaleDateString() : "—" })
1254
- ] })
1255
- ]
1915
+ model: "plugin::better-auth.organization",
1916
+ documentId: strapiOrgQuery.data?.documentId,
1917
+ document: strapiOrgQuery.data ?? void 0
1256
1918
  }
1257
1919
  )
1258
- ] }) })
1920
+ ] })
1259
1921
  ] }) }),
1260
1922
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "members", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 5, paddingTop: 6, children: [
1261
1923
  /* @__PURE__ */ jsxRuntime.jsxs(FormSection, { children: [
@@ -1430,7 +2092,127 @@ function OrganizationDetail({
1430
2092
  },
1431
2093
  provider.id
1432
2094
  )) })
1433
- ] }) }) })
2095
+ ] }) }) }),
2096
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "invitations", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 5, paddingTop: 6, children: [
2097
+ /* @__PURE__ */ jsxRuntime.jsxs(FormSection, { children: [
2098
+ /* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { children: "Invite member by email" }),
2099
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Grid.Root, { gap: 3, children: [
2100
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 8, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { children: [
2101
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Email address" }),
2102
+ /* @__PURE__ */ jsxRuntime.jsx(
2103
+ designSystem.TextInput,
2104
+ {
2105
+ type: "email",
2106
+ value: inviteEmail,
2107
+ onChange: (e) => setInviteEmail(e.target.value),
2108
+ placeholder: "user@example.com"
2109
+ }
2110
+ )
2111
+ ] }) }),
2112
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 4, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { children: [
2113
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Role" }),
2114
+ /* @__PURE__ */ jsxRuntime.jsxs(
2115
+ designSystem.SingleSelect,
2116
+ {
2117
+ value: inviteRole,
2118
+ onChange: (v) => setInviteRole(String(v)),
2119
+ "aria-label": "Invite role",
2120
+ children: [
2121
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "member", children: "Member" }),
2122
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "admin", children: "Admin" }),
2123
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "owner", children: "Owner" })
2124
+ ]
2125
+ }
2126
+ )
2127
+ ] }) })
2128
+ ] }),
2129
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(
2130
+ designSystem.Button,
2131
+ {
2132
+ size: "S",
2133
+ startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Plus, {}),
2134
+ disabled: !inviteEmail,
2135
+ loading: inviteMemberMutation.isLoading,
2136
+ onClick: () => inviteMemberMutation.mutate(),
2137
+ children: "Send invitation"
2138
+ }
2139
+ ) })
2140
+ ] }),
2141
+ /* @__PURE__ */ jsxRuntime.jsxs(FormSection, { children: [
2142
+ /* @__PURE__ */ jsxRuntime.jsxs(SectionLabel, { children: [
2143
+ "Invitations (",
2144
+ invitations.length,
2145
+ ")"
2146
+ ] }),
2147
+ invitationsQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: "neutral500", children: "Loading…" }) : invitations.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: "No invitations yet." }) : /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { direction: "column", gap: 2, children: invitations.map((inv) => {
2148
+ const statusColor = {
2149
+ pending: "#f59e0b",
2150
+ accepted: "#5cb176",
2151
+ rejected: "#d02b20",
2152
+ canceled: "#8e8ea9"
2153
+ };
2154
+ const color = statusColor[inv.status] ?? "#8e8ea9";
2155
+ const isPending = inv.status === "pending";
2156
+ return /* @__PURE__ */ jsxRuntime.jsxs(AccountRow, { children: [
2157
+ /* @__PURE__ */ jsxRuntime.jsxs(
2158
+ designSystem.Flex,
2159
+ {
2160
+ direction: "column",
2161
+ gap: 1,
2162
+ alignItems: "flex-start",
2163
+ style: { flex: 1 },
2164
+ children: [
2165
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", fontWeight: "semiBold", children: inv.user?.name ?? inv.email }),
2166
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: [
2167
+ inv.email,
2168
+ " · role: ",
2169
+ inv.role
2170
+ ] }),
2171
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: [
2172
+ "Expires",
2173
+ " ",
2174
+ new Date(inv.expiresAt).toLocaleDateString()
2175
+ ] })
2176
+ ]
2177
+ }
2178
+ ),
2179
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "center", children: [
2180
+ /* @__PURE__ */ jsxRuntime.jsx(
2181
+ MonoChip,
2182
+ {
2183
+ style: {
2184
+ color,
2185
+ borderColor: color,
2186
+ background: `${color}18`
2187
+ },
2188
+ children: inv.status
2189
+ }
2190
+ ),
2191
+ isPending && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2192
+ /* @__PURE__ */ jsxRuntime.jsx(
2193
+ designSystem.Button,
2194
+ {
2195
+ size: "S",
2196
+ variant: "secondary",
2197
+ loading: resendInvitationMutation.isLoading,
2198
+ onClick: () => resendInvitationMutation.mutate(inv.id),
2199
+ children: "Resend"
2200
+ }
2201
+ ),
2202
+ /* @__PURE__ */ jsxRuntime.jsx(
2203
+ designSystem.IconButton,
2204
+ {
2205
+ label: "Cancel invitation",
2206
+ onClick: () => setConfirmCancelInvitationId(inv.id),
2207
+ children: /* @__PURE__ */ jsxRuntime.jsx(icons.Trash, {})
2208
+ }
2209
+ )
2210
+ ] })
2211
+ ] })
2212
+ ] }, inv.id);
2213
+ }) })
2214
+ ] })
2215
+ ] }) })
1434
2216
  ] }),
1435
2217
  confirmRemoveMemberId && /* @__PURE__ */ jsxRuntime.jsx(
1436
2218
  ConfirmDialog,
@@ -1464,6 +2246,17 @@ function OrganizationDetail({
1464
2246
  onConfirm: () => deleteSsoMutation.mutate(confirmDeleteSsoId),
1465
2247
  onCancel: () => setConfirmDeleteSsoId(null)
1466
2248
  }
2249
+ ),
2250
+ confirmCancelInvitationId && /* @__PURE__ */ jsxRuntime.jsx(
2251
+ ConfirmDialog,
2252
+ {
2253
+ title: "Cancel invitation",
2254
+ message: "Are you sure you want to cancel this invitation? The invite link will no longer work.",
2255
+ confirmLabel: "Cancel invitation",
2256
+ loading: cancelInvitationMutation.isLoading,
2257
+ onConfirm: () => cancelInvitationMutation.mutate(confirmCancelInvitationId),
2258
+ onCancel: () => setConfirmCancelInvitationId(null)
2259
+ }
1467
2260
  )
1468
2261
  ]
1469
2262
  }
@@ -1485,7 +2278,7 @@ function TeamRow({
1485
2278
  organizationId
1486
2279
  ].teams[team.id].members(
1487
2280
  { params: { orgId: organizationId, teamId: team.id } },
1488
- withContext({ organizationId })
2281
+ withContext({ organizationId }, getAuthHeaders())
1489
2282
  );
1490
2283
  if (result.error) throw new Error(result.error.message ?? "Failed");
1491
2284
  return result.data ?? [];
@@ -1497,7 +2290,7 @@ function TeamRow({
1497
2290
  mutationFn: async () => {
1498
2291
  const result = await client.dash.organization.addTeamMember(
1499
2292
  { teamId: team.id, userId: addUserId },
1500
- withContext({ organizationId })
2293
+ withContext({ organizationId }, getAuthHeaders())
1501
2294
  );
1502
2295
  if (result.error) throw new Error(result.error.message ?? "Failed");
1503
2296
  return result.data;
@@ -1513,7 +2306,7 @@ function TeamRow({
1513
2306
  mutationFn: async (userId) => {
1514
2307
  const result = await client.dash.organization.removeTeamMember(
1515
2308
  { teamId: team.id, userId },
1516
- withContext({ organizationId })
2309
+ withContext({ organizationId }, getAuthHeaders())
1517
2310
  );
1518
2311
  if (result.error) throw new Error(result.error.message ?? "Failed");
1519
2312
  },
@@ -1609,12 +2402,12 @@ function TeamRow({
1609
2402
  )
1610
2403
  ] });
1611
2404
  }
1612
- const PAGE_SIZE$2 = 25;
1613
- const fadeUp$3 = styled.keyframes`
2405
+ const PAGE_SIZE$1 = 25;
2406
+ const fadeUp$2 = styled.keyframes`
1614
2407
  from { opacity: 0; transform: translateY(6px); }
1615
2408
  to { opacity: 1; transform: translateY(0); }
1616
2409
  `;
1617
- const Wrap$3 = styled__default.default.div`
2410
+ const Wrap$2 = styled__default.default.div`
1618
2411
  padding: 28px 32px;
1619
2412
  background: #f6f6f9;
1620
2413
  min-height: 100%;
@@ -1622,39 +2415,39 @@ const Wrap$3 = styled__default.default.div`
1622
2415
  flex-direction: column;
1623
2416
  gap: 20px;
1624
2417
  `;
1625
- const PageHeader$2 = styled__default.default.div`
2418
+ const PageHeader$1 = styled__default.default.div`
1626
2419
  display: flex;
1627
2420
  justify-content: space-between;
1628
2421
  align-items: flex-start;
1629
2422
  `;
1630
- const TitleBlock$2 = styled__default.default.div`
2423
+ const TitleBlock$1 = styled__default.default.div`
1631
2424
  display: flex;
1632
2425
  flex-direction: column;
1633
2426
  gap: 4px;
1634
2427
  `;
1635
- const PageTitle$2 = styled__default.default.h1`
2428
+ const PageTitle$1 = styled__default.default.h1`
1636
2429
  margin: 0;
1637
2430
  font-size: 22px;
1638
2431
  font-weight: 800;
1639
2432
  color: #32324d;
1640
2433
  letter-spacing: -0.03em;
1641
2434
  `;
1642
- const PageSubtitle$2 = styled__default.default.p`
2435
+ const PageSubtitle$1 = styled__default.default.p`
1643
2436
  margin: 0;
1644
2437
  font-size: 12px;
1645
2438
  color: #8e8ea9;
1646
2439
  `;
1647
- const TableCard$2 = styled__default.default.div`
2440
+ const TableCard$1 = styled__default.default.div`
1648
2441
  background: #ffffff;
1649
2442
  border: 1px solid #eaeaef;
1650
2443
  border-radius: 10px;
1651
2444
  overflow: hidden;
1652
2445
  `;
1653
- const Table$2 = styled__default.default.table`
2446
+ const Table$1 = styled__default.default.table`
1654
2447
  width: 100%;
1655
2448
  border-collapse: collapse;
1656
2449
  `;
1657
- const TH$2 = styled__default.default.th`
2450
+ const TH$1 = styled__default.default.th`
1658
2451
  text-align: left;
1659
2452
  padding: 10px 14px;
1660
2453
  font-size: 10px;
@@ -1668,13 +2461,13 @@ const TH$2 = styled__default.default.th`
1668
2461
  &:first-child { padding-left: 20px; }
1669
2462
  &:last-child { padding-right: 20px; }
1670
2463
  `;
1671
- const THCheck$1 = styled__default.default(TH$2)`
2464
+ const THCheck$1 = styled__default.default(TH$1)`
1672
2465
  width: 44px;
1673
2466
  `;
1674
- const THActions$1 = styled__default.default(TH$2)`
2467
+ const THActions$1 = styled__default.default(TH$1)`
1675
2468
  width: 80px;
1676
2469
  `;
1677
- const TD$2 = styled__default.default.td`
2470
+ const TD$1 = styled__default.default.td`
1678
2471
  padding: 11px 14px;
1679
2472
  font-size: 12px;
1680
2473
  color: #32324d;
@@ -1683,14 +2476,14 @@ const TD$2 = styled__default.default.td`
1683
2476
  &:first-child { padding-left: 20px; }
1684
2477
  &:last-child { padding-right: 20px; }
1685
2478
  `;
1686
- const TDCheck$1 = styled__default.default(TD$2)`
2479
+ const TDCheck$1 = styled__default.default(TD$1)`
1687
2480
  width: 44px;
1688
2481
  `;
1689
- const TDActions$1 = styled__default.default(TD$2)`
2482
+ const TDActions$1 = styled__default.default(TD$1)`
1690
2483
  width: 80px;
1691
2484
  `;
1692
- const TR$2 = styled__default.default.tr`
1693
- animation: ${fadeUp$3} 280ms ease both;
2485
+ const TR$1 = styled__default.default.tr`
2486
+ animation: ${fadeUp$2} 280ms ease both;
1694
2487
  animation-delay: ${(p) => (p.$i ?? 0) * 25}ms;
1695
2488
  background: ${(p) => p.$selected ? "#f0f0ff" : "transparent"};
1696
2489
  transition: background 120ms ease;
@@ -1777,7 +2570,9 @@ function OrgAvatar({ name, logo }) {
1777
2570
  return /* @__PURE__ */ jsxRuntime.jsx(OrgLogoFallback, { $bg: color.bg, $fg: color.fg, children: initials });
1778
2571
  }
1779
2572
  function OrganizationsPage({ teamsEnabled }) {
2573
+ const rbac = admin.useRBAC(index.PERMISSIONS.organization);
1780
2574
  const qc = reactQuery.useQueryClient();
2575
+ const { toggleNotification } = admin.useNotification();
1781
2576
  const [page, setPage] = react.useState(1);
1782
2577
  const [search, setSearch] = react.useState("");
1783
2578
  const [searchInput, setSearchInput] = react.useState("");
@@ -1786,13 +2581,13 @@ function OrganizationsPage({ teamsEnabled }) {
1786
2581
  const [detailOrgId, setDetailOrgId] = react.useState(null);
1787
2582
  const [confirmDelete, setConfirmDelete] = react.useState(null);
1788
2583
  const [confirmDeleteMany, setConfirmDeleteMany] = react.useState(false);
1789
- const offset = (page - 1) * PAGE_SIZE$2;
2584
+ const offset = (page - 1) * PAGE_SIZE$1;
1790
2585
  const orgsQuery = reactQuery.useQuery({
1791
2586
  queryKey: ["dash-organizations", page, search],
1792
2587
  queryFn: async () => {
1793
2588
  const result = await client.dash.listOrganizations({
1794
2589
  query: {
1795
- limit: PAGE_SIZE$2,
2590
+ limit: PAGE_SIZE$1,
1796
2591
  offset,
1797
2592
  sortBy: "createdAt",
1798
2593
  sortOrder: "desc",
@@ -1809,7 +2604,7 @@ function OrganizationsPage({ teamsEnabled }) {
1809
2604
  mutationFn: async (organizationId) => {
1810
2605
  const result = await client.dash.organization.delete(
1811
2606
  { organizationId },
1812
- withContext({ organizationId })
2607
+ withContext({ organizationId }, getAuthHeaders())
1813
2608
  );
1814
2609
  if (result.error)
1815
2610
  throw new Error(result.error.message ?? "Delete failed");
@@ -1817,27 +2612,44 @@ function OrganizationsPage({ teamsEnabled }) {
1817
2612
  onSuccess: () => {
1818
2613
  setConfirmDelete(null);
1819
2614
  qc.invalidateQueries({ queryKey: ["dash-organizations"] });
2615
+ toggleNotification({ type: "success", message: "Organization deleted" });
2616
+ },
2617
+ onError: (err) => {
2618
+ toggleNotification({
2619
+ type: "danger",
2620
+ message: err.message ?? "Failed to delete organization"
2621
+ });
1820
2622
  }
1821
2623
  });
1822
2624
  const deleteManyMutation = reactQuery.useMutation({
1823
2625
  mutationFn: async (organizationIds) => {
1824
2626
  const result = await client.dash.organization.deleteMany(
1825
2627
  {},
1826
- withContext({ organizationIds })
2628
+ withContext({ organizationIds }, getAuthHeaders())
1827
2629
  );
1828
2630
  if (result.error)
1829
2631
  throw new Error(result.error.message ?? "Delete failed");
1830
2632
  return result.data;
1831
2633
  },
1832
- onSuccess: () => {
2634
+ onSuccess: (_data, organizationIds) => {
1833
2635
  setConfirmDeleteMany(false);
1834
2636
  setSelected(/* @__PURE__ */ new Set());
1835
2637
  qc.invalidateQueries({ queryKey: ["dash-organizations"] });
2638
+ toggleNotification({
2639
+ type: "success",
2640
+ message: `${organizationIds.length} organization${organizationIds.length !== 1 ? "s" : ""} deleted`
2641
+ });
2642
+ },
2643
+ onError: (err) => {
2644
+ toggleNotification({
2645
+ type: "danger",
2646
+ message: err.message ?? "Failed to delete organizations"
2647
+ });
1836
2648
  }
1837
2649
  });
1838
2650
  const orgs = orgsQuery.data && "organizations" in orgsQuery.data ? orgsQuery.data.organizations : [];
1839
2651
  const total = orgsQuery.data && "total" in orgsQuery.data ? orgsQuery.data.total : 0;
1840
- const pageCount = Math.ceil(total / PAGE_SIZE$2);
2652
+ const pageCount = Math.ceil(total / PAGE_SIZE$1);
1841
2653
  const allSelected = orgs.length > 0 && orgs.every((o) => selected.has(o.id));
1842
2654
  const someSelected = selected.size > 0;
1843
2655
  const toggleSelect = (id) => {
@@ -1857,212 +2669,220 @@ function OrganizationsPage({ teamsEnabled }) {
1857
2669
  setSearch(searchInput);
1858
2670
  setPage(1);
1859
2671
  };
1860
- return /* @__PURE__ */ jsxRuntime.jsxs(Wrap$3, { "data-testid": "organizations-page", children: [
1861
- /* @__PURE__ */ jsxRuntime.jsxs(PageHeader$2, { children: [
1862
- /* @__PURE__ */ jsxRuntime.jsxs(TitleBlock$2, { children: [
1863
- /* @__PURE__ */ jsxRuntime.jsx(PageTitle$2, { children: "Organizations" }),
1864
- /* @__PURE__ */ jsxRuntime.jsxs(PageSubtitle$2, { children: [
1865
- total.toLocaleString(),
1866
- " total"
1867
- ] })
1868
- ] }),
1869
- /* @__PURE__ */ jsxRuntime.jsx(
1870
- designSystem.Button,
1871
- {
1872
- startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Plus, {}),
1873
- onClick: () => setShowCreate(true),
1874
- "data-testid": "create-org-btn",
1875
- children: "Create organization"
1876
- }
1877
- )
1878
- ] }),
1879
- /* @__PURE__ */ jsxRuntime.jsxs(Toolbar$1, { children: [
1880
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.SearchForm, { onSubmit: handleSearch, children: /* @__PURE__ */ jsxRuntime.jsx(
1881
- designSystem.Searchbar,
1882
- {
1883
- clearLabel: "Clear",
1884
- name: "search",
1885
- placeholder: "Search organizations…",
1886
- value: searchInput,
1887
- onChange: (e) => setSearchInput(e.target.value),
1888
- onClear: () => {
1889
- setSearchInput("");
1890
- setSearch("");
1891
- setPage(1);
1892
- },
1893
- children: "Search organizations"
1894
- }
1895
- ) }),
1896
- someSelected && /* @__PURE__ */ jsxRuntime.jsxs(
1897
- designSystem.Button,
1898
- {
1899
- variant: "danger-light",
1900
- size: "S",
1901
- onClick: () => setConfirmDeleteMany(true),
1902
- "data-testid": "delete-selected-orgs-btn",
1903
- children: [
1904
- "Delete ",
1905
- selected.size,
1906
- " selected"
1907
- ]
1908
- }
1909
- )
1910
- ] }),
1911
- orgsQuery.isError && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { color: "#d02b20", fontSize: 12, padding: "8px 0" }, children: orgsQuery.error instanceof Error ? orgsQuery.error.message : "An error occurred" }),
1912
- /* @__PURE__ */ jsxRuntime.jsx(TableCard$2, { children: orgsQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", padding: 8, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading organizations…" }) }) : /* @__PURE__ */ jsxRuntime.jsxs(Table$2, { children: [
1913
- /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
1914
- /* @__PURE__ */ jsxRuntime.jsx(THCheck$1, { children: /* @__PURE__ */ jsxRuntime.jsx(
1915
- designSystem.Checkbox,
1916
- {
1917
- checked: allSelected,
1918
- onCheckedChange: handleSelectAll,
1919
- "aria-label": "Select all"
1920
- }
1921
- ) }),
1922
- /* @__PURE__ */ jsxRuntime.jsx(TH$2, { children: "Name" }),
1923
- /* @__PURE__ */ jsxRuntime.jsx(TH$2, { children: "Slug" }),
1924
- /* @__PURE__ */ jsxRuntime.jsx(TH$2, { children: "Members" }),
1925
- /* @__PURE__ */ jsxRuntime.jsx(TH$2, { children: "Created" }),
1926
- /* @__PURE__ */ jsxRuntime.jsx(THActions$1, {})
1927
- ] }) }),
1928
- /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: orgs.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsx(
1929
- TD$2,
1930
- {
1931
- colSpan: 6,
1932
- style: {
1933
- textAlign: "center",
1934
- padding: "40px",
1935
- color: "#8e8ea9"
1936
- },
1937
- "data-testid": "orgs-empty",
1938
- children: search ? `No organizations matching "${search}"` : "No organizations found"
1939
- }
1940
- ) }) : orgs.map((org, i) => /* @__PURE__ */ jsxRuntime.jsxs(
1941
- TR$2,
1942
- {
1943
- $selected: selected.has(org.id),
1944
- $i: i,
1945
- "data-testid": "org-row",
1946
- children: [
1947
- /* @__PURE__ */ jsxRuntime.jsx(TDCheck$1, { children: /* @__PURE__ */ jsxRuntime.jsx(
2672
+ return /* @__PURE__ */ jsxRuntime.jsx(
2673
+ admin.Page.Protect,
2674
+ {
2675
+ permissions: index.PERMISSIONS.organization.filter(
2676
+ (p) => p.action === `plugin::${index.PLUGIN_ID}.organization.read`
2677
+ ),
2678
+ children: /* @__PURE__ */ jsxRuntime.jsxs(Wrap$2, { "data-testid": "organizations-page", children: [
2679
+ /* @__PURE__ */ jsxRuntime.jsxs(PageHeader$1, { children: [
2680
+ /* @__PURE__ */ jsxRuntime.jsxs(TitleBlock$1, { children: [
2681
+ /* @__PURE__ */ jsxRuntime.jsx(PageTitle$1, { children: "Organizations" }),
2682
+ /* @__PURE__ */ jsxRuntime.jsxs(PageSubtitle$1, { children: [
2683
+ total.toLocaleString(),
2684
+ " total"
2685
+ ] })
2686
+ ] }),
2687
+ rbac.allowedActions.canCreate && /* @__PURE__ */ jsxRuntime.jsx(
2688
+ designSystem.Button,
2689
+ {
2690
+ startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Plus, {}),
2691
+ onClick: () => setShowCreate(true),
2692
+ "data-testid": "create-org-btn",
2693
+ children: "Create organization"
2694
+ }
2695
+ )
2696
+ ] }),
2697
+ /* @__PURE__ */ jsxRuntime.jsxs(Toolbar$1, { children: [
2698
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.SearchForm, { onSubmit: handleSearch, children: /* @__PURE__ */ jsxRuntime.jsx(
2699
+ designSystem.Searchbar,
2700
+ {
2701
+ clearLabel: "Clear",
2702
+ name: "search",
2703
+ placeholder: "Search organizations…",
2704
+ value: searchInput,
2705
+ onChange: (e) => setSearchInput(e.target.value),
2706
+ onClear: () => {
2707
+ setSearchInput("");
2708
+ setSearch("");
2709
+ setPage(1);
2710
+ },
2711
+ children: "Search organizations"
2712
+ }
2713
+ ) }),
2714
+ someSelected && rbac.allowedActions.canDelete && /* @__PURE__ */ jsxRuntime.jsxs(
2715
+ designSystem.Button,
2716
+ {
2717
+ variant: "danger-light",
2718
+ size: "S",
2719
+ onClick: () => setConfirmDeleteMany(true),
2720
+ "data-testid": "delete-selected-orgs-btn",
2721
+ children: [
2722
+ "Delete ",
2723
+ selected.size,
2724
+ " selected"
2725
+ ]
2726
+ }
2727
+ )
2728
+ ] }),
2729
+ orgsQuery.isError && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { color: "#d02b20", fontSize: 12, padding: "8px 0" }, children: orgsQuery.error instanceof Error ? orgsQuery.error.message : "An error occurred" }),
2730
+ /* @__PURE__ */ jsxRuntime.jsx(TableCard$1, { children: orgsQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", padding: 8, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading organizations…" }) }) : /* @__PURE__ */ jsxRuntime.jsxs(Table$1, { children: [
2731
+ /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
2732
+ rbac.allowedActions.canDelete && /* @__PURE__ */ jsxRuntime.jsx(THCheck$1, { children: /* @__PURE__ */ jsxRuntime.jsx(
1948
2733
  designSystem.Checkbox,
1949
2734
  {
1950
- checked: selected.has(org.id),
1951
- onCheckedChange: () => toggleSelect(org.id),
1952
- "aria-label": `Select ${org.name}`
2735
+ checked: allSelected,
2736
+ onCheckedChange: handleSelectAll,
2737
+ "aria-label": "Select all"
1953
2738
  }
1954
2739
  ) }),
1955
- /* @__PURE__ */ jsxRuntime.jsx(TD$2, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { alignItems: "center", gap: 2, children: [
1956
- /* @__PURE__ */ jsxRuntime.jsx(OrgAvatar, { name: org.name, logo: org.logo }),
1957
- /* @__PURE__ */ jsxRuntime.jsx(OrgName, { children: org.name })
1958
- ] }) }),
1959
- /* @__PURE__ */ jsxRuntime.jsx(TD$2, { children: /* @__PURE__ */ jsxRuntime.jsx(SlugChip, { children: org.slug }) }),
1960
- /* @__PURE__ */ jsxRuntime.jsx(TD$2, { children: /* @__PURE__ */ jsxRuntime.jsx(CountChip, { children: org.memberCount }) }),
1961
- /* @__PURE__ */ jsxRuntime.jsx(TD$2, { children: /* @__PURE__ */ jsxRuntime.jsx(DateText$1, { children: new Date(org.createdAt).toLocaleDateString() }) }),
1962
- /* @__PURE__ */ jsxRuntime.jsx(TDActions$1, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 1, justifyContent: "flex-end", children: [
1963
- /* @__PURE__ */ jsxRuntime.jsx(
1964
- designSystem.IconButton,
1965
- {
1966
- label: "Edit organization",
1967
- onClick: () => setDetailOrgId(org.id),
1968
- "data-testid": "edit-org-btn",
1969
- children: /* @__PURE__ */ jsxRuntime.jsx(icons.Pencil, {})
1970
- }
1971
- ),
1972
- /* @__PURE__ */ jsxRuntime.jsx(
1973
- designSystem.IconButton,
1974
- {
1975
- label: "Delete organization",
1976
- onClick: () => setConfirmDelete(org.id),
1977
- "data-testid": "delete-org-btn",
1978
- children: /* @__PURE__ */ jsxRuntime.jsx(icons.Trash, {})
1979
- }
1980
- )
1981
- ] }) })
1982
- ]
1983
- },
1984
- org.id
1985
- )) })
1986
- ] }) }),
1987
- pageCount > 1 && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "flex-end", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, children: [
1988
- /* @__PURE__ */ jsxRuntime.jsx(
1989
- designSystem.Button,
1990
- {
1991
- variant: "tertiary",
1992
- size: "S",
1993
- disabled: page === 1,
1994
- onClick: () => setPage((p) => p - 1),
1995
- children: "Previous"
1996
- }
1997
- ),
1998
- /* @__PURE__ */ jsxRuntime.jsx(
1999
- designSystem.Button,
2000
- {
2001
- variant: "tertiary",
2002
- size: "S",
2003
- disabled: page >= pageCount,
2004
- onClick: () => setPage((p) => p + 1),
2005
- children: "Next"
2006
- }
2007
- )
2008
- ] }) }),
2009
- showCreate && /* @__PURE__ */ jsxRuntime.jsx(
2010
- CreateOrganizationDialog,
2011
- {
2012
- teamsEnabled,
2013
- onClose: () => setShowCreate(false)
2014
- }
2015
- ),
2016
- detailOrgId && /* @__PURE__ */ jsxRuntime.jsx(
2017
- OrganizationDetail,
2018
- {
2019
- organizationId: detailOrgId,
2020
- teamsEnabled,
2021
- onClose: () => setDetailOrgId(null)
2022
- }
2023
- ),
2024
- confirmDelete && /* @__PURE__ */ jsxRuntime.jsx(
2025
- ConfirmDialog,
2026
- {
2027
- title: "Delete organization",
2028
- message: "Are you sure you want to delete this organization? All members and teams will be removed. This action cannot be undone.",
2029
- confirmLabel: "Delete",
2030
- loading: deleteMutation.isLoading,
2031
- onConfirm: () => deleteMutation.mutate(confirmDelete),
2032
- onCancel: () => setConfirmDelete(null)
2033
- }
2034
- ),
2035
- confirmDeleteMany && /* @__PURE__ */ jsxRuntime.jsx(
2036
- ConfirmDialog,
2037
- {
2038
- title: `Delete ${selected.size} organization${selected.size !== 1 ? "s" : ""}`,
2039
- message: `Are you sure you want to delete ${selected.size} organization${selected.size !== 1 ? "s" : ""}? This action cannot be undone.`,
2040
- confirmLabel: "Delete all",
2041
- loading: deleteManyMutation.isLoading,
2042
- onConfirm: () => deleteManyMutation.mutate([...selected]),
2043
- onCancel: () => setConfirmDeleteMany(false)
2044
- }
2045
- )
2046
- ] });
2047
- }
2048
- const PALETTE = [
2049
- { bg: "#EAF5FF", fg: "#0C75AF" },
2050
- { bg: "#F0F0FF", fg: "#4945FF" },
2051
- { bg: "#EAFBE7", fg: "#328048" },
2052
- { bg: "#FFF3D3", fg: "#8E6A00" },
2053
- { bg: "#FCECEA", fg: "#D02B20" },
2054
- { bg: "#F6ECFC", fg: "#8312D1" },
2055
- { bg: "#E8F8F5", fg: "#1A7F64" }
2056
- ];
2057
- const Circle = styled__default.default.div`
2058
- width: ${(p) => p.$size}px;
2059
- height: ${(p) => p.$size}px;
2060
- border-radius: 50%;
2061
- background: ${(p) => p.$bg};
2062
- color: ${(p) => p.$fg};
2063
- display: inline-flex;
2064
- align-items: center;
2065
- justify-content: center;
2740
+ /* @__PURE__ */ jsxRuntime.jsx(TH$1, { children: "Name" }),
2741
+ /* @__PURE__ */ jsxRuntime.jsx(TH$1, { children: "Slug" }),
2742
+ /* @__PURE__ */ jsxRuntime.jsx(TH$1, { children: "Members" }),
2743
+ /* @__PURE__ */ jsxRuntime.jsx(TH$1, { children: "Created" }),
2744
+ /* @__PURE__ */ jsxRuntime.jsx(THActions$1, {})
2745
+ ] }) }),
2746
+ /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: orgs.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsx(
2747
+ TD$1,
2748
+ {
2749
+ colSpan: 6,
2750
+ style: {
2751
+ textAlign: "center",
2752
+ padding: "40px",
2753
+ color: "#8e8ea9"
2754
+ },
2755
+ "data-testid": "orgs-empty",
2756
+ children: search ? `No organizations matching "${search}"` : "No organizations found"
2757
+ }
2758
+ ) }) : orgs.map((org, i) => /* @__PURE__ */ jsxRuntime.jsxs(
2759
+ TR$1,
2760
+ {
2761
+ $selected: selected.has(org.id),
2762
+ $i: i,
2763
+ "data-testid": "org-row",
2764
+ children: [
2765
+ rbac.allowedActions.canDelete && /* @__PURE__ */ jsxRuntime.jsx(TDCheck$1, { children: /* @__PURE__ */ jsxRuntime.jsx(
2766
+ designSystem.Checkbox,
2767
+ {
2768
+ checked: selected.has(org.id),
2769
+ onCheckedChange: () => toggleSelect(org.id),
2770
+ "aria-label": `Select ${org.name}`
2771
+ }
2772
+ ) }),
2773
+ /* @__PURE__ */ jsxRuntime.jsx(TD$1, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { alignItems: "center", gap: 2, children: [
2774
+ /* @__PURE__ */ jsxRuntime.jsx(OrgAvatar, { name: org.name, logo: org.logo }),
2775
+ /* @__PURE__ */ jsxRuntime.jsx(OrgName, { children: org.name })
2776
+ ] }) }),
2777
+ /* @__PURE__ */ jsxRuntime.jsx(TD$1, { children: /* @__PURE__ */ jsxRuntime.jsx(SlugChip, { children: org.slug }) }),
2778
+ /* @__PURE__ */ jsxRuntime.jsx(TD$1, { children: /* @__PURE__ */ jsxRuntime.jsx(CountChip, { children: org.memberCount }) }),
2779
+ /* @__PURE__ */ jsxRuntime.jsx(TD$1, { children: /* @__PURE__ */ jsxRuntime.jsx(DateText$1, { children: new Date(org.createdAt).toLocaleDateString() }) }),
2780
+ /* @__PURE__ */ jsxRuntime.jsx(TDActions$1, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 1, justifyContent: "flex-end", children: [
2781
+ rbac.allowedActions.canUpdate && /* @__PURE__ */ jsxRuntime.jsx(
2782
+ designSystem.IconButton,
2783
+ {
2784
+ label: "Edit organization",
2785
+ onClick: () => setDetailOrgId(org.id),
2786
+ "data-testid": "edit-org-btn",
2787
+ children: /* @__PURE__ */ jsxRuntime.jsx(icons.Pencil, {})
2788
+ }
2789
+ ),
2790
+ rbac.allowedActions.canDelete && /* @__PURE__ */ jsxRuntime.jsx(
2791
+ designSystem.IconButton,
2792
+ {
2793
+ label: "Delete organization",
2794
+ onClick: () => setConfirmDelete(org.id),
2795
+ "data-testid": "delete-org-btn",
2796
+ children: /* @__PURE__ */ jsxRuntime.jsx(icons.Trash, {})
2797
+ }
2798
+ )
2799
+ ] }) })
2800
+ ]
2801
+ },
2802
+ org.id
2803
+ )) })
2804
+ ] }) }),
2805
+ pageCount > 1 && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "flex-end", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, children: [
2806
+ /* @__PURE__ */ jsxRuntime.jsx(
2807
+ designSystem.Button,
2808
+ {
2809
+ variant: "tertiary",
2810
+ size: "S",
2811
+ disabled: page === 1,
2812
+ onClick: () => setPage((p) => p - 1),
2813
+ children: "Previous"
2814
+ }
2815
+ ),
2816
+ /* @__PURE__ */ jsxRuntime.jsx(
2817
+ designSystem.Button,
2818
+ {
2819
+ variant: "tertiary",
2820
+ size: "S",
2821
+ disabled: page >= pageCount,
2822
+ onClick: () => setPage((p) => p + 1),
2823
+ children: "Next"
2824
+ }
2825
+ )
2826
+ ] }) }),
2827
+ rbac.allowedActions.canCreate && showCreate && /* @__PURE__ */ jsxRuntime.jsx(
2828
+ CreateOrganizationDialog,
2829
+ {
2830
+ teamsEnabled,
2831
+ onClose: () => setShowCreate(false)
2832
+ }
2833
+ ),
2834
+ rbac.allowedActions.canUpdate && detailOrgId && /* @__PURE__ */ jsxRuntime.jsx(
2835
+ OrganizationDetail,
2836
+ {
2837
+ organizationId: detailOrgId,
2838
+ teamsEnabled,
2839
+ onClose: () => setDetailOrgId(null)
2840
+ }
2841
+ ),
2842
+ rbac.allowedActions.canDelete && confirmDelete && /* @__PURE__ */ jsxRuntime.jsx(
2843
+ ConfirmDialog,
2844
+ {
2845
+ title: "Delete organization",
2846
+ message: "Are you sure you want to delete this organization? All members and teams will be removed. This action cannot be undone.",
2847
+ confirmLabel: "Delete",
2848
+ loading: deleteMutation.isLoading,
2849
+ onConfirm: () => deleteMutation.mutate(confirmDelete),
2850
+ onCancel: () => setConfirmDelete(null)
2851
+ }
2852
+ ),
2853
+ rbac.allowedActions.canDelete && confirmDeleteMany && /* @__PURE__ */ jsxRuntime.jsx(
2854
+ ConfirmDialog,
2855
+ {
2856
+ title: `Delete ${selected.size} organization${selected.size !== 1 ? "s" : ""}`,
2857
+ message: `Are you sure you want to delete ${selected.size} organization${selected.size !== 1 ? "s" : ""}? This action cannot be undone.`,
2858
+ confirmLabel: "Delete all",
2859
+ loading: deleteManyMutation.isLoading,
2860
+ onConfirm: () => deleteManyMutation.mutate([...selected]),
2861
+ onCancel: () => setConfirmDeleteMany(false)
2862
+ }
2863
+ )
2864
+ ] })
2865
+ }
2866
+ );
2867
+ }
2868
+ const PALETTE = [
2869
+ { bg: "#EAF5FF", fg: "#0C75AF" },
2870
+ { bg: "#F0F0FF", fg: "#4945FF" },
2871
+ { bg: "#EAFBE7", fg: "#328048" },
2872
+ { bg: "#FFF3D3", fg: "#8E6A00" },
2873
+ { bg: "#FCECEA", fg: "#D02B20" },
2874
+ { bg: "#F6ECFC", fg: "#8312D1" },
2875
+ { bg: "#E8F8F5", fg: "#1A7F64" }
2876
+ ];
2877
+ const Circle = styled__default.default.div`
2878
+ width: ${(p) => p.$size}px;
2879
+ height: ${(p) => p.$size}px;
2880
+ border-radius: 50%;
2881
+ background: ${(p) => p.$bg};
2882
+ color: ${(p) => p.$fg};
2883
+ display: inline-flex;
2884
+ align-items: center;
2885
+ justify-content: center;
2066
2886
  font-size: ${(p) => Math.round(p.$size * 0.38)}px;
2067
2887
  font-weight: 700;
2068
2888
  flex-shrink: 0;
@@ -2105,95 +2925,143 @@ function Avatar({
2105
2925
  const color = getColor(name || "?");
2106
2926
  return /* @__PURE__ */ jsxRuntime.jsx(Circle, { $bg: color.bg, $fg: color.fg, $size: size, children: getInitials(name || "?") });
2107
2927
  }
2108
- const fadeUp$2 = styled.keyframes`
2109
- from { opacity: 0; transform: translateY(8px); }
2928
+ const T = {
2929
+ bg: "#f6f6f9",
2930
+ bgCard: "#ffffff",
2931
+ border: "#eaeaef",
2932
+ borderHover: "rgba(73,69,255,0.35)",
2933
+ accent: "#4945ff",
2934
+ green: "#5cb176",
2935
+ greenDim: "rgba(92,177,118,0.12)",
2936
+ amber: "#d9822f",
2937
+ red: "#d02b20",
2938
+ redDim: "rgba(208,43,32,0.1)",
2939
+ purple: "#8460b8",
2940
+ textPrimary: "#32324d",
2941
+ textSecondary: "#666687",
2942
+ textMuted: "#b8b8c7",
2943
+ mono: `'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace`
2944
+ };
2945
+ const fadeUp$1 = styled.keyframes`
2946
+ from { opacity: 0; transform: translateY(10px); }
2110
2947
  to { opacity: 1; transform: translateY(0); }
2111
2948
  `;
2112
- const Wrap$2 = styled__default.default.div`
2113
- padding: 28px 32px;
2114
- background: #f6f6f9;
2949
+ const Wrap$1 = styled__default.default.div`
2950
+ padding: 28px 32px 48px;
2951
+ background: ${T.bg};
2115
2952
  min-height: 100%;
2116
2953
  display: flex;
2117
2954
  flex-direction: column;
2118
- gap: 20px;
2955
+ gap: 22px;
2119
2956
  `;
2120
- const StatRow = styled__default.default.div`
2121
- display: grid;
2122
- grid-template-columns: repeat(5, 1fr);
2123
- gap: 12px;
2124
- @media (max-width: 1200px) { grid-template-columns: repeat(3, 1fr); }
2125
- @media (max-width: 768px) { grid-template-columns: repeat(2, 1fr); }
2957
+ const SectionDivider = styled__default.default.div`
2958
+ display: flex;
2959
+ align-items: center;
2960
+ gap: 10px;
2961
+ margin-bottom: -6px;
2126
2962
  `;
2127
- const MainRow = styled__default.default.div`
2128
- display: grid;
2129
- grid-template-columns: 1fr 300px;
2130
- gap: 12px;
2131
- @media (max-width: 960px) { grid-template-columns: 1fr; }
2963
+ const DivLabel = styled__default.default.span`
2964
+ font-size: 9px;
2965
+ font-weight: 700;
2966
+ letter-spacing: 0.14em;
2967
+ text-transform: uppercase;
2968
+ color: ${T.textMuted};
2969
+ white-space: nowrap;
2132
2970
  `;
2133
- const ActiveRow = styled__default.default.div`
2134
- display: grid;
2135
- grid-template-columns: repeat(3, 1fr);
2136
- gap: 12px;
2971
+ const DivLine = styled__default.default.div`
2972
+ flex: 1;
2973
+ height: 1px;
2974
+ background: ${T.border};
2137
2975
  `;
2138
2976
  const Card = styled__default.default.div`
2139
- background: #ffffff;
2140
- border: 1px solid #eaeaef;
2141
- border-radius: 10px;
2977
+ background: ${T.bgCard};
2978
+ border: 1px solid ${T.border};
2979
+ border-radius: 12px;
2142
2980
  overflow: hidden;
2143
2981
  position: relative;
2144
- animation: ${fadeUp$2} 360ms ease both;
2982
+ animation: ${fadeUp$1} 380ms ease both;
2145
2983
  animation-delay: ${(p) => (p.$delay ?? 0) * 55}ms;
2146
- transition: border-color 180ms ease, box-shadow 180ms ease;
2147
-
2148
- ${(p) => p.$accent && `
2149
- &::before {
2150
- content: '';
2151
- position: absolute;
2152
- top: 0; left: 0; right: 0;
2153
- height: 3px;
2154
- background: ${p.$accent};
2155
- z-index: 1;
2156
- }
2157
- `}
2158
-
2984
+ transition: border-color 200ms, box-shadow 200ms;
2985
+ &:hover {
2986
+ border-color: ${T.borderHover};
2987
+ box-shadow: 0 0 0 1px ${T.borderHover}, 0 8px 32px rgba(124,109,250,0.07);
2988
+ }
2989
+ `;
2990
+ const PillGroup = styled__default.default.div`
2991
+ display: flex;
2992
+ background: rgba(0,0,0,0.03);
2993
+ border: 1px solid ${T.border};
2994
+ border-radius: 9px;
2995
+ padding: 3px;
2996
+ gap: 2px;
2997
+ `;
2998
+ const Pill = styled__default.default.button`
2999
+ appearance: none;
3000
+ border: none;
3001
+ cursor: pointer;
3002
+ padding: 5px 16px;
3003
+ border-radius: 6px;
3004
+ font-size: 11px;
3005
+ font-weight: 600;
3006
+ letter-spacing: 0.03em;
3007
+ transition: background 180ms, color 180ms;
3008
+ background: ${(p) => p.$active ? T.accent : "transparent"};
3009
+ color: ${(p) => p.$active ? "#fff" : T.textSecondary};
2159
3010
  &:hover {
2160
- border-color: #c0bfff;
2161
- box-shadow: 0 2px 16px rgba(73,69,255,0.08);
3011
+ background: ${(p) => p.$active ? T.accent : "rgba(0,0,0,0.05)"};
3012
+ color: ${(p) => p.$active ? "#fff" : T.textPrimary};
2162
3013
  }
2163
3014
  `;
2164
- const StatTop = styled__default.default.div`
2165
- padding: 18px 18px 10px;
3015
+ const StatGrid = styled__default.default.div`
3016
+ display: grid;
3017
+ grid-template-columns: repeat(5, 1fr);
3018
+ gap: 10px;
3019
+ @media (max-width: 1200px) { grid-template-columns: repeat(3, 1fr); }
3020
+ `;
3021
+ const StatCard = styled__default.default(Card)`
3022
+ padding: 18px 20px 0;
3023
+ &::after {
3024
+ content: '';
3025
+ position: absolute;
3026
+ top: 0; left: 0; right: 0;
3027
+ height: 2px;
3028
+ background: ${(p) => p.$accent};
3029
+ opacity: 0.75;
3030
+ }
2166
3031
  `;
2167
3032
  const StatLabel = styled__default.default.div`
2168
- font-size: 10px;
3033
+ font-size: 9px;
2169
3034
  font-weight: 700;
2170
- letter-spacing: 0.08em;
3035
+ letter-spacing: 0.1em;
2171
3036
  text-transform: uppercase;
2172
- color: #8e8ea9;
2173
- margin-bottom: 8px;
3037
+ color: ${T.textMuted};
3038
+ margin-bottom: 10px;
2174
3039
  `;
2175
3040
  const StatValue = styled__default.default.div`
2176
3041
  font-size: 30px;
2177
3042
  font-weight: 800;
2178
- color: #32324d;
3043
+ color: ${T.textPrimary};
2179
3044
  line-height: 1;
2180
- letter-spacing: -0.05em;
3045
+ letter-spacing: -0.045em;
2181
3046
  font-variant-numeric: tabular-nums;
2182
3047
  margin-bottom: 10px;
2183
3048
  `;
2184
3049
  const TrendBadge = styled__default.default.span`
2185
3050
  display: inline-flex;
2186
3051
  align-items: center;
2187
- gap: 2px;
2188
- padding: 2px 7px;
3052
+ gap: 3px;
3053
+ padding: 2px 8px;
2189
3054
  border-radius: 20px;
2190
- font-size: 10px;
3055
+ font-size: 9px;
2191
3056
  font-weight: 700;
2192
- background: ${(p) => p.$pos ? "#eafbe7" : "#fcecea"};
2193
- color: ${(p) => p.$pos ? "#5cb176" : "#d02b20"};
3057
+ letter-spacing: 0.03em;
3058
+ background: ${(p) => p.$pos ? T.greenDim : T.redDim};
3059
+ color: ${(p) => p.$pos ? T.green : T.red};
3060
+ margin-bottom: 12px;
2194
3061
  `;
2195
3062
  const SparkWrap = styled__default.default.div`
2196
- height: 48px;
3063
+ height: 44px;
3064
+ margin: 0 -20px;
2197
3065
  `;
2198
3066
  const ChartCard = styled__default.default(Card)`
2199
3067
  padding: 20px 20px 12px;
@@ -2205,31 +3073,42 @@ const ChartHeader = styled__default.default.div`
2205
3073
  display: flex;
2206
3074
  align-items: center;
2207
3075
  justify-content: space-between;
3076
+ gap: 12px;
2208
3077
  flex-shrink: 0;
2209
3078
  `;
2210
3079
  const ChartTitle = styled__default.default.div`
2211
- font-size: 13px;
3080
+ font-size: 12px;
2212
3081
  font-weight: 700;
2213
- color: #32324d;
3082
+ color: ${T.textPrimary};
3083
+ letter-spacing: 0.01em;
2214
3084
  `;
2215
- const LegendRow = styled__default.default.div`
3085
+ const SeriesRow = styled__default.default.div`
2216
3086
  display: flex;
2217
3087
  align-items: center;
2218
- gap: 14px;
3088
+ gap: 6px;
2219
3089
  `;
2220
- const LegendItem = styled__default.default.div`
3090
+ const SeriesBtn = styled__default.default.button`
3091
+ appearance: none;
3092
+ border: 1px solid ${(p) => p.$on ? `${p.$c}55` : T.border};
3093
+ background: ${(p) => p.$on ? `${p.$c}18` : "transparent"};
3094
+ color: ${(p) => p.$on ? p.$c : T.textMuted};
3095
+ font-size: 10px;
3096
+ font-weight: 600;
3097
+ padding: 3px 10px 3px 8px;
3098
+ border-radius: 6px;
3099
+ cursor: pointer;
2221
3100
  display: flex;
2222
3101
  align-items: center;
2223
3102
  gap: 5px;
2224
- font-size: 10px;
2225
- color: #8e8ea9;
2226
- font-weight: 600;
2227
- text-transform: uppercase;
2228
- letter-spacing: 0.04em;
3103
+ transition: all 160ms;
3104
+ &:hover {
3105
+ border-color: ${(p) => `${p.$c}88`};
3106
+ color: ${(p) => p.$c};
3107
+ background: ${(p) => `${p.$c}22`};
3108
+ }
2229
3109
  `;
2230
- const LegendDot = styled__default.default.span`
2231
- width: 8px;
2232
- height: 8px;
3110
+ const SDot = styled__default.default.span`
3111
+ width: 6px; height: 6px;
2233
3112
  border-radius: 50%;
2234
3113
  background: ${(p) => p.$c};
2235
3114
  display: inline-block;
@@ -2237,140 +3116,188 @@ const LegendDot = styled__default.default.span`
2237
3116
  `;
2238
3117
  const HoverInfo = styled__default.default.div`
2239
3118
  font-size: 11px;
2240
- color: #8e8ea9;
3119
+ color: ${T.textSecondary};
2241
3120
  font-variant-numeric: tabular-nums;
2242
- min-height: 16px;
3121
+ font-family: ${T.mono};
3122
+ min-height: 15px;
3123
+ `;
3124
+ const TwoPanel = styled__default.default.div`
3125
+ display: grid;
3126
+ grid-template-columns: ${(p) => p.$ratio ?? "1fr 288px"};
3127
+ gap: 10px;
3128
+ align-items: stretch;
3129
+ @media (max-width: 900px) { grid-template-columns: 1fr; }
2243
3130
  `;
2244
3131
  const FeedCard = styled__default.default(Card)`
2245
3132
  display: flex;
2246
3133
  flex-direction: column;
2247
- max-height: 380px;
3134
+ overflow: hidden;
2248
3135
  `;
2249
- const FeedHeader = styled__default.default.div`
2250
- padding: 16px 18px 12px;
2251
- border-bottom: 1px solid #f0f0f7;
2252
- flex-shrink: 0;
2253
- font-size: 10px;
3136
+ const FeedHead = styled__default.default.div`
3137
+ padding: 13px 16px 9px;
3138
+ border-bottom: 1px solid ${T.border};
3139
+ font-size: 11px;
2254
3140
  font-weight: 700;
2255
- letter-spacing: 0.08em;
2256
- text-transform: uppercase;
2257
- color: #8e8ea9;
3141
+ color: ${T.textPrimary};
3142
+ flex-shrink: 0;
3143
+ display: flex;
3144
+ justify-content: space-between;
3145
+ align-items: center;
2258
3146
  `;
2259
- const FeedList = styled__default.default.div`
3147
+ const FeedSelect = styled__default.default.select`
3148
+ appearance: none;
3149
+ border: 1px solid ${T.border};
3150
+ border-radius: 6px;
3151
+ background: ${T.bgCard};
3152
+ color: ${T.textSecondary};
3153
+ font-size: 10px;
3154
+ font-weight: 600;
3155
+ padding: 3px 22px 3px 8px;
3156
+ cursor: pointer;
3157
+ outline: none;
3158
+ 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");
3159
+ background-repeat: no-repeat;
3160
+ background-position: right 7px center;
3161
+ transition: border-color 160ms;
3162
+ &:hover, &:focus { border-color: ${T.accent}; color: ${T.textPrimary}; }
3163
+ `;
3164
+ const FeedScroll = styled__default.default.div`
2260
3165
  flex: 1;
2261
3166
  overflow-y: auto;
2262
- padding: 6px 0;
2263
- &::-webkit-scrollbar { width: 3px; }
2264
- &::-webkit-scrollbar-track { background: transparent; }
2265
- &::-webkit-scrollbar-thumb { background: #d9d8ff; border-radius: 2px; }
3167
+ min-height: 0;
3168
+ &::-webkit-scrollbar { width: 4px; }
3169
+ &::-webkit-scrollbar-thumb { background: ${T.textMuted}; border-radius: 2px; }
2266
3170
  `;
2267
3171
  const FeedItem = styled__default.default.div`
3172
+ padding: 9px 14px;
3173
+ border-bottom: 1px solid #f0f0f5;
2268
3174
  display: flex;
2269
- align-items: flex-start;
2270
- gap: 10px;
2271
- padding: 9px 18px;
2272
- &:hover { background: #fafafe; }
3175
+ flex-direction: column;
3176
+ gap: 3px;
3177
+ transition: background 130ms;
3178
+ &:hover { background: #fafaff; }
3179
+ &:last-child { border-bottom: none; }
2273
3180
  `;
2274
- const FeedContent = styled__default.default.div`
2275
- flex: 1;
2276
- min-width: 0;
3181
+ const FeedTop = styled__default.default.div`
3182
+ display: flex;
3183
+ align-items: center;
3184
+ justify-content: space-between;
3185
+ gap: 6px;
2277
3186
  `;
2278
- const FeedName = styled__default.default.div`
2279
- font-size: 12px;
3187
+ const FeedName = styled__default.default.span`
3188
+ font-size: 11px;
2280
3189
  font-weight: 600;
2281
- color: #32324d;
2282
- white-space: nowrap;
3190
+ color: ${T.textPrimary};
2283
3191
  overflow: hidden;
2284
3192
  text-overflow: ellipsis;
3193
+ white-space: nowrap;
2285
3194
  `;
2286
- const FeedSub = styled__default.default.div`
3195
+ const FeedEmail = styled__default.default.span`
2287
3196
  font-size: 10px;
2288
- color: #8e8ea9;
2289
- white-space: nowrap;
3197
+ color: ${T.textSecondary};
2290
3198
  overflow: hidden;
2291
3199
  text-overflow: ellipsis;
2292
- margin-top: 1px;
3200
+ white-space: nowrap;
3201
+ display: block;
2293
3202
  `;
2294
- const FeedTime = styled__default.default.div`
2295
- font-size: 10px;
2296
- color: #b8b8c7;
3203
+ const FeedMeta = styled__default.default.span`
3204
+ font-size: 9px;
3205
+ color: ${T.textMuted};
2297
3206
  white-space: nowrap;
2298
3207
  font-variant-numeric: tabular-nums;
2299
- margin-top: 1px;
2300
3208
  `;
2301
- const UsersCard = styled__default.default(Card)``;
2302
- const TableWrap = styled__default.default.div`
2303
- overflow-x: auto;
3209
+ const RtnCard = styled__default.default(Card)`
3210
+ padding: 20px;
3211
+ display: flex;
3212
+ flex-direction: column;
3213
+ gap: 12px;
2304
3214
  `;
2305
- const Table$1 = styled__default.default.table`
2306
- width: 100%;
2307
- border-collapse: collapse;
3215
+ const RtnRow = styled__default.default.div`
3216
+ display: flex;
3217
+ align-items: center;
3218
+ gap: 10px;
3219
+ padding: 4px 6px;
3220
+ border-radius: 7px;
3221
+ transition: background 130ms;
3222
+ background: ${(p) => p.$hov ? "#f0f0f6" : "transparent"};
3223
+ cursor: default;
2308
3224
  `;
2309
- const TH$1 = styled__default.default.th`
2310
- text-align: left;
2311
- padding: 10px 14px;
3225
+ const RtnLabel = styled__default.default.span`
2312
3226
  font-size: 10px;
2313
- font-weight: 700;
2314
- color: #8e8ea9;
2315
- text-transform: uppercase;
2316
- letter-spacing: 0.06em;
2317
- border-bottom: 1px solid #eaeaef;
2318
- background: #fafafa;
3227
+ color: ${T.textSecondary};
3228
+ min-width: 80px;
3229
+ text-align: right;
2319
3230
  white-space: nowrap;
2320
- &:first-child { padding-left: 20px; }
2321
- &:last-child { padding-right: 20px; }
2322
3231
  `;
2323
- const TR$1 = styled__default.default.tr`
2324
- &:hover td { background: #fafafe; }
2325
- &:last-child td { border-bottom: none; }
3232
+ const RtnSize = styled__default.default.span`
3233
+ font-size: 9px;
3234
+ color: ${T.textMuted};
3235
+ min-width: 52px;
3236
+ text-align: right;
3237
+ white-space: nowrap;
3238
+ font-variant-numeric: tabular-nums;
2326
3239
  `;
2327
- const TD$1 = styled__default.default.td`
2328
- padding: 10px 14px;
2329
- font-size: 12px;
2330
- color: #32324d;
2331
- border-bottom: 1px solid #f5f5f9;
2332
- vertical-align: middle;
2333
- &:first-child { padding-left: 20px; }
2334
- &:last-child { padding-right: 20px; }
3240
+ const RtnTrack = styled__default.default.div`
3241
+ flex: 1;
3242
+ height: 10px;
3243
+ background: #eaeaef;
3244
+ border-radius: 4px;
3245
+ overflow: hidden;
2335
3246
  `;
2336
- const StatusChip$1 = styled__default.default.span`
2337
- display: inline-flex;
2338
- align-items: center;
2339
- gap: 4px;
2340
- padding: 2px 8px;
2341
- border-radius: 20px;
3247
+ const RtnBar = styled__default.default.div`
3248
+ width: ${(p) => Math.max(p.$w, 0.5)}%;
3249
+ height: 100%;
3250
+ background: hsl(${(p) => p.$hue}, 60%, 50%);
3251
+ border-radius: 4px;
3252
+ transition: width 0.55s cubic-bezier(0.25, 0.46, 0.45, 0.94);
3253
+ `;
3254
+ const RtnPct = styled__default.default.span`
2342
3255
  font-size: 10px;
2343
3256
  font-weight: 700;
2344
- background: ${(p) => p.$banned ? "#fcecea" : p.$verified ? "#eafbe7" : "#f0f0ff"};
2345
- color: ${(p) => p.$banned ? "#d02b20" : p.$verified ? "#5cb176" : "#8e8ea9"};
2346
-
2347
- &::before {
2348
- content: '';
2349
- display: inline-block;
2350
- width: 5px;
2351
- height: 5px;
2352
- border-radius: 50%;
2353
- background: ${(p) => p.$banned ? "#d02b20" : p.$verified ? "#5cb176" : "#b8b8c7"};
2354
- }
3257
+ color: hsl(${(p) => p.$hue}, 60%, 60%);
3258
+ min-width: 38px;
3259
+ text-align: right;
3260
+ font-variant-numeric: tabular-nums;
3261
+ font-family: ${T.mono};
2355
3262
  `;
2356
- const Divider = styled__default.default.div`
3263
+ const RtnTip = styled__default.default.div`
3264
+ font-size: 10px;
3265
+ color: ${T.textSecondary};
3266
+ min-height: 14px;
3267
+ font-variant-numeric: tabular-nums;
3268
+ padding: 0 6px;
3269
+ `;
3270
+ const RingGrid = styled__default.default.div`
2357
3271
  display: flex;
2358
- align-items: center;
3272
+ flex-direction: column;
2359
3273
  gap: 10px;
2360
- margin-bottom: -8px;
2361
3274
  `;
2362
- const DivLabel = styled__default.default.span`
2363
- font-size: 10px;
2364
- font-weight: 700;
2365
- text-transform: uppercase;
2366
- letter-spacing: 0.08em;
2367
- color: #b8b8c7;
2368
- white-space: nowrap;
3275
+ const RingCard = styled__default.default(Card)`
3276
+ padding: 18px 20px;
3277
+ display: flex;
3278
+ align-items: center;
3279
+ gap: 16px;
3280
+ flex: 1;
2369
3281
  `;
2370
- const DivLine = styled__default.default.div`
2371
- flex: 1;
2372
- height: 1px;
2373
- background: #eaeaef;
3282
+ const RingInfo = styled__default.default.div`
3283
+ display: flex;
3284
+ flex-direction: column;
3285
+ gap: 4px;
3286
+ `;
3287
+ const RingVal = styled__default.default.div`
3288
+ font-size: 24px;
3289
+ font-weight: 800;
3290
+ color: ${T.textPrimary};
3291
+ letter-spacing: -0.04em;
3292
+ font-variant-numeric: tabular-nums;
3293
+ line-height: 1;
3294
+ `;
3295
+ const RingLabel = styled__default.default.div`
3296
+ font-size: 9px;
3297
+ font-weight: 700;
3298
+ text-transform: uppercase;
3299
+ letter-spacing: 0.1em;
3300
+ color: ${T.textMuted};
2374
3301
  `;
2375
3302
  const Empty = styled__default.default.div`
2376
3303
  display: flex;
@@ -2378,32 +3305,52 @@ const Empty = styled__default.default.div`
2378
3305
  align-items: center;
2379
3306
  justify-content: center;
2380
3307
  padding: 32px;
2381
- color: #8e8ea9;
3308
+ color: ${T.textMuted};
2382
3309
  font-size: 12px;
2383
- gap: 6px;
3310
+ gap: 8px;
2384
3311
  `;
3312
+ function useCountUp(target, duration = 850) {
3313
+ const [count, setCount] = react.useState(0);
3314
+ const prevRef = react.useRef(0);
3315
+ react.useEffect(() => {
3316
+ let raf;
3317
+ let start = 0;
3318
+ const from = prevRef.current;
3319
+ const diff = target - from;
3320
+ const step = (ts) => {
3321
+ if (!start) start = ts;
3322
+ const progress = Math.min((ts - start) / duration, 1);
3323
+ const eased = 1 - (1 - progress) ** 3;
3324
+ setCount(Math.round(from + eased * diff));
3325
+ if (progress < 1) raf = requestAnimationFrame(step);
3326
+ else prevRef.current = target;
3327
+ };
3328
+ raf = requestAnimationFrame(step);
3329
+ return () => cancelAnimationFrame(raf);
3330
+ }, [target, duration]);
3331
+ return count;
3332
+ }
2385
3333
  function Sparkline({
2386
3334
  data,
2387
3335
  color,
2388
3336
  id
2389
3337
  }) {
2390
3338
  const W = 300;
2391
- const H = 48;
3339
+ const H = 44;
2392
3340
  if (data.length < 2) return /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "100%", height: H });
2393
3341
  const min = Math.min(...data);
2394
3342
  const max = Math.max(...data, min + 1);
2395
- const range = max - min;
2396
3343
  const pts = data.map((v, i) => ({
2397
3344
  x: i / (data.length - 1) * W,
2398
- y: H - 4 - (v - min) / range * (H - 10)
3345
+ y: H - 3 - (v - min) / (max - min) * (H - 8)
2399
3346
  }));
2400
- const line = pts.reduce((acc, p, i) => {
3347
+ const line = pts.reduce((a, p, i) => {
2401
3348
  if (i === 0) return `M ${p.x} ${p.y}`;
2402
3349
  const pr = pts[i - 1];
2403
3350
  const cx = (pr.x + p.x) / 2;
2404
- return `${acc} C ${cx} ${pr.y} ${cx} ${p.y} ${p.x} ${p.y}`;
3351
+ return `${a} C ${cx} ${pr.y} ${cx} ${p.y} ${p.x} ${p.y}`;
2405
3352
  }, "");
2406
- const area = `${line} L ${pts[pts.length - 1].x} ${H} L 0 ${H} Z`;
3353
+ const area = `${line} L ${pts.at(-1).x} ${H} L 0 ${H} Z`;
2407
3354
  const gid = `spk-${id}`;
2408
3355
  return /* @__PURE__ */ jsxRuntime.jsxs(
2409
3356
  "svg",
@@ -2412,10 +3359,10 @@ function Sparkline({
2412
3359
  height: H,
2413
3360
  viewBox: `0 0 ${W} ${H}`,
2414
3361
  preserveAspectRatio: "none",
2415
- "aria-hidden": "true",
3362
+ "aria-hidden": true,
2416
3363
  children: [
2417
3364
  /* @__PURE__ */ jsxRuntime.jsx("defs", { children: /* @__PURE__ */ jsxRuntime.jsxs("linearGradient", { id: gid, x1: "0", y1: "0", x2: "0", y2: "1", children: [
2418
- /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "0%", stopColor: color, stopOpacity: "0.25" }),
3365
+ /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "0%", stopColor: color, stopOpacity: "0.32" }),
2419
3366
  /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "100%", stopColor: color, stopOpacity: "0" })
2420
3367
  ] }) }),
2421
3368
  /* @__PURE__ */ jsxRuntime.jsx("path", { d: area, fill: `url(#${gid})` }),
@@ -2424,57 +3371,58 @@ function Sparkline({
2424
3371
  {
2425
3372
  d: line,
2426
3373
  stroke: color,
2427
- strokeWidth: "2",
3374
+ strokeWidth: "1.6",
2428
3375
  fill: "none",
2429
- strokeLinecap: "round",
2430
- strokeLinejoin: "round"
3376
+ strokeLinecap: "round"
2431
3377
  }
2432
3378
  )
2433
3379
  ]
2434
3380
  }
2435
3381
  );
2436
3382
  }
2437
- const SERIES = [
2438
- { key: "totalUsers", color: "#4945FF", label: "Total" },
2439
- { key: "newUsers", color: "#5CB176", label: "New" },
2440
- { key: "activeUsers", color: "#E57553", label: "Active" }
3383
+ const ALL_SERIES = [
3384
+ { key: "totalUsers", color: T.accent, label: "Total" },
3385
+ { key: "newUsers", color: T.green, label: "New" },
3386
+ { key: "activeUsers", color: T.amber, label: "Active" }
2441
3387
  ];
2442
- function AreaChart({
3388
+ function GrowthChart({
2443
3389
  data,
2444
3390
  hovered,
2445
- onHover
3391
+ onHover,
3392
+ activeSeries
2446
3393
  }) {
2447
3394
  const W = 600;
2448
- const H = 220;
2449
- const PL = 44;
3395
+ const H = 200;
3396
+ const PL = 40;
2450
3397
  const PR = 12;
2451
3398
  const PT = 12;
2452
3399
  const PB = 28;
2453
3400
  const CW = W - PL - PR;
2454
3401
  const CH = H - PT - PB;
2455
- if (data.length === 0) {
3402
+ if (!data.length) {
2456
3403
  return /* @__PURE__ */ jsxRuntime.jsxs(Empty, { children: [
2457
3404
  /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 24 }, children: "📊" }),
2458
3405
  /* @__PURE__ */ jsxRuntime.jsx("div", { children: "No growth data for this period" })
2459
3406
  ] });
2460
3407
  }
2461
- const allVals = data.flatMap((d) => SERIES.map((s) => d[s.key]));
3408
+ const active = ALL_SERIES.filter((s) => activeSeries.has(s.key));
3409
+ const allVals = data.flatMap((d) => active.map((s) => d[s.key]));
2462
3410
  const maxV = Math.max(...allVals, 10);
2463
3411
  const yMax = Math.ceil(maxV * 1.15);
2464
3412
  const xp = (i) => PL + i / Math.max(data.length - 1, 1) * CW;
2465
3413
  const yp = (v) => PT + (1 - v / yMax) * CH;
2466
3414
  const smooth = (vals) => {
2467
3415
  const pts = vals.map((v, i) => ({ x: xp(i), y: yp(v) }));
2468
- const path = pts.reduce((acc, p, i) => {
3416
+ const path = pts.reduce((a, p, i) => {
2469
3417
  if (i === 0) return `M ${p.x} ${p.y}`;
2470
3418
  const pr = pts[i - 1];
2471
3419
  const cx = (pr.x + p.x) / 2;
2472
- return `${acc} C ${cx} ${pr.y} ${cx} ${p.y} ${p.x} ${p.y}`;
3420
+ return `${a} C ${cx} ${pr.y} ${cx} ${p.y} ${p.x} ${p.y}`;
2473
3421
  }, "");
2474
- const area = `${path} L ${pts[pts.length - 1].x} ${PT + CH} L ${pts[0].x} ${PT + CH} Z`;
3422
+ const area = `${path} L ${pts.at(-1).x} ${PT + CH} L ${pts[0].x} ${PT + CH} Z`;
2475
3423
  return { pts, path, area };
2476
3424
  };
2477
- const lines = SERIES.map((s) => ({
3425
+ const lines = active.map((s) => ({
2478
3426
  ...s,
2479
3427
  ...smooth(data.map((d) => d[s.key]))
2480
3428
  }));
@@ -2497,17 +3445,17 @@ function AreaChart({
2497
3445
  /* @__PURE__ */ jsxRuntime.jsx("defs", { children: lines.map((s) => /* @__PURE__ */ jsxRuntime.jsxs(
2498
3446
  "linearGradient",
2499
3447
  {
2500
- id: `ag-${s.color.replace("#", "")}`,
3448
+ id: `ag-${s.key}`,
2501
3449
  x1: "0",
2502
3450
  y1: "0",
2503
3451
  x2: "0",
2504
3452
  y2: "1",
2505
3453
  children: [
2506
- /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "0%", stopColor: s.color, stopOpacity: "0.13" }),
3454
+ /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "0%", stopColor: s.color, stopOpacity: "0.18" }),
2507
3455
  /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "100%", stopColor: s.color, stopOpacity: "0" })
2508
3456
  ]
2509
3457
  },
2510
- s.color
3458
+ s.key
2511
3459
  )) }),
2512
3460
  yTicks.map((t) => /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
2513
3461
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -2527,7 +3475,7 @@ function AreaChart({
2527
3475
  x: PL - 6,
2528
3476
  y: t.y + 4,
2529
3477
  textAnchor: "end",
2530
- fill: "#b8b8c7",
3478
+ fill: T.textMuted,
2531
3479
  fontSize: "9",
2532
3480
  children: t.v >= 1e3 ? `${(t.v / 1e3).toFixed(1)}k` : t.v
2533
3481
  }
@@ -2544,25 +3492,18 @@ function AreaChart({
2544
3492
  strokeWidth: "1"
2545
3493
  }
2546
3494
  ),
2547
- lines.map((s) => /* @__PURE__ */ jsxRuntime.jsx(
2548
- "path",
2549
- {
2550
- d: s.area,
2551
- fill: `url(#ag-${s.color.replace("#", "")})`
2552
- },
2553
- `a-${s.color}`
2554
- )),
3495
+ lines.map((s) => /* @__PURE__ */ jsxRuntime.jsx("path", { d: s.area, fill: `url(#ag-${s.key})` }, `area-${s.key}`)),
2555
3496
  lines.map((s) => /* @__PURE__ */ jsxRuntime.jsx(
2556
3497
  "path",
2557
3498
  {
2558
3499
  d: s.path,
2559
3500
  stroke: s.color,
2560
- strokeWidth: "1.8",
3501
+ strokeWidth: "2",
2561
3502
  fill: "none",
2562
3503
  strokeLinecap: "round",
2563
3504
  strokeLinejoin: "round"
2564
3505
  },
2565
- `l-${s.color}`
3506
+ `line-${s.key}`
2566
3507
  )),
2567
3508
  data.map((d, i) => {
2568
3509
  if (i % step !== 0 && i !== data.length - 1) return null;
@@ -2572,7 +3513,7 @@ function AreaChart({
2572
3513
  x: xp(i),
2573
3514
  y: H - 6,
2574
3515
  textAnchor: "middle",
2575
- fill: "#b8b8c7",
3516
+ fill: T.textMuted,
2576
3517
  fontSize: "9",
2577
3518
  children: d.label
2578
3519
  },
@@ -2582,7 +3523,7 @@ function AreaChart({
2582
3523
  data.map((d, i) => {
2583
3524
  const isHov = hovered === i;
2584
3525
  return (
2585
- // biome-ignore lint/a11y/noStaticElementInteractions: SVG chart hover hit area
3526
+ // biome-ignore lint/a11y/noStaticElementInteractions: SVG chart hover
2586
3527
  /* @__PURE__ */ jsxRuntime.jsxs(
2587
3528
  "g",
2588
3529
  {
@@ -2607,10 +3548,9 @@ function AreaChart({
2607
3548
  y1: PT,
2608
3549
  x2: xp(i),
2609
3550
  y2: PT + CH,
2610
- stroke: "#32324d",
3551
+ stroke: "rgba(50,50,77,0.2)",
2611
3552
  strokeWidth: "1",
2612
- strokeDasharray: "3 3",
2613
- opacity: "0.35"
3553
+ strokeDasharray: "3 3"
2614
3554
  }
2615
3555
  ),
2616
3556
  lines.map((s) => /* @__PURE__ */ jsxRuntime.jsx(
@@ -2620,11 +3560,11 @@ function AreaChart({
2620
3560
  cy: yp(d[s.key]),
2621
3561
  r: isHov ? 4 : 2.5,
2622
3562
  fill: s.color,
2623
- stroke: "white",
3563
+ stroke: T.bg,
2624
3564
  strokeWidth: isHov ? 2 : 1,
2625
- opacity: isHov ? 1 : 0.45
3565
+ opacity: isHov ? 1 : 0.5
2626
3566
  },
2627
- s.color
3567
+ s.key
2628
3568
  ))
2629
3569
  ]
2630
3570
  },
@@ -2636,23 +3576,70 @@ function AreaChart({
2636
3576
  }
2637
3577
  );
2638
3578
  }
3579
+ function ProgressRing({
3580
+ value,
3581
+ max,
3582
+ color,
3583
+ size = 56
3584
+ }) {
3585
+ const r = (size - 9) / 2;
3586
+ const circ = 2 * Math.PI * r;
3587
+ const pct = Math.min(value / Math.max(max, 1), 1);
3588
+ const offset = circ * (1 - pct);
3589
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3590
+ "svg",
3591
+ {
3592
+ width: size,
3593
+ height: size,
3594
+ style: { flexShrink: 0 },
3595
+ "aria-hidden": true,
3596
+ children: [
3597
+ /* @__PURE__ */ jsxRuntime.jsx(
3598
+ "circle",
3599
+ {
3600
+ cx: size / 2,
3601
+ cy: size / 2,
3602
+ r,
3603
+ fill: "none",
3604
+ stroke: "#e5e7eb",
3605
+ strokeWidth: "7"
3606
+ }
3607
+ ),
3608
+ /* @__PURE__ */ jsxRuntime.jsx(
3609
+ "circle",
3610
+ {
3611
+ cx: size / 2,
3612
+ cy: size / 2,
3613
+ r,
3614
+ fill: "none",
3615
+ stroke: color,
3616
+ strokeWidth: "7",
3617
+ strokeDasharray: `${circ} ${circ}`,
3618
+ strokeDashoffset: offset,
3619
+ strokeLinecap: "round",
3620
+ transform: `rotate(-90 ${size / 2} ${size / 2})`,
3621
+ style: {
3622
+ transition: "stroke-dashoffset 0.9s cubic-bezier(0.4,0,0.2,1)"
3623
+ }
3624
+ }
3625
+ )
3626
+ ]
3627
+ }
3628
+ );
3629
+ }
2639
3630
  function relTime(date) {
2640
- const d = typeof date === "string" ? new Date(date) : date;
2641
- const m = Math.floor((Date.now() - d.getTime()) / 6e4);
2642
- if (m < 1) return "just now";
2643
- if (m < 60) return `${m}m ago`;
2644
- const h = Math.floor(m / 60);
2645
- if (h < 24) return `${h}h ago`;
2646
- return `${Math.floor(h / 24)}d ago`;
3631
+ const diff = Date.now() - new Date(date).getTime();
3632
+ const mins = Math.floor(diff / 6e4);
3633
+ if (mins < 1) return "just now";
3634
+ if (mins < 60) return `${mins}m`;
3635
+ const hrs = Math.floor(mins / 60);
3636
+ if (hrs < 24) return `${hrs}h`;
3637
+ return `${Math.floor(hrs / 24)}d`;
2647
3638
  }
2648
- function fmtDate(date) {
2649
- return new Date(date).toLocaleDateString(void 0, {
2650
- month: "short",
2651
- day: "numeric",
2652
- year: "numeric"
2653
- });
3639
+ function rateHue(r) {
3640
+ return r >= 70 ? 142 : r >= 40 ? 38 : 4;
2654
3641
  }
2655
- function StatCardItem({
3642
+ function StatItem({
2656
3643
  id,
2657
3644
  label,
2658
3645
  value,
@@ -2661,26 +3648,39 @@ function StatCardItem({
2661
3648
  color,
2662
3649
  delay = 0
2663
3650
  }) {
3651
+ const animated = useCountUp(value);
2664
3652
  const isPos = pct === void 0 || pct >= 0;
2665
- return /* @__PURE__ */ jsxRuntime.jsxs(Card, { $delay: delay, $accent: color, children: [
2666
- /* @__PURE__ */ jsxRuntime.jsxs(StatTop, { children: [
2667
- /* @__PURE__ */ jsxRuntime.jsx(StatLabel, { children: label }),
2668
- /* @__PURE__ */ jsxRuntime.jsx(StatValue, { children: typeof value === "number" ? value.toLocaleString() : value }),
2669
- pct !== void 0 && /* @__PURE__ */ jsxRuntime.jsxs(TrendBadge, { $pos: isPos, children: [
2670
- isPos ? "↑" : "↓",
2671
- " ",
2672
- Math.abs(pct).toFixed(1),
2673
- "%"
2674
- ] })
3653
+ return /* @__PURE__ */ jsxRuntime.jsxs(StatCard, { $delay: delay, $accent: color, children: [
3654
+ /* @__PURE__ */ jsxRuntime.jsx(StatLabel, { children: label }),
3655
+ /* @__PURE__ */ jsxRuntime.jsx(StatValue, { children: animated.toLocaleString() }),
3656
+ pct !== void 0 && /* @__PURE__ */ jsxRuntime.jsxs(TrendBadge, { $pos: isPos, children: [
3657
+ isPos ? "↑" : "↓",
3658
+ " ",
3659
+ Math.abs(pct).toFixed(1),
3660
+ "%"
2675
3661
  ] }),
2676
3662
  sparkline && sparkline.length > 1 && /* @__PURE__ */ jsxRuntime.jsx(SparkWrap, { children: /* @__PURE__ */ jsxRuntime.jsx(Sparkline, { data: sparkline, color, id }) })
2677
3663
  ] });
2678
3664
  }
2679
3665
  function OverviewPage() {
3666
+ const { get } = admin.useFetchClient();
2680
3667
  const [period, setPeriod] = react.useState(
2681
3668
  "weekly"
2682
3669
  );
3670
+ const [feedMode, setFeedMode] = react.useState("signups");
2683
3671
  const [hovIdx, setHovIdx] = react.useState(null);
3672
+ const [rtnHov, setRtnHov] = react.useState(null);
3673
+ const [activeSeries, setActiveSeries] = react.useState(
3674
+ () => /* @__PURE__ */ new Set(["totalUsers", "newUsers", "activeUsers"])
3675
+ );
3676
+ const toggleSeries = (key) => {
3677
+ setActiveSeries((prev) => {
3678
+ const next = new Set(prev);
3679
+ if (next.has(key) && next.size > 1) next.delete(key);
3680
+ else next.add(key);
3681
+ return next;
3682
+ });
3683
+ };
2684
3684
  const statsQuery = reactQuery.useQuery({
2685
3685
  queryKey: ["dash-user-stats"],
2686
3686
  queryFn: async () => {
@@ -2697,12 +3697,10 @@ function OverviewPage() {
2697
3697
  return r.data;
2698
3698
  }
2699
3699
  });
2700
- const sessionsQuery = reactQuery.useQuery({
2701
- queryKey: ["dash-recent-sessions"],
3700
+ const retentionQuery = reactQuery.useQuery({
3701
+ queryKey: ["dash-user-retention", period],
2702
3702
  queryFn: async () => {
2703
- const r = await client.dash.listAllSessions({
2704
- query: { page: 1, limit: 10 }
2705
- });
3703
+ const r = await client.dash.userRetentionData({ query: { period } });
2706
3704
  if (r.error) throw new Error(r.error.message ?? "Failed");
2707
3705
  return r.data;
2708
3706
  }
@@ -2711,12 +3709,22 @@ function OverviewPage() {
2711
3709
  queryKey: ["dash-recent-users"],
2712
3710
  queryFn: async () => {
2713
3711
  const r = await client.dash.listUsers({
2714
- query: { page: 1, limit: 8, search: "" }
3712
+ query: { limit: 12, offset: 0, sortBy: "createdAt", sortOrder: "desc" }
2715
3713
  });
2716
3714
  if (r.error) throw new Error(r.error.message ?? "Failed");
2717
3715
  return r.data;
2718
3716
  }
2719
3717
  });
3718
+ const sessionsQuery = reactQuery.useQuery({
3719
+ queryKey: ["dash-recent-sessions"],
3720
+ queryFn: async () => {
3721
+ const { data } = await get(
3722
+ "/better-auth-dashboard/db?uid=plugin::better-auth.session&pagination[pageSize]=12&sort[0]=updatedAt:desc"
3723
+ );
3724
+ return data.results ?? [];
3725
+ },
3726
+ refetchInterval: 3e4
3727
+ });
2720
3728
  const orgsQuery = reactQuery.useQuery({
2721
3729
  queryKey: ["dash-orgs-count"],
2722
3730
  queryFn: async () => {
@@ -2727,596 +3735,403 @@ function OverviewPage() {
2727
3735
  return r.data;
2728
3736
  }
2729
3737
  });
3738
+ const sessionsRaw = sessionsQuery.data ?? [];
3739
+ const activeUserIds = [];
3740
+ const lastActiveByUserId = /* @__PURE__ */ new Map();
3741
+ for (const s of sessionsRaw) {
3742
+ const uid = String(s.userId);
3743
+ if (!lastActiveByUserId.has(uid)) {
3744
+ lastActiveByUserId.set(uid, s.updatedAt ?? s.createdAt);
3745
+ activeUserIds.push(uid);
3746
+ }
3747
+ }
3748
+ const activeUsersQuery = reactQuery.useQuery({
3749
+ queryKey: ["dash-active-users", activeUserIds],
3750
+ queryFn: async () => {
3751
+ if (activeUserIds.length === 0) return [];
3752
+ const params = new URLSearchParams({ uid: "plugin::better-auth.user" });
3753
+ for (let i = 0; i < activeUserIds.length; i++) {
3754
+ params.set(`filters[id][$in][${i}]`, activeUserIds[i]);
3755
+ }
3756
+ params.set("pagination[pageSize]", "12");
3757
+ const { data } = await get(
3758
+ `/better-auth-dashboard/db?${params}`
3759
+ );
3760
+ return data.results ?? [];
3761
+ },
3762
+ enabled: feedMode === "active" && activeUserIds.length > 0
3763
+ });
3764
+ const chartRef = react.useRef(null);
3765
+ const [chartHeight, setChartHeight] = react.useState(0);
3766
+ react.useEffect(() => {
3767
+ const el = chartRef.current;
3768
+ if (!el) return;
3769
+ const ro = new ResizeObserver((entries) => {
3770
+ for (const entry of entries) setChartHeight(entry.contentRect.height);
3771
+ });
3772
+ ro.observe(el);
3773
+ return () => ro.disconnect();
3774
+ }, []);
2730
3775
  if (statsQuery.isLoading) {
2731
- return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", alignItems: "center", padding: 12, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading…" }) });
3776
+ return /* @__PURE__ */ jsxRuntime.jsx(
3777
+ designSystem.Flex,
3778
+ {
3779
+ justifyContent: "center",
3780
+ alignItems: "center",
3781
+ padding: 12,
3782
+ style: { background: T.bg, minHeight: "100%" },
3783
+ children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading…" })
3784
+ }
3785
+ );
2732
3786
  }
2733
3787
  const stats = statsQuery.data;
2734
3788
  if (!stats) return null;
2735
3789
  const graphData = graphQuery.data?.data ?? [];
2736
- const sessions = sessionsQuery.data?.sessions ?? [];
2737
- const recentUsers = usersQuery.data?.users ?? [];
3790
+ const rtnData = retentionQuery.data?.data ?? [];
3791
+ const users = usersQuery.data?.users ?? [];
2738
3792
  const orgCount = orgsQuery.data?.total ?? 0;
3793
+ const activeUserMap = /* @__PURE__ */ new Map();
3794
+ for (const u of activeUsersQuery.data ?? []) {
3795
+ activeUserMap.set(String(u.id), u);
3796
+ }
3797
+ const sortedActiveUsers = activeUserIds.map((uid) => activeUserMap.get(uid)).filter((u) => u !== void 0);
2739
3798
  const totalSpark = graphData.map((d) => d.totalUsers);
2740
3799
  const newSpark = graphData.map((d) => d.newUsers);
2741
3800
  const activeSpark = graphData.map((d) => d.activeUsers);
2742
3801
  const hovRow = hovIdx !== null ? graphData[hovIdx] : null;
2743
- return /* @__PURE__ */ jsxRuntime.jsxs(Wrap$2, { "data-testid": "overview-page", children: [
2744
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "flex-end", children: [
2745
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { children: [
2746
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "alpha", textColor: "neutral800", children: "Overview" }),
2747
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { paddingTop: 1, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: (/* @__PURE__ */ new Date()).toLocaleDateString(void 0, {
2748
- weekday: "long",
2749
- year: "numeric",
2750
- month: "long",
2751
- day: "numeric"
2752
- }) }) })
2753
- ] }),
2754
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { width: "150px", children: /* @__PURE__ */ jsxRuntime.jsxs(
2755
- designSystem.SingleSelect,
2756
- {
2757
- value: period,
2758
- onChange: (v) => setPeriod(v),
2759
- size: "S",
2760
- "aria-label": "Select period",
2761
- children: [
2762
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "daily", children: "Daily" }),
2763
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "weekly", children: "Weekly" }),
2764
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "monthly", children: "Monthly" })
2765
- ]
2766
- }
2767
- ) })
2768
- ] }),
2769
- /* @__PURE__ */ jsxRuntime.jsxs(Divider, { children: [
2770
- /* @__PURE__ */ jsxRuntime.jsx(DivLabel, { children: "Sign-ups" }),
2771
- /* @__PURE__ */ jsxRuntime.jsx(DivLine, {})
2772
- ] }),
2773
- /* @__PURE__ */ jsxRuntime.jsxs(StatRow, { children: [
2774
- /* @__PURE__ */ jsxRuntime.jsx(
2775
- StatCardItem,
2776
- {
2777
- id: "total",
2778
- label: "Total Users",
2779
- value: stats.total ?? 0,
2780
- sparkline: totalSpark,
2781
- color: "#4945FF",
2782
- delay: 0
2783
- }
2784
- ),
2785
- /* @__PURE__ */ jsxRuntime.jsx(
2786
- StatCardItem,
2787
- {
2788
- id: "d-sig",
2789
- label: "Daily Sign-ups",
2790
- value: stats.daily.signUps ?? 0,
2791
- pct: stats.daily.percentage ?? void 0,
2792
- sparkline: newSpark,
2793
- color: "#5CB176",
2794
- delay: 1
2795
- }
2796
- ),
2797
- /* @__PURE__ */ jsxRuntime.jsx(
2798
- StatCardItem,
2799
- {
2800
- id: "w-sig",
2801
- label: "Weekly Sign-ups",
2802
- value: stats.weekly.signUps ?? 0,
2803
- pct: stats.weekly.percentage ?? void 0,
2804
- sparkline: newSpark,
2805
- color: "#5CB176",
2806
- delay: 2
2807
- }
2808
- ),
2809
- /* @__PURE__ */ jsxRuntime.jsx(
2810
- StatCardItem,
2811
- {
2812
- id: "m-sig",
2813
- label: "Monthly Sign-ups",
2814
- value: stats.monthly.signUps ?? 0,
2815
- pct: stats.monthly.percentage ?? void 0,
2816
- sparkline: newSpark,
2817
- color: "#5CB176",
2818
- delay: 3
2819
- }
3802
+ const rtnHovRow = rtnHov !== null ? rtnData[rtnHov] : null;
3803
+ const activeMax = Math.max(
3804
+ stats.activeUsers.daily.active ?? 0,
3805
+ stats.activeUsers.weekly.active ?? 0,
3806
+ stats.activeUsers.monthly.active ?? 0,
3807
+ 1
3808
+ );
3809
+ return /* @__PURE__ */ jsxRuntime.jsx(
3810
+ admin.Page.Protect,
3811
+ {
3812
+ permissions: index.PERMISSIONS.overview.filter(
3813
+ (p) => p.action === `plugin::${index.PLUGIN_ID}.overview.read`
2820
3814
  ),
2821
- /* @__PURE__ */ jsxRuntime.jsx(
2822
- StatCardItem,
2823
- {
2824
- id: "orgs",
2825
- label: "Organizations",
2826
- value: orgCount,
2827
- color: "#9E6BF9",
2828
- delay: 4
2829
- }
2830
- )
2831
- ] }),
2832
- /* @__PURE__ */ jsxRuntime.jsxs(MainRow, { children: [
2833
- /* @__PURE__ */ jsxRuntime.jsxs(ChartCard, { $delay: 5, children: [
2834
- /* @__PURE__ */ jsxRuntime.jsxs(ChartHeader, { children: [
2835
- /* @__PURE__ */ jsxRuntime.jsx(ChartTitle, { children: "User Growth" }),
2836
- /* @__PURE__ */ jsxRuntime.jsx(LegendRow, { children: SERIES.map((s) => /* @__PURE__ */ jsxRuntime.jsxs(LegendItem, { children: [
2837
- /* @__PURE__ */ jsxRuntime.jsx(LegendDot, { $c: s.color }),
2838
- s.label
2839
- ] }, s.color)) })
3815
+ children: /* @__PURE__ */ jsxRuntime.jsxs(Wrap$1, { "data-testid": "overview-page", children: [
3816
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "flex-end", children: [
3817
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
3818
+ /* @__PURE__ */ jsxRuntime.jsx(
3819
+ "div",
3820
+ {
3821
+ style: {
3822
+ fontSize: 22,
3823
+ fontWeight: 800,
3824
+ color: T.textPrimary,
3825
+ letterSpacing: "-0.03em"
3826
+ },
3827
+ children: "Overview"
3828
+ }
3829
+ ),
3830
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 11, color: T.textSecondary, marginTop: 4 }, children: (/* @__PURE__ */ new Date()).toLocaleDateString(void 0, {
3831
+ weekday: "long",
3832
+ year: "numeric",
3833
+ month: "long",
3834
+ day: "numeric"
3835
+ }) })
3836
+ ] }),
3837
+ /* @__PURE__ */ jsxRuntime.jsx(PillGroup, { children: ["daily", "weekly", "monthly"].map((p) => /* @__PURE__ */ jsxRuntime.jsx(Pill, { $active: period === p, onClick: () => setPeriod(p), children: p[0].toUpperCase() + p.slice(1) }, p)) })
2840
3838
  ] }),
2841
- /* @__PURE__ */ jsxRuntime.jsx(HoverInfo, { children: hovRow ? `${hovRow.label} · ${hovRow.totalUsers.toLocaleString()} total · +${hovRow.newUsers.toLocaleString()} new · ${hovRow.activeUsers.toLocaleString()} active` : "Hover the chart to inspect a data point" }),
2842
- graphQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(
2843
- designSystem.Flex,
2844
- {
2845
- justifyContent: "center",
2846
- alignItems: "center",
2847
- style: { flex: 1, minHeight: 180 },
2848
- children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading…" })
2849
- }
2850
- ) : /* @__PURE__ */ jsxRuntime.jsx(AreaChart, { data: graphData, hovered: hovIdx, onHover: setHovIdx })
2851
- ] }),
2852
- /* @__PURE__ */ jsxRuntime.jsxs(FeedCard, { $delay: 6, children: [
2853
- /* @__PURE__ */ jsxRuntime.jsx(FeedHeader, { children: "Recent Sessions" }),
2854
- /* @__PURE__ */ jsxRuntime.jsx(FeedList, { children: sessionsQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(Empty, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading…" }) }) : sessions.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(Empty, { children: "No recent sessions" }) : sessions.map((s) => /* @__PURE__ */ jsxRuntime.jsxs(FeedItem, { children: [
3839
+ /* @__PURE__ */ jsxRuntime.jsxs(SectionDivider, { children: [
3840
+ /* @__PURE__ */ jsxRuntime.jsx(DivLabel, { children: "Metrics" }),
3841
+ /* @__PURE__ */ jsxRuntime.jsx(DivLine, {})
3842
+ ] }),
3843
+ /* @__PURE__ */ jsxRuntime.jsxs(StatGrid, { children: [
2855
3844
  /* @__PURE__ */ jsxRuntime.jsx(
2856
- Avatar,
3845
+ StatItem,
2857
3846
  {
2858
- name: s.user?.name ?? "?",
2859
- src: s.user?.image,
2860
- size: 28
3847
+ id: "total",
3848
+ label: "Total Users",
3849
+ value: stats.total ?? 0,
3850
+ sparkline: totalSpark,
3851
+ color: T.accent,
3852
+ delay: 0
2861
3853
  }
2862
- ),
2863
- /* @__PURE__ */ jsxRuntime.jsxs(FeedContent, { children: [
2864
- /* @__PURE__ */ jsxRuntime.jsx(FeedName, { children: s.user?.name ?? "Unknown" }),
2865
- /* @__PURE__ */ jsxRuntime.jsx(FeedSub, { children: s.user?.email ?? "" }),
2866
- /* @__PURE__ */ jsxRuntime.jsx(FeedTime, { children: relTime(s.createdAt) })
2867
- ] })
2868
- ] }, s.id)) })
2869
- ] })
2870
- ] }),
2871
- /* @__PURE__ */ jsxRuntime.jsxs(Divider, { children: [
2872
- /* @__PURE__ */ jsxRuntime.jsx(DivLabel, { children: "Active Users" }),
2873
- /* @__PURE__ */ jsxRuntime.jsx(DivLine, {})
2874
- ] }),
2875
- /* @__PURE__ */ jsxRuntime.jsxs(ActiveRow, { children: [
2876
- /* @__PURE__ */ jsxRuntime.jsx(
2877
- StatCardItem,
2878
- {
2879
- id: "d-act",
2880
- label: "Daily Active",
2881
- value: stats.activeUsers.daily.active ?? 0,
2882
- pct: stats.activeUsers.daily.percentage ?? void 0,
2883
- sparkline: activeSpark,
2884
- color: "#E57553",
2885
- delay: 7
2886
- }
2887
- ),
2888
- /* @__PURE__ */ jsxRuntime.jsx(
2889
- StatCardItem,
2890
- {
2891
- id: "w-act",
2892
- label: "Weekly Active",
2893
- value: stats.activeUsers.weekly.active ?? 0,
2894
- pct: stats.activeUsers.weekly.percentage ?? void 0,
2895
- sparkline: activeSpark,
2896
- color: "#E57553",
2897
- delay: 8
2898
- }
2899
- ),
2900
- /* @__PURE__ */ jsxRuntime.jsx(
2901
- StatCardItem,
2902
- {
2903
- id: "m-act",
2904
- label: "Monthly Active",
2905
- value: stats.activeUsers.monthly.active ?? 0,
2906
- pct: stats.activeUsers.monthly.percentage ?? void 0,
2907
- sparkline: activeSpark,
2908
- color: "#E57553",
2909
- delay: 9
2910
- }
2911
- )
2912
- ] }),
2913
- /* @__PURE__ */ jsxRuntime.jsxs(Divider, { children: [
2914
- /* @__PURE__ */ jsxRuntime.jsx(DivLabel, { children: "Recent Users" }),
2915
- /* @__PURE__ */ jsxRuntime.jsx(DivLine, {})
2916
- ] }),
2917
- /* @__PURE__ */ jsxRuntime.jsx(UsersCard, { $delay: 10, children: /* @__PURE__ */ jsxRuntime.jsx(TableWrap, { children: /* @__PURE__ */ jsxRuntime.jsxs(Table$1, { children: [
2918
- /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsx("tr", { children: ["User", "Email", "Joined", "Status"].map((h) => /* @__PURE__ */ jsxRuntime.jsx(TH$1, { children: h }, h)) }) }),
2919
- /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: usersQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsx(TD$1, { colSpan: 4, style: { textAlign: "center", padding: 28 }, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading users…" }) }) }) : recentUsers.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsx(
2920
- TD$1,
2921
- {
2922
- colSpan: 4,
2923
- style: {
2924
- textAlign: "center",
2925
- padding: 28,
2926
- color: "#8e8ea9"
2927
- },
2928
- children: "No users yet"
2929
- }
2930
- ) }) : recentUsers.map((u) => /* @__PURE__ */ jsxRuntime.jsxs(TR$1, { children: [
2931
- /* @__PURE__ */ jsxRuntime.jsx(TD$1, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { alignItems: "center", gap: 2, children: [
2932
- /* @__PURE__ */ jsxRuntime.jsx(Avatar, { name: u.name, src: u.image, size: 26 }),
2933
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontWeight: 600, fontSize: 12 }, children: u.name })
2934
- ] }) }),
2935
- /* @__PURE__ */ jsxRuntime.jsx(
2936
- TD$1,
2937
- {
2938
- style: {
2939
- color: "#8e8ea9",
2940
- fontFamily: "monospace",
2941
- fontSize: 11
2942
- },
2943
- children: u.email
2944
- }
2945
- ),
2946
- /* @__PURE__ */ jsxRuntime.jsx(
2947
- TD$1,
2948
- {
2949
- style: {
2950
- color: "#8e8ea9",
2951
- fontSize: 11,
2952
- whiteSpace: "nowrap"
2953
- },
2954
- children: fmtDate(u.createdAt)
2955
- }
2956
- ),
2957
- /* @__PURE__ */ jsxRuntime.jsx(TD$1, { children: /* @__PURE__ */ jsxRuntime.jsx(
2958
- StatusChip$1,
2959
- {
2960
- $verified: u.emailVerified,
2961
- $banned: u.banned,
2962
- children: u.banned ? "Banned" : u.emailVerified ? "Verified" : "Unverified"
2963
- }
2964
- ) })
2965
- ] }, u.id)) })
2966
- ] }) }) })
2967
- ] });
2968
- }
2969
- const PAGE_SIZE$1 = 25;
2970
- const fadeUp$1 = styled.keyframes`
2971
- from { opacity: 0; transform: translateY(6px); }
2972
- to { opacity: 1; transform: translateY(0); }
2973
- `;
2974
- const Wrap$1 = styled__default.default.div`
2975
- padding: 28px 32px;
2976
- background: #f6f6f9;
2977
- min-height: 100%;
2978
- display: flex;
2979
- flex-direction: column;
2980
- gap: 20px;
2981
- `;
2982
- const PageHeader$1 = styled__default.default.div`
2983
- display: flex;
2984
- justify-content: space-between;
2985
- align-items: flex-start;
2986
- `;
2987
- const TitleBlock$1 = styled__default.default.div`
2988
- display: flex;
2989
- flex-direction: column;
2990
- gap: 4px;
2991
- `;
2992
- const PageTitle$1 = styled__default.default.h1`
2993
- margin: 0;
2994
- font-size: 22px;
2995
- font-weight: 800;
2996
- color: #32324d;
2997
- letter-spacing: -0.03em;
2998
- `;
2999
- const PageSubtitle$1 = styled__default.default.p`
3000
- margin: 0;
3001
- font-size: 12px;
3002
- color: #8e8ea9;
3003
- `;
3004
- const TableCard$1 = styled__default.default.div`
3005
- background: #ffffff;
3006
- border: 1px solid #eaeaef;
3007
- border-radius: 10px;
3008
- overflow: hidden;
3009
- `;
3010
- const ColumnHeader = styled__default.default.div`
3011
- padding: 10px 20px;
3012
- display: flex;
3013
- align-items: center;
3014
- gap: 12px;
3015
- background: #fafafa;
3016
- border-bottom: 1px solid #eaeaef;
3017
- font-size: 10px;
3018
- font-weight: 700;
3019
- text-transform: uppercase;
3020
- letter-spacing: 0.08em;
3021
- color: #8e8ea9;
3022
- `;
3023
- const ColumnHeaderRight = styled__default.default.div`
3024
- margin-left: auto;
3025
- font-size: 10px;
3026
- font-weight: 700;
3027
- text-transform: uppercase;
3028
- letter-spacing: 0.08em;
3029
- color: #8e8ea9;
3030
- `;
3031
- const UserGroup = styled__default.default.div`
3032
- border-bottom: 1px solid #eaeaef;
3033
- animation: ${fadeUp$1} 280ms ease both;
3034
- animation-delay: ${(p) => (p.$i ?? 0) * 30}ms;
3035
- &:last-child { border-bottom: none; }
3036
- `;
3037
- const UserRowHeader = styled__default.default.div`
3038
- padding: 14px 20px;
3039
- display: flex;
3040
- align-items: center;
3041
- gap: 12px;
3042
- border-bottom: 1px solid #f5f5f9;
3043
- cursor: default;
3044
- &:hover { background: #fafafe; }
3045
- `;
3046
- const UserInfo = styled__default.default.div`
3047
- flex: 1;
3048
- display: flex;
3049
- flex-direction: column;
3050
- gap: 2px;
3051
- `;
3052
- const UserName$1 = styled__default.default.span`
3053
- font-size: 12px;
3054
- font-weight: 600;
3055
- color: #32324d;
3056
- `;
3057
- const UserEmail = styled__default.default.span`
3058
- font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
3059
- font-size: 11px;
3060
- color: #8e8ea9;
3061
- `;
3062
- const SessionCountChip = styled__default.default.span`
3063
- display: inline-flex;
3064
- align-items: center;
3065
- padding: 2px 8px;
3066
- border-radius: 20px;
3067
- font-size: 10px;
3068
- font-weight: 700;
3069
- background: #f0f0ff;
3070
- color: #4945ff;
3071
- `;
3072
- const SessionCard = styled__default.default.div`
3073
- padding: 10px 20px 10px 64px;
3074
- border-top: 1px solid #f5f5f9;
3075
- background: #fafafa;
3076
- display: flex;
3077
- align-items: flex-start;
3078
- justify-content: space-between;
3079
- gap: 16px;
3080
- &:hover { background: #f5f5ff; }
3081
- `;
3082
- const SessionMeta = styled__default.default.div`
3083
- display: flex;
3084
- flex-direction: column;
3085
- gap: 3px;
3086
- flex: 1;
3087
- min-width: 0;
3088
- `;
3089
- const IpChip = styled__default.default.span`
3090
- display: inline-block;
3091
- background: #f0f0ff;
3092
- color: #4945ff;
3093
- border-radius: 5px;
3094
- padding: 2px 6px;
3095
- font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
3096
- font-size: 11px;
3097
- font-weight: 600;
3098
- `;
3099
- const TimestampText = styled__default.default.span`
3100
- font-size: 11px;
3101
- color: #8e8ea9;
3102
- font-variant-numeric: tabular-nums;
3103
- `;
3104
- const AgentText = styled__default.default.span`
3105
- font-size: 11px;
3106
- color: #b8b8c7;
3107
- overflow: hidden;
3108
- text-overflow: ellipsis;
3109
- white-space: nowrap;
3110
- display: block;
3111
- max-width: 400px;
3112
- `;
3113
- const EmptyState = styled__default.default.div`
3114
- display: flex;
3115
- justify-content: center;
3116
- align-items: center;
3117
- padding: 48px;
3118
- font-size: 12px;
3119
- color: #8e8ea9;
3120
- `;
3121
- function SessionsPage() {
3122
- const qc = reactQuery.useQueryClient();
3123
- const [page, setPage] = react.useState(1);
3124
- const [selected, setSelected] = react.useState(/* @__PURE__ */ new Set());
3125
- const [confirmRevokeSessionId, setConfirmRevokeSessionId] = react.useState(null);
3126
- const [confirmRevokeMany, setConfirmRevokeMany] = react.useState(false);
3127
- const sessionsQuery = reactQuery.useQuery({
3128
- queryKey: ["dash-all-sessions", page],
3129
- queryFn: async () => {
3130
- const result = await client.dash.listAllSessions({});
3131
- if (result.error)
3132
- throw new Error(result.error.message ?? "Failed to load sessions");
3133
- return result.data ?? [];
3134
- },
3135
- keepPreviousData: true
3136
- });
3137
- const revokeSessionMutation = reactQuery.useMutation({
3138
- mutationFn: async (sessionId) => {
3139
- const result = await client.dash.sessions.revoke(
3140
- {},
3141
- withContext({ sessionId })
3142
- );
3143
- if (result.error)
3144
- throw new Error(result.error.message ?? "Revoke failed");
3145
- },
3146
- onSuccess: () => {
3147
- setConfirmRevokeSessionId(null);
3148
- qc.invalidateQueries({ queryKey: ["dash-all-sessions"] });
3149
- }
3150
- });
3151
- const revokeManyMutation = reactQuery.useMutation({
3152
- mutationFn: async (userIds) => {
3153
- const result = await client.dash.sessions.revokeMany(
3154
- {},
3155
- withContext({ userIds })
3156
- );
3157
- if (result.error)
3158
- throw new Error(result.error.message ?? "Revoke failed");
3159
- return result.data;
3160
- },
3161
- onSuccess: () => {
3162
- setConfirmRevokeMany(false);
3163
- setSelected(/* @__PURE__ */ new Set());
3164
- qc.invalidateQueries({ queryKey: ["dash-all-sessions"] });
3165
- }
3166
- });
3167
- const usersWithSessions = sessionsQuery.data ?? [];
3168
- const allUserIds = usersWithSessions.map((u) => u.id);
3169
- const allSelected = allUserIds.length > 0 && allUserIds.every((id) => selected.has(id));
3170
- const someSelected = selected.size > 0;
3171
- const toggleSelect = (id) => {
3172
- setSelected((prev) => {
3173
- const next = new Set(prev);
3174
- if (next.has(id)) next.delete(id);
3175
- else next.add(id);
3176
- return next;
3177
- });
3178
- };
3179
- const handleSelectAll = () => {
3180
- if (allSelected) setSelected(/* @__PURE__ */ new Set());
3181
- else setSelected(new Set(allUserIds));
3182
- };
3183
- return /* @__PURE__ */ jsxRuntime.jsxs(Wrap$1, { "data-testid": "sessions-page", children: [
3184
- /* @__PURE__ */ jsxRuntime.jsxs(PageHeader$1, { children: [
3185
- /* @__PURE__ */ jsxRuntime.jsxs(TitleBlock$1, { children: [
3186
- /* @__PURE__ */ jsxRuntime.jsx(PageTitle$1, { children: "Sessions" }),
3187
- /* @__PURE__ */ jsxRuntime.jsx(PageSubtitle$1, { children: usersWithSessions.length > 0 ? `${usersWithSessions.length} user${usersWithSessions.length !== 1 ? "s" : ""} with active sessions` : "Active sessions across all users" })
3188
- ] }),
3189
- someSelected && /* @__PURE__ */ jsxRuntime.jsxs(
3190
- designSystem.Button,
3191
- {
3192
- variant: "danger-light",
3193
- size: "S",
3194
- onClick: () => setConfirmRevokeMany(true),
3195
- "data-testid": "revoke-selected-btn",
3196
- children: [
3197
- "Revoke sessions for ",
3198
- selected.size,
3199
- " user",
3200
- selected.size !== 1 ? "s" : ""
3201
- ]
3202
- }
3203
- )
3204
- ] }),
3205
- sessionsQuery.isError && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { color: "#d02b20", fontSize: 12, padding: "8px 0" }, children: sessionsQuery.error instanceof Error ? sessionsQuery.error.message : "An error occurred" }),
3206
- /* @__PURE__ */ jsxRuntime.jsx(TableCard$1, { children: sessionsQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", padding: 8, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading sessions…" }) }) : usersWithSessions.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(EmptyState, { children: "No active sessions" }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3207
- /* @__PURE__ */ jsxRuntime.jsxs(ColumnHeader, { children: [
3208
- /* @__PURE__ */ jsxRuntime.jsx(
3209
- designSystem.Checkbox,
3210
- {
3211
- checked: allSelected,
3212
- onCheckedChange: handleSelectAll,
3213
- "aria-label": "Select all users"
3214
- }
3215
- ),
3216
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: "User / Sessions" }),
3217
- /* @__PURE__ */ jsxRuntime.jsx(ColumnHeaderRight, { children: "Sessions" })
3218
- ] }),
3219
- usersWithSessions.map((userRow, i) => /* @__PURE__ */ jsxRuntime.jsxs(UserGroup, { $i: i, "data-testid": "session-user-row", children: [
3220
- /* @__PURE__ */ jsxRuntime.jsxs(UserRowHeader, { children: [
3854
+ ),
3221
3855
  /* @__PURE__ */ jsxRuntime.jsx(
3222
- designSystem.Checkbox,
3856
+ StatItem,
3223
3857
  {
3224
- checked: selected.has(userRow.id),
3225
- onCheckedChange: () => toggleSelect(userRow.id),
3226
- "aria-label": `Select ${userRow.name}`
3858
+ id: "d-sig",
3859
+ label: "Daily Sign-ups",
3860
+ value: stats.daily.signUps ?? 0,
3861
+ pct: stats.daily.percentage ?? void 0,
3862
+ sparkline: newSpark,
3863
+ color: T.green,
3864
+ delay: 1
3227
3865
  }
3228
3866
  ),
3229
- /* @__PURE__ */ jsxRuntime.jsx(Avatar, { name: userRow.name ?? "", src: null, size: 30 }),
3230
- /* @__PURE__ */ jsxRuntime.jsxs(UserInfo, { children: [
3231
- /* @__PURE__ */ jsxRuntime.jsx(UserName$1, { children: userRow.name }),
3232
- /* @__PURE__ */ jsxRuntime.jsx(UserEmail, { children: userRow.email })
3233
- ] }),
3234
- /* @__PURE__ */ jsxRuntime.jsxs(SessionCountChip, { children: [
3235
- userRow.sessions.length,
3236
- " session",
3237
- userRow.sessions.length !== 1 ? "s" : ""
3238
- ] })
3867
+ /* @__PURE__ */ jsxRuntime.jsx(
3868
+ StatItem,
3869
+ {
3870
+ id: "w-sig",
3871
+ label: "Weekly Sign-ups",
3872
+ value: stats.weekly.signUps ?? 0,
3873
+ pct: stats.weekly.percentage ?? void 0,
3874
+ sparkline: newSpark,
3875
+ color: T.green,
3876
+ delay: 2
3877
+ }
3878
+ ),
3879
+ /* @__PURE__ */ jsxRuntime.jsx(
3880
+ StatItem,
3881
+ {
3882
+ id: "m-sig",
3883
+ label: "Monthly Sign-ups",
3884
+ value: stats.monthly.signUps ?? 0,
3885
+ pct: stats.monthly.percentage ?? void 0,
3886
+ sparkline: newSpark,
3887
+ color: T.green,
3888
+ delay: 3
3889
+ }
3890
+ ),
3891
+ /* @__PURE__ */ jsxRuntime.jsx(
3892
+ StatItem,
3893
+ {
3894
+ id: "orgs",
3895
+ label: "Organizations",
3896
+ value: orgCount,
3897
+ color: T.purple,
3898
+ delay: 4
3899
+ }
3900
+ )
3239
3901
  ] }),
3240
- userRow.sessions.map((session) => /* @__PURE__ */ jsxRuntime.jsxs(SessionCard, { "data-testid": "session-row", children: [
3241
- /* @__PURE__ */ jsxRuntime.jsxs(SessionMeta, { children: [
3242
- /* @__PURE__ */ jsxRuntime.jsxs(
3902
+ /* @__PURE__ */ jsxRuntime.jsxs(SectionDivider, { children: [
3903
+ /* @__PURE__ */ jsxRuntime.jsx(DivLabel, { children: "Growth" }),
3904
+ /* @__PURE__ */ jsxRuntime.jsx(DivLine, {})
3905
+ ] }),
3906
+ /* @__PURE__ */ jsxRuntime.jsxs(TwoPanel, { children: [
3907
+ /* @__PURE__ */ jsxRuntime.jsxs(ChartCard, { ref: chartRef, $delay: 5, children: [
3908
+ /* @__PURE__ */ jsxRuntime.jsxs(ChartHeader, { children: [
3909
+ /* @__PURE__ */ jsxRuntime.jsx(ChartTitle, { children: "User Growth" }),
3910
+ /* @__PURE__ */ jsxRuntime.jsx(SeriesRow, { children: ALL_SERIES.map((s) => /* @__PURE__ */ jsxRuntime.jsxs(
3911
+ SeriesBtn,
3912
+ {
3913
+ $on: activeSeries.has(s.key),
3914
+ $c: s.color,
3915
+ onClick: () => toggleSeries(s.key),
3916
+ children: [
3917
+ /* @__PURE__ */ jsxRuntime.jsx(SDot, { $c: s.color }),
3918
+ s.label
3919
+ ]
3920
+ },
3921
+ s.key
3922
+ )) })
3923
+ ] }),
3924
+ /* @__PURE__ */ jsxRuntime.jsx(HoverInfo, { children: hovRow ? `${hovRow.label} · ${hovRow.totalUsers.toLocaleString()} total · +${hovRow.newUsers.toLocaleString()} new · ${hovRow.activeUsers.toLocaleString()} active` : "Hover the chart to inspect a data point" }),
3925
+ graphQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(
3243
3926
  designSystem.Flex,
3244
3927
  {
3245
- gap: 2,
3928
+ justifyContent: "center",
3246
3929
  alignItems: "center",
3247
- style: { flexWrap: "wrap" },
3248
- children: [
3249
- session.ipAddress && /* @__PURE__ */ jsxRuntime.jsx(IpChip, { children: session.ipAddress }),
3250
- /* @__PURE__ */ jsxRuntime.jsxs(TimestampText, { children: [
3251
- "Created ",
3252
- new Date(session.createdAt).toLocaleString(),
3253
- " ",
3254
- "· Expires",
3255
- " ",
3256
- new Date(session.expiresAt).toLocaleString()
3257
- ] })
3258
- ]
3930
+ style: { flex: 1, minHeight: 170 },
3931
+ children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading…" })
3259
3932
  }
3260
- ),
3261
- session.userAgent && /* @__PURE__ */ jsxRuntime.jsx(AgentText, { children: session.userAgent })
3933
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
3934
+ GrowthChart,
3935
+ {
3936
+ data: graphData,
3937
+ hovered: hovIdx,
3938
+ onHover: setHovIdx,
3939
+ activeSeries
3940
+ }
3941
+ )
3262
3942
  ] }),
3263
- /* @__PURE__ */ jsxRuntime.jsx(
3264
- designSystem.IconButton,
3943
+ /* @__PURE__ */ jsxRuntime.jsxs(
3944
+ FeedCard,
3265
3945
  {
3266
- label: "Revoke session",
3267
- onClick: () => setConfirmRevokeSessionId(session.id),
3268
- style: { flexShrink: 0 },
3269
- "data-testid": "revoke-session-btn",
3270
- children: /* @__PURE__ */ jsxRuntime.jsx(icons.Trash, {})
3946
+ $delay: 6,
3947
+ style: chartHeight > 0 ? { height: chartHeight } : void 0,
3948
+ children: [
3949
+ /* @__PURE__ */ jsxRuntime.jsxs(FeedHead, { children: [
3950
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: feedMode === "signups" ? "Recent Sign-ups" : "Recently Active" }),
3951
+ /* @__PURE__ */ jsxRuntime.jsxs(
3952
+ FeedSelect,
3953
+ {
3954
+ value: feedMode,
3955
+ onChange: (e) => setFeedMode(e.target.value),
3956
+ children: [
3957
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "signups", children: "Recent Sign-ups" }),
3958
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "active", children: "Recently Active" })
3959
+ ]
3960
+ }
3961
+ )
3962
+ ] }),
3963
+ /* @__PURE__ */ jsxRuntime.jsx(FeedScroll, { children: feedMode === "signups" ? usersQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading…" }) }) : users.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(Empty, { children: "No users yet" }) : users.map((u) => /* @__PURE__ */ jsxRuntime.jsxs(FeedItem, { children: [
3964
+ /* @__PURE__ */ jsxRuntime.jsxs(FeedTop, { children: [
3965
+ /* @__PURE__ */ jsxRuntime.jsxs(
3966
+ designSystem.Flex,
3967
+ {
3968
+ alignItems: "center",
3969
+ gap: 1,
3970
+ style: { minWidth: 0 },
3971
+ children: [
3972
+ /* @__PURE__ */ jsxRuntime.jsx(Avatar, { name: u.name, src: u.image, size: 20 }),
3973
+ /* @__PURE__ */ jsxRuntime.jsx(FeedName, { title: u.name, children: u.name })
3974
+ ]
3975
+ }
3976
+ ),
3977
+ /* @__PURE__ */ jsxRuntime.jsx(FeedMeta, { children: relTime(u.createdAt) })
3978
+ ] }),
3979
+ /* @__PURE__ */ jsxRuntime.jsx(FeedEmail, { title: u.email, children: u.email })
3980
+ ] }, u.id)) : activeUsersQuery.isLoading || sessionsQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading…" }) }) : sortedActiveUsers.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(Empty, { children: "No recent activity" }) : sortedActiveUsers.map((u) => /* @__PURE__ */ jsxRuntime.jsxs(FeedItem, { children: [
3981
+ /* @__PURE__ */ jsxRuntime.jsxs(FeedTop, { children: [
3982
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { alignItems: "center", gap: 1, style: { minWidth: 0 }, children: [
3983
+ /* @__PURE__ */ jsxRuntime.jsx(
3984
+ Avatar,
3985
+ {
3986
+ name: u.name,
3987
+ src: u.image ?? void 0,
3988
+ size: 20
3989
+ }
3990
+ ),
3991
+ /* @__PURE__ */ jsxRuntime.jsx(FeedName, { title: u.name, children: u.name })
3992
+ ] }),
3993
+ /* @__PURE__ */ jsxRuntime.jsx(FeedMeta, { children: relTime(
3994
+ lastActiveByUserId.get(String(u.id)) ?? u.createdAt
3995
+ ) })
3996
+ ] }),
3997
+ /* @__PURE__ */ jsxRuntime.jsx(FeedEmail, { title: u.email, children: u.email })
3998
+ ] }, u.documentId)) })
3999
+ ]
3271
4000
  }
3272
4001
  )
3273
- ] }, session.id))
3274
- ] }, userRow.id))
3275
- ] }) }),
3276
- usersWithSessions.length === PAGE_SIZE$1 && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "flex-end", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, children: [
3277
- /* @__PURE__ */ jsxRuntime.jsx(
3278
- designSystem.Button,
3279
- {
3280
- variant: "tertiary",
3281
- size: "S",
3282
- disabled: page === 1,
3283
- onClick: () => setPage((p) => p - 1),
3284
- children: "Previous"
3285
- }
3286
- ),
3287
- /* @__PURE__ */ jsxRuntime.jsx(
3288
- designSystem.Button,
3289
- {
3290
- variant: "tertiary",
3291
- size: "S",
3292
- onClick: () => setPage((p) => p + 1),
3293
- children: "Next"
3294
- }
3295
- )
3296
- ] }) }),
3297
- confirmRevokeSessionId && /* @__PURE__ */ jsxRuntime.jsx(
3298
- ConfirmDialog,
3299
- {
3300
- title: "Revoke session",
3301
- message: "Are you sure you want to revoke this session? The user will be signed out on this device.",
3302
- confirmLabel: "Revoke",
3303
- loading: revokeSessionMutation.isLoading,
3304
- onConfirm: () => revokeSessionMutation.mutate(confirmRevokeSessionId),
3305
- onCancel: () => setConfirmRevokeSessionId(null)
3306
- }
3307
- ),
3308
- confirmRevokeMany && /* @__PURE__ */ jsxRuntime.jsx(
3309
- ConfirmDialog,
3310
- {
3311
- title: `Revoke sessions for ${selected.size} user${selected.size !== 1 ? "s" : ""}`,
3312
- message: `Are you sure you want to revoke all sessions for ${selected.size} user${selected.size !== 1 ? "s" : ""}? They will be signed out on all their devices.`,
3313
- confirmLabel: "Revoke all",
3314
- loading: revokeManyMutation.isLoading,
3315
- onConfirm: () => revokeManyMutation.mutate([...selected]),
3316
- onCancel: () => setConfirmRevokeMany(false)
3317
- }
3318
- )
3319
- ] });
4002
+ ] }),
4003
+ /* @__PURE__ */ jsxRuntime.jsxs(SectionDivider, { children: [
4004
+ /* @__PURE__ */ jsxRuntime.jsx(DivLabel, { children: "Retention & Activity" }),
4005
+ /* @__PURE__ */ jsxRuntime.jsx(DivLine, {})
4006
+ ] }),
4007
+ /* @__PURE__ */ jsxRuntime.jsxs(TwoPanel, { children: [
4008
+ /* @__PURE__ */ jsxRuntime.jsxs(RtnCard, { $delay: 7, children: [
4009
+ /* @__PURE__ */ jsxRuntime.jsxs(ChartHeader, { children: [
4010
+ /* @__PURE__ */ jsxRuntime.jsx(ChartTitle, { children: "Cohort Retention" }),
4011
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { alignItems: "center", gap: 2, children: [
4012
+ { hue: 142, label: "≥70%" },
4013
+ { hue: 38, label: "40–70%" },
4014
+ { hue: 4, label: "<40%" }
4015
+ ].map(({ hue, label }) => /* @__PURE__ */ jsxRuntime.jsxs(
4016
+ designSystem.Flex,
4017
+ {
4018
+ alignItems: "center",
4019
+ gap: 1,
4020
+ style: { marginLeft: 8 },
4021
+ children: [
4022
+ /* @__PURE__ */ jsxRuntime.jsx(
4023
+ "span",
4024
+ {
4025
+ style: {
4026
+ width: 8,
4027
+ height: 8,
4028
+ borderRadius: "50%",
4029
+ background: `hsl(${hue},60%,50%)`,
4030
+ display: "inline-block",
4031
+ flexShrink: 0
4032
+ }
4033
+ }
4034
+ ),
4035
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: 9, color: T.textMuted }, children: label })
4036
+ ]
4037
+ },
4038
+ hue
4039
+ )) })
4040
+ ] }),
4041
+ /* @__PURE__ */ jsxRuntime.jsx(RtnTip, { children: rtnHovRow ? `Cohort ${rtnHovRow.label} · ${rtnHovRow.cohortSize.toLocaleString()} users · active ${rtnHovRow.activeStart} – ${rtnHovRow.activeEnd}` : "Hover a row to see cohort details" }),
4042
+ retentionQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(
4043
+ designSystem.Flex,
4044
+ {
4045
+ justifyContent: "center",
4046
+ alignItems: "center",
4047
+ style: { minHeight: 80 },
4048
+ children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading…" })
4049
+ }
4050
+ ) : rtnData.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs(Empty, { children: [
4051
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 22 }, children: "📉" }),
4052
+ /* @__PURE__ */ jsxRuntime.jsx("div", { children: "No retention data for this period" })
4053
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", flexDirection: "column", gap: 5 }, children: rtnData.map((row, i) => {
4054
+ const hue = rateHue(row.retentionRate);
4055
+ return /* @__PURE__ */ jsxRuntime.jsxs(
4056
+ RtnRow,
4057
+ {
4058
+ $hov: rtnHov === i,
4059
+ onMouseEnter: () => setRtnHov(i),
4060
+ onMouseLeave: () => setRtnHov(null),
4061
+ children: [
4062
+ /* @__PURE__ */ jsxRuntime.jsx(RtnLabel, { children: row.label }),
4063
+ /* @__PURE__ */ jsxRuntime.jsx(RtnSize, { children: row.cohortSize.toLocaleString() }),
4064
+ /* @__PURE__ */ jsxRuntime.jsx(RtnTrack, { children: /* @__PURE__ */ jsxRuntime.jsx(RtnBar, { $w: row.retentionRate, $hue: hue }) }),
4065
+ /* @__PURE__ */ jsxRuntime.jsxs(RtnPct, { $hue: hue, children: [
4066
+ row.retentionRate.toFixed(1),
4067
+ "%"
4068
+ ] })
4069
+ ]
4070
+ },
4071
+ row.n
4072
+ );
4073
+ }) })
4074
+ ] }),
4075
+ /* @__PURE__ */ jsxRuntime.jsx(RingGrid, { children: [
4076
+ {
4077
+ label: "Daily Active",
4078
+ value: stats.activeUsers.daily.active,
4079
+ pct: stats.activeUsers.daily.percentage,
4080
+ color: T.amber,
4081
+ sparkline: activeSpark,
4082
+ delay: 8
4083
+ },
4084
+ {
4085
+ label: "Weekly Active",
4086
+ value: stats.activeUsers.weekly.active,
4087
+ pct: stats.activeUsers.weekly.percentage,
4088
+ color: T.green,
4089
+ sparkline: activeSpark,
4090
+ delay: 9
4091
+ },
4092
+ {
4093
+ label: "Monthly Active",
4094
+ value: stats.activeUsers.monthly.active,
4095
+ pct: stats.activeUsers.monthly.percentage,
4096
+ color: T.accent,
4097
+ sparkline: activeSpark,
4098
+ delay: 10
4099
+ }
4100
+ ].map(({ label, value, pct, color, delay }) => {
4101
+ const isPos = pct == null || pct >= 0;
4102
+ return /* @__PURE__ */ jsxRuntime.jsxs(RingCard, { $delay: delay, children: [
4103
+ /* @__PURE__ */ jsxRuntime.jsx(
4104
+ ProgressRing,
4105
+ {
4106
+ value: value ?? 0,
4107
+ max: activeMax,
4108
+ color,
4109
+ size: 56
4110
+ }
4111
+ ),
4112
+ /* @__PURE__ */ jsxRuntime.jsxs(RingInfo, { children: [
4113
+ /* @__PURE__ */ jsxRuntime.jsx(RingLabel, { children: label }),
4114
+ /* @__PURE__ */ jsxRuntime.jsx(RingVal, { children: (value ?? 0).toLocaleString() }),
4115
+ pct != null && /* @__PURE__ */ jsxRuntime.jsxs(
4116
+ TrendBadge,
4117
+ {
4118
+ $pos: isPos,
4119
+ style: { marginTop: 4, marginBottom: 0 },
4120
+ children: [
4121
+ isPos ? "↑" : "↓",
4122
+ " ",
4123
+ Math.abs(pct).toFixed(1),
4124
+ "%"
4125
+ ]
4126
+ }
4127
+ )
4128
+ ] })
4129
+ ] }, label);
4130
+ }) })
4131
+ ] })
4132
+ ] })
4133
+ }
4134
+ );
3320
4135
  }
3321
4136
  function useUsers(options = {}) {
3322
4137
  const { limit = 25, offset = 0, search } = options;
@@ -3327,7 +4142,14 @@ function useUsers(options = {}) {
3327
4142
  {
3328
4143
  field: "email",
3329
4144
  operator: "contains",
3330
- value: search
4145
+ value: search,
4146
+ connector: "OR"
4147
+ },
4148
+ {
4149
+ field: "name",
4150
+ operator: "contains",
4151
+ value: search,
4152
+ connector: "OR"
3331
4153
  }
3332
4154
  ]) : void 0;
3333
4155
  const result = await client.dash.listUsers({
@@ -3556,6 +4378,45 @@ const ReadOnlyCodeInput = styled__default.default.div`
3556
4378
  word-break: break-all;
3557
4379
  overflow-wrap: anywhere;
3558
4380
  `;
4381
+ const SessionCard = styled__default.default.div`
4382
+ display: flex;
4383
+ align-items: flex-start;
4384
+ justify-content: space-between;
4385
+ gap: 12px;
4386
+ padding: 10px 14px;
4387
+ background: white;
4388
+ border: 1px solid #eaeaef;
4389
+ border-radius: 8px;
4390
+ `;
4391
+ const SessionMeta = styled__default.default.div`
4392
+ display: flex;
4393
+ flex-direction: column;
4394
+ gap: 3px;
4395
+ flex: 1;
4396
+ min-width: 0;
4397
+ `;
4398
+ const IpChip = styled__default.default.span`
4399
+ display: inline-block;
4400
+ background: #f0f0ff;
4401
+ color: #4945ff;
4402
+ border-radius: 5px;
4403
+ padding: 2px 6px;
4404
+ font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
4405
+ font-size: 11px;
4406
+ font-weight: 600;
4407
+ `;
4408
+ const TimestampText = styled__default.default.span`
4409
+ font-size: 11px;
4410
+ color: #8e8ea9;
4411
+ `;
4412
+ const AgentText = styled__default.default.span`
4413
+ font-size: 11px;
4414
+ color: #b8b8c7;
4415
+ overflow: hidden;
4416
+ text-overflow: ellipsis;
4417
+ white-space: nowrap;
4418
+ display: block;
4419
+ `;
3559
4420
  const STANDARD_FIELDS = /* @__PURE__ */ new Set([
3560
4421
  "id",
3561
4422
  "name",
@@ -3577,11 +4438,15 @@ function UserDetailDrawer({
3577
4438
  }) {
3578
4439
  const qc = reactQuery.useQueryClient();
3579
4440
  const { toggleNotification } = admin.useNotification();
4441
+ const { get, put } = admin.useFetchClient();
3580
4442
  const schemaQuery = useModelSchema("user");
3581
4443
  const userQuery = reactQuery.useQuery({
3582
4444
  queryKey: ["dash-user", userId],
3583
4445
  queryFn: async () => {
3584
- const result = await client.dash.user({}, withContext({ userId }));
4446
+ const result = await client.dash.user(
4447
+ {},
4448
+ withContext({ userId }, getAuthHeaders())
4449
+ );
3585
4450
  if (result.error)
3586
4451
  throw new Error(result.error.message ?? "Failed to load user");
3587
4452
  return result.data;
@@ -3592,12 +4457,33 @@ function UserDetailDrawer({
3592
4457
  queryFn: async () => {
3593
4458
  const result = await client.dash.userOrganizations(
3594
4459
  {},
3595
- withContext({ userId })
4460
+ withContext({ userId }, getAuthHeaders())
3596
4461
  );
3597
4462
  if (result.error) throw new Error(result.error.message ?? "Failed");
3598
4463
  return result.data?.organizations ?? [];
3599
4464
  }
3600
4465
  });
4466
+ const sessionsQuery = reactQuery.useQuery({
4467
+ queryKey: ["dash-user-sessions", userId],
4468
+ queryFn: async () => {
4469
+ const { data } = await get(
4470
+ `/better-auth-dashboard/db?uid=plugin::better-auth.session&filters[userId][$eq]=${userId}&sort[0]=createdAt:desc&pagination[pageSize]=50`
4471
+ );
4472
+ return data.results ?? [];
4473
+ }
4474
+ });
4475
+ const strapiUserQuery = reactQuery.useQuery({
4476
+ queryKey: ["dash-strapi-user", userId],
4477
+ enabled: !!schemaQuery.data,
4478
+ queryFn: async () => {
4479
+ const relationFields = Object.entries(schemaQuery.data).filter(([, attr]) => attr.type === "relation").map(([fieldName]) => fieldName);
4480
+ const populateParam = relationFields.length > 0 ? `&populate=${encodeURIComponent(relationFields.join(","))}` : "";
4481
+ const { data } = await get(
4482
+ `/better-auth-dashboard/db?uid=plugin::better-auth.user&filters[id][$eq]=${userId}&pagination[pageSize]=1${populateParam}`
4483
+ );
4484
+ return data.results?.[0] ?? null;
4485
+ }
4486
+ });
3601
4487
  const [activeTab, setActiveTab] = react.useState("profile");
3602
4488
  const [editName, setEditName] = react.useState(void 0);
3603
4489
  const [editEmail, setEditEmail] = react.useState(void 0);
@@ -3608,6 +4494,8 @@ function UserDetailDrawer({
3608
4494
  const [banReason, setBanReason] = react.useState("");
3609
4495
  const [banExpiresDays, setBanExpiresDays] = react.useState("");
3610
4496
  const [confirmRevokeAll, setConfirmRevokeAll] = react.useState(false);
4497
+ const [confirmRevokeSessionId, setConfirmRevokeSessionId] = react.useState(null);
4498
+ const [confirmBan, setConfirmBan] = react.useState(false);
3611
4499
  const [confirmUnban, setConfirmUnban] = react.useState(false);
3612
4500
  const [confirmUnlinkAccountId, setConfirmUnlinkAccountId] = react.useState(null);
3613
4501
  const [confirmDisable2FA, setConfirmDisable2FA] = react.useState(false);
@@ -3626,26 +4514,33 @@ function UserDetailDrawer({
3626
4514
  setEditExtra((prev) => ({ ...prev, [name]: value }));
3627
4515
  };
3628
4516
  const extraData = {
3629
- ...user,
4517
+ ...strapiUserQuery.data ?? {},
3630
4518
  ...editExtra
3631
4519
  };
3632
4520
  const updateMutation = reactQuery.useMutation({
3633
4521
  mutationFn: async () => {
3634
- const body = { ...editExtra };
4522
+ const body = {};
3635
4523
  if (editName !== void 0) body.name = editName;
3636
4524
  if (editEmail !== void 0) body.email = editEmail;
3637
4525
  if (editEmailVerified !== void 0)
3638
4526
  body.emailVerified = editEmailVerified;
3639
4527
  if (editImage !== void 0) body.image = editImage;
3640
- const result = await client.dash.updateUser(
3641
- body,
3642
- withContext({ userId })
4528
+ const documentId = strapiUserQuery.data?.documentId;
4529
+ if (!documentId) throw new Error("Could not resolve documentId for user");
4530
+ const updateUser = await client.dash.updateUser(
4531
+ { userId, ...body },
4532
+ withContext({ userId }, getAuthHeaders())
3643
4533
  );
3644
- if (result.error)
3645
- throw new Error(result.error.message ?? "Update failed");
3646
- return result.data;
4534
+ if (!updateUser.data) throw new Error("Update failed");
4535
+ if (Object.keys(editExtra).length > 0) {
4536
+ await put(
4537
+ `/better-auth-dashboard/db/${documentId}?uid=plugin::better-auth.user`,
4538
+ editExtra
4539
+ );
4540
+ }
3647
4541
  },
3648
- onSuccess: () => {
4542
+ onSuccess: async () => {
4543
+ await qc.invalidateQueries({ queryKey: ["dash-strapi-user", userId] });
3649
4544
  qc.invalidateQueries({ queryKey: ["dash-user", userId] });
3650
4545
  qc.invalidateQueries({ queryKey: ["dash-users"] });
3651
4546
  setEditName(void 0);
@@ -3666,11 +4561,32 @@ function UserDetailDrawer({
3666
4561
  });
3667
4562
  }
3668
4563
  });
4564
+ const revokeSessionMutation = reactQuery.useMutation({
4565
+ mutationFn: async (sessionId) => {
4566
+ const result = await client.dash.sessions.revoke(
4567
+ {},
4568
+ withContext({ sessionId, userId }, getAuthHeaders())
4569
+ );
4570
+ if (result.error)
4571
+ throw new Error(result.error.message ?? "Revoke failed");
4572
+ },
4573
+ onSuccess: () => {
4574
+ setConfirmRevokeSessionId(null);
4575
+ qc.invalidateQueries({ queryKey: ["dash-user-sessions", userId] });
4576
+ toggleNotification({ type: "success", message: "Session revoked" });
4577
+ },
4578
+ onError: (err) => {
4579
+ toggleNotification({
4580
+ type: "danger",
4581
+ message: err.message ?? "Failed to revoke session"
4582
+ });
4583
+ }
4584
+ });
3669
4585
  const passwordMutation = reactQuery.useMutation({
3670
4586
  mutationFn: async () => {
3671
4587
  const result = await client.dash.setPassword(
3672
4588
  { password: newPassword },
3673
- withContext({ userId })
4589
+ withContext({ userId }, getAuthHeaders())
3674
4590
  );
3675
4591
  if (result.error)
3676
4592
  throw new Error(result.error.message ?? "Password update failed");
@@ -3698,11 +4614,15 @@ function UserDetailDrawer({
3698
4614
  const days = parseInt(banExpiresDays, 10);
3699
4615
  if (!Number.isNaN(days)) body.banExpires = Date.now() + days * 864e5;
3700
4616
  }
3701
- const result = await client.dash.banUser(body, withContext({ userId }));
4617
+ const result = await client.dash.banUser(
4618
+ body,
4619
+ withContext({ userId }, getAuthHeaders())
4620
+ );
3702
4621
  if (result.error) throw new Error(result.error.message ?? "Ban failed");
3703
4622
  return result.data;
3704
4623
  },
3705
4624
  onSuccess: () => {
4625
+ setConfirmBan(false);
3706
4626
  qc.invalidateQueries({ queryKey: ["dash-user", userId] });
3707
4627
  qc.invalidateQueries({ queryKey: ["dash-users"] });
3708
4628
  setBanReason("");
@@ -3718,7 +4638,10 @@ function UserDetailDrawer({
3718
4638
  });
3719
4639
  const unbanMutation = reactQuery.useMutation({
3720
4640
  mutationFn: async () => {
3721
- const result = await client.dash.unbanUser({}, withContext({ userId }));
4641
+ const result = await client.dash.unbanUser(
4642
+ {},
4643
+ withContext({ userId }, getAuthHeaders())
4644
+ );
3722
4645
  if (result.error) throw new Error(result.error.message ?? "Unban failed");
3723
4646
  return result.data;
3724
4647
  },
@@ -3739,7 +4662,7 @@ function UserDetailDrawer({
3739
4662
  mutationFn: async (providerId) => {
3740
4663
  const result = await client.dash.unlinkAccount(
3741
4664
  { providerId },
3742
- withContext({ userId })
4665
+ withContext({ userId }, getAuthHeaders())
3743
4666
  );
3744
4667
  if (result.error)
3745
4668
  throw new Error(result.error.message ?? "Failed to unlink account");
@@ -3760,7 +4683,7 @@ function UserDetailDrawer({
3760
4683
  mutationFn: async () => {
3761
4684
  const result = await client.dash.sessions.revokeAll(
3762
4685
  { userId },
3763
- withContext({})
4686
+ withContext({}, getAuthHeaders())
3764
4687
  );
3765
4688
  if (result.error)
3766
4689
  throw new Error(result.error.message ?? "Revoke failed");
@@ -3782,7 +4705,7 @@ function UserDetailDrawer({
3782
4705
  const callbackUrl = new URL(window.location.href, window.location.origin);
3783
4706
  const result = await client.dash.sendVerificationEmail(
3784
4707
  { callbackUrl: callbackUrl.toString() },
3785
- withContext({ userId })
4708
+ withContext({ userId }, getAuthHeaders())
3786
4709
  );
3787
4710
  if (result.error) throw new Error(result.error.message ?? "Failed");
3788
4711
  return result.data;
@@ -3805,7 +4728,7 @@ function UserDetailDrawer({
3805
4728
  const callbackUrl = new URL(window.location.href, window.location.origin);
3806
4729
  const result = await client.dash.sendResetPasswordEmail(
3807
4730
  { callbackUrl: callbackUrl.toString() },
3808
- withContext({ userId })
4731
+ withContext({ userId }, getAuthHeaders())
3809
4732
  );
3810
4733
  if (result.error) throw new Error(result.error.message ?? "Failed");
3811
4734
  return result.data;
@@ -3827,7 +4750,7 @@ function UserDetailDrawer({
3827
4750
  mutationFn: async () => {
3828
4751
  const result = await client.dash.enableTwoFactor(
3829
4752
  {},
3830
- withContext({ userId })
4753
+ withContext({ userId }, getAuthHeaders())
3831
4754
  );
3832
4755
  if (result.error)
3833
4756
  throw new Error(result.error.message ?? "Failed to enable 2FA");
@@ -3852,7 +4775,7 @@ function UserDetailDrawer({
3852
4775
  mutationFn: async () => {
3853
4776
  const result = await client.dash.disableTwoFactor(
3854
4777
  {},
3855
- withContext({ userId })
4778
+ withContext({ userId }, getAuthHeaders())
3856
4779
  );
3857
4780
  if (result.error)
3858
4781
  throw new Error(result.error.message ?? "Failed to disable 2FA");
@@ -3880,7 +4803,7 @@ function UserDetailDrawer({
3880
4803
  mutationFn: async () => {
3881
4804
  const result = await client.dash.viewTwoFactorTotpUri(
3882
4805
  {},
3883
- withContext({ userId })
4806
+ withContext({ userId }, getAuthHeaders())
3884
4807
  );
3885
4808
  if (result.error)
3886
4809
  throw new Error(result.error.message ?? "Failed to load TOTP URI");
@@ -3903,7 +4826,7 @@ function UserDetailDrawer({
3903
4826
  mutationFn: async () => {
3904
4827
  const result = await client.dash.viewBackupCodes(
3905
4828
  {},
3906
- withContext({ userId })
4829
+ withContext({ userId }, getAuthHeaders())
3907
4830
  );
3908
4831
  if (result.error)
3909
4832
  throw new Error(result.error.message ?? "Failed to load backup codes");
@@ -3926,7 +4849,7 @@ function UserDetailDrawer({
3926
4849
  mutationFn: async () => {
3927
4850
  const result = await client.dash.generateBackupCodes(
3928
4851
  {},
3929
- withContext({ userId })
4852
+ withContext({ userId }, getAuthHeaders())
3930
4853
  );
3931
4854
  if (result.error)
3932
4855
  throw new Error(
@@ -4122,7 +5045,15 @@ function UserDetailDrawer({
4122
5045
  }
4123
5046
  )
4124
5047
  ] })
4125
- ] })
5048
+ ] }),
5049
+ /* @__PURE__ */ jsxRuntime.jsx(
5050
+ EditViewSidePanels,
5051
+ {
5052
+ model: "plugin::better-auth.user",
5053
+ documentId: strapiUserQuery.data?.documentId,
5054
+ document: strapiUserQuery.data ?? void 0
5055
+ }
5056
+ )
4126
5057
  ] })
4127
5058
  ] }) }),
4128
5059
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "security", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 5, paddingTop: 6, children: [
@@ -4318,7 +5249,8 @@ function UserDetailDrawer({
4318
5249
  ] })
4319
5250
  ] })
4320
5251
  ] }) }),
4321
- banEnabled && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "ban", children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { direction: "column", gap: 4, paddingTop: 6, children: user?.banned ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5252
+ banEnabled && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "ban", children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { direction: "column", gap: 5, paddingTop: 6, children: user?.banned ? /* @__PURE__ */ jsxRuntime.jsxs(FormSection, { children: [
5253
+ /* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { children: "Ban status" }),
4322
5254
  /* @__PURE__ */ jsxRuntime.jsxs(WarnCard, { children: [
4323
5255
  /* @__PURE__ */ jsxRuntime.jsx(
4324
5256
  designSystem.Typography,
@@ -4333,7 +5265,7 @@ function UserDetailDrawer({
4333
5265
  "Reason: ",
4334
5266
  user.banReason
4335
5267
  ] }),
4336
- user.banExpires && /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", textColor: "danger600", children: [
5268
+ user.banExpires ? /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", textColor: "danger600", children: [
4337
5269
  "Expires:",
4338
5270
  " ",
4339
5271
  new Date(user.banExpires).toLocaleDateString(
@@ -4345,8 +5277,7 @@ function UserDetailDrawer({
4345
5277
  day: "numeric"
4346
5278
  }
4347
5279
  )
4348
- ] }),
4349
- !user.banExpires && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "danger600", children: "Duration: Permanent" })
5280
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "danger600", children: "Duration: Permanent" })
4350
5281
  ] }),
4351
5282
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(
4352
5283
  designSystem.Button,
@@ -4356,27 +5287,28 @@ function UserDetailDrawer({
4356
5287
  children: "Lift ban"
4357
5288
  }
4358
5289
  ) })
4359
- ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5290
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(FormSection, { children: [
5291
+ /* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { children: "Apply ban" }),
5292
+ /* @__PURE__ */ jsxRuntime.jsxs(
5293
+ designSystem.Field.Root,
5294
+ {
5295
+ hint: "Optional — will be shown to the user",
5296
+ style: { width: "100%" },
5297
+ children: [
5298
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Reason" }),
5299
+ /* @__PURE__ */ jsxRuntime.jsx(
5300
+ designSystem.TextInput,
5301
+ {
5302
+ value: banReason,
5303
+ onChange: (e) => setBanReason(e.target.value),
5304
+ placeholder: "e.g. Violated terms of service"
5305
+ }
5306
+ ),
5307
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {})
5308
+ ]
5309
+ }
5310
+ ),
4360
5311
  /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Grid.Root, { gap: 4, children: [
4361
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 12, children: /* @__PURE__ */ jsxRuntime.jsxs(
4362
- designSystem.Field.Root,
4363
- {
4364
- hint: "Optional — will be shown to the user",
4365
- style: { width: "100%" },
4366
- children: [
4367
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Reason" }),
4368
- /* @__PURE__ */ jsxRuntime.jsx(
4369
- designSystem.TextInput,
4370
- {
4371
- value: banReason,
4372
- onChange: (e) => setBanReason(e.target.value),
4373
- placeholder: "e.g. Violated terms of service"
4374
- }
4375
- ),
4376
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {})
4377
- ]
4378
- }
4379
- ) }),
4380
5312
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 6, children: /* @__PURE__ */ jsxRuntime.jsxs(
4381
5313
  designSystem.Field.Root,
4382
5314
  {
@@ -4397,24 +5329,13 @@ function UserDetailDrawer({
4397
5329
  ]
4398
5330
  }
4399
5331
  ) }),
4400
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 6, children: banExpiryPreview ? /* @__PURE__ */ jsxRuntime.jsx(
4401
- designSystem.Flex,
4402
- {
4403
- direction: "column",
4404
- justifyContent: "flex-end",
4405
- style: { height: "100%", paddingBottom: 4 },
4406
- children: /* @__PURE__ */ jsxRuntime.jsxs(PreviewPill, { children: [
4407
- "⏱ Expires ",
4408
- banExpiryPreview
4409
- ] })
4410
- }
4411
- ) : /* @__PURE__ */ jsxRuntime.jsx(
5332
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 6, children: /* @__PURE__ */ jsxRuntime.jsx(
4412
5333
  designSystem.Flex,
4413
5334
  {
4414
5335
  direction: "column",
4415
5336
  justifyContent: "flex-end",
4416
5337
  style: { height: "100%", paddingBottom: 4 },
4417
- children: /* @__PURE__ */ jsxRuntime.jsx(PreviewPill, { children: "♾ Permanent ban" })
5338
+ children: /* @__PURE__ */ jsxRuntime.jsx(PreviewPill, { children: banExpiryPreview ? `⏱ Expires ${banExpiryPreview}` : "♾ Permanent ban" })
4418
5339
  }
4419
5340
  ) })
4420
5341
  ] }),
@@ -4422,31 +5343,68 @@ function UserDetailDrawer({
4422
5343
  designSystem.Button,
4423
5344
  {
4424
5345
  variant: "danger",
4425
- loading: banMutation.isLoading,
4426
- onClick: () => banMutation.mutate(),
5346
+ onClick: () => setConfirmBan(true),
4427
5347
  children: "Ban user"
4428
5348
  }
4429
5349
  ) })
4430
5350
  ] }) }) }),
4431
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "sessions", children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { direction: "column", gap: 5, paddingTop: 6, children: /* @__PURE__ */ jsxRuntime.jsxs(FormSection, { children: [
4432
- /* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { children: "Session management" }),
4433
- /* @__PURE__ */ jsxRuntime.jsxs(AccountRow, { children: [
4434
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 1, alignItems: "flex-start", children: [
4435
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", fontWeight: "semiBold", children: "Revoke all sessions" }),
4436
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: "Signs the user out immediately on all devices." })
4437
- ] }),
4438
- /* @__PURE__ */ jsxRuntime.jsx(
4439
- designSystem.Button,
4440
- {
4441
- variant: "danger-light",
4442
- size: "S",
4443
- onClick: () => setConfirmRevokeAll(true),
4444
- style: { flexShrink: 0 },
4445
- children: "Revoke all"
4446
- }
4447
- )
5351
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "sessions", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 5, paddingTop: 6, children: [
5352
+ /* @__PURE__ */ jsxRuntime.jsxs(FormSection, { children: [
5353
+ /* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { children: "Session management" }),
5354
+ /* @__PURE__ */ jsxRuntime.jsxs(AccountRow, { children: [
5355
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 1, alignItems: "flex-start", children: [
5356
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", fontWeight: "semiBold", children: "Revoke all sessions" }),
5357
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: "Signs the user out immediately on all devices." })
5358
+ ] }),
5359
+ /* @__PURE__ */ jsxRuntime.jsx(
5360
+ designSystem.Button,
5361
+ {
5362
+ variant: "danger-light",
5363
+ size: "S",
5364
+ onClick: () => setConfirmRevokeAll(true),
5365
+ style: { flexShrink: 0 },
5366
+ children: "Revoke all"
5367
+ }
5368
+ )
5369
+ ] })
5370
+ ] }),
5371
+ /* @__PURE__ */ jsxRuntime.jsxs(FormSection, { children: [
5372
+ /* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { children: "Active sessions" }),
5373
+ sessionsQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading sessions…" }) }) : (sessionsQuery.data ?? []).length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: "No active sessions." }) : /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { direction: "column", gap: 2, alignItems: "stretch", children: (sessionsQuery.data ?? []).map((session) => /* @__PURE__ */ jsxRuntime.jsxs(SessionCard, { children: [
5374
+ /* @__PURE__ */ jsxRuntime.jsxs(SessionMeta, { children: [
5375
+ /* @__PURE__ */ jsxRuntime.jsxs(
5376
+ designSystem.Flex,
5377
+ {
5378
+ gap: 2,
5379
+ alignItems: "center",
5380
+ style: { flexWrap: "wrap" },
5381
+ children: [
5382
+ session.ipAddress && /* @__PURE__ */ jsxRuntime.jsx(IpChip, { children: session.ipAddress }),
5383
+ /* @__PURE__ */ jsxRuntime.jsxs(TimestampText, { children: [
5384
+ "Created",
5385
+ " ",
5386
+ new Date(session.createdAt).toLocaleString(),
5387
+ " · Expires",
5388
+ " ",
5389
+ new Date(session.expiresAt).toLocaleString()
5390
+ ] })
5391
+ ]
5392
+ }
5393
+ ),
5394
+ session.userAgent && /* @__PURE__ */ jsxRuntime.jsx(AgentText, { children: session.userAgent })
5395
+ ] }),
5396
+ /* @__PURE__ */ jsxRuntime.jsx(
5397
+ designSystem.IconButton,
5398
+ {
5399
+ label: "Revoke session",
5400
+ onClick: () => setConfirmRevokeSessionId(String(session.id)),
5401
+ style: { flexShrink: 0 },
5402
+ children: /* @__PURE__ */ jsxRuntime.jsx(icons.Trash, {})
5403
+ }
5404
+ )
5405
+ ] }, session.documentId)) })
4448
5406
  ] })
4449
- ] }) }) }),
5407
+ ] }) }),
4450
5408
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "organizations", children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { direction: "column", gap: 5, paddingTop: 6, children: /* @__PURE__ */ jsxRuntime.jsxs(FormSection, { children: [
4451
5409
  /* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { children: "Memberships" }),
4452
5410
  orgsQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: "neutral500", children: "Loading…" }) : (orgsQuery.data ?? []).length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: "Not a member of any organizations." }) : /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { direction: "column", gap: 2, children: (orgsQuery.data ?? []).map(
@@ -4483,6 +5441,29 @@ function UserDetailDrawer({
4483
5441
  onCancel: () => setConfirmRevokeAll(false)
4484
5442
  }
4485
5443
  ),
5444
+ confirmRevokeSessionId && /* @__PURE__ */ jsxRuntime.jsx(
5445
+ ConfirmDialog,
5446
+ {
5447
+ title: "Revoke session",
5448
+ message: "Are you sure you want to revoke this session? The user will be signed out on this device.",
5449
+ confirmLabel: "Revoke",
5450
+ loading: revokeSessionMutation.isLoading,
5451
+ onConfirm: () => revokeSessionMutation.mutate(confirmRevokeSessionId),
5452
+ onCancel: () => setConfirmRevokeSessionId(null)
5453
+ }
5454
+ ),
5455
+ confirmBan && /* @__PURE__ */ jsxRuntime.jsx(
5456
+ ConfirmDialog,
5457
+ {
5458
+ title: "Ban user",
5459
+ message: banReason ? `Ban this user for the following reason: "${banReason}"? They will be prevented from signing in.` : "Are you sure you want to ban this user? They will be prevented from signing in.",
5460
+ confirmLabel: "Ban user",
5461
+ variant: "danger",
5462
+ loading: banMutation.isLoading,
5463
+ onConfirm: () => banMutation.mutate(),
5464
+ onCancel: () => setConfirmBan(false)
5465
+ }
5466
+ ),
4486
5467
  confirmUnban && /* @__PURE__ */ jsxRuntime.jsx(
4487
5468
  ConfirmDialog,
4488
5469
  {
@@ -4712,7 +5693,9 @@ const Toolbar = styled__default.default.div`
4712
5693
  gap: 12px;
4713
5694
  `;
4714
5695
  function UsersPage({ config }) {
5696
+ const rbac = admin.useRBAC(index.PERMISSIONS.user);
4715
5697
  const qc = reactQuery.useQueryClient();
5698
+ const { toggleNotification } = admin.useNotification();
4716
5699
  const banEnabled = hasPlugin(config, "admin");
4717
5700
  const emailVerificationEnabled = config.emailVerification.sendVerificationEmailEnabled;
4718
5701
  const twoFactorEnabled = hasPlugin(config, "two-factor");
@@ -4736,7 +5719,10 @@ function UsersPage({ config }) {
4736
5719
  const pageCount = Math.ceil(total / PAGE_SIZE);
4737
5720
  const deleteMutation = reactQuery.useMutation({
4738
5721
  mutationFn: async (userId) => {
4739
- const result = await client.dash.deleteUser({}, withContext({ userId }));
5722
+ const result = await client.dash.deleteUser(
5723
+ {},
5724
+ withContext({ userId }, getAuthHeaders())
5725
+ );
4740
5726
  if (result.error)
4741
5727
  throw new Error(result.error.message ?? "Delete failed");
4742
5728
  },
@@ -4744,38 +5730,65 @@ function UsersPage({ config }) {
4744
5730
  setConfirmDelete(null);
4745
5731
  qc.invalidateQueries({ queryKey: ["dash-users"] });
4746
5732
  qc.invalidateQueries({ queryKey: ["dash-user-stats"] });
5733
+ toggleNotification({ type: "success", message: "User deleted" });
5734
+ },
5735
+ onError: (err) => {
5736
+ toggleNotification({
5737
+ type: "danger",
5738
+ message: err.message ?? "Failed to delete user"
5739
+ });
4747
5740
  }
4748
5741
  });
4749
5742
  const deleteManyMutation = reactQuery.useMutation({
4750
5743
  mutationFn: async (userIds) => {
4751
5744
  const result = await client.dash.deleteManyUsers(
4752
5745
  {},
4753
- withContext({ userIds })
5746
+ withContext({ userIds }, getAuthHeaders())
4754
5747
  );
4755
5748
  if (result.error)
4756
5749
  throw new Error(result.error.message ?? "Delete failed");
4757
5750
  return result.data;
4758
5751
  },
4759
- onSuccess: () => {
5752
+ onSuccess: (_data, userIds) => {
4760
5753
  setConfirmDeleteMany(false);
4761
5754
  setSelected(/* @__PURE__ */ new Set());
4762
5755
  qc.invalidateQueries({ queryKey: ["dash-users"] });
4763
5756
  qc.invalidateQueries({ queryKey: ["dash-user-stats"] });
5757
+ toggleNotification({
5758
+ type: "success",
5759
+ message: `${userIds.length} user${userIds.length !== 1 ? "s" : ""} deleted`
5760
+ });
5761
+ },
5762
+ onError: (err) => {
5763
+ toggleNotification({
5764
+ type: "danger",
5765
+ message: err.message ?? "Failed to delete users"
5766
+ });
4764
5767
  }
4765
5768
  });
4766
5769
  const banManyMutation = reactQuery.useMutation({
4767
5770
  mutationFn: async (userIds) => {
4768
5771
  const result = await client.dash.banManyUsers(
4769
5772
  {},
4770
- withContext({ userIds })
5773
+ withContext({ userIds }, getAuthHeaders())
4771
5774
  );
4772
5775
  if (result.error) throw new Error(result.error.message ?? "Ban failed");
4773
5776
  return result.data;
4774
5777
  },
4775
- onSuccess: () => {
5778
+ onSuccess: (_data, userIds) => {
4776
5779
  setConfirmBanMany(false);
4777
5780
  setSelected(/* @__PURE__ */ new Set());
4778
5781
  qc.invalidateQueries({ queryKey: ["dash-users"] });
5782
+ toggleNotification({
5783
+ type: "success",
5784
+ message: `${userIds.length} user${userIds.length !== 1 ? "s" : ""} banned`
5785
+ });
5786
+ },
5787
+ onError: (err) => {
5788
+ toggleNotification({
5789
+ type: "danger",
5790
+ message: err.message ?? "Failed to ban users"
5791
+ });
4779
5792
  }
4780
5793
  });
4781
5794
  const toggleSelect = (id) => {
@@ -4800,334 +5813,244 @@ function UsersPage({ config }) {
4800
5813
  setSearch(searchInput);
4801
5814
  setPage(1);
4802
5815
  };
4803
- return /* @__PURE__ */ jsxRuntime.jsxs(Wrap, { "data-testid": "users-page", children: [
4804
- /* @__PURE__ */ jsxRuntime.jsxs(PageHeader, { children: [
4805
- /* @__PURE__ */ jsxRuntime.jsxs(TitleBlock, { children: [
4806
- /* @__PURE__ */ jsxRuntime.jsx(PageTitle, { children: "Users" }),
4807
- /* @__PURE__ */ jsxRuntime.jsxs(PageSubtitle, { children: [
4808
- total.toLocaleString(),
4809
- " total",
4810
- data?.onlineUsers ? ` · ${data.onlineUsers} online` : ""
4811
- ] })
4812
- ] }),
4813
- /* @__PURE__ */ jsxRuntime.jsx(
4814
- designSystem.Button,
4815
- {
4816
- startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Plus, {}),
4817
- onClick: () => setShowCreate(true),
4818
- "data-testid": "create-user-btn",
4819
- children: "Create user"
4820
- }
4821
- )
4822
- ] }),
4823
- /* @__PURE__ */ jsxRuntime.jsxs(Toolbar, { children: [
4824
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.SearchForm, { onSubmit: handleSearch, children: /* @__PURE__ */ jsxRuntime.jsx(
4825
- designSystem.Searchbar,
4826
- {
4827
- clearLabel: "Clear",
4828
- name: "search",
4829
- placeholder: "Search by email…",
4830
- value: searchInput,
4831
- onChange: (e) => setSearchInput(e.target.value),
4832
- onClear: () => {
4833
- setSearchInput("");
4834
- setSearch("");
4835
- setPage(1);
4836
- },
4837
- "data-testid": "user-search",
4838
- children: "Search users"
4839
- }
4840
- ) }),
4841
- someSelected && /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, children: [
4842
- /* @__PURE__ */ jsxRuntime.jsxs(
4843
- designSystem.Button,
4844
- {
4845
- variant: "danger-light",
4846
- size: "S",
4847
- onClick: () => setConfirmDeleteMany(true),
4848
- "data-testid": "delete-selected-btn",
4849
- children: [
4850
- "Delete ",
4851
- selected.size,
4852
- " selected"
4853
- ]
4854
- }
4855
- ),
4856
- banEnabled && /* @__PURE__ */ jsxRuntime.jsxs(
4857
- designSystem.Button,
4858
- {
4859
- variant: "secondary",
4860
- size: "S",
4861
- onClick: () => setConfirmBanMany(true),
4862
- children: [
4863
- "Ban ",
4864
- selected.size,
4865
- " selected"
4866
- ]
4867
- }
4868
- )
4869
- ] })
4870
- ] }),
4871
- isError && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { color: "#d02b20", fontSize: 12, padding: "8px 0" }, children: error?.message }),
4872
- /* @__PURE__ */ jsxRuntime.jsx(TableCard, { children: isLoading ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", padding: 8, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading users…" }) }) : /* @__PURE__ */ jsxRuntime.jsxs(Table, { children: [
4873
- /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
4874
- /* @__PURE__ */ jsxRuntime.jsx(THCheck, { children: /* @__PURE__ */ jsxRuntime.jsx(
4875
- designSystem.Checkbox,
4876
- {
4877
- checked: allSelected,
4878
- onCheckedChange: handleSelectAll,
4879
- "aria-label": "Select all"
4880
- }
4881
- ) }),
4882
- /* @__PURE__ */ jsxRuntime.jsx(TH, { children: "Name" }),
4883
- /* @__PURE__ */ jsxRuntime.jsx(TH, { children: "Email" }),
4884
- /* @__PURE__ */ jsxRuntime.jsx(TH, { children: "Status" }),
4885
- /* @__PURE__ */ jsxRuntime.jsx(TH, { children: "Created" }),
4886
- /* @__PURE__ */ jsxRuntime.jsx(THActions, {})
4887
- ] }) }),
4888
- /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: users.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsx(
4889
- TD,
4890
- {
4891
- colSpan: 6,
4892
- style: {
4893
- textAlign: "center",
4894
- padding: "40px",
4895
- color: "#8e8ea9"
4896
- },
4897
- "data-testid": "users-empty",
4898
- children: search ? `No users matching "${search}"` : "No users found"
4899
- }
4900
- ) }) : users.map((user, i) => /* @__PURE__ */ jsxRuntime.jsxs(
4901
- TR,
4902
- {
4903
- $selected: selected.has(user.id),
4904
- $i: i,
4905
- "data-testid": "user-row",
4906
- children: [
4907
- /* @__PURE__ */ jsxRuntime.jsx(TDCheck, { children: /* @__PURE__ */ jsxRuntime.jsx(
5816
+ return /* @__PURE__ */ jsxRuntime.jsx(
5817
+ admin.Page.Protect,
5818
+ {
5819
+ permissions: index.PERMISSIONS.user.filter(
5820
+ (p) => p.action === `plugin::${index.PLUGIN_ID}.user.read`
5821
+ ),
5822
+ children: /* @__PURE__ */ jsxRuntime.jsxs(Wrap, { "data-testid": "users-page", children: [
5823
+ /* @__PURE__ */ jsxRuntime.jsxs(PageHeader, { children: [
5824
+ /* @__PURE__ */ jsxRuntime.jsxs(TitleBlock, { children: [
5825
+ /* @__PURE__ */ jsxRuntime.jsx(PageTitle, { children: "Users" }),
5826
+ /* @__PURE__ */ jsxRuntime.jsxs(PageSubtitle, { children: [
5827
+ total.toLocaleString(),
5828
+ " total",
5829
+ data?.onlineUsers ? ` · ${data.onlineUsers} online` : ""
5830
+ ] })
5831
+ ] }),
5832
+ rbac.allowedActions.canCreate && /* @__PURE__ */ jsxRuntime.jsx(
5833
+ designSystem.Button,
5834
+ {
5835
+ startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Plus, {}),
5836
+ onClick: () => setShowCreate(true),
5837
+ "data-testid": "create-user-btn",
5838
+ children: "Create user"
5839
+ }
5840
+ )
5841
+ ] }),
5842
+ /* @__PURE__ */ jsxRuntime.jsxs(Toolbar, { children: [
5843
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.SearchForm, { onSubmit: handleSearch, children: /* @__PURE__ */ jsxRuntime.jsx(
5844
+ designSystem.Searchbar,
5845
+ {
5846
+ clearLabel: "Clear",
5847
+ name: "search",
5848
+ placeholder: "Search by email…",
5849
+ value: searchInput,
5850
+ onChange: (e) => setSearchInput(e.target.value),
5851
+ onClear: () => {
5852
+ setSearchInput("");
5853
+ setSearch("");
5854
+ setPage(1);
5855
+ },
5856
+ "data-testid": "user-search",
5857
+ children: "Search users"
5858
+ }
5859
+ ) }),
5860
+ someSelected && /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, children: [
5861
+ rbac.allowedActions.canDelete && /* @__PURE__ */ jsxRuntime.jsxs(
5862
+ designSystem.Button,
5863
+ {
5864
+ variant: "danger-light",
5865
+ size: "S",
5866
+ onClick: () => setConfirmDeleteMany(true),
5867
+ "data-testid": "delete-selected-btn",
5868
+ children: [
5869
+ "Delete ",
5870
+ selected.size,
5871
+ " selected"
5872
+ ]
5873
+ }
5874
+ ),
5875
+ banEnabled && rbac.allowedActions.canUpdate && /* @__PURE__ */ jsxRuntime.jsxs(
5876
+ designSystem.Button,
5877
+ {
5878
+ variant: "secondary",
5879
+ size: "S",
5880
+ onClick: () => setConfirmBanMany(true),
5881
+ children: [
5882
+ "Ban ",
5883
+ selected.size,
5884
+ " selected"
5885
+ ]
5886
+ }
5887
+ )
5888
+ ] })
5889
+ ] }),
5890
+ isError && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { color: "#d02b20", fontSize: 12, padding: "8px 0" }, children: error?.message }),
5891
+ /* @__PURE__ */ jsxRuntime.jsx(TableCard, { children: isLoading ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", padding: 8, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading users…" }) }) : /* @__PURE__ */ jsxRuntime.jsxs(Table, { children: [
5892
+ /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
5893
+ (rbac.allowedActions.canDelete || rbac.allowedActions.canUpdate) && /* @__PURE__ */ jsxRuntime.jsx(THCheck, { children: /* @__PURE__ */ jsxRuntime.jsx(
4908
5894
  designSystem.Checkbox,
4909
5895
  {
4910
- checked: selected.has(user.id),
4911
- onCheckedChange: () => toggleSelect(user.id),
4912
- "aria-label": `Select ${user.name}`
5896
+ checked: allSelected,
5897
+ onCheckedChange: handleSelectAll,
5898
+ "aria-label": "Select all"
4913
5899
  }
4914
5900
  ) }),
4915
- /* @__PURE__ */ jsxRuntime.jsx(TD, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { alignItems: "center", gap: 2, children: [
4916
- /* @__PURE__ */ jsxRuntime.jsx(
4917
- Avatar,
4918
- {
4919
- name: user.name ?? "",
4920
- src: user.image,
4921
- size: 28
4922
- }
4923
- ),
4924
- /* @__PURE__ */ jsxRuntime.jsx(UserName, { children: user.name })
4925
- ] }) }),
4926
- /* @__PURE__ */ jsxRuntime.jsx(TD, { children: /* @__PURE__ */ jsxRuntime.jsx(MonoText, { children: user.email }) }),
4927
- /* @__PURE__ */ jsxRuntime.jsx(TD, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 1, children: [
4928
- user.emailVerified ? /* @__PURE__ */ jsxRuntime.jsx(StatusChip, { $variant: "verified", children: "Verified" }) : /* @__PURE__ */ jsxRuntime.jsx(StatusChip, { $variant: "unverified", children: "Unverified" }),
4929
- user.banned && /* @__PURE__ */ jsxRuntime.jsx(StatusChip, { $variant: "banned", children: "Banned" })
4930
- ] }) }),
4931
- /* @__PURE__ */ jsxRuntime.jsx(TD, { children: /* @__PURE__ */ jsxRuntime.jsx(DateText, { children: new Date(user.createdAt).toLocaleDateString() }) }),
4932
- /* @__PURE__ */ jsxRuntime.jsx(TDActions, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 1, justifyContent: "flex-end", children: [
4933
- /* @__PURE__ */ jsxRuntime.jsx(
4934
- designSystem.IconButton,
4935
- {
4936
- label: "Edit user",
4937
- onClick: () => setDetailUserId(user.id),
4938
- "data-testid": "edit-user-btn",
4939
- children: /* @__PURE__ */ jsxRuntime.jsx(icons.Pencil, {})
4940
- }
4941
- ),
4942
- /* @__PURE__ */ jsxRuntime.jsx(
4943
- designSystem.IconButton,
4944
- {
4945
- label: "Delete user",
4946
- onClick: () => setConfirmDelete(user.id),
4947
- "data-testid": "delete-user-btn",
4948
- children: /* @__PURE__ */ jsxRuntime.jsx(icons.Trash, {})
4949
- }
4950
- )
4951
- ] }) })
4952
- ]
4953
- },
4954
- user.id
4955
- )) })
4956
- ] }) }),
4957
- pageCount > 1 && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "flex-end", children: /* @__PURE__ */ jsxRuntime.jsx(
4958
- designSystem.Pagination,
4959
- {
4960
- activePage: page,
4961
- pageCount,
4962
- onChangePage: setPage
4963
- }
4964
- ) }),
4965
- showCreate && /* @__PURE__ */ jsxRuntime.jsx(CreateUserDialog, { onClose: () => setShowCreate(false) }),
4966
- detailUserId && /* @__PURE__ */ jsxRuntime.jsx(
4967
- UserDetailDrawer,
4968
- {
4969
- userId: detailUserId,
4970
- banEnabled,
4971
- emailVerificationEnabled,
4972
- twoFactorEnabled,
4973
- onClose: () => setDetailUserId(null)
4974
- }
4975
- ),
4976
- confirmDelete && /* @__PURE__ */ jsxRuntime.jsx(
4977
- ConfirmDialog,
4978
- {
4979
- title: "Delete user",
4980
- message: "Are you sure you want to delete this user? This action cannot be undone.",
4981
- confirmLabel: "Delete",
4982
- loading: deleteMutation.isLoading,
4983
- onConfirm: () => deleteMutation.mutate(confirmDelete),
4984
- onCancel: () => setConfirmDelete(null)
4985
- }
4986
- ),
4987
- confirmDeleteMany && /* @__PURE__ */ jsxRuntime.jsx(
4988
- ConfirmDialog,
4989
- {
4990
- title: `Delete ${selected.size} user${selected.size !== 1 ? "s" : ""}`,
4991
- message: `Are you sure you want to delete ${selected.size} user${selected.size !== 1 ? "s" : ""}? This action cannot be undone.`,
4992
- confirmLabel: "Delete all",
4993
- loading: deleteManyMutation.isLoading,
4994
- onConfirm: () => deleteManyMutation.mutate([...selected]),
4995
- onCancel: () => setConfirmDeleteMany(false)
4996
- }
4997
- ),
4998
- confirmBanMany && /* @__PURE__ */ jsxRuntime.jsx(
4999
- ConfirmDialog,
5000
- {
5001
- title: `Ban ${selected.size} user${selected.size !== 1 ? "s" : ""}`,
5002
- message: `Are you sure you want to ban ${selected.size} user${selected.size !== 1 ? "s" : ""}? They will be prevented from signing in.`,
5003
- confirmLabel: "Ban all",
5004
- variant: "danger",
5005
- loading: banManyMutation.isLoading,
5006
- onConfirm: () => banManyMutation.mutate([...selected]),
5007
- onCancel: () => setConfirmBanMany(false)
5008
- }
5009
- )
5010
- ] });
5011
- }
5012
- const Accent = styled__default.default.div`
5013
- height: 3px;
5014
- background: linear-gradient(90deg, #4945ff 0%, #7b79ff 55%, #9593ff 100%);
5015
- flex-shrink: 0;
5016
- `;
5017
- const BrandIcon = styled__default.default.div`
5018
- width: 30px;
5019
- height: 30px;
5020
- border-radius: 7px;
5021
- background: linear-gradient(135deg, #4945ff 0%, #7b79ff 100%);
5022
- display: flex;
5023
- align-items: center;
5024
- justify-content: center;
5025
- color: white;
5026
- font-size: 12px;
5027
- font-weight: 900;
5028
- flex-shrink: 0;
5029
- letter-spacing: -0.06em;
5030
- user-select: none;
5031
- `;
5032
- const PathTag = styled__default.default.code`
5033
- padding: 2px 8px;
5034
- background: #f0f0ff;
5035
- border: 1px solid #d9d8ff;
5036
- border-radius: 4px;
5037
- font-size: 11px;
5038
- color: #4945ff;
5039
- font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
5040
- font-weight: 500;
5041
- `;
5042
- function App() {
5043
- const { data: config, isLoading, isError, error } = useDashConfig();
5044
- const orgEnabled = hasPlugin(config, "organization");
5045
- const orgOptionsQuery = reactQuery.useQuery({
5046
- queryKey: ["dash-org-options"],
5047
- queryFn: async () => {
5048
- const result = await client.dash.organization.options();
5049
- if (result.error) return { teamsEnabled: false };
5050
- return result.data ?? { teamsEnabled: false };
5051
- },
5052
- enabled: orgEnabled
5053
- });
5054
- const teamsEnabled = orgEnabled ? orgOptionsQuery.data?.teamsEnabled ?? false : false;
5055
- if (isLoading) {
5056
- return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", alignItems: "center", padding: 12, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading Better Auth dashboard…" }) });
5057
- }
5058
- if (isError) {
5059
- return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { padding: 6, children: /* @__PURE__ */ jsxRuntime.jsx(
5060
- designSystem.Alert,
5061
- {
5062
- closeLabel: "Close",
5063
- title: "Error loading configuration",
5064
- variant: "danger",
5065
- children: error?.message
5066
- }
5067
- ) });
5068
- }
5069
- if (!config) return null;
5070
- return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { background: "neutral100", minHeight: "100vh", "data-testid": "dashboard-root", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Tabs.Root, { defaultValue: "overview", children: [
5071
- /* @__PURE__ */ jsxRuntime.jsxs(
5072
- designSystem.Box,
5073
- {
5074
- background: "neutral0",
5075
- borderColor: "neutral150",
5076
- borderStyle: "solid",
5077
- borderWidth: "1px",
5078
- style: { overflow: "hidden" },
5079
- children: [
5080
- /* @__PURE__ */ jsxRuntime.jsx(Accent, {}),
5081
- /* @__PURE__ */ jsxRuntime.jsxs(
5082
- designSystem.Box,
5901
+ /* @__PURE__ */ jsxRuntime.jsx(TH, { children: "Name" }),
5902
+ /* @__PURE__ */ jsxRuntime.jsx(TH, { children: "Email" }),
5903
+ /* @__PURE__ */ jsxRuntime.jsx(TH, { children: "Status" }),
5904
+ /* @__PURE__ */ jsxRuntime.jsx(TH, { children: "Created" }),
5905
+ /* @__PURE__ */ jsxRuntime.jsx(THActions, {})
5906
+ ] }) }),
5907
+ /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: users.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsx(
5908
+ TD,
5083
5909
  {
5084
- paddingLeft: 6,
5085
- paddingRight: 6,
5086
- paddingTop: 4,
5087
- paddingBottom: 0,
5910
+ colSpan: 6,
5911
+ style: {
5912
+ textAlign: "center",
5913
+ padding: "40px",
5914
+ color: "#8e8ea9"
5915
+ },
5916
+ "data-testid": "users-empty",
5917
+ children: search ? `No users matching "${search}"` : "No users found"
5918
+ }
5919
+ ) }) : users.map((user, i) => /* @__PURE__ */ jsxRuntime.jsxs(
5920
+ TR,
5921
+ {
5922
+ $selected: selected.has(user.id),
5923
+ $i: i,
5924
+ "data-testid": "user-row",
5088
5925
  children: [
5089
- /* @__PURE__ */ jsxRuntime.jsxs(
5090
- designSystem.Flex,
5926
+ (rbac.allowedActions.canDelete || rbac.allowedActions.canUpdate) && /* @__PURE__ */ jsxRuntime.jsx(TDCheck, { children: /* @__PURE__ */ jsxRuntime.jsx(
5927
+ designSystem.Checkbox,
5091
5928
  {
5092
- justifyContent: "space-between",
5093
- alignItems: "center",
5094
- paddingBottom: 4,
5095
- children: [
5096
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "center", children: [
5097
- /* @__PURE__ */ jsxRuntime.jsx(BrandIcon, { children: "BA" }),
5098
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { children: [
5099
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "beta", textColor: "neutral800", children: "Better Auth" }),
5100
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { paddingTop: "2px", children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: "Authentication Dashboard" }) })
5101
- ] })
5102
- ] }),
5103
- /* @__PURE__ */ jsxRuntime.jsx(PathTag, { children: config.basePath })
5104
- ]
5929
+ checked: selected.has(user.id),
5930
+ onCheckedChange: () => toggleSelect(user.id),
5931
+ "aria-label": `Select ${user.name}`
5105
5932
  }
5106
- ),
5107
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Tabs.List, { "aria-label": "Dashboard navigation", "data-testid": "main-nav", children: [
5108
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Trigger, { value: "overview", "data-testid": "nav-overview", children: "Overview" }),
5109
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Trigger, { value: "users", "data-testid": "nav-users", children: "Users" }),
5110
- orgEnabled && /* @__PURE__ */ jsxRuntime.jsx(
5111
- designSystem.Tabs.Trigger,
5933
+ ) }),
5934
+ /* @__PURE__ */ jsxRuntime.jsx(TD, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { alignItems: "center", gap: 2, children: [
5935
+ /* @__PURE__ */ jsxRuntime.jsx(
5936
+ Avatar,
5112
5937
  {
5113
- value: "organizations",
5114
- "data-testid": "nav-organizations",
5115
- children: "Organizations"
5938
+ name: user.name ?? "",
5939
+ src: user.image,
5940
+ size: 28
5116
5941
  }
5117
5942
  ),
5118
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Trigger, { value: "sessions", "data-testid": "nav-sessions", children: "Sessions" })
5119
- ] })
5943
+ /* @__PURE__ */ jsxRuntime.jsx(UserName, { children: user.name })
5944
+ ] }) }),
5945
+ /* @__PURE__ */ jsxRuntime.jsx(TD, { children: /* @__PURE__ */ jsxRuntime.jsx(MonoText, { children: user.email }) }),
5946
+ /* @__PURE__ */ jsxRuntime.jsx(TD, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 1, children: [
5947
+ user.emailVerified ? /* @__PURE__ */ jsxRuntime.jsx(StatusChip, { $variant: "verified", children: "Verified" }) : /* @__PURE__ */ jsxRuntime.jsx(StatusChip, { $variant: "unverified", children: "Unverified" }),
5948
+ user.banned && /* @__PURE__ */ jsxRuntime.jsx(StatusChip, { $variant: "banned", children: "Banned" })
5949
+ ] }) }),
5950
+ /* @__PURE__ */ jsxRuntime.jsx(TD, { children: /* @__PURE__ */ jsxRuntime.jsx(DateText, { children: new Date(user.createdAt).toLocaleDateString() }) }),
5951
+ /* @__PURE__ */ jsxRuntime.jsx(TDActions, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 1, justifyContent: "flex-end", children: [
5952
+ rbac.allowedActions.canUpdate && /* @__PURE__ */ jsxRuntime.jsx(
5953
+ designSystem.IconButton,
5954
+ {
5955
+ label: "Edit user",
5956
+ onClick: () => setDetailUserId(user.id),
5957
+ "data-testid": "edit-user-btn",
5958
+ children: /* @__PURE__ */ jsxRuntime.jsx(icons.Pencil, {})
5959
+ }
5960
+ ),
5961
+ rbac.allowedActions.canDelete && /* @__PURE__ */ jsxRuntime.jsx(
5962
+ designSystem.IconButton,
5963
+ {
5964
+ label: "Delete user",
5965
+ onClick: () => setConfirmDelete(user.id),
5966
+ "data-testid": "delete-user-btn",
5967
+ children: /* @__PURE__ */ jsxRuntime.jsx(icons.Trash, {})
5968
+ }
5969
+ )
5970
+ ] }) })
5120
5971
  ]
5972
+ },
5973
+ user.id
5974
+ )) })
5975
+ ] }) }),
5976
+ pageCount > 1 && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "flex-end", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, children: [
5977
+ /* @__PURE__ */ jsxRuntime.jsx(
5978
+ designSystem.Button,
5979
+ {
5980
+ variant: "tertiary",
5981
+ size: "S",
5982
+ disabled: page === 1,
5983
+ onClick: () => setPage((p) => p - 1),
5984
+ children: "Previous"
5985
+ }
5986
+ ),
5987
+ /* @__PURE__ */ jsxRuntime.jsx(
5988
+ designSystem.Button,
5989
+ {
5990
+ variant: "tertiary",
5991
+ size: "S",
5992
+ disabled: page >= pageCount,
5993
+ onClick: () => setPage((p) => p + 1),
5994
+ children: "Next"
5121
5995
  }
5122
5996
  )
5123
- ]
5124
- }
5125
- ),
5126
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "overview", "data-testid": "tab-overview", children: /* @__PURE__ */ jsxRuntime.jsx(OverviewPage, {}) }),
5127
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "users", "data-testid": "tab-users", children: /* @__PURE__ */ jsxRuntime.jsx(UsersPage, { config }) }),
5128
- orgEnabled && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "organizations", "data-testid": "tab-organizations", children: /* @__PURE__ */ jsxRuntime.jsx(OrganizationsPage, { teamsEnabled }) }),
5129
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tabs.Content, { value: "sessions", "data-testid": "tab-sessions", children: /* @__PURE__ */ jsxRuntime.jsx(SessionsPage, {}) })
5130
- ] }) });
5997
+ ] }) }),
5998
+ rbac.allowedActions.canCreate && showCreate && /* @__PURE__ */ jsxRuntime.jsx(CreateUserDialog, { onClose: () => setShowCreate(false) }),
5999
+ rbac.allowedActions.canUpdate && detailUserId && /* @__PURE__ */ jsxRuntime.jsx(
6000
+ UserDetailDrawer,
6001
+ {
6002
+ userId: detailUserId,
6003
+ banEnabled,
6004
+ emailVerificationEnabled,
6005
+ twoFactorEnabled,
6006
+ onClose: () => setDetailUserId(null)
6007
+ }
6008
+ ),
6009
+ rbac.allowedActions.canDelete && confirmDelete && /* @__PURE__ */ jsxRuntime.jsx(
6010
+ ConfirmDialog,
6011
+ {
6012
+ title: "Delete user",
6013
+ message: "Are you sure you want to delete this user? This action cannot be undone.",
6014
+ confirmLabel: "Delete",
6015
+ loading: deleteMutation.isLoading,
6016
+ onConfirm: () => deleteMutation.mutate(confirmDelete),
6017
+ onCancel: () => setConfirmDelete(null)
6018
+ }
6019
+ ),
6020
+ rbac.allowedActions.canDelete && confirmDeleteMany && /* @__PURE__ */ jsxRuntime.jsx(
6021
+ ConfirmDialog,
6022
+ {
6023
+ title: `Delete ${selected.size} user${selected.size !== 1 ? "s" : ""}`,
6024
+ message: `Are you sure you want to delete ${selected.size} user${selected.size !== 1 ? "s" : ""}? This action cannot be undone.`,
6025
+ confirmLabel: "Delete all",
6026
+ loading: deleteManyMutation.isLoading,
6027
+ onConfirm: () => deleteManyMutation.mutate([...selected]),
6028
+ onCancel: () => setConfirmDeleteMany(false)
6029
+ }
6030
+ ),
6031
+ rbac.allowedActions.canUpdate && confirmBanMany && /* @__PURE__ */ jsxRuntime.jsx(
6032
+ ConfirmDialog,
6033
+ {
6034
+ title: `Ban ${selected.size} user${selected.size !== 1 ? "s" : ""}`,
6035
+ message: `Are you sure you want to ban ${selected.size} user${selected.size !== 1 ? "s" : ""}? They will be prevented from signing in.`,
6036
+ confirmLabel: "Ban all",
6037
+ variant: "danger",
6038
+ loading: banManyMutation.isLoading,
6039
+ onConfirm: () => banManyMutation.mutate([...selected]),
6040
+ onCancel: () => setConfirmBanMany(false)
6041
+ }
6042
+ )
6043
+ ] })
6044
+ }
6045
+ );
6046
+ }
6047
+ function UsersRoute() {
6048
+ const { config } = reactRouterDom.useOutletContext();
6049
+ return /* @__PURE__ */ jsxRuntime.jsx(UsersPage, { config });
6050
+ }
6051
+ function OrganizationsRoute() {
6052
+ const { teamsEnabled } = reactRouterDom.useOutletContext();
6053
+ return /* @__PURE__ */ jsxRuntime.jsx(OrganizationsPage, { teamsEnabled });
5131
6054
  }
5132
6055
  function Root() {
5133
6056
  const queryClient = react.useMemo(
@@ -5142,7 +6065,12 @@ function Root() {
5142
6065
  }),
5143
6066
  []
5144
6067
  );
5145
- return /* @__PURE__ */ jsxRuntime.jsx(reactQuery.QueryClientProvider, { client: queryClient, children: /* @__PURE__ */ jsxRuntime.jsx(App, {}) });
6068
+ return /* @__PURE__ */ jsxRuntime.jsx(reactQuery.QueryClientProvider, { client: queryClient, children: /* @__PURE__ */ jsxRuntime.jsx(reactRouterDom.Routes, { children: /* @__PURE__ */ jsxRuntime.jsxs(reactRouterDom.Route, { path: "/", element: /* @__PURE__ */ jsxRuntime.jsx(ProtectedApp, {}), children: [
6069
+ /* @__PURE__ */ jsxRuntime.jsx(reactRouterDom.Route, { index: true, element: /* @__PURE__ */ jsxRuntime.jsx(reactRouterDom.Navigate, { to: "overview", replace: true }) }),
6070
+ /* @__PURE__ */ jsxRuntime.jsx(reactRouterDom.Route, { path: "overview", element: /* @__PURE__ */ jsxRuntime.jsx(OverviewPage, {}) }),
6071
+ /* @__PURE__ */ jsxRuntime.jsx(reactRouterDom.Route, { path: "users", element: /* @__PURE__ */ jsxRuntime.jsx(UsersRoute, {}) }),
6072
+ /* @__PURE__ */ jsxRuntime.jsx(reactRouterDom.Route, { path: "organizations", element: /* @__PURE__ */ jsxRuntime.jsx(OrganizationsRoute, {}) })
6073
+ ] }) }) });
5146
6074
  }
5147
6075
  exports.Root = Root;
5148
6076
  exports.default = Root;