@odla-ai/chapter 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  BrandStyle
3
- } from "./chunk-3JG5X2LT.js";
3
+ } from "./chunk-VZPBTTO3.js";
4
4
 
5
5
  // src/ui/admin.tsx
6
6
  import { useEffect as useEffect7, useMemo, useState as useState11 } from "react";
@@ -13,7 +13,6 @@ import {
13
13
  clerkAppearanceFromTokens
14
14
  } from "@odla-ai/auth-clerk";
15
15
  import { CrmClient } from "@odla-ai/crm";
16
- import { ThemeScope } from "@odla-ai/ui/components";
17
16
 
18
17
  // src/ui/chrome.tsx
19
18
  import { useCallback, useEffect as useEffect2, useState as useState2 } from "react";
@@ -22,7 +21,14 @@ import { Tabs, TopBar, TopBarLink } from "@odla-ai/ui/components";
22
21
  // src/ui/admin-layout.tsx
23
22
  import { useEffect, useState } from "react";
24
23
  import { jsx, jsxs } from "react/jsx-runtime";
25
- var PAGE = { maxWidth: 1120, margin: "0 auto", padding: "28px 20px 64px", display: "flex", flexDirection: "column", gap: 20 };
24
+ var PAGE = {
25
+ maxWidth: "var(--ui-content-width, 1120px)",
26
+ margin: "0 auto",
27
+ padding: "var(--chapter-admin-page-padding, 28px 20px 64px)",
28
+ display: "flex",
29
+ flexDirection: "column",
30
+ gap: 20
31
+ };
26
32
  var HEAD = { display: "flex", alignItems: "baseline", justifyContent: "space-between", gap: 16, flexWrap: "wrap" };
27
33
  function AdminPage({ children }) {
28
34
  return /* @__PURE__ */ jsx("div", { style: PAGE, children });
@@ -155,7 +161,11 @@ var S = {
155
161
  fontSize: 22,
156
162
  textDecoration: "none"
157
163
  },
158
- workspaceTabs: { maxWidth: 1160, margin: "0 auto", padding: "18px 20px 0" }
164
+ workspaceTabs: {
165
+ maxWidth: "var(--ui-content-width, 1160px)",
166
+ margin: "0 auto",
167
+ padding: "var(--chapter-admin-workspace-padding, 18px 20px 0)"
168
+ }
159
169
  };
160
170
  function Gate(props) {
161
171
  const { brand, tagline, children } = props;
@@ -284,28 +294,51 @@ function AdminShell({
284
294
  getToken,
285
295
  signOut,
286
296
  email,
297
+ currentUser,
287
298
  routing = "fragment",
288
- chrome = "editorial",
289
- renderHeader
299
+ chrome = "embedded",
300
+ renderHeader,
301
+ renderAccountMenu,
302
+ renderWorkspaceNav
290
303
  }) {
304
+ const normalizeRoute = useCallback((candidate) => {
305
+ if (candidate.viewId) return candidate;
306
+ const workspace = workspaces.find((item) => item.id === candidate.workspaceId);
307
+ return workspace?.defaultViewId ? { ...candidate, viewId: workspace.defaultViewId } : candidate;
308
+ }, [workspaces]);
291
309
  const routeFromLocation = useCallback(() => {
292
- if (typeof window === "undefined") return { workspaceId: workspaces[0]?.id ?? "" };
293
- return adminRouteFromUrl(new URL(window.location.href), basePath, workspaces.map((workspace) => workspace.id));
294
- }, [workspaces, basePath]);
310
+ if (typeof window === "undefined") {
311
+ return normalizeRoute({ workspaceId: workspaces[0]?.id ?? "" });
312
+ }
313
+ return normalizeRoute(
314
+ adminRouteFromUrl(new URL(window.location.href), basePath, workspaces.map((workspace) => workspace.id))
315
+ );
316
+ }, [workspaces, basePath, normalizeRoute]);
295
317
  const [route, setRoute] = useState2(routeFromLocation);
296
318
  const href = useCallback(
297
- (target) => typeof window === "undefined" ? `#${mergeRoute(route, target).workspaceId}` : adminRouteHref(new URL(window.location.href), basePath, mergeRoute(route, target), routing),
298
- [route, basePath, routing]
319
+ (target) => {
320
+ const next = normalizeRoute(mergeRoute(route, target));
321
+ if (typeof window === "undefined") {
322
+ return `#${[
323
+ next.workspaceId,
324
+ next.viewId,
325
+ next.recordId,
326
+ next.detailTab
327
+ ].filter((part) => Boolean(part)).map(encodeURIComponent).join("/")}`;
328
+ }
329
+ return adminRouteHref(new URL(window.location.href), basePath, next, routing);
330
+ },
331
+ [route, basePath, routing, normalizeRoute]
299
332
  );
300
333
  const navigate = useCallback(
301
334
  (target) => {
302
- const next = mergeRoute(route, target);
335
+ const next = normalizeRoute(mergeRoute(route, target));
303
336
  if (typeof window !== "undefined") {
304
337
  window.history.pushState(null, "", adminRouteHref(new URL(window.location.href), basePath, next, routing));
305
338
  }
306
339
  setRoute(next);
307
340
  },
308
- [route, basePath, routing]
341
+ [route, basePath, routing, normalizeRoute]
309
342
  );
