@odla-ai/chapter 0.22.1 → 0.23.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,51 @@
1
1
  import { CrmConfig, Crm } from '@odla-ai/crm';
2
2
  import { initAdmin } from '@odla-ai/db';
3
3
 
4
+ type TextFields<Key extends string> = {
5
+ [Field in Key]: string;
6
+ };
7
+ /**
8
+ * Complete user-visible language owned by Chapter's packaged surfaces.
9
+ *
10
+ * Copy is serializable text; use render slots when presentation needs markup and
11
+ * `formatChapterCopy()` for typed placeholders.
12
+ */
13
+ interface ChapterCopy {
14
+ common: TextFields<"loading" | "saved" | "unknown">;
15
+ join: {
16
+ form: TextFields<"submit" | "submitting" | "submitFailed" | "unexpectedFailure">;
17
+ booking: TextFields<"unavailable" | "loadFailed" | "slotTaken" | "failed" | "loading" | "book" | "booking">;
18
+ payment: TextFields<"setupFailed" | "preparing" | "pending" | "processing" | "payAndContinue" | "incomplete">;
19
+ done: TextFields<"label" | "calendarInvite" | "memberArea">;
20
+ };
21
+ members: {
22
+ loadFailed: string;
23
+ account: TextFields<"signOut" | "adminConsole">;
24
+ provisional: TextFields<"cardLabel" | "applicationNeeded" | "applicationNeededBody" | "apply" | "refunded" | "refundedBody" | "active" | "renews" | "introductionCall" | "calendarInvite" | "joinCall" | "bookCall" | "bookCallBody" | "chooseTime">;
25
+ full: TextFields<"welcome">;
26
+ reschedule: TextFields<"changeTime" | "unavailable" | "loadFailed" | "slotGone" | "loading" | "noTimes" | "rescheduling" | "keepTime">;
27
+ };
28
+ admin: {
29
+ auth: TextFields<"checking" | "notAuthorized" | "accountNotAuthorized" | "thisAccount" | "signOut" | "loading" | "signInNotConfigured" | "missingPublishableKey" | "noWorkspaces" | "signInTagline">;
30
+ shell: TextFields<"adminRole" | "adminConsole" | "adminName" | "navigationLabel">;
31
+ workspaces: TextFields<"dashboard" | "overview" | "billing" | "people" | "settings" | "calendar" | "email" | "dashboardViewsLabel" | "settingsViewsLabel" | "collectionsLabel">;
32
+ dashboard: TextFields<"loading" | "loadFailed" | "upcomingCalls" | "callsNeedAttention" | "noUpcomingCalls" | "drift" | "open" | "meet" | "applications" | "newMembers" | "newMembersUnavailable" | "revenueAdded" | "revenue" | "revenueUnavailable" | "pipeline" | "pipelineLabel" | "thisWeek" | "noChange" | "activeMemberships" | "annualRunRate" | "testMode">;
33
+ billing: TextFields<"loading" | "loadFailed" | "notConfigured" | "active" | "annualized" | "renewingSoon" | "pastDue" | "subscriptions" | "testMode" | "truncated" | "name" | "email" | "application" | "subscription" | "cancelling" | "amount" | "renews">;
34
+ availability: TextFields<"loading" | "loadFailed" | "saved" | "title" | "days" | "startHour" | "endHour" | "slotMinutes" | "minNoticeHours" | "windowDays" | "timezone" | "summaryTemplate" | "save"> & {
35
+ dayLabels: readonly string[];
36
+ };
37
+ email: TextFields<"loading" | "loadFailed" | "saved" | "sent" | "testFailed" | "delivery" | "notificationAddress" | "replyTo" | "debugInbox" | "debugInboxHint" | "sendTest" | "enabled" | "subject" | "body" | "save" | "sendLog" | "sentColumn" | "templateColumn" | "statusColumn" | "toColumn" | "failed" | "redirected" | "delivered">;
38
+ meetings: TextFields<"loading" | "loadFailed" | "cancelConfirm" | "newStartPrompt" | "parseFailed" | "agenda" | "empty" | "unknown" | "drift" | "meet" | "reschedule" | "cancel">;
39
+ network: TextFields<"title" | "allowlistDescription" | "sharing" | "shareWith" | "shared" | "shareFailed" | "deliveryFailed">;
40
+ records: TextFields<"workflowMissing" | "comms" | "commsHistory" | "scheduling" | "billing" | "notes" | "access" | "sharing" | "lifecycle" | "role" | "superAdminHint" | "noAccount" | "approve" | "refund" | "approved" | "refunded" | "roleSet" | "messageSent" | "messageNotSent" | "loadingTemplates" | "template" | "chooseTemplate" | "sending" | "send" | "noMessages" | "message" | "newStartPrompt" | "parseFailed" | "cancelConfirm" | "callCancelled" | "callRescheduled" | "noApplication" | "loadingMeetings" | "noMeetings" | "joinMeeting" | "openCalendar" | "reschedule" | "cancel">;
41
+ };
42
+ }
43
+ type DeepPartial<T> = T extends readonly (infer Item)[] ? readonly Item[] : T extends object ? {
44
+ [Key in keyof T]?: DeepPartial<T[Key]>;
45
+ } : T;
46
+ /** Recursively partial copy overrides accepted by `defineChapter({ copy })`. */
47
+ type ChapterCopyInput = DeepPartial<ChapterCopy>;
48
+
4
49
  /** Which feature profile a site runs. `chapter` is the full public member site
5
50
  * (join, Stripe membership, booking, member area, admin, CRM); `hub` is
6
51
  * admin-only and CRM-focused (a directory/registry over the same CRM). */
