@odla-ai/chapter 0.8.0 → 0.10.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.
package/README.md CHANGED
@@ -55,11 +55,21 @@ npm i @odla-ai/chapter
55
55
  the crm namespaces + the chapter namespaces (`applications`, `groups`,
56
56
  `meetings`, `emailLog`, plus the auth tables) + a guarded group-row seed. Drop
57
57
  it in `odla.config.mjs` `integrations: [...]`.
58
- - **The UI kit (`./ui`).** Member islands `JoinIsland` (form payment
59
- booking), `MembersArea`, `Rescheduler`, `SlotPicker`, `PaymentStep`; the admin
60
- console `ChapterAdmin` + the section catalog (`peopleSection`); and brand tokens
61
- (`brandTokens`/`<BrandStyle>`) that re-skin the whole UI from `brand`. Authored
62
- against React, rendered as Preact via `preact/compat` in the reference sites.
58
+ - **The UI kit, adoptable in pieces.** Three entries, split on the dependency
59
+ boundary so you never pay for what you don't use:
60
+ - `@odla-ai/chapter/ui/member` **react-only**: `SlotPicker`, the date/timezone
61
+ helpers, `JoinIsland` (form payment booking), `MembersArea`,
62
+ `Rescheduler`, `PaymentStep`, `<BrandStyle>`. It never imports
63
+ `@odla-ai/auth-clerk` or `@odla-ai/crm`, so you can adopt a single
64
+ presentational component without the Clerk browser SDK. (A bundle-graph test
65
+ enforces this.)
66
+ - `@odla-ai/chapter/ui/admin` — the Clerk-gated console `ChapterAdmin` + the
67
+ section catalog (`peopleSection`). This one does need auth-clerk + crm/ui.
68
+ - `@odla-ai/chapter/ui` — the full barrel, for back-compat.
69
+
70
+ Authored against React, rendered as Preact via `preact/compat` in the reference
71
+ sites. Brand tokens (`brandTokens`/`<BrandStyle>`) re-skin all of it from
72
+ `brand`.
63
73
 
64
74
  ## Quick start
65
75
 
@@ -125,10 +135,22 @@ These bite silently — a smoke test won't catch them:
125
135
  - **`services` default** is `["db","calendar","o11y"]`; `smoke` compares config
126
136
  against the platform, so set `services` explicitly if you don't run the o11y
127
137
  collector.
128
- - **Which email fires from which route.** `adminNotification` fires from
129
- `POST /api/applications` (on submit), `prepEmail` from `/api/schedule/book`,
130
- `paymentConfirmation` from the Stripe webhook. If your policy differs (e.g.
131
- notify on payment, not submit), override that route.
138
+ - **Which email fires from which route.** Content and addressing are owner-editable
139
+ on the `groups` row; the *trigger* is config:
140
+
141
+ | Template | Fires from | Configurable |
142
+ |---|---|---|
143
+ | `adminNotification` | `POST /api/applications` (submit) — or the Stripe webhook (first payment) | `sends.adminNotification: "submit" \| "payment" \| "never"` (default `"submit"`) |
144
+ | `prepEmail` | `POST /api/schedule/book` | template `enabled` flag |
145
+ | `paymentConfirmation` | Stripe webhook, first successful invoice | template `enabled` flag |
146
+
147
+ ```ts
148
+ defineChapter({ /* … */ sends: { adminNotification: "payment" } });
149
+ ```
150
+
151
+ **Re-check this on every chapter upgrade.** Wiring a send changes a site's
152
+ outbound mail with no local diff — release notes call out send changes
153
+ explicitly for that reason.
132
154
  - **Account model.** `account: "invite"` (default) mints a Clerk invitation,
133
155
  `"create"` makes the account server-side (so join can say the account is
134
156
  ready), `"none"` skips it — all need `clerk_secret_key` in the tenant vault. A
@@ -143,14 +165,30 @@ These bite silently — a smoke test won't catch them:
143
165
 
144
166
  ### Install + scope notes
145
167
 
