@odla-ai/chapter 0.7.0 → 0.9.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
 
@@ -97,18 +107,72 @@ export default {
97
107
 
98
108
  ## Adopting into an existing site
99
109
 
100
- 1. **Schema + provisioning (safe, verifiable).** Replace a hand-rolled
101
- `schema.mjs` / `rules.mjs` / group seed / provisioner with `defineChapter` +
102
- `createChapterIntegration`. The emitted schema is directly comparable — diff
103
- `chapter.schema` against your namespaces and expect byte-equality so this is
104
- a no-behavior-change deletion of hand-maintained code that also installs the
105
- deny-all `crm_*` rules a hand-rolled provisioner tends to skip.
106
- 2. **The worker.** Either use `chapterWorker({ chapter })` directly, or keep your
107
- bespoke routes and pass them via `chapterWorker({ chapter, routes })` — they
108
- run first and reuse chapter's auth through the shared context, so you never
109
- verify a JWT twice. Path renames your frontend depends on are your own alias
110
- routes in that array.
111
-
112
- Read `dist/*.d.ts` for the authoritative surface.
110
+ A real conversion (the site this was extracted from) went from a 2,094-line
111
+ worker to 6 lines and deleted ~2,500 lines. The order that worked:
112
+
113
+ 1. **Config first, assert parity BEFORE deleting anything.** `defineChapter()`
114
+ your site, then diff `chapter.schema` against your existing schema in a test
115
+ and require byte-equality. That single assertion is what makes the deletion
116
+ safe rather than hopeful.
117
+ 2. **Freeze the old schema as a test fixture** (e.g. `test/fixtures/legacy-schema.mjs`)
118
+ and keep asserting against it, so an upstream default change fails a test
119
+ instead of a live `provision`. It is the only durable guard against drift in a
120
+ generated schema.
121
+ 3. **Swap provisioning** — `createChapterIntegration(chapter)`, inert until the
122
+ next provision run. It supplies schema + rules + seeds, so your
123
+ `odla.config.mjs` `db` block goes away entirely.
124
+ 4. **Then the worker**, keeping every bespoke route as a host route
125
+ (`chapterWorker({ chapter, routes })`). Don't hand routes to chapter in the
126
+ same change as the framework swap.
127
+ 5. **Override rather than inherit** wherever local behavior was a decision.
128
+
129
+ ### Behavior deltas to audit
130
+
131
+ These bite silently — a smoke test won't catch them:
132
+
133
+ - **Per-field caps.** `application.defaultMaxLen` is 2000. If your form accepts
134
+ longer input, pass `maxLen` explicitly or the default starts rejecting it.
135
+ - **`services` default** is `["db","calendar","o11y"]`; `smoke` compares config
136
+ against the platform, so set `services` explicitly if you don't run the o11y
137
+ collector.
138
+ - **Which email fires from which route.** `adminNotification` fires from
139
+ `POST /api/applications` (on submit), `prepEmail` from `/api/schedule/book`,
140
+ `paymentConfirmation` from the Stripe webhook. If your policy differs (e.g.
141
+ notify on payment, not submit), override that route.
142
+ - **Account model.** `account: "invite"` (default) mints a Clerk invitation,
143
+ `"create"` makes the account server-side (so join can say the account is
144
+ ready), `"none"` skips it — all need `clerk_secret_key` in the tenant vault. A
145
+ site that wants server-side create must set `account: "create"`; inheriting the
146
+ default silently changes the model.
147
+ - **CRM projection points.** chapter projects the person on application submit
148
+ (`projectApplicant`), not on booking or on webhook status change. If you mirror
149
+ pipeline stage into the CRM, keep those routes.
150
+ - **Route names.** chapter serves `/api/config`, `/api/join-config`, etc. Alias
151
+ legacy names in ~4 lines with a host route in `chapterWorker({ routes })` rather
152
+ than rewriting pages.
153
+
154
+ ### Install + scope notes
155
+
156
+ - **`@odla-ai/auth-clerk` is only needed for the admin console.** The worker never
157
+ imports it (JWTs are verified with `jose` via `ctx.verifyUser`), and neither
158
+ does `@odla-ai/chapter/ui/member`. Install it when you adopt
159
+ `@odla-ai/chapter/ui/admin` (or want the themed sign-in components); reach for
160
+ `@odla-ai/auth-clerk/invitations` when you want to send your own branded
161
+ invitation mail. Importing the full `@odla-ai/chapter/ui` barrel pulls it, so
162
+ prefer the narrower entry.
163
+ - **`--legacy-peer-deps` is a diagnostic, not a setting.** It suppresses exactly
164
+ the peer conflict that tells you a pair is unsupported. If you need it, find out
165
+ why first.
166
+ - **The admin surface is intentionally minimal** — `/api/admin/scheduling` +
167
+ `/api/admin/meetings`, plus `peopleSection`. Dashboard, billing, per-person
168
+ comms, approve/refund, etc. are your own host routes via the seam; don't plan
169
+ around them shipping.
170
+
171
+ ### Verify from the types, not this file
172
+
173
+ At several releases a day, prose lags. Treat this README as intent and verify the
174
+ real surface from `dist/*.d.ts` and by grepping the built bundle for route
175
+ strings. One testing gotcha: Clerk session tokens expire in ~60s, so a script
176
+ that mints a JWT then runs a batch of curls must re-mint per batch.
113
177
 
114
178
  MIT © odla
@@ -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"]}