@@ -72,6 +117,8 @@ interface ChapterBrandTokens {
72
117
  interface ChapterBrand {
73
118
  /** Preloaded @odla-ai/ui theme name used by ThemeScope. */
74
119
  theme?: string;
120
+ /** Named accent family supplied by the selected @odla-ai/ui theme. */
121
+ accent?: string;
75
122
  /** Admin/application color mode. Explicit light is the safe default. */
76
123
  colorScheme?: "light" | "dark" | "system";
77
124
  tokens?: ChapterBrandTokens;
@@ -84,6 +131,9 @@ interface ChapterBrand {
84
131
  * `:root[data-theme="dark"]` and `@media (prefers-color-scheme: dark)`, so a
85
132
  * site brands both modes. */
86
133
  paletteDark?: Record<string, string>;
134
+ /** Full token map for `.ui-invert` islands. A compiled custom theme normally
135
+ * uses its dark map here so inverted content never inherits stale composites. */
136
+ paletteInvert?: Record<string, string>;
87
137
  fonts?: {
88
138
  display?: string;
89
139
  body?: string;
@@ -239,17 +289,13 @@ interface ChapterApplication {
239
289
  /** Max JSON request body in bytes. Default 32768. */
240
290
  bodyCap?: number;
241
291
  /** Reject a submit that carries no truthy `disclaimerAck` (400), instead of
242
- * writing a row with no consent record. Default `false` for back-compat —
243
- * but turn it on if the disclaimer is a compliance record: a missing ack is
244
- * otherwise silent, permanent and unreconstructible. Failure is deterministic
245
- * and surfaces on the first test submit, not intermittently in production. */
292
+ * writing a row with no consent record. Default `true`. Set `false`
293
+ * deliberately only when the site renders no consent control. */
246
294
  requireDisclaimerAck?: boolean;
247
295
  /** Allowlist of fields that reach the Clerk account's client-readable
248
- * `public_metadata.profile`. Default (unset) projects every non-identity
249
- * configured field convenient, but it also exposes free-text and
250
- * third-party fields (`message`, `referral`). Set this to a curated list
251
- * (e.g. `["phone", "state", "focus"]`) to keep confidential fields db-only.
252
- * Expected to become required-in-spirit at 1.0. */
296
+ * `public_metadata.profile`. Default `[]`, so application details remain
297
+ * db-only. Set a curated list (e.g. `["phone", "state", "focus"]`) for
298
+ * fields the browser may read. */
253
299
  profileFields?: readonly string[];
254
300
  /** Extra application fields carried into the one-way CRM projection, on top of
255
301
  * the built-in identity/contact set. Each MUST be declared on your crm person
@@ -273,8 +319,8 @@ interface ResolvedApplication {
273
319
  defaultMaxLen: number;
274
320
  bodyCap: number;
275
321
  requireDisclaimerAck: boolean;
276
- /** Resolved Clerk-metadata allowlist; `null` means "all non-identity fields". */
277
- profileFields: readonly string[] | null;
322
+ /** Resolved Clerk-metadata allowlist. Empty means no profile projection. */
323
+ profileFields: readonly string[];
278
324
  crmFields: readonly string[];
279
325
  maxArrayLen: number;
280
326
  validateEmail: boolean;
@@ -290,6 +336,9 @@ interface ChapterConfig {
290
336
  /** A `defineCrm()` config or a resolved `Crm`. Omit for the per-mode default. */
291
337
  crm?: CrmConfig | Crm;
292
338
  brand?: ChapterBrand;
339
+ /** User-visible language for packaged Chapter surfaces. Recursively partial;
340
+ * `defineChapter()` resolves every omitted leaf from the package defaults. */
341
+ copy?: ChapterCopyInput;
293
342
  /** Leader → follower delivery targets. Secret values stay in the leader
294
343
  * tenant vault; see {@link ChapterNetworkTarget}. */
295
344
  network?: ChapterNetwork;
@@ -308,13 +357,10 @@ interface ChapterConfig {
308
357
  auth?: ChapterAuth;
309
358
  /** odla services (db implied). Default `["db","calendar","o11y"]`. */
310
359
  services?: readonly string[];
311
- /** Apply-time account provisioning. **Default `"none"`** it provisions
312
- * nothing, because the alternatives have an outbound side effect and a site
313
- * that never made the choice must not be mailing people. `"create"` makes the
314
- * Clerk account server-side (so join can say the account is ready);
315
- * `"invite"` **emails the applicant a Clerk invitation**. Both non-default
316
- * models need a `clerk_secret_key` vault secret to act. Opt in explicitly —
317
- * leaving this unset provisions no accounts. */
360
+ /** Apply-time account provisioning. Required in `chapter` mode so a site must
361
+ * make the decision explicitly. `"none"` provisions nothing, `"create"` makes
362
+ * the Clerk account server-side, and `"invite"` emails a real invitation.
363
+ * Both side-effecting models need a `clerk_secret_key` vault secret. */
318
364
  account?: AccountModel;
319
365
  /** WHEN lifecycle email fires. Addressing and content live on the group row
320
366
  * (owner-editable at runtime); this is the trigger, which is a build-time
@@ -382,6 +428,8 @@ interface Chapter {
382
428
  /** Resolved site identity. `wordmark` always falls back to `name`, so the
383
429
  * admin/member UI never needs a second brand declaration. */
384
430
  brand: ChapterBrand;
431
+ /** Fully resolved user-visible text for packaged Chapter surfaces. */
432
+ copy: ChapterCopy;
385
433
  /** Validated follower targets for leader-driven record pushes. */
386
434
  network: ResolvedNetwork;
387
435
  /** Resolved CRM engine (from `defineCrm`). */
@@ -396,7 +444,7 @@ interface Chapter {
396
444
  schema: DbSchema;
397
445
  rules: DbRules;
398
446
  services: readonly string[];
399
- /** Resolved apply-time account provisioning model (default `"none"`). */
447
+ /** Resolved apply-time account provisioning model. */
400
448
  account: AccountModel;
401
449
  /** Resolved send policy — when each lifecycle email fires. */
402
450
  sends: ResolvedSends;
@@ -1,6 +1,51 @@
1
1
  import { CrmConfig, Crm } from '@odla-ai/crm';
2
2
  import { initAdmin } from '@odla-ai/db';
3
3
 
4
+ type TextFields<Key extends string> = {
5
+ [Field in Key]: string;
6
+ };
7
+ /**
8
+ * Complete user-visible language owned by Chapter's packaged surfaces.
9
+ *
10
+ * Copy is serializable text; use render slots when presentation needs markup and
11
+ * `formatChapterCopy()` for typed placeholders.
12
+ */
13
+ interface ChapterCopy {
14
+ common: TextFields<"loading" | "saved" | "unknown">;
15
+ join: {
16
+ form: TextFields<"submit" | "submitting" | "submitFailed" | "unexpectedFailure">;
17
+ booking: TextFields<"unavailable" | "loadFailed" | "slotTaken" | "failed" | "loading" | "book" | "booking">;
18
+ payment: TextFields<"setupFailed" | "preparing" | "pending" | "processing" | "payAndContinue" | "incomplete">;
19
+ done: TextFields<"label" | "calendarInvite" | "memberArea">;
20
+ };
21
+ members: {
22
+ loadFailed: string;
23
+ account: TextFields<"signOut" | "adminConsole">;
24
+ provisional: TextFields<"cardLabel" | "applicationNeeded" | "applicationNeededBody" | "apply" | "refunded" | "refundedBody" | "active" | "renews" | "introductionCall" | "calendarInvite" | "joinCall" | "bookCall" | "bookCallBody" | "chooseTime">;
25
+ full: TextFields<"welcome">;
26
+ reschedule: TextFields<"changeTime" | "unavailable" | "loadFailed" | "slotGone" | "loading" | "noTimes" | "rescheduling" | "keepTime">;
27
+ };
28
+ admin: {
29
+ auth: TextFields<"checking" | "notAuthorized" | "accountNotAuthorized" | "thisAccount" | "signOut" | "loading" | "signInNotConfigured" | "missingPublishableKey" | "noWorkspaces" | "signInTagline">;
30
+ shell: TextFields<"adminRole" | "adminConsole" | "adminName" | "navigationLabel">;
31
+ workspaces: TextFields<"dashboard" | "overview" | "billing" | "people" | "settings" | "calendar" | "email" | "dashboardViewsLabel" | "settingsViewsLabel" | "collectionsLabel">;
32
+ dashboard: TextFields<"loading" | "loadFailed" | "upcomingCalls" | "callsNeedAttention" | "noUpcomingCalls" | "drift" | "open" | "meet" | "applications" | "newMembers" | "newMembersUnavailable" | "revenueAdded" | "revenue" | "revenueUnavailable" | "pipeline" | "pipelineLabel" | "thisWeek" | "noChange" | "activeMemberships" | "annualRunRate" | "testMode">;
33
+ billing: TextFields<"loading" | "loadFailed" | "notConfigured" | "active" | "annualized" | "renewingSoon" | "pastDue" | "subscriptions" | "testMode" | "truncated" | "name" | "email" | "application" | "subscription" | "cancelling" | "amount" | "renews">;
34
+ availability: TextFields<"loading" | "loadFailed" | "saved" | "title" | "days" | "startHour" | "endHour" | "slotMinutes" | "minNoticeHours" | "windowDays" | "timezone" | "summaryTemplate" | "save"> & {
35
+ dayLabels: readonly string[];
36
+ };
37
+ email: TextFields<"loading" | "loadFailed" | "saved" | "sent" | "testFailed" | "delivery" | "notificationAddress" | "replyTo" | "debugInbox" | "debugInboxHint" | "sendTest" | "enabled" | "subject" | "body" | "save" | "sendLog" | "sentColumn" | "templateColumn" | "statusColumn" | "toColumn" | "failed" | "redirected" | "delivered">;
38
+ meetings: TextFields<"loading" | "loadFailed" | "cancelConfirm" | "newStartPrompt" | "parseFailed" | "agenda" | "empty" | "unknown" | "drift" | "meet" | "reschedule" | "cancel">;
39
+ network: TextFields<"title" | "allowlistDescription" | "sharing" | "shareWith" | "shared" | "shareFailed" | "deliveryFailed">;
40
+ records: TextFields<"workflowMissing" | "comms" | "commsHistory" | "scheduling" | "billing" | "notes" | "access" | "sharing" | "lifecycle" | "role" | "superAdminHint" | "noAccount" | "approve" | "refund" | "approved" | "refunded" | "roleSet" | "messageSent" | "messageNotSent" | "loadingTemplates" | "template" | "chooseTemplate" | "sending" | "send" | "noMessages" | "message" | "newStartPrompt" | "parseFailed" | "cancelConfirm" | "callCancelled" | "callRescheduled" | "noApplication" | "loadingMeetings" | "noMeetings" | "joinMeeting" | "openCalendar" | "reschedule" | "cancel">;
41
+ };
42
+ }
43
+ type DeepPartial<T> = T extends readonly (infer Item)[] ? readonly Item[] : T extends object ? {
44
+ [Key in keyof T]?: DeepPartial<T[Key]>;
45
+ } : T;
46
+ /** Recursively partial copy overrides accepted by `defineChapter({ copy })`. */
47
+ type ChapterCopyInput = DeepPartial<ChapterCopy>;
48
+
4
49
  /** Which feature profile a site runs. `chapter` is the full public member site
5
50
  * (join, Stripe membership, booking, member area, admin, CRM); `hub` is
6
51
  * admin-only and CRM-focused (a directory/registry over the same CRM). */
@@ -72,6 +117,8 @@ interface ChapterBrandTokens {
72
117
  interface ChapterBrand {
73
118
  /** Preloaded @odla-ai/ui theme name used by ThemeScope. */
74
119
  theme?: string;
120
+ /** Named accent family supplied by the selected @odla-ai/ui theme. */
121
+ accent?: string;
75
122
  /** Admin/application color mode. Explicit light is the safe default. */
76
123
  colorScheme?: "light" | "dark" | "system";
77
124
  tokens?: ChapterBrandTokens;
@@ -84,6 +131,9 @@ interface ChapterBrand {
84
131
  * `:root[data-theme="dark"]` and `@media (prefers-color-scheme: dark)`, so a
85
132
  * site brands both modes. */
86
133
  paletteDark?: Record<string, string>;
134
+ /** Full token map for `.ui-invert` islands. A compiled custom theme normally
135
+ * uses its dark map here so inverted content never inherits stale composites. */
136
+ paletteInvert?: Record<string, string>;
87
137
  fonts?: {
88
138
  display?: string;
89
139
  body?: string;
@@ -239,17 +289,13 @@ interface ChapterApplication {
239
289
  /** Max JSON request body in bytes. Default 32768. */
240
290
  bodyCap?: number;
241
291
  /** Reject a submit that carries no truthy `disclaimerAck` (400), instead of
242
- * writing a row with no consent record. Default `false` for back-compat —
243
- * but turn it on if the disclaimer is a compliance record: a missing ack is
244
- * otherwise silent, permanent and unreconstructible. Failure is deterministic
245
- * and surfaces on the first test submit, not intermittently in production. */
292
+ * writing a row with no consent record. Default `true`. Set `false`
293
+ * deliberately only when the site renders no consent control. */
246
294
  requireDisclaimerAck?: boolean;
247
295
  /** Allowlist of fields that reach the Clerk account's client-readable
248
- * `public_metadata.profile`. Default (unset) projects every non-identity
249
- * configured field convenient, but it also exposes free-text and
250
- * third-party fields (`message`, `referral`). Set this to a curated list
251
- * (e.g. `["phone", "state", "focus"]`) to keep confidential fields db-only.
252
- * Expected to become required-in-spirit at 1.0. */
296
+ * `public_metadata.profile`. Default `[]`, so application details remain
297
+ * db-only. Set a curated list (e.g. `["phone", "state", "focus"]`) for
298
+ * fields the browser may read. */
253
299
  profileFields?: readonly string[];
254
300
  /** Extra application fields carried into the one-way CRM projection, on top of
255
301
  * the built-in identity/contact set. Each MUST be declared on your crm person
@@ -273,8 +319,8 @@ interface ResolvedApplication {
273
319
  defaultMaxLen: number;
274
320
  bodyCap: number;
275
321
  requireDisclaimerAck: boolean;
276
- /** Resolved Clerk-metadata allowlist; `null` means "all non-identity fields". */
277
- profileFields: readonly string[] | null;
322
+ /** Resolved Clerk-metadata allowlist. Empty means no profile projection. */
323
+ profileFields: readonly string[];
278
324
  crmFields: readonly string[];
279
325
  maxArrayLen: number;
280
326
  validateEmail: boolean;
@@ -290,6 +336,9 @@ interface ChapterConfig {
290
336
  /** A `defineCrm()` config or a resolved `Crm`. Omit for the per-mode default. */
291
337
  crm?: CrmConfig | Crm;
292
338
  brand?: ChapterBrand;
339
+ /** User-visible language for packaged Chapter surfaces. Recursively partial;
340
+ * `defineChapter()` resolves every omitted leaf from the package defaults. */
341
+ copy?: ChapterCopyInput;
293
342
  /** Leader → follower delivery targets. Secret values stay in the leader
294
343
  * tenant vault; see {@link ChapterNetworkTarget}. */
295
344
  network?: ChapterNetwork;
@@ -308,13 +357,10 @@ interface ChapterConfig {
308
357
  auth?: ChapterAuth;
309
358
  /** odla services (db implied). Default `["db","calendar","o11y"]`. */
310
359
  services?: readonly string[];
311
- /** Apply-time account provisioning. **Default `"none"`** it provisions
312
- * nothing, because the alternatives have an outbound side effect and a site
313
- * that never made the choice must not be mailing people. `"create"` makes the
314
- * Clerk account server-side (so join can say the account is ready);
315
- * `"invite"` **emails the applicant a Clerk invitation**. Both non-default
316
- * models need a `clerk_secret_key` vault secret to act. Opt in explicitly —
317
- * leaving this unset provisions no accounts. */
360
+ /** Apply-time account provisioning. Required in `chapter` mode so a site must
361
+ * make the decision explicitly. `"none"` provisions nothing, `"create"` makes
362
+ * the Clerk account server-side, and `"invite"` emails a real invitation.
363
+ * Both side-effecting models need a `clerk_secret_key` vault secret. */
318
364
  account?: AccountModel;
319
365
  /** WHEN lifecycle email fires. Addressing and content live on the group row
320
366
  * (owner-editable at runtime); this is the trigger, which is a build-time
@@ -382,6 +428,8 @@ interface Chapter {
382
428
  /** Resolved site identity. `wordmark` always falls back to `name`, so the
383
429
  * admin/member UI never needs a second brand declaration. */
384
430
  brand: ChapterBrand;
431
+ /** Fully resolved user-visible text for packaged Chapter surfaces. */
432
+ copy: ChapterCopy;
385
433
  /** Validated follower targets for leader-driven record pushes. */
386
434
  network: ResolvedNetwork;
387
435
  /** Resolved CRM engine (from `defineCrm`). */
@@ -396,7 +444,7 @@ interface Chapter {
396
444
  schema: DbSchema;
397
445
  rules: DbRules;
398
446
  services: readonly string[];
399
- /** Resolved apply-time account provisioning model (default `"none"`). */
447
+ /** Resolved apply-time account provisioning model. */
400
448
  account: AccountModel;
401
449
  /** Resolved send policy — when each lifecycle email fires. */
402
450
  sends: ResolvedSends;