@odla-ai/chapter 0.22.1 → 0.24.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
@@ -8,7 +8,7 @@ email. The host still builds the public pages, routing, and brand presentation;
8
8
  Chapter supplies their application mechanics.
9
9
 
10
10
  ```sh
11
- npm i --save-exact @odla-ai/chapter@0.22.0
11
+ npm i --save-exact @odla-ai/chapter@0.24.0
12
12
  ```
13
13
 
14
14
  > **Agentic experiment.** Built and maintained by AI agents from bounded runbooks
@@ -37,6 +37,12 @@ Do not combine the two flows. A provisioned canary is not a completed adoption,
37
37
  and a greenfield build does not need migration machinery. Both runbooks keep
38
38
  secrets out of source and require development proof before production.
39
39
 
40
+ The repository also contains a build-tested
41
+ [generic Preact reference host](https://github.com/cory/odla-ai/tree/main/examples/chapter-site).
42
+ It is the executable companion to the greenfield runbook: site-owned global
43
+ navigation, public routes, brand, voice, join fields, and member framing wrap
44
+ Chapter-owned application, admin, CRM, payment, and scheduling behavior.
45
+
40
46
  ## The shape
41
47
 
42
48
  - **One config, two profiles.** `defineChapter()` validates at import and returns
@@ -50,14 +56,15 @@ secrets out of source and require development proof before production.
50
56
  Cloudflare `ExportedHandler`. It serves `/api/health`, `/api/config`,
51
57
  `/api/me`, `/api/crm/*`, `/api/network/shared`; configured leaders also get
52
58
  `/api/admin/network/{targets,push}`. Chapter mode adds the public
53
- member surface (`/api/join-config`, `/api/applications`,
59
+ member surface (`/api/join-config`, `/api/join/resume`, `/api/applications`,
54
60
  `/api/schedule/{slots,book}`, `/api/payments/subscription`,
55
61
  `/api/webhooks/stripe`). The `/api/admin/*` handlers are registered in both
56
62
  modes; expose only the sections compatible with the selected profile and its
57
63
  provisioned namespaces. The Worker then falls back to static assets. Your
58
64
  `src/worker.ts` is ~3 lines. Observability is a host concern — wrap it with
59
65
  `withObservability` from `@odla-ai/o11y`.
60
- Operational values that owners may change (prices, policy copy, email
66
+ Unknown `/api/*` paths terminate as JSON `404` responses rather than falling
67
+ through to an SPA document. Operational values that owners may change (prices, policy copy, email
61
68
  templates, scheduling rules) are read at runtime from a single odla-db
62
69
  `groups` row. Brand identity and build-time tokens remain in the checked-in
63
70
  Chapter config.
@@ -111,6 +118,17 @@ secrets out of source and require development proof before production.
111
118
  Authored and shipped against Preact. Brand tokens
112
119
  (`brandTokens`/`<BrandStyle>`) re-skin all of it from
113
120
  `brand` (now light **and** dark, via `brand.palette` + `brand.paletteDark`).
121
+ - **Voice is part of the brand.** `copy` is a recursively partial
122
+ `ChapterCopy`; `defineChapter()` resolves it into a complete `chapter.copy`
123
+ contract. Packaged join, member, and admin surfaces read that contract, so a
124
+ follower can own terminology and tone without forking behavior. Text remains
125
+ serializable; use render slots when the host needs markup or a different
126
+ composition.
127
+ - **The host owns global navigation.** Admin defaults to `chrome="embedded"` and
128
+ three familiar workspaces. Dashboard/Billing and Calendar/Email remain
129
+ link-backed page tabs; record operations remain record tabs. A host can keep
130
+ one site header around public, join, member, and admin routes without Chapter
131
+ introducing another top-level navigation model.
114
132
 
115
133
  ### Theme tokens and scoped branding
116
134
 
@@ -134,6 +152,26 @@ and semantic `brand.tokens` / `brand.tokensDark` for normal configuration.
134
152
  escape hatch. Chapter scopes these overrides to `[data-chapter-admin]`, so an
135
153
  admin brand cannot recolor the document root or vendor sign-in UI.
136
154
 
155
+ `brand.accent` selects a named `data-ui-accent` family supplied by the chosen
156
+ theme. Each `AdminWorkspace` may set its own `accent` for a scoped page-level
157
+ variation. For a fully custom palette, compile the brand book through the pure
158
+ token entry and adapt it structurally:
159
+
160
+ ```ts
161
+ import { compileBrandTokens } from "@odla-ai/brand/tokens";
162
+ import { chapterBrandFromTokens } from "@odla-ai/chapter";
163
+
164
+ const compiled = compileBrandTokens({ swatches });
165
+ const brand = chapterBrandFromTokens(compiled, {
166
+ theme: "paper",
167
+ wordmark: "Example Chapter",
168
+ });
169
+ ```
170
+
171
+ This carries the compiler's complete light map, derived dark map, and invert
172
+ map into Chapter without adding an `@odla-ai/brand` runtime dependency to
173
+ Chapter itself.
174
+
137
175
  ## API quick start
138
176
 
139
177
  ```ts
@@ -157,8 +195,13 @@ export const chapter = defineChapter({
157
195
  },
158
196
  fonts: { display: "GT Sectra" },
159
197
  },
198
+ copy: {
199
+ join: { form: { submit: "Start the conversation" } },
200
+ admin: { workspaces: { people: "Community" } },
201
+ },
160
202
  prices: { standardCents: 100000, foundingDiscountCents: 10000 },
161
203
  emails: { notificationEmail: "hello@example.com" },
204
+ account: "none",
162
205
  });
163
206
  ```
164
207
 
@@ -198,6 +241,42 @@ from the approved product and brand brief plus `@odla-ai/ui` marketing
198
241
  components. Do not copy a reference site's identity or fork auth, admin routing,
199
242
  CRM, payment, booking, or account logic to achieve a different brand.
200
243
 
244
+ ### Join and member composition
245
+
246
+ `JoinIsland` keeps application mechanics while exposing presentation seams:
247
+
248
+ ```tsx
249
+ <JoinIsland
250
+ config={joinConfig}
251
+ renderStepHeader={({ state }) => <FlowHeading step={state.step} />}
252
+ renderSubmit={({ disabled, submitting }) => (
253
+ <BrandedSubmit disabled={disabled}>{submitting ? "Sending…" : "Apply"}</BrandedSubmit>
254
+ )}
255
+ renderDone={({ booked, membersHref }) => (
256
+ <Confirmation booked={booked} membersHref={membersHref} />
257
+ )}
258
+ payment={{
259
+ appearance: stripeAppearance,
260
+ fonts: stripeFonts,
261
+ renderPriceLines: (lines) => <PriceSummary lines={lines} />,
262
+ }}
263
+ >
264
+ <ApplicationFields />
265
+ </JoinIsland>
266
+ ```
267
+
268
+ `initialState` accepts trusted server state. On a browser redirect,
269
+ `JoinIsland` reads the `application` capability from the query string and asks
270
+ `GET /api/join/resume` for the canonical payment, booking, or done state; raw
271
+ `redirect_status` and `reschedule` values never choose a UI step. Supply
272
+ `loadResume` when a host stores the capability elsewhere.
273
+
274
+ `MembersArea.renderProvisional` receives the loaded application, authenticated
275
+ API function, reload callback, apply URL, and `defaultContent`, so the host can
276
+ wrap or replace the provisional card just as admin workspaces can be composed.
277
+ Nested admin/page/record tabs use fragment anchors by default, not query-string
278
+ state.
279
+
201
280
  ### Leader → follower delivery
202
281
 
203
282
  The leader declares where records may go and exactly which fields each follower
@@ -503,28 +582,23 @@ These bite silently — a smoke test won't catch them:
503
582
  **Re-check this on every chapter upgrade.** Wiring a send changes a site's
504
583
  outbound mail with no local diff — release notes call out send changes
505
584
  explicitly for that reason.
506
- - **Account model — the default is `"none"`, and that is deliberate.**
585
+ - **Account model is an explicit chapter-mode decision.**
507
586
  `account: "create"` makes the Clerk account server-side (so join can say the
508
587
  account is ready), `"invite"` **emails the applicant a Clerk invitation**, and
509
- `"none"` (the default) provisions nothing. Both non-default models need
510
- `clerk_secret_key` in the tenant vault. The default is side-effect-free on
511
- purpose: inviting mails a real person, and a site that never made that choice
512
- must not be sending mail. **You must opt in a site that wants accounts and
513
- doesn't set `account` will silently provision none.** (Changed in 0.15.0: the
514
- default was `"invite"`, which mailed applicants from a config nobody wrote.)
588
+ `"none"` provisions nothing. `defineChapter()` rejects a chapter-mode config
589
+ that omits `account`, so forgetting the decision cannot silently disable or
590
+ enable account provisioning. Both side-effecting models need
591
+ `clerk_secret_key` in the tenant vault. Hub mode resolves to inert `"none"`.
515
592
  - **What lands on the Clerk account (and its `public_metadata` is
516
- client-readable).** Both models write `public_metadata` as
517
- `{ applicationId, profile }`. By default `profile` is every configured
518
- `application.required`/`optional` field Clerk doesn't already carry natively
519
- (so *not* email/firstName/lastName), plus `focus` **which means free-text or
520
- third-party fields like `message` or `referral` are exposed to the browser
521
- unless you curate.** Set `application: { profileFields: ["phone", "state",
522
- "focus", ...] }` to an allowlist and everything else stays db-only. Array fields
593
+ client-readable).** Both side-effecting models write `applicationId` and,
594
+ when explicitly selected fields are present, `profile`. The
595
+ `application.profileFields` default is `[]`, so application details remain
596
+ db-only. Set `profileFields: ["phone", "state", "focus"]` to a deliberate
597
+ browser-readable allowlist. Array fields
523
598
  (`focus`) are clamped to `maxArrayLen` (default 100) and non-primitive elements
524
599
  dropped, so a client can't post an unbounded array into metadata.
525
600
  `applicantProfile(chapter, fields)` is exported to assert the exact shape in a
526
- test before deleting a local override. (Default is back-compat today; expected
527
- to tighten at 1.0.)
601
+ test before deleting a local override.
528
602
  - **Email + input validation.** The field literally named `email` is checked
529
603
  against a permissive email shape (400 on `"notanemail"`, so it fails cleanly
530
604
  here rather than at the downstream Clerk create) — set
@@ -549,12 +623,10 @@ These bite silently — a smoke test won't catch them:
549
623
  one integration test and a join page that quietly stops posting the flag can't
550
624
  go unnoticed.
551
625
 
552
- **If the disclaimer is a compliance record, set
553
- `application: { requireDisclaimerAck: true }`** and a submit with no ack is a
554
- 400 instead of a row with no consent. It defaults to `false` so an upgrade
555
- never starts rejecting traffic; enabling it fails *deterministically* on your
556
- first test submit, not intermittently in production. Expect this to default to
557
- `true` at 1.0.
626
+ `requireDisclaimerAck` now defaults to `true`, and a submit with no ack is a
627
+ 400 instead of a row with no consent. Set it to `false` deliberately only when
628
+ the site renders no consent control. Existing adopters must test this before
629
+ cutover.
558
630
  - **CRM projection points.** chapter projects the person on application submit
559
631
  (`projectApplicant`), not on booking or on webhook status change. If you mirror
560
632
  pipeline stage into the CRM, keep those routes.
@@ -582,7 +654,8 @@ These bite silently — a smoke test won't catch them:
582
654
  invitation mail. Importing the full `@odla-ai/chapter/ui` barrel pulls the admin
583
655
  half, so prefer the narrower entry.
584
656
  - **Known-good application set** (installs clean, no flags):
585
- `@odla-ai/chapter` 0.22.1, `@odla-ai/ui` 0.12.1, `@odla-ai/crm` 0.3.1,
657
+ `@odla-ai/chapter` 0.24.0, `@odla-ai/brand` 0.2.0,
658
+ `@odla-ai/ui` 0.12.1, `@odla-ai/crm` 0.3.1,
586
659
  `@odla-ai/db` 0.6.7,
587
660
  `@odla-ai/calendar` 0.2.0, `@odla-ai/email` 0.3.1,
588
661
  `@odla-ai/auth-clerk` 0.4.1, `@odla-ai/o11y` 2.2.2, `jose` 6.2.3, and