146
- - **Skip `@odla-ai/auth-clerk` for a worker-only adoption.** The worker never
147
- imports it (JWTs are verified with `jose` via `ctx.verifyUser`); it is an
148
- optional peer for `chapter/ui` only. Reach for `@odla-ai/auth-clerk/invitations`
149
- when you want to send your own branded invitation mail.
150
- - **The admin surface is intentionally minimal** `/api/admin/scheduling` +
151
- `/api/admin/meetings`, plus `peopleSection`. Dashboard, billing, per-person
152
- comms, approve/refund, etc. are your own host routes via the seam; don't plan
153
- around them shipping.
168
+ - **`@odla-ai/auth-clerk` is not a chapter peer.** It is deliberately absent from
169
+ this package's manifest, because only one entry imports it: the worker verifies
170
+ JWTs with `jose` via `ctx.verifyUser`, and `@odla-ai/chapter/ui/member` is
171
+ react-only. **Install `@odla-ai/auth-clerk` yourself if (and only if) you adopt
172
+ `@odla-ai/chapter/ui/admin`** or want the themed sign-in components; reach for
173
+ `@odla-ai/auth-clerk/invitations` when you want to send your own branded
174
+ invitation mail. Importing the full `@odla-ai/chapter/ui` barrel pulls the admin
175
+ half, so prefer the narrower entry.
176
+ - **Known-good version set** (installs clean, no flags): `@odla-ai/chapter` 0.10.0,
177
+ `@odla-ai/ui` 0.9.0, `@odla-ai/crm` 0.1.4, `@odla-ai/db` 0.6.6,
178
+ `@odla-ai/calendar` 0.2.0, `@odla-ai/email` 0.3.1, CLI 0.16.7, plus `jose` and
179
+ react (or `preact` + `preact/compat`). chapter is the package that pulls the
180
+ others together, so this is the line to check when an install complains.
181
+ - **`--legacy-peer-deps` is a diagnostic, not a setting.** It suppresses exactly
182
+ the peer conflict that tells you a pair is unsupported. If you need it, find out
183
+ why first.
184
+ - **The admin surface is intentionally minimal, and staying that way.** chapter
185
+ ships `/api/admin/scheduling` (booking rules, with field-keyed validation
186
+ messages) and `/api/admin/meetings` (the reconciled agenda, with the applicant
187
+ joined, `?all=1`, `from`/`to`, drift fields, Meet/Calendar links and the group
188
+ timezone), plus `peopleSection`. **Dashboard, billing, email config/log/test,
189
+ per-person comms, people list, role changes, approve and refund are out of
190
+ scope** — they are yours as host routes via the seam. Don't plan around them
191
+ shipping.
154
192
 
155
193
  ### Verify from the types, not this file
156
194
 
@@ -0,0 +1,281 @@
1
+ // src/ui/admin.tsx
2
+ import { useEffect as useEffect2, useMemo, useState as useState2 } from "react";
3
+ import { ClerkGate, SignedIn, SignedOut, SignIn, useClerkAuth, clerkAppearanceFromTokens } from "@odla-ai/auth-clerk";
4
+ import { CrmClient } from "@odla-ai/crm";
5
+
6
+ // src/ui/chrome.tsx
7
+ import { useCallback, useEffect, useState } from "react";
8
+ import { TopBar, TopBarLink } from "@odla-ai/ui/components";
9
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
10
+ var badgeText = (brand) => brand.badge ?? brand.name.slice(0, 3).toUpperCase();
11
+ var brandDisplay = (brand) => brand.wordmark ?? brand.name;
12
+ var S = {
13
+ gate: {
14
+ minHeight: "100vh",
15
+ display: "grid",
16
+ placeItems: "center",
17
+ padding: 24,
18
+ background: "radial-gradient(900px 480px at 50% -8%, var(--ui-accent-soft), transparent 70%), radial-gradient(700px 500px at 110% 10%, var(--ui-good-soft), transparent 60%)"
19
+ },
20
+ gateInner: { width: "100%", maxWidth: 400, display: "flex", flexDirection: "column", alignItems: "center" },
21
+ brandBox: { textAlign: "center", marginBottom: 22 },
22
+ badge: {
23
+ width: 52,
24
+ height: 52,
25
+ margin: "0 auto 14px",
26
+ display: "grid",
27
+ placeItems: "center",
28
+ fontSize: 15,
29
+ fontWeight: 700,
30
+ color: "var(--ui-on-accent)",
31
+ background: "linear-gradient(135deg, var(--ui-accent), var(--ui-accent-strong))",
32
+ borderRadius: 14,
33
+ boxShadow: "0 10px 28px var(--ui-accent-soft)"
34
+ },
35
+ badgeSm: {
36
+ width: 26,
37
+ height: 26,
38
+ display: "grid",
39
+ placeItems: "center",
40
+ fontSize: 10,
41
+ fontWeight: 700,
42
+ color: "var(--ui-on-accent)",
43
+ background: "linear-gradient(135deg, var(--ui-accent), var(--ui-accent-strong))",
44
+ borderRadius: 7,
45
+ marginRight: 9
46
+ },
47
+ h1: { margin: 0, fontSize: 28, letterSpacing: "-0.02em", fontWeight: 700 },
48
+ muted: { color: "var(--ui-text-muted)" },
49
+ role: {
50
+ fontFamily: "var(--ui-font-mono)",
51
+ fontSize: 11,
52
+ textTransform: "uppercase",
53
+ letterSpacing: "0.04em",
54
+ padding: "2px 8px",
55
+ borderRadius: 999,
56
+ background: "var(--ui-accent-soft)",
57
+ border: "1px solid var(--ui-accent)",
58
+ color: "var(--ui-accent-strong)"
59
+ },
60
+ whoami: { display: "flex", alignItems: "center", gap: 8, fontSize: 12 },
61
+ // Sections own their own width/padding (e.g. a .wrap container).
62
+ main: { minHeight: "calc(100vh - 61px)" },
63
+ card: { width: "100%", textAlign: "center" }
64
+ };
65
+ function Gate(props) {
66
+ const { brand, tagline, children } = props;
67
+ return /* @__PURE__ */ jsx("div", { style: S.gate, children: /* @__PURE__ */ jsxs("div", { style: S.gateInner, children: [
68
+ /* @__PURE__ */ jsxs("div", { style: S.brandBox, children: [
69
+ /* @__PURE__ */ jsx("div", { style: S.badge, children: badgeText(brand) }),
70
+ /* @__PURE__ */ jsx("h1", { style: S.h1, children: brandDisplay(brand) }),
71
+ tagline ? /* @__PURE__ */ jsx("p", { style: { ...S.muted, margin: "8px 0 0", fontSize: 14 }, children: tagline }) : null
72
+ ] }),
73
+ children
74
+ ] }) });
75
+ }
76
+ function AdminShell(props) {
77
+ const { sections, basePath, brand, client, getToken, signOut, email } = props;
78
+ const sectionFromPath = useCallback(() => {
79
+ const fallback = sections[0]?.id ?? "";
80
+ if (typeof window === "undefined") return fallback;
81
+ const rest = window.location.pathname.slice(basePath.length).replace(/^\//, "");
82
+ const id = rest.split("/")[0] ?? "";
83
+ return sections.some((s) => s.id === id) ? id : fallback;
84
+ }, [sections, basePath]);
85
+ const [section, setSection] = useState(sectionFromPath);
86
+ const go = useCallback(
87
+ (id) => {
88
+ window.history.pushState(null, "", `${basePath}/${id}`);
89
+ setSection(id);
90
+ },
91
+ [basePath]
92
+ );
93
+ useEffect(() => {
94
+ const onPop = () => setSection(sectionFromPath());
95
+ window.addEventListener("popstate", onPop);
96
+ return () => window.removeEventListener("popstate", onPop);
97
+ }, [sectionFromPath]);
98
+ const active = sections.find((s) => s.id === section) ?? sections[0];
99
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
100
+ /* @__PURE__ */ jsxs(TopBar, { children: [
101
+ /* @__PURE__ */ jsxs("a", { className: "topbar-brand", href: "/", style: { display: "inline-flex", alignItems: "center" }, children: [
102
+ /* @__PURE__ */ jsx("span", { style: S.badgeSm, children: badgeText(brand) }),
103
+ brandDisplay(brand)
104
+ ] }),
105
+ /* @__PURE__ */ jsx("nav", { className: "topbar-nav", "aria-label": "Admin navigation", children: sections.map((s) => /* @__PURE__ */ jsx(TopBarLink, { active: section === s.id, onClick: () => go(s.id), children: s.label }, s.id)) }),
106
+ /* @__PURE__ */ jsx("div", { className: "topbar-spacer" }),
107
+ /* @__PURE__ */ jsxs("div", { className: "topbar-actions", children: [
108
+ /* @__PURE__ */ jsxs("span", { style: S.whoami, children: [
109
+ /* @__PURE__ */ jsx("span", { style: S.role, children: "admin" }),
110
+ email ? /* @__PURE__ */ jsx("span", { style: S.muted, children: email }) : null
111
+ ] }),
112
+ /* @__PURE__ */ jsx("button", { className: "btn secondary mini", onClick: () => signOut(), children: "Sign out" })
113
+ ] })
114
+ ] }),
115
+ /* @__PURE__ */ jsx("main", { className: "shell-main", style: S.main, children: active ? active.render({ client, getToken, navigate: go }) : null })
116
+ ] });
117
+ }
118
+
119
+ // src/ui/admin.tsx
120
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
121
+ function Authed(props) {
122
+ const { sections, basePath, brand, crmBasePath, apiBase } = props;
123
+ const { getToken, signOut } = useClerkAuth();
124
+ const client = useMemo(
125
+ () => new CrmClient({
126
+ basePath: crmBasePath,
127
+ headers: async () => {
128
+ const t = await getToken();
129
+ return t ? { authorization: `Bearer ${t}` } : {};
130
+ }
131
+ }),
132
+ [getToken, crmBasePath]
133
+ );
134
+ const [state, setState] = useState2({
135
+ status: "checking"
136
+ });
137
+ useEffect2(() => {
138
+ let live = true;
139
+ void (async () => {
140
+ try {
141
+ const t = await getToken();
142
+ const res = await fetch(`${apiBase}/api/me`, { headers: t ? { authorization: `Bearer ${t}` } : {} });
143
+ const body = await res.json().catch(() => ({}));
144
+ if (live) setState({ status: body.authorized ? "ok" : "denied", email: body.email ?? null });
145
+ } catch {
146
+ if (live) setState({ status: "denied" });
147
+ }
148
+ })();
149
+ return () => {
150
+ live = false;
151
+ };
152
+ }, [getToken, apiBase]);
153
+ if (state.status === "checking") {
154
+ return /* @__PURE__ */ jsx2(Gate, { brand, children: /* @__PURE__ */ jsx2("p", { style: S.muted, children: "Checking access\u2026" }) });
155
+ }
156
+ if (state.status === "denied") {
157
+ return /* @__PURE__ */ jsx2(Gate, { brand, children: /* @__PURE__ */ jsxs2("div", { className: "card", style: S.card, children: [
158
+ /* @__PURE__ */ jsx2("h2", { style: { marginTop: 0 }, children: "Not authorized" }),
159
+ /* @__PURE__ */ jsxs2("p", { style: S.muted, children: [
160
+ state.email ? `${state.email} isn't` : "This account isn't",
161
+ " on the admin list. Ask an existing admin to add you in odla Studio."
162
+ ] }),
163
+ /* @__PURE__ */ jsx2("button", { className: "btn secondary", onClick: () => signOut(), style: { marginTop: 12 }, children: "Sign out" })
164
+ ] }) });
165
+ }
166
+ return /* @__PURE__ */ jsx2(
167
+ AdminShell,
168
+ {
169
+ sections,
170
+ basePath,
171
+ brand,
172
+ client,
173
+ getToken,
174
+ signOut,
175
+ email: state.email ?? null
176
+ }
177
+ );
178
+ }
179
+ function ChapterAdmin(props) {
180
+ const sections = props.sections;
181
+ const basePath = props.basePath ?? "/admin";
182
+ const brand = props.brand ?? { name: "Admin" };
183
+ const crmBasePath = props.crmBasePath ?? "/api/crm";
184
+ const apiBase = props.apiBase ?? "";
185
+ const [pk, setPk] = useState2(void 0);
186
+ useEffect2(() => {
187
+ let live = true;
188
+ void (async () => {
189
+ try {
190
+ const res = await fetch(`${apiBase}/api/config`);
191
+ const body = await res.json();
192
+ if (live) setPk(body.clerkPublishableKey ?? null);
193
+ } catch {
194
+ if (live) setPk(null);
195
+ }
196
+ })();
197
+ return () => {
198
+ live = false;
199
+ };
200
+ }, [apiBase]);
201
+ if (pk === void 0) {
202
+ return /* @__PURE__ */ jsx2(Gate, { brand, children: /* @__PURE__ */ jsx2("p", { style: S.muted, children: "Loading\u2026" }) });
203
+ }
204
+ if (!pk) {
205
+ return /* @__PURE__ */ jsx2(Gate, { brand, children: /* @__PURE__ */ jsxs2("div", { className: "card", style: S.card, children: [
206
+ /* @__PURE__ */ jsx2("h2", { style: { marginTop: 0 }, children: "Sign-in not configured" }),
207
+ /* @__PURE__ */ jsx2("p", { style: S.muted, children: "No Clerk publishable key is set for this environment yet." })
208
+ ] }) });
209
+ }
210
+ return /* @__PURE__ */ jsxs2(ClerkGate, { publishableKey: pk, appearance: clerkAppearanceFromTokens(), afterSignOutUrl: "/", children: [
211
+ /* @__PURE__ */ jsx2(SignedOut, { children: /* @__PURE__ */ jsx2(Gate, { brand, tagline: "Admin sign-in \u2014 invite only", children: /* @__PURE__ */ jsx2(SignIn, { routing: "hash", forceRedirectUrl: basePath, signUpForceRedirectUrl: basePath }) }) }),
212
+ /* @__PURE__ */ jsx2(SignedIn, { children: /* @__PURE__ */ jsx2(Authed, { sections, basePath, brand, crmBasePath, apiBase }) })
213
+ ] });
214
+ }
215
+
216
+ // src/ui/admin-people.tsx
217
+ import { useState as useState3 } from "react";
218
+ import { CrmList, RecordPanel, useCrmQuery, useCrmRecord } from "@odla-ai/crm/ui";
219
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
220
+ function PeopleBody(props) {
221
+ const { crm, client, type } = props;
222
+ const hookClient = client;
223
+ const query = useCrmQuery(hookClient, type);
224
+ const [openId, setOpenId] = useState3(null);
225
+ const record = useCrmRecord(hookClient, openId);
226
+ const [saving, setSaving] = useState3(false);
227
+ const saveFields = async (input) => {
228
+ if (!openId) return;
229
+ setSaving(true);
230
+ try {
231
+ await client.updateRecord(openId, { input });
232
+ record.refresh();
233
+ query.refresh();
234
+ } finally {
235
+ setSaving(false);
236
+ }
237
+ };
238
+ const moveStage = async (to) => {
239
+ if (!openId) return;
240
+ await client.setStage(openId, to);
241
+ record.refresh();
242
+ query.refresh();
243
+ };
244
+ const addTag = async (tag) => {
245
+ if (openId) {
246
+ await client.addTag(openId, tag);
247
+ record.refresh();
248
+ }
249
+ };
250
+ const removeTag = async (tag) => {
251
+ if (openId) {
252
+ await client.removeTag(openId, tag);
253
+ record.refresh();
254
+ }
255
+ };
256
+ return /* @__PURE__ */ jsxs3("div", { className: "wrap", children: [
257
+ /* @__PURE__ */ jsx3(CrmList, { crm, type, query, onOpenRecord: (r) => setOpenId(r.id) }),
258
+ record.detail ? /* @__PURE__ */ jsx3(
259
+ RecordPanel,
260
+ {
261
+ crm,
262
+ detail: record.detail,
263
+ onSaveFields: saveFields,
264
+ onMoveStage: moveStage,
265
+ onAddTag: addTag,
266
+ onRemoveTag: removeTag,
267
+ saving
268
+ }
269
+ ) : null
270
+ ] });
271
+ }
272
+ function peopleSection(options) {
273
+ const { crm, id = "people", label = "People", type = "person" } = options;
274
+ return { id, label, render: (ctx) => /* @__PURE__ */ jsx3(PeopleBody, { crm, client: ctx.client, type }) };
275
+ }
276
+
277
+ export {
278
+ ChapterAdmin,
279
+ peopleSection
280
+ };
281
+ //# sourceMappingURL=chunk-YO6DY5DL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/ui/admin.tsx","../src/ui/chrome.tsx","../src/ui/admin-people.tsx"],"sourcesContent":["// ChapterAdmin — the whole admin console for a chapter/hub, owned by the package\n// so no site re-wires (or re-bugs) it: the Clerk publishable-key fetch\n// (/api/config), the themed sign-in with the post-sign-in redirect DERIVED from\n// basePath (so it never bounces to \"/\"), the odla-db admins-allowlist check\n// (/api/me), and a TopBar shell (chrome.tsx) that renders the caller's sections.\nimport { useEffect, useMemo, useState } from \"react\";\nimport { ClerkGate, SignedIn, SignedOut, SignIn, useClerkAuth, clerkAppearanceFromTokens } from \"@odla-ai/auth-clerk\";\nimport { CrmClient } from \"@odla-ai/crm\";\nimport { S, Gate, AdminShell } from \"./chrome.js\";\nimport type { AdminSection, Brand } from \"./chrome.js\";\n\nexport type { AdminSection, AdminSectionContext } from \"./chrome.js\";\n\n/** Props for {@link ChapterAdmin}. */\nexport interface ChapterAdminProps {\n /** The admin sections to render (nav label + body). */\n sections: AdminSection[];\n /** Admin mount path. The sign-in redirect + section routing derive from it,\n * so the \"bounce to home after sign-in\" bug is impossible. Default \"/admin\". */\n basePath?: string;\n /** Wordmark shown in the gate + top bar. */\n brand?: Brand;\n /** CRM route mount. Default \"/api/crm\". */\n crmBasePath?: string;\n /** Origin for /api/config and /api/me. Default same origin. */\n apiBase?: string;\n}\n\n// After sign-in: build a token-bound CrmClient, confirm the account is an\n// allowlisted admin (/api/me), then render the shell.\nfunction Authed(props: {\n sections: AdminSection[];\n basePath: string;\n brand: Brand;\n crmBasePath: string;\n apiBase: string;\n}) {\n const { sections, basePath, brand, crmBasePath, apiBase } = props;\n const { getToken, signOut } = useClerkAuth();\n const client = useMemo(\n () =>\n new CrmClient({\n basePath: crmBasePath,\n headers: async (): Promise<Record<string, string>> => {\n const t = await getToken();\n return t ? { authorization: `Bearer ${t}` } : {};\n },\n }),\n [getToken, crmBasePath],\n );\n const [state, setState] = useState<{ status: \"checking\" | \"ok\" | \"denied\"; email?: string | null }>({\n status: \"checking\",\n });\n\n useEffect(() => {\n let live = true;\n void (async () => {\n try {\n const t = await getToken();\n const res = await fetch(`${apiBase}/api/me`, { headers: t ? { authorization: `Bearer ${t}` } : {} });\n const body = (await res.json().catch(() => ({}))) as { authorized?: boolean; email?: string | null };\n if (live) setState({ status: body.authorized ? \"ok\" : \"denied\", email: body.email ?? null });\n } catch {\n if (live) setState({ status: \"denied\" });\n }\n })();\n return () => {\n live = false;\n };\n }, [getToken, apiBase]);\n\n if (state.status === \"checking\") {\n return (\n <Gate brand={brand}>\n <p style={S.muted}>Checking access…</p>\n </Gate>\n );\n }\n if (state.status === \"denied\") {\n return (\n <Gate brand={brand}>\n <div className=\"card\" style={S.card}>\n <h2 style={{ marginTop: 0 }}>Not authorized</h2>\n <p style={S.muted}>\n {state.email ? `${state.email} isn't` : \"This account isn't\"} on the admin list. Ask an existing admin to add\n you in odla Studio.\n </p>\n <button className=\"btn secondary\" onClick={() => signOut()} style={{ marginTop: 12 }}>\n Sign out\n </button>\n </div>\n </Gate>\n );\n }\n return (\n <AdminShell\n sections={sections}\n basePath={basePath}\n brand={brand}\n client={client}\n getToken={getToken}\n signOut={signOut}\n email={state.email ?? null}\n />\n );\n}\n\n/**\n * The whole admin console for a chapter/hub: fetches the Clerk publishable key\n * (/api/config), gates on sign-in with the post-sign-in redirect derived from\n * `basePath`, checks the odla-db admins allowlist (/api/me), and renders a\n * TopBar shell over the caller's `sections`. The gate/shell/redirect are owned\n * here so no site re-wires (or re-bugs) them.\n */\nexport function ChapterAdmin(props: ChapterAdminProps) {\n const sections = props.sections;\n const basePath = props.basePath ?? \"/admin\";\n const brand = props.brand ?? { name: \"Admin\" };\n const crmBasePath = props.crmBasePath ?? \"/api/crm\";\n const apiBase = props.apiBase ?? \"\";\n\n const [pk, setPk] = useState<string | null | undefined>(undefined);\n useEffect(() => {\n let live = true;\n void (async () => {\n try {\n const res = await fetch(`${apiBase}/api/config`);\n const body = (await res.json()) as { clerkPublishableKey?: string | null };\n if (live) setPk(body.clerkPublishableKey ?? null);\n } catch {\n if (live) setPk(null);\n }\n })();\n return () => {\n live = false;\n };\n }, [apiBase]);\n\n if (pk === undefined) {\n return (\n <Gate brand={brand}>\n <p style={S.muted}>Loading…</p>\n </Gate>\n );\n }\n if (!pk) {\n return (\n <Gate brand={brand}>\n <div className=\"card\" style={S.card}>\n <h2 style={{ marginTop: 0 }}>Sign-in not configured</h2>\n <p style={S.muted}>No Clerk publishable key is set for this environment yet.</p>\n </div>\n </Gate>\n );\n }\n return (\n <ClerkGate publishableKey={pk} appearance={clerkAppearanceFromTokens()} afterSignOutUrl=\"/\">\n <SignedOut>\n <Gate brand={brand} tagline=\"Admin sign-in — invite only\">\n <SignIn routing=\"hash\" forceRedirectUrl={basePath} signUpForceRedirectUrl={basePath} />\n </Gate>\n </SignedOut>\n <SignedIn>\n <Authed sections={sections} basePath={basePath} brand={brand} crmBasePath={crmBasePath} apiBase={apiBase} />\n </SignedIn>\n </ClerkGate>\n );\n}\n","// Shared chrome for ChapterAdmin: the section contract, the centered gate\n// screen, the signed-in TopBar shell, and the inline style tokens. Split out of\n// admin.tsx to stay under the repo's per-file LOC cap. Uses only @odla-ai/ui\n// classes + --ui-* tokens, so a consumer needs nothing beyond the odla-ui sheet.\nimport { useCallback, useEffect, useState } from \"react\";\nimport type { CSSProperties, ReactNode } from \"react\";\nimport { TopBar, TopBarLink } from \"@odla-ai/ui/components\";\nimport type { CrmClient } from \"@odla-ai/crm\";\n\n/** What each admin section receives. `client` is a CrmClient bound to the\n * signed-in admin's bearer token; `navigate` switches sections. */\nexport interface AdminSectionContext {\n client: CrmClient;\n getToken: () => Promise<string | null>;\n /** Switch to another section by id (e.g. Overview \"jump in\" buttons). */\n navigate: (sectionId: string) => void;\n}\n\n/** One admin-console section: a nav label and a body renderer. */\nexport interface AdminSection {\n id: string;\n label: string;\n render: (ctx: AdminSectionContext) => ReactNode;\n}\n\n/** Wordmark shown in the gate and top bar. `name` is the plain-text identity (and\n * the badge fallback); `wordmark` is an optional rich display node for brands\n * whose typography can't be a plain string (e.g. an ampersand wrapped to render\n * upright), and `badge` overrides the glyph. */\nexport type Brand = { name: string; badge?: string; wordmark?: ReactNode };\n\n/** The badge glyph: the explicit `badge`, else the first 3 letters of `name`. */\nexport const badgeText = (brand: Brand): string => brand.badge ?? brand.name.slice(0, 3).toUpperCase();\n\n/** The display node: the rich `wordmark` if given, else the plain `name`. */\nexport const brandDisplay = (brand: Brand): ReactNode => brand.wordmark ?? brand.name;\n\n/** Inline style tokens (keyed to --ui-* custom properties). */\nexport const S: Record<string, CSSProperties> = {\n gate: {\n minHeight: \"100vh\",\n display: \"grid\",\n placeItems: \"center\",\n padding: 24,\n background:\n \"radial-gradient(900px 480px at 50% -8%, var(--ui-accent-soft), transparent 70%),\" +\n \" radial-gradient(700px 500px at 110% 10%, var(--ui-good-soft), transparent 60%)\",\n },\n gateInner: { width: \"100%\", maxWidth: 400, display: \"flex\", flexDirection: \"column\", alignItems: \"center\" },\n brandBox: { textAlign: \"center\", marginBottom: 22 },\n badge: {\n width: 52,\n height: 52,\n margin: \"0 auto 14px\",\n display: \"grid\",\n placeItems: \"center\",\n fontSize: 15,\n fontWeight: 700,\n color: \"var(--ui-on-accent)\",\n background: \"linear-gradient(135deg, var(--ui-accent), var(--ui-accent-strong))\",\n borderRadius: 14,\n boxShadow: \"0 10px 28px var(--ui-accent-soft)\",\n },\n badgeSm: {\n width: 26,\n height: 26,\n display: \"grid\",\n placeItems: \"center\",\n fontSize: 10,\n fontWeight: 700,\n color: \"var(--ui-on-accent)\",\n background: \"linear-gradient(135deg, var(--ui-accent), var(--ui-accent-strong))\",\n borderRadius: 7,\n marginRight: 9,\n },\n h1: { margin: 0, fontSize: 28, letterSpacing: \"-0.02em\", fontWeight: 700 },\n muted: { color: \"var(--ui-text-muted)\" },\n role: {\n fontFamily: \"var(--ui-font-mono)\",\n fontSize: 11,\n textTransform: \"uppercase\",\n letterSpacing: \"0.04em\",\n padding: \"2px 8px\",\n borderRadius: 999,\n background: \"var(--ui-accent-soft)\",\n border: \"1px solid var(--ui-accent)\",\n color: \"var(--ui-accent-strong)\",\n },\n whoami: { display: \"flex\", alignItems: \"center\", gap: 8, fontSize: 12 },\n // Sections own their own width/padding (e.g. a .wrap container).\n main: { minHeight: \"calc(100vh - 61px)\" },\n card: { width: \"100%\", textAlign: \"center\" },\n};\n\n/** Centered gate screen used for loading / sign-in / denied / not-configured. */\nexport function Gate(props: { brand: Brand; tagline?: string; children: ReactNode }) {\n const { brand, tagline, children } = props;\n return (\n <div style={S.gate}>\n <div style={S.gateInner}>\n <div style={S.brandBox}>\n <div style={S.badge}>{badgeText(brand)}</div>\n <h1 style={S.h1}>{brandDisplay(brand)}</h1>\n {tagline ? <p style={{ ...S.muted, margin: \"8px 0 0\", fontSize: 14 }}>{tagline}</p> : null}\n </div>\n {children}\n </div>\n </div>\n );\n}\n\n/** The signed-in shell: a TopBar of the caller's sections + the active body. */\nexport function AdminShell(props: {\n sections: AdminSection[];\n basePath: string;\n brand: Brand;\n client: CrmClient;\n getToken: () => Promise<string | null>;\n signOut: () => void;\n email: string | null;\n}) {\n const { sections, basePath, brand, client, getToken, signOut, email } = props;\n\n const sectionFromPath = useCallback(() => {\n const fallback = sections[0]?.id ?? \"\";\n if (typeof window === \"undefined\") return fallback;\n const rest = window.location.pathname.slice(basePath.length).replace(/^\\//, \"\");\n const id = rest.split(\"/\")[0] ?? \"\";\n return sections.some((s) => s.id === id) ? id : fallback;\n }, [sections, basePath]);\n\n const [section, setSection] = useState(sectionFromPath);\n\n const go = useCallback(\n (id: string) => {\n window.history.pushState(null, \"\", `${basePath}/${id}`);\n setSection(id);\n },\n [basePath],\n );\n\n useEffect(() => {\n const onPop = () => setSection(sectionFromPath());\n window.addEventListener(\"popstate\", onPop);\n return () => window.removeEventListener(\"popstate\", onPop);\n }, [sectionFromPath]);\n\n const active = sections.find((s) => s.id === section) ?? sections[0];\n\n return (\n <>\n <TopBar>\n <a className=\"topbar-brand\" href=\"/\" style={{ display: \"inline-flex\", alignItems: \"center\" }}>\n <span style={S.badgeSm}>{badgeText(brand)}</span>\n {brandDisplay(brand)}\n </a>\n <nav className=\"topbar-nav\" aria-label=\"Admin navigation\">\n {sections.map((s) => (\n <TopBarLink key={s.id} active={section === s.id} onClick={() => go(s.id)}>\n {s.label}\n </TopBarLink>\n ))}\n </nav>\n <div className=\"topbar-spacer\" />\n <div className=\"topbar-actions\">\n <span style={S.whoami}>\n <span style={S.role}>admin</span>\n {email ? <span style={S.muted}>{email}</span> : null}\n </span>\n <button className=\"btn secondary mini\" onClick={() => signOut()}>\n Sign out\n </button>\n </div>\n </TopBar>\n <main className=\"shell-main\" style={S.main}>\n {active ? active.render({ client, getToken, navigate: go }) : null}\n </main>\n </>\n );\n}\n","// People/CRM admin section — the core admin workflow, assembled from the shipped\n// @odla-ai/crm/ui kit against the /api/crm/* routes the worker already mounts.\n// It's a FACTORY: peopleSection({ crm }) → an AdminSection you drop into\n// ChapterAdmin's `sections`. Every lifecycle action (edit fields, move pipeline\n// stage, tag) goes through the token-bound CrmClient — never a direct db write,\n// so the package's operational invariants hold (G7).\nimport { useState } from \"react\";\nimport { CrmList, RecordPanel, useCrmQuery, useCrmRecord } from \"@odla-ai/crm/ui\";\nimport type { Crm, CrmClient, CrmRecord } from \"@odla-ai/crm\";\nimport type { AdminSection } from \"./chrome.js\";\n\nfunction PeopleBody(props: { crm: Crm; client: CrmClient; type: string }) {\n const { crm, client, type } = props;\n // The crm/ui hooks type their client against @odla-ai/crm's ui-entry bundled\n // declaration; ctx.client is the core-entry declaration. Same runtime class,\n // but their private field makes the two .d.ts copies nominally distinct — cast\n // to the param type the hooks expect.\n const hookClient = client as unknown as Parameters<typeof useCrmQuery>[0];\n const query = useCrmQuery(hookClient, type);\n const [openId, setOpenId] = useState<string | null>(null);\n const record = useCrmRecord(hookClient, openId);\n const [saving, setSaving] = useState(false);\n\n const saveFields = async (input: Record<string, unknown>) => {\n if (!openId) return;\n setSaving(true);\n try {\n await client.updateRecord(openId, { input });\n record.refresh();\n query.refresh();\n } finally {\n setSaving(false);\n }\n };\n const moveStage = async (to: string) => {\n if (!openId) return;\n await client.setStage(openId, to);\n record.refresh();\n query.refresh();\n };\n const addTag = async (tag: string) => {\n if (openId) {\n await client.addTag(openId, tag);\n record.refresh();\n }\n };\n const removeTag = async (tag: string) => {\n if (openId) {\n await client.removeTag(openId, tag);\n record.refresh();\n }\n };\n\n return (\n <div className=\"wrap\">\n <CrmList crm={crm} type={type} query={query} onOpenRecord={(r: CrmRecord) => setOpenId(r.id)} />\n {record.detail ? (\n <RecordPanel\n crm={crm}\n detail={record.detail}\n onSaveFields={saveFields}\n onMoveStage={moveStage}\n onAddTag={addTag}\n onRemoveTag={removeTag}\n saving={saving}\n />\n ) : null}\n </div>\n );\n}\n\n/** Options for {@link peopleSection}. */\nexport interface PeopleSectionOptions {\n /** The chapter's resolved CRM engine — `chapter.crm` from defineChapter. */\n crm: Crm;\n /** Section id / route segment. Default \"people\". */\n id?: string;\n /** Nav label. Default \"People\". */\n label?: string;\n /** The record type to list. Default \"person\". */\n type?: string;\n}\n\n/**\n * Build the People/CRM admin section: a server-driven records table plus a\n * facet-composed detail drawer (fields, pipeline stage, tags) over the\n * token-bound CrmClient. Drop the result into `ChapterAdmin`'s `sections`.\n * (Notes/activity feed and saved-view navigation are noted follow-ups.)\n */\nexport function peopleSection(options: PeopleSectionOptions): AdminSection {\n const { crm, id = \"people\", label = \"People\", type = \"person\" } = options;\n return { id, label, render: (ctx) => <PeopleBody crm={crm} client={ctx.client} type={type} /> };\n}\n"],"mappings":"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","names":["useEffect","useState","jsx","jsxs","useState","useEffect","useState","jsx","jsxs"]}