310
343
  useEffect2(() => {
311
344
  const sync = () => setRoute(routeFromLocation());
@@ -322,9 +355,22 @@ function AdminShell({
322
355
  const actual = `${window.location.pathname}${window.location.search}${window.location.hash}`;
323
356
  if (canonical !== actual) window.history.replaceState(null, "", canonical);
324
357
  }, []);
325
- const context = { client, getToken, route, navigate, href };
358
+ const context = {
359
+ client,
360
+ getToken,
361
+ ...currentUser ? { currentUser } : {},
362
+ route,
363
+ navigate,
364
+ href
365
+ };
326
366
  const active = workspaces.find((workspace) => workspace.id === route.workspaceId) ?? workspaces[0];
327
- const identity = /* @__PURE__ */ jsxs3(Fragment, { children: [
367
+ const accountProps = {
368
+ brand,
369
+ email,
370
+ ...currentUser ? { currentUser } : {},
371
+ signOut
372
+ };
373
+ const identity = renderAccountMenu?.(accountProps) ?? /* @__PURE__ */ jsxs3(Fragment, { children: [
328
374
  /* @__PURE__ */ jsx3("span", { style: { ...S.role, color: "inherit", borderColor: "currentColor", background: "transparent" }, children: "admin" }),
329
375
  email ? /* @__PURE__ */ jsx3("span", { style: { opacity: 0.72, fontSize: 12 }, children: email }) : null,
330
376
  /* @__PURE__ */ jsx3("button", { className: "btn secondary mini", onClick: signOut, children: "Sign out" })
@@ -346,7 +392,7 @@ function AdminShell({
346
392
  ] })
347
393
  ] });
348
394
  }
349
- const header = renderHeader ? renderHeader({ brand, email, signOut }) : chrome === "editorial" ? /* @__PURE__ */ jsxs3("header", { style: S.masthead, children: [
395
+ const header = renderHeader ? renderHeader(accountProps) : chrome === "standalone" || chrome === "editorial" ? /* @__PURE__ */ jsxs3("header", { style: S.masthead, children: [
350
396
  /* @__PURE__ */ jsxs3("a", { href: "/", style: S.mastheadBrand, children: [
351
397
  /* @__PURE__ */ jsx3("span", { style: S.badgeSm, children: badgeText(brand) }),
352
398
  brandDisplay(brand)
@@ -359,18 +405,22 @@ function AdminShell({
359
405
  header,
360
406
  /* @__PURE__ */ jsxs3("main", { className: "shell-main", style: S.main, children: [
361
407
  /* @__PURE__ */ jsx3(ThemeWarning, {}),
362
- /* @__PURE__ */ jsx3(
408
+ renderWorkspaceNav ? /* @__PURE__ */ jsxs3(Fragment, { children: [
409
+ /* @__PURE__ */ jsx3("div", { style: S.workspaceTabs, children: renderWorkspaceNav({ workspaces, route, navigate, href }) }),
410
+ active?.render(context)
411
+ ] }) : /* @__PURE__ */ jsx3(
363
412
  Tabs,
364
413
  {
365
414
  style: S.workspaceTabs,
366
415
  ariaLabel: "Admin workspaces",
367
416
  value: route.workspaceId,
368
- onValueChange: (workspaceId) => setRoute({ workspaceId }),
417
+ mount: "active",
418
+ onValueChange: (workspaceId) => setRoute(normalizeRoute({ workspaceId })),
369
419
  items: workspaces.map((workspace) => ({
370
420
  value: workspace.id,
371
421
  label: workspace.label,
372
422
  href: href(workspace.id),
373
- panel: workspace.render(context)
423
+ panel: workspace.id === route.workspaceId ? workspace.render(context) : null
374
424
  }))
375
425
  }
376
426
  )
@@ -685,11 +735,8 @@ function emailSection(options = {}) {
685
735
  return { id, label, render: (ctx) => /* @__PURE__ */ jsx7(EmailBody, { ctx }) };
686
736
  }
687
737
 
688
- // src/ui/admin-people.tsx
689
- import { BillingCard } from "@odla-ai/crm/ui";
690
- import {
691
- CrmWorkspace
692
- } from "@odla-ai/crm/ui";
738
+ // src/ui/admin-people-workspace.tsx
739
+ import { BillingCard, CrmWorkspace } from "@odla-ai/crm/ui";
693
740
 
694
741
  // src/ui/admin-record-actions.tsx
695
742
  import { useEffect as useEffect4, useState as useState6 } from "react";
@@ -955,13 +1002,51 @@ function RecordSchedulingPanel(props) {
955
1002
  ] });
956
1003
  }
957
1004
 
958
- // src/ui/admin-people.tsx
1005
+ // src/ui/admin-people-workspace.tsx
959
1006
  import { jsx as jsx13 } from "react/jsx-runtime";
960
- var DEFAULT_ROLES = ["provisional", "member", "admin"];
1007
+ function applicationLifecycleAdapter(ctx) {
1008
+ return {
1009
+ transitionStage: async ({ record, to }) => {
1010
+ const applicationId = typeof record.fields?.applicationId === "string" ? record.fields.applicationId : null;
1011
+ if (!applicationId) {
1012
+ throw new Error("This record is not linked to an application workflow.");
1013
+ }
1014
+ const path = `/api/admin/applications/${encodeURIComponent(applicationId)}`;
1015
+ if (to === "approved") {
1016
+ await adminFetch(ctx.getToken, `${path}/approve`, { method: "POST" });
1017
+ } else if (to === "refunded") {
1018
+ await adminFetch(ctx.getToken, `${path}/refund`, { method: "POST" });
1019
+ } else {
1020
+ await adminFetch(ctx.getToken, path, {
1021
+ method: "PATCH",
1022
+ body: JSON.stringify({ status: to })
1023
+ });
1024
+ }
1025
+ }
1026
+ };
1027
+ }
961
1028
  function PeopleBody(props) {
962
- const { crm, client, type, ctx, roles, lifecycle, networkSharing, extendRecordTabs } = props;
1029
+ const {
1030
+ crm,
1031
+ client,
1032
+ type,
1033
+ ctx,
1034
+ roles,
1035
+ lifecycle,
1036
+ networkSharing,
1037
+ lifecycleAdapter,
1038
+ requireLifecycleAdapter,
1039
+ renderSummary,
1040
+ renderMaster,
1041
+ renderDetailHeader,
1042
+ renderEmptyDetail,
1043
+ hrefForRecord,
1044
+ extendRecordTabs
1045
+ } = props;
963
1046
  const def = crm.type(type);
964
1047
  const recordId = ctx.route?.recordId ?? null;
1048
+ const builtInLifecycle = lifecycle ? applicationLifecycleAdapter(ctx) : void 0;
1049
+ const resolvedLifecycle = typeof lifecycleAdapter === "function" ? lifecycleAdapter(ctx) : lifecycleAdapter ?? builtInLifecycle;
965
1050
  const workspaceClient = client;
966
1051
  return /* @__PURE__ */ jsx13(AdminPage, { children: /* @__PURE__ */ jsx13(
967
1052
  CrmWorkspace,
@@ -973,7 +1058,15 @@ function PeopleBody(props) {
973
1058
  detailTab: ctx.route?.detailTab,
974
1059
  defaultDetailTab: "profile",
975
1060
  onRecordIdChange: (id) => ctx.navigate({ recordId: id, detailTab: null }),
1061
+ hrefForRecord: (record) => hrefForRecord?.(record, ctx) ?? ctx.href({ recordId: record.id, detailTab: "profile" }),
1062
+ hrefForList: ctx.href({ recordId: null, detailTab: null }),
976
1063
  hrefForDetailTab: (detailTab) => ctx.href({ detailTab }),
1064
+ lifecycle: resolvedLifecycle,
1065
+ requireLifecycleAdapter: requireLifecycleAdapter ?? lifecycle,
1066
+ renderSummary: renderSummary ? (context) => renderSummary(context, ctx) : void 0,
1067
+ renderMaster: renderMaster ? (context) => renderMaster(context, ctx) : void 0,
1068
+ renderDetailHeader: renderDetailHeader ? (context) => renderDetailHeader(context, ctx) : void 0,
1069
+ renderEmptyDetail: renderEmptyDetail ? (context) => renderEmptyDetail(context, ctx) : void 0,
977
1070
  extendRecordTabs: (base, context) => {
978
1071
  const record = context.detail.record;
979
1072
  const applicationId = typeof record.fields?.applicationId === "string" ? String(record.fields.applicationId) : null;
@@ -1028,7 +1121,8 @@ function PeopleBody(props) {
1028
1121
  context.refreshList();
1029
1122
  }
1030
1123
  }
1031
- )
1124
+ ),
1125
+ visible: () => ctx.currentUser?.superAdmin === true
1032
1126
  });
1033
1127
  }
1034
1128
  if (networkSharing) {
@@ -1043,6 +1137,10 @@ function PeopleBody(props) {
1043
1137
  }
1044
1138
  ) });
1045
1139
  }
1140
+
1141
+ // src/ui/admin-people.tsx
1142
+ import { jsx as jsx14 } from "react/jsx-runtime";
1143
+ var DEFAULT_ROLES = ["provisional", "member", "admin"];
1046
1144
  function collectionSection(options) {
1047
1145
  const { crm, type } = options;
1048
1146
  let fallbackLabel = type;
@@ -1056,12 +1154,19 @@ function collectionSection(options) {
1056
1154
  lifecycle = type === "person",
1057
1155
  roles = DEFAULT_ROLES,
1058
1156
  networkSharing = false,
1157
+ lifecycleAdapter,
1158
+ requireLifecycleAdapter,
1159
+ renderSummary,
1160
+ renderMaster,
1161
+ renderDetailHeader,
1162
+ renderEmptyDetail,
1163
+ hrefForRecord,
1059
1164
  extendRecordTabs
1060
1165
  } = options;
1061
1166
  return {
1062
1167
  id,
1063
1168
  label,
1064
- render: (ctx) => /* @__PURE__ */ jsx13(
1169
+ render: (ctx) => /* @__PURE__ */ jsx14(
1065
1170
  PeopleBody,
1066
1171
  {
1067
1172
  crm,
@@ -1071,6 +1176,13 @@ function collectionSection(options) {
1071
1176
  roles,
1072
1177
  lifecycle,
1073
1178
  networkSharing,
1179
+ lifecycleAdapter,
1180
+ requireLifecycleAdapter,
1181
+ renderSummary,
1182
+ renderMaster,
1183
+ renderDetailHeader,
1184
+ renderEmptyDetail,
1185
+ hrefForRecord,
1074
1186
  extendRecordTabs
1075
1187
  }
1076
1188
  )
@@ -1084,6 +1196,13 @@ function peopleSection(options) {
1084
1196
  type = "person",
1085
1197
  roles,
1086
1198
  networkSharing,
1199
+ lifecycleAdapter,
1200
+ requireLifecycleAdapter,
1201
+ renderSummary,
1202
+ renderMaster,
1203
+ renderDetailHeader,
1204
+ renderEmptyDetail,
1205
+ hrefForRecord,
1087
1206
  extendRecordTabs
1088
1207
  } = options;
1089
1208
  return collectionSection({
@@ -1093,12 +1212,19 @@ function peopleSection(options) {
1093
1212
  label,
1094
1213
  ...roles ? { roles } : {},
1095
1214
  ...networkSharing != null ? { networkSharing } : {},
1215
+ ...lifecycleAdapter ? { lifecycleAdapter } : {},
1216
+ ...requireLifecycleAdapter != null ? { requireLifecycleAdapter } : {},
1217
+ ...renderSummary ? { renderSummary } : {},
1218
+ ...renderMaster ? { renderMaster } : {},
1219
+ ...renderDetailHeader ? { renderDetailHeader } : {},
1220
+ ...renderEmptyDetail ? { renderEmptyDetail } : {},
1221
+ ...hrefForRecord ? { hrefForRecord } : {},
1096
1222
  ...extendRecordTabs ? { extendRecordTabs } : {}
1097
1223
  });
1098
1224
  }
1099
1225
 
1100
1226
  // src/ui/admin-workspaces.tsx
1101
- import { jsx as jsx14 } from "react/jsx-runtime";
1227
+ import { jsx as jsx15 } from "react/jsx-runtime";
1102
1228
  var adminViewContext = (ctx, viewId, active) => {
1103
1229
  const route = {
1104
1230
  workspaceId: ctx.route.workspaceId,
@@ -1123,11 +1249,12 @@ function DashboardWorkspace({ ctx }) {
1123
1249
  const active = ctx.route.viewId === "billing" ? "billing" : "overview";
1124
1250
  const overview = dashboardSection({ id: "overview", label: "Overview" });
1125
1251
  const billing = billingSection();
1126
- return /* @__PURE__ */ jsx14(
1252
+ return /* @__PURE__ */ jsx15(
1127
1253
  Tabs2,
1128
1254
  {
1129
1255
  ariaLabel: "Dashboard views",
1130
1256
  value: active,
1257
+ mount: "active",
1131
1258
  items: [
1132
1259
  {
1133
1260
  value: "overview",
@@ -1149,11 +1276,12 @@ function SettingsWorkspace({ ctx }) {
1149
1276
  const active = ctx.route.viewId === "email" ? "email" : "calendar";
1150
1277
  const calendar = availabilitySection({ id: "calendar", label: "Calendar" });
1151
1278
  const email = emailSection();
1152
- return /* @__PURE__ */ jsx14(
1279
+ return /* @__PURE__ */ jsx15(
1153
1280
  Tabs2,
1154
1281
  {
1155
1282
  ariaLabel: "Settings views",
1156
1283
  value: active,
1284
+ mount: "active",
1157
1285
  items: [
1158
1286
  {
1159
1287
  value: "calendar",
@@ -1176,12 +1304,13 @@ function PeopleWorkspace({ chapter, ctx }) {
1176
1304
  const fallback = types[0]?.[0] ?? "person";
1177
1305
  const active = types.some(([type]) => type === ctx.route.viewId) ? ctx.route.viewId : fallback;
1178
1306
  const networkSharing = chapter.network.targets.length > 0;
1179
- return /* @__PURE__ */ jsx14(
1307
+ return /* @__PURE__ */ jsx15(
1180
1308
  Tabs2,
1181
1309
  {
1182
1310
  ariaLabel: "CRM collections",
1183
1311
  value: active,
1184
1312
  variant: "pill",
1313
+ mount: "active",
1185
1314
  items: types.map(([type, def]) => {
1186
1315
  const section = collectionSection({
1187
1316
  crm: chapter.crm,
@@ -1204,18 +1333,30 @@ function defaultAdminWorkspaces(chapter) {
1204
1333
  const people = {
1205
1334
  id: "people",
1206
1335
  label: "People",
1207
- render: (ctx) => /* @__PURE__ */ jsx14(PeopleWorkspace, { chapter, ctx })
1336
+ defaultViewId: Object.keys(chapter.crm.config.types)[0] ?? "person",
1337
+ render: (ctx) => /* @__PURE__ */ jsx15(PeopleWorkspace, { chapter, ctx })
1208
1338
  };
1209
1339
  if (chapter.mode === "hub") return [people];
1210
1340
  return [
1211
- { id: "dashboard", label: "Dashboard", render: (ctx) => /* @__PURE__ */ jsx14(DashboardWorkspace, { ctx }) },
1341
+ {
1342
+ id: "dashboard",
1343
+ label: "Dashboard",
1344
+ defaultViewId: "overview",
1345
+ render: (ctx) => /* @__PURE__ */ jsx15(DashboardWorkspace, { ctx })
1346
+ },
1212
1347
  people,
1213
- { id: "settings", label: "Settings", render: (ctx) => /* @__PURE__ */ jsx14(SettingsWorkspace, { ctx }) }
1348
+ {
1349
+ id: "settings",
1350
+ label: "Settings",
1351
+ defaultViewId: "calendar",
1352
+ render: (ctx) => /* @__PURE__ */ jsx15(SettingsWorkspace, { ctx })
1353
+ }
1214
1354
  ];
1215
1355
  }
1216
1356
 
1217
- // src/ui/admin.tsx
1218
- import { jsx as jsx15, jsxs as jsxs12 } from "react/jsx-runtime";
1357
+ // src/ui/admin-frame.tsx
1358
+ import { ThemeScope } from "@odla-ai/ui/components";
1359
+ import { jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
1219
1360
  function AdminTheme(props) {
1220
1361
  const brand = props.chapter?.brand;
1221
1362
  return /* @__PURE__ */ jsxs12(
@@ -1225,12 +1366,51 @@ function AdminTheme(props) {
1225
1366
  theme: brand?.theme,
1226
1367
  colorScheme: brand?.colorScheme ?? "light",
1227
1368
  children: [
1228
- /* @__PURE__ */ jsx15(BrandStyle, { brand, selector: "[data-chapter-admin]" }),
1369
+ /* @__PURE__ */ jsx16(BrandStyle, { brand, selector: "[data-chapter-admin]" }),
1229
1370
  props.children
1230
1371
  ]
1231
1372
  }
1232
1373
  );
1233
1374
  }
1375
+ function signInRedirect(basePath, workspaces) {
1376
+ if (typeof window === "undefined") return basePath;
1377
+ const current = new URL(window.location.href);
1378
+ const inbound = adminRouteFromUrl(
1379
+ current,
1380
+ basePath,
1381
+ workspaces.map((workspace2) => workspace2.id)
1382
+ );
1383
+ const workspace = workspaces.find((item) => item.id === inbound.workspaceId);
1384
+ const route = inbound.viewId || !workspace?.defaultViewId ? inbound : { ...inbound, viewId: workspace.defaultViewId };
1385
+ return adminRouteHref(current, basePath, route, "query");
1386
+ }
1387
+
1388
+ // src/ui/admin-auth.ts
1389
+ var apiUrl = (apiBase, path) => `${apiBase.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
1390
+ var DEFAULT_ADMIN_AUTH = {};
1391
+ async function loadChapterAdminConfig(adapter, apiBase) {
1392
+ if (adapter.loadConfig) return adapter.loadConfig({ apiBase });
1393
+ const response = await fetch(apiUrl(apiBase, adapter.configPath ?? "/api/config"));
1394
+ const body = await response.json();
1395
+ if (adapter.mapConfig) return adapter.mapConfig(body);
1396
+ return {
1397
+ publishableKey: typeof body.clerkPublishableKey === "string" ? body.clerkPublishableKey : null
1398
+ };
1399
+ }
1400
+ async function loadChapterAdminUser(adapter, apiBase, getToken) {
1401
+ if (adapter.loadCurrentUser) {
1402
+ return adapter.loadCurrentUser({ apiBase, getToken });
1403
+ }
1404
+ const token = await getToken();
1405
+ const response = await fetch(apiUrl(apiBase, adapter.currentUserPath ?? "/api/me"), {
1406
+ headers: token ? { authorization: `Bearer ${token}` } : {}
1407
+ });
1408
+ const body = await response.json().catch(() => ({}));
1409
+ return adapter.mapCurrentUser?.(body) ?? body;
1410
+ }
1411
+
1412
+ // src/ui/admin.tsx
1413
+ import { jsx as jsx17, jsxs as jsxs13 } from "react/jsx-runtime";
1234
1414
  function Authed(props) {
1235
1415
  const {
1236
1416
  workspaces,
@@ -1240,7 +1420,10 @@ function Authed(props) {
1240
1420
  apiBase,
1241
1421
  routing,
1242
1422
  chrome,
1243
- renderHeader
1423
+ auth,
1424
+ renderHeader,
1425
+ renderAccountMenu,
1426
+ renderWorkspaceNav
1244
1427
  } = props;
1245
1428
  const { getToken, signOut } = useClerkAuth();
1246
1429
  const client = useMemo(
@@ -1258,14 +1441,12 @@ function Authed(props) {
1258
1441
  let live = true;
1259
1442
  void (async () => {
1260
1443
  try {
1261
- const token = await getToken();
1262
- const response = await fetch(`${apiBase}/api/me`, {
1263
- headers: token ? { authorization: `Bearer ${token}` } : {}
1264
- });
1265
- const body = await response.json().catch(() => ({}));
1444
+ const body = await loadChapterAdminUser(auth, apiBase, getToken);
1445
+ const authorized = auth.isAuthorized ? auth.isAuthorized(body) : body.authorized === true;
1266
1446
  if (live) setState({
1267
- status: body.authorized ? "ok" : "denied",
1268
- email: body.email ?? null
1447
+ status: authorized ? "ok" : "denied",
1448
+ email: typeof body.email === "string" ? body.email : null,
1449
+ user: body
1269
1450
  });
1270
1451
  } catch {
1271
1452
  if (live) setState({ status: "denied" });
@@ -1274,19 +1455,19 @@ function Authed(props) {
1274
1455
  return () => {
1275
1456
  live = false;
1276
1457
  };
1277
- }, [getToken, apiBase]);
1278
- if (state.status === "checking") return /* @__PURE__ */ jsx15(Gate, { brand, children: /* @__PURE__ */ jsx15("p", { style: S.muted, children: "Checking access\u2026" }) });
1458
+ }, [getToken, apiBase, auth]);
1459
+ if (state.status === "checking") return /* @__PURE__ */ jsx17(Gate, { brand, children: /* @__PURE__ */ jsx17("p", { style: S.muted, children: "Checking access\u2026" }) });
1279
1460
  if (state.status === "denied") {
1280
- return /* @__PURE__ */ jsx15(Gate, { brand, children: /* @__PURE__ */ jsxs12("div", { className: "card", style: S.card, children: [
1281
- /* @__PURE__ */ jsx15("h2", { style: { marginTop: 0 }, children: "Not authorized" }),
1282
- /* @__PURE__ */ jsxs12("p", { style: S.muted, children: [
1461
+ return /* @__PURE__ */ jsx17(Gate, { brand, children: /* @__PURE__ */ jsxs13("div", { className: "card", style: S.card, children: [
1462
+ /* @__PURE__ */ jsx17("h2", { style: { marginTop: 0 }, children: "Not authorized" }),
1463
+ /* @__PURE__ */ jsxs13("p", { style: S.muted, children: [
1283
1464
  state.email ? `${state.email} isn't` : "This account isn't",
1284
1465
  " on the admin list."
1285
1466
  ] }),
1286
- /* @__PURE__ */ jsx15("button", { className: "btn secondary", onClick: () => signOut(), children: "Sign out" })
1467
+ /* @__PURE__ */ jsx17("button", { className: "btn secondary", onClick: () => signOut(), children: "Sign out" })
1287
1468
  ] }) });
1288
1469
  }
1289
- return /* @__PURE__ */ jsx15(
1470
+ return /* @__PURE__ */ jsx17(
1290
1471
  AdminShell,
1291
1472
  {
1292
1473
  workspaces,
@@ -1296,18 +1477,15 @@ function Authed(props) {
1296
1477
  getToken,
1297
1478
  signOut,
1298
1479
  email: state.email ?? null,
1480
+ currentUser: state.user,
1299
1481
  routing,
1300
1482
  chrome,
1301
- renderHeader
1483
+ renderHeader,
1484
+ renderAccountMenu,
1485
+ renderWorkspaceNav
1302
1486
  }
1303
1487
  );
1304
1488
  }
1305
- var signInRedirect = (basePath, workspaces) => {
1306
- if (typeof window === "undefined") return basePath;
1307
- const current = new URL(window.location.href);
1308
- const route = adminRouteFromUrl(current, basePath, workspaces.map((workspace) => workspace.id));
1309
- return adminRouteHref(current, basePath, route, "query");
1310
- };
1311
1489
  function ChapterAdmin(props) {
1312
1490
  const defaults = useMemo(
1313
1491
  () => props.chapter ? defaultAdminWorkspaces(props.chapter) : [],
@@ -1326,35 +1504,36 @@ function ChapterAdmin(props) {
1326
1504
  const crmBasePath = props.crmBasePath ?? "/api/crm";
1327
1505
  const apiBase = props.apiBase ?? "";
1328
1506
  const routing = props.routing ?? "fragment";
1329
- const chrome = props.chrome ?? "editorial";
1507
+ const chrome = props.chrome ?? "embedded";
1508
+ const auth = props.auth ?? DEFAULT_ADMIN_AUTH;
1330
1509
  const redirect = signInRedirect(basePath, workspaces);
1331
1510
  const [publishableKey, setPublishableKey] = useState11(void 0);
1332
1511
  useEffect7(() => {
1333
1512
  let live = true;
1334
- void fetch(`${apiBase}/api/config`).then((response) => response.json()).then((body) => live && setPublishableKey(body.clerkPublishableKey ?? null)).catch(() => live && setPublishableKey(null));
1513
+ void loadChapterAdminConfig(auth, apiBase).then((config) => live && setPublishableKey(config.publishableKey ?? null)).catch(() => live && setPublishableKey(null));
1335
1514
  return () => {
1336
1515
  live = false;
1337
1516
  };
1338
- }, [apiBase]);
1517
+ }, [apiBase, auth]);
1339
1518
  let content;
1340
1519
  if (publishableKey === void 0) {
1341
- content = /* @__PURE__ */ jsx15(Gate, { brand, children: /* @__PURE__ */ jsx15("p", { style: S.muted, children: "Loading\u2026" }) });
1520
+ content = /* @__PURE__ */ jsx17(Gate, { brand, children: /* @__PURE__ */ jsx17("p", { style: S.muted, children: "Loading\u2026" }) });
1342
1521
  } else if (!publishableKey) {
1343
- content = /* @__PURE__ */ jsx15(Gate, { brand, children: /* @__PURE__ */ jsxs12("div", { className: "card", style: S.card, children: [
1344
- /* @__PURE__ */ jsx15("h2", { style: { marginTop: 0 }, children: "Sign-in not configured" }),
1345
- /* @__PURE__ */ jsx15("p", { style: S.muted, children: "No Clerk publishable key is set for this environment yet." })
1522
+ content = /* @__PURE__ */ jsx17(Gate, { brand, children: /* @__PURE__ */ jsxs13("div", { className: "card", style: S.card, children: [
1523
+ /* @__PURE__ */ jsx17("h2", { style: { marginTop: 0 }, children: "Sign-in not configured" }),
1524
+ /* @__PURE__ */ jsx17("p", { style: S.muted, children: "No Clerk publishable key is set for this environment yet." })
1346
1525
  ] }) });
1347
1526
  } else if (workspaces.length === 0) {
1348
- content = /* @__PURE__ */ jsx15(Gate, { brand, children: /* @__PURE__ */ jsx15("p", { style: S.muted, children: "No admin workspaces are configured." }) });
1527
+ content = /* @__PURE__ */ jsx17(Gate, { brand, children: /* @__PURE__ */ jsx17("p", { style: S.muted, children: "No admin workspaces are configured." }) });
1349
1528
  } else {
1350
- content = /* @__PURE__ */ jsxs12(
1529
+ content = /* @__PURE__ */ jsxs13(
1351
1530
  ClerkGate,
1352
1531
  {
1353
1532
  publishableKey,
1354
1533
  appearance: clerkAppearanceFromTokens(),
1355
1534
  afterSignOutUrl: "/",
1356
1535
  children: [
1357
- /* @__PURE__ */ jsx15(SignedOut, { children: /* @__PURE__ */ jsx15(Gate, { brand, tagline: "Admin sign-in \u2014 invite only", children: /* @__PURE__ */ jsx15(
1536
+ /* @__PURE__ */ jsx17(SignedOut, { children: /* @__PURE__ */ jsx17(Gate, { brand, tagline: "Admin sign-in \u2014 invite only", children: /* @__PURE__ */ jsx17(
1358
1537
  SignIn,
1359
1538
  {
1360
1539
  routing: "hash",
@@ -1362,7 +1541,7 @@ function ChapterAdmin(props) {
1362
1541
  signUpForceRedirectUrl: redirect
1363
1542
  }
1364
1543
  ) }) }),
1365
- /* @__PURE__ */ jsx15(SignedIn, { children: /* @__PURE__ */ jsx15(
1544
+ /* @__PURE__ */ jsx17(SignedIn, { children: /* @__PURE__ */ jsx17(
1366
1545
  Authed,
1367
1546
  {
1368
1547
  workspaces,
@@ -1372,14 +1551,17 @@ function ChapterAdmin(props) {
1372
1551
  apiBase,
1373
1552
  routing,
1374
1553
  chrome,
1375
- renderHeader: props.renderHeader
1554
+ auth,
1555
+ renderHeader: props.renderHeader,
1556
+ renderAccountMenu: props.renderAccountMenu,
1557
+ renderWorkspaceNav: props.renderWorkspaceNav
1376
1558
  }
1377
1559
  ) })
1378
1560
  ]
1379
1561
  }
1380
1562
  );
1381
1563
  }
1382
- return /* @__PURE__ */ jsx15(AdminTheme, { chapter: props.chapter, children: content });
1564
+ return /* @__PURE__ */ jsx17(AdminTheme, { chapter: props.chapter, children: content });
1383
1565
  }
1384
1566
 
1385
1567
  // src/ui/admin-defaults.ts
@@ -1390,14 +1572,14 @@ function defaultAdminSections(chapter) {
1390
1572
  // src/ui/admin-meetings.tsx
1391
1573
  import { useState as useState12 } from "react";
1392
1574
  import { Button as Button6 } from "@odla-ai/ui/components";
1393
- import { Fragment as Fragment4, jsx as jsx16, jsxs as jsxs13 } from "react/jsx-runtime";
1575
+ import { Fragment as Fragment4, jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
1394
1576
  var when2 = (t) => new Date(t).toLocaleString([], { dateStyle: "medium", timeStyle: "short" });
1395
1577
  function MeetingsBody({ ctx }) {
1396
1578
  const { data, loading, error, refresh } = useAdminResource(ctx.getToken, "/api/admin/meetings?all=1");
1397
1579
  const [busy, setBusy] = useState12(null);
1398
1580
  const [note, setNote] = useState12(null);
1399
- if (loading) return /* @__PURE__ */ jsx16(AdminNote, { children: "Loading\u2026" });
1400
- if (error || !data) return /* @__PURE__ */ jsxs13(AdminNote, { children: [
1581
+ if (loading) return /* @__PURE__ */ jsx18(AdminNote, { children: "Loading\u2026" });
1582
+ if (error || !data) return /* @__PURE__ */ jsxs14(AdminNote, { children: [
1401
1583
  "Couldn\u2019t load meetings",
1402
1584
  error ? `: ${error}` : "",
1403
1585
  "."
@@ -1428,30 +1610,30 @@ function MeetingsBody({ ctx }) {
1428
1610
  }
1429
1611
  void run(id, () => adminFetch(ctx.getToken, `/api/admin/meetings/${id}/reschedule`, { method: "POST", body: JSON.stringify({ startAt }) }));
1430
1612
  };
1431
- return /* @__PURE__ */ jsx16(AdminPage, { children: /* @__PURE__ */ jsx16(Panel, { title: /* @__PURE__ */ jsxs13(Fragment4, { children: [
1613
+ return /* @__PURE__ */ jsx18(AdminPage, { children: /* @__PURE__ */ jsx18(Panel, { title: /* @__PURE__ */ jsxs14(Fragment4, { children: [
1432
1614
  "Agenda ",
1433
- /* @__PURE__ */ jsxs13("span", { className: "muted", style: { fontWeight: 400 }, children: [
1615
+ /* @__PURE__ */ jsxs14("span", { className: "muted", style: { fontWeight: 400 }, children: [
1434
1616
  "\xB7 ",
1435
1617
  data.timezone
1436
1618
  ] })
1437
- ] }), actions: note ? /* @__PURE__ */ jsx16("span", { className: "muted", children: note }) : void 0, children: data.meetings.length === 0 ? /* @__PURE__ */ jsx16("p", { className: "muted", style: { margin: 0 }, children: "No meetings." }) : /* @__PURE__ */ jsx16("ul", { style: { listStyle: "none", margin: 0, padding: 0, display: "flex", flexDirection: "column", gap: 12 }, children: data.meetings.map((m) => /* @__PURE__ */ jsxs13("li", { style: { display: "flex", alignItems: "center", gap: 12, flexWrap: "wrap", opacity: m.status === "cancelled" ? 0.55 : 1 }, children: [
1438
- /* @__PURE__ */ jsx16("strong", { style: { minWidth: 170 }, children: when2(m.startAt) }),
1439
- /* @__PURE__ */ jsx16("span", { style: { flex: 1 }, children: m.applicant ? `${m.applicant.name} \xB7 ${m.applicant.email}` : "(unknown)" }),
1440
- /* @__PURE__ */ jsx16("span", { className: "badge", children: m.status }),
1441
- m.drift && m.drift !== "none" ? /* @__PURE__ */ jsxs13("span", { className: "badge", children: [
1619
+ ] }), actions: note ? /* @__PURE__ */ jsx18("span", { className: "muted", children: note }) : void 0, children: data.meetings.length === 0 ? /* @__PURE__ */ jsx18("p", { className: "muted", style: { margin: 0 }, children: "No meetings." }) : /* @__PURE__ */ jsx18("ul", { style: { listStyle: "none", margin: 0, padding: 0, display: "flex", flexDirection: "column", gap: 12 }, children: data.meetings.map((m) => /* @__PURE__ */ jsxs14("li", { style: { display: "flex", alignItems: "center", gap: 12, flexWrap: "wrap", opacity: m.status === "cancelled" ? 0.55 : 1 }, children: [
1620
+ /* @__PURE__ */ jsx18("strong", { style: { minWidth: 170 }, children: when2(m.startAt) }),
1621
+ /* @__PURE__ */ jsx18("span", { style: { flex: 1 }, children: m.applicant ? `${m.applicant.name} \xB7 ${m.applicant.email}` : "(unknown)" }),
1622
+ /* @__PURE__ */ jsx18("span", { className: "badge", children: m.status }),
1623
+ m.drift && m.drift !== "none" ? /* @__PURE__ */ jsxs14("span", { className: "badge", children: [
1442
1624
  "drift: ",
1443
1625
  m.drift
1444
1626
  ] }) : null,
1445
- m.meetUrl ? /* @__PURE__ */ jsx16("a", { href: m.meetUrl, target: "_blank", rel: "noreferrer", children: "Meet" }) : null,
1446
- m.status === "scheduled" ? /* @__PURE__ */ jsxs13(Fragment4, { children: [
1447
- /* @__PURE__ */ jsx16(Button6, { variant: "secondary", mini: true, disabled: busy === m.id, onClick: () => reschedule(m.id), children: "Reschedule" }),
1448
- /* @__PURE__ */ jsx16(Button6, { variant: "danger", mini: true, disabled: busy === m.id, onClick: () => cancel(m.id), children: "Cancel" })
1627
+ m.meetUrl ? /* @__PURE__ */ jsx18("a", { href: m.meetUrl, target: "_blank", rel: "noreferrer", children: "Meet" }) : null,
1628
+ m.status === "scheduled" ? /* @__PURE__ */ jsxs14(Fragment4, { children: [
1629
+ /* @__PURE__ */ jsx18(Button6, { variant: "secondary", mini: true, disabled: busy === m.id, onClick: () => reschedule(m.id), children: "Reschedule" }),
1630
+ /* @__PURE__ */ jsx18(Button6, { variant: "danger", mini: true, disabled: busy === m.id, onClick: () => cancel(m.id), children: "Cancel" })
1449
1631
  ] }) : null
1450
1632
  ] }, m.id)) }) }) });
1451
1633
  }
1452
1634
  function meetingsSection(options = {}) {
1453
1635
  const { id = "meetings", label = "Meetings" } = options;
1454
- return { id, label, render: (ctx) => /* @__PURE__ */ jsx16(MeetingsBody, { ctx }) };
1636
+ return { id, label, render: (ctx) => /* @__PURE__ */ jsx18(MeetingsBody, { ctx }) };
1455
1637
  }
1456
1638
 
1457
1639
  export {
@@ -1471,11 +1653,14 @@ export {
1471
1653
  emailSection,
1472
1654
  RecordActions,
1473
1655
  NetworkShareActions,
1656
+ applicationLifecycleAdapter,
1474
1657
  collectionSection,
1475
1658
  peopleSection,
1476
1659
  defaultAdminWorkspaces,
1660
+ loadChapterAdminConfig,
1661
+ loadChapterAdminUser,
1477
1662
  ChapterAdmin,
1478
1663
  defaultAdminSections,
1479
1664
  meetingsSection
1480
1665
  };
1481
- //# sourceMappingURL=chunk-AOTWFOH4.js.map
1666
+ //# sourceMappingURL=chunk-IWW5VAAC.js.map