@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.
- package/README.md +99 -26
- package/dist/chunk-NTZSXAFT.js +411 -0
- package/dist/chunk-NTZSXAFT.js.map +1 -0
- package/dist/{chunk-4COPR4SY.js → chunk-OYXNI6VR.js} +266 -84
- package/dist/chunk-OYXNI6VR.js.map +1 -0
- package/dist/{chunk-C4XQSRW7.js → chunk-VCC4S6JU.js} +199 -171
- package/dist/chunk-VCC4S6JU.js.map +1 -0
- package/dist/{types-CgWUQ2MH.d.ts → copy-context-pDogTR4x.d.ts} +78 -20
- package/dist/index.cjs +322 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +90 -24
- package/dist/index.d.ts +90 -24
- package/dist/index.js +322 -3
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +5 -2
- package/dist/ui/admin/index.js +8 -3
- package/dist/ui/index.d.ts +2 -2
- package/dist/ui/index.js +11 -5
- package/dist/ui/member/index.d.ts +118 -2
- package/dist/ui/member/index.js +10 -4
- package/dist/worker/index.cjs +180 -130
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +67 -19
- package/dist/worker/index.d.ts +67 -19
- package/dist/worker/index.js +180 -130
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
- package/runbooks/adopt-existing.md +23 -5
- package/runbooks/greenfield.md +85 -4
- package/dist/chunk-3QKGKUTX.js +0 -91
- package/dist/chunk-3QKGKUTX.js.map +0 -1
- package/dist/chunk-4COPR4SY.js.map +0 -1
- package/dist/chunk-C4XQSRW7.js.map +0 -1
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.
|
|
11
|
+
npm i --save-exact @odla-ai/chapter@0.23.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
|
-
|
|
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
|
|
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"`
|
|
510
|
-
`
|
|
511
|
-
|
|
512
|
-
|
|
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 `
|
|
517
|
-
|
|
518
|
-
`application.
|
|
519
|
-
|
|
520
|
-
|
|
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.
|
|
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
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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.
|
|
657
|
+
`@odla-ai/chapter` 0.23.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
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
// src/ui/copy-context.tsx
|
|
2
|
+
import { createContext } from "preact";
|
|
3
|
+
import { useContext } from "preact/hooks";
|
|
4
|
+
|
|
5
|
+
// src/copy-defaults-admin.ts
|
|
6
|
+
var DEFAULT_ADMIN_COPY = {
|
|
7
|
+
auth: {
|
|
8
|
+
checking: "Checking access\u2026",
|
|
9
|
+
notAuthorized: "Not authorized",
|
|
10
|
+
accountNotAuthorized: "{account} isn't on the admin list.",
|
|
11
|
+
thisAccount: "This account",
|
|
12
|
+
signOut: "Sign out",
|
|
13
|
+
loading: "Loading\u2026",
|
|
14
|
+
signInNotConfigured: "Sign-in not configured",
|
|
15
|
+
missingPublishableKey: "No Clerk publishable key is set for this environment yet.",
|
|
16
|
+
noWorkspaces: "No admin workspaces are configured.",
|
|
17
|
+
signInTagline: "Admin sign-in, invite only"
|
|
18
|
+
},
|
|
19
|
+
shell: {
|
|
20
|
+
adminRole: "admin",
|
|
21
|
+
adminConsole: "Admin Console",
|
|
22
|
+
adminName: "Admin",
|
|
23
|
+
navigationLabel: "Admin navigation"
|
|
24
|
+
},
|
|
25
|
+
workspaces: {
|
|
26
|
+
dashboard: "Dashboard",
|
|
27
|
+
overview: "Overview",
|
|
28
|
+
billing: "Billing",
|
|
29
|
+
people: "People",
|
|
30
|
+
settings: "Settings",
|
|
31
|
+
calendar: "Calendar",
|
|
32
|
+
email: "Email",
|
|
33
|
+
dashboardViewsLabel: "Dashboard views",
|
|
34
|
+
settingsViewsLabel: "Settings views",
|
|
35
|
+
collectionsLabel: "CRM collections"
|
|
36
|
+
},
|
|
37
|
+
dashboard: {
|
|
38
|
+
loading: "Loading dashboard\u2026",
|
|
39
|
+
loadFailed: "Couldn't load the dashboard",
|
|
40
|
+
upcomingCalls: "Upcoming calls",
|
|
41
|
+
callsNeedAttention: "{count} need attention",
|
|
42
|
+
noUpcomingCalls: "No upcoming calls.",
|
|
43
|
+
drift: "drift",
|
|
44
|
+
open: "Open",
|
|
45
|
+
meet: "Meet",
|
|
46
|
+
applications: "Applications",
|
|
47
|
+
newMembers: "New members",
|
|
48
|
+
newMembersUnavailable: "Appears once billing is connected.",
|
|
49
|
+
revenueAdded: "Revenue \xB7 annual run rate added",
|
|
50
|
+
revenue: "Revenue",
|
|
51
|
+
revenueUnavailable: "Appears once billing is connected.",
|
|
52
|
+
pipeline: "Pipeline",
|
|
53
|
+
pipelineLabel: "Membership pipeline",
|
|
54
|
+
thisWeek: "+{count} this week",
|
|
55
|
+
noChange: "no change",
|
|
56
|
+
activeMemberships: "Active memberships",
|
|
57
|
+
annualRunRate: "Annual run rate",
|
|
58
|
+
testMode: "test mode"
|
|
59
|
+
},
|
|
60
|
+
billing: {
|
|
61
|
+
loading: "Loading\u2026",
|
|
62
|
+
loadFailed: "Couldn't load billing",
|
|
63
|
+
notConfigured: "Billing isn't configured. No Stripe key is vaulted for this group.",
|
|
64
|
+
active: "Active",
|
|
65
|
+
annualized: "Annualized",
|
|
66
|
+
renewingSoon: "Renewing within 60 days",
|
|
67
|
+
pastDue: "Past due",
|
|
68
|
+
subscriptions: "Subscriptions",
|
|
69
|
+
testMode: "Stripe test mode",
|
|
70
|
+
truncated: "Showing the first 100 subscriptions. The list is truncated.",
|
|
71
|
+
name: "Name",
|
|
72
|
+
email: "Email",
|
|
73
|
+
application: "Application",
|
|
74
|
+
subscription: "Subscription",
|
|
75
|
+
cancelling: "cancelling",
|
|
76
|
+
amount: "Amount",
|
|
77
|
+
renews: "Renews"
|
|
78
|
+
},
|
|
79
|
+
availability: {
|
|
80
|
+
loading: "Loading\u2026",
|
|
81
|
+
loadFailed: "Couldn't load availability",
|
|
82
|
+
saved: "Saved.",
|
|
83
|
+
title: "Booking availability",
|
|
84
|
+
days: "Days",
|
|
85
|
+
startHour: "Start hour (0\u201324)",
|
|
86
|
+
endHour: "End hour (0\u201324)",
|
|
87
|
+
slotMinutes: "Slot length (minutes)",
|
|
88
|
+
minNoticeHours: "Minimum notice (hours)",
|
|
89
|
+
windowDays: "Booking window (days)",
|
|
90
|
+
timezone: "Timezone",
|
|
91
|
+
summaryTemplate: "Event summary template",
|
|
92
|
+
save: "Save availability",
|
|
93
|
+
dayLabels: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
|
|
94
|
+
},
|
|
95
|
+
email: {
|
|
96
|
+
loading: "Loading\u2026",
|
|
97
|
+
loadFailed: "Couldn't load email config",
|
|
98
|
+
saved: "Saved.",
|
|
99
|
+
sent: "Sent {template} to {address}{redirected}.",
|
|
100
|
+
testFailed: "Test send did not deliver.",
|
|
101
|
+
delivery: "Delivery",
|
|
102
|
+
notificationAddress: "Notification address",
|
|
103
|
+
replyTo: "Reply-to",
|
|
104
|
+
debugInbox: "Debug inbox",
|
|
105
|
+
debugInboxHint: "Non-production sends redirect here.",
|
|
106
|
+
sendTest: "Send test",
|
|
107
|
+
enabled: "Enabled",
|
|
108
|
+
subject: "Subject",
|
|
109
|
+
body: "Body",
|
|
110
|
+
save: "Save changes",
|
|
111
|
+
sendLog: "Send log",
|
|
112
|
+
sentColumn: "Sent",
|
|
113
|
+
templateColumn: "Template",
|
|
114
|
+
statusColumn: "Status",
|
|
115
|
+
toColumn: "To",
|
|
116
|
+
failed: "failed",
|
|
117
|
+
redirected: "redirected",
|
|
118
|
+
delivered: "delivered"
|
|
119
|
+
},
|
|
120
|
+
meetings: {
|
|
121
|
+
loading: "Loading\u2026",
|
|
122
|
+
loadFailed: "Couldn't load meetings",
|
|
123
|
+
cancelConfirm: "Cancel this call? The calendar provider notifies the attendee.",
|
|
124
|
+
newStartPrompt: "New start time (for example, 2026-08-01 14:00):",
|
|
125
|
+
parseFailed: "Couldn't parse that time.",
|
|
126
|
+
agenda: "Agenda",
|
|
127
|
+
empty: "No meetings.",
|
|
128
|
+
unknown: "(unknown)",
|
|
129
|
+
drift: "drift",
|
|
130
|
+
meet: "Meet",
|
|
131
|
+
reschedule: "Reschedule",
|
|
132
|
+
cancel: "Cancel"
|
|
133
|
+
},
|
|
134
|
+
network: {
|
|
135
|
+
title: "Share to a follower",
|
|
136
|
+
allowlistDescription: "Sends the target's allowlisted fields. Its pipeline and account state remain local.",
|
|
137
|
+
sharing: "Sharing\u2026",
|
|
138
|
+
shareWith: "Share with {target}",
|
|
139
|
+
shared: "Shared with {target}.",
|
|
140
|
+
shareFailed: "Could not share with {target}.",
|
|
141
|
+
deliveryFailed: "Delivery failed."
|
|
142
|
+
},
|
|
143
|
+
records: {
|
|
144
|
+
workflowMissing: "This record is not linked to an application workflow.",
|
|
145
|
+
comms: "Comms",
|
|
146
|
+
commsHistory: "Comms history",
|
|
147
|
+
scheduling: "Scheduling",
|
|
148
|
+
billing: "Billing",
|
|
149
|
+
notes: "Notes",
|
|
150
|
+
access: "Access",
|
|
151
|
+
sharing: "Sharing",
|
|
152
|
+
lifecycle: "Access & lifecycle",
|
|
153
|
+
role: "Role",
|
|
154
|
+
superAdminHint: "Super-admin, managed in odla Studio.",
|
|
155
|
+
noAccount: "No linked account yet. The role is set once they sign in.",
|
|
156
|
+
approve: "Approve",
|
|
157
|
+
refund: "Refund",
|
|
158
|
+
approved: "Approved.",
|
|
159
|
+
refunded: "Refund issued.",
|
|
160
|
+
roleSet: "Role set to {role}.",
|
|
161
|
+
messageSent: "Message sent.",
|
|
162
|
+
messageNotSent: "Message was not sent.",
|
|
163
|
+
loadingTemplates: "Loading templates\u2026",
|
|
164
|
+
template: "Template",
|
|
165
|
+
chooseTemplate: "Choose a template\u2026",
|
|
166
|
+
sending: "Sending\u2026",
|
|
167
|
+
send: "Send",
|
|
168
|
+
noMessages: "No messages recorded.",
|
|
169
|
+
message: "Message",
|
|
170
|
+
newStartPrompt: "New start time:",
|
|
171
|
+
parseFailed: "Couldn't parse that time.",
|
|
172
|
+
cancelConfirm: "Cancel this call?",
|
|
173
|
+
callCancelled: "Call cancelled.",
|
|
174
|
+
callRescheduled: "Call rescheduled.",
|
|
175
|
+
noApplication: "No application is linked to this record.",
|
|
176
|
+
loadingMeetings: "Loading meetings\u2026",
|
|
177
|
+
noMeetings: "No meetings recorded.",
|
|
178
|
+
joinMeeting: "Join meeting",
|
|
179
|
+
openCalendar: "Open calendar",
|
|
180
|
+
reschedule: "Reschedule",
|
|
181
|
+
cancel: "Cancel"
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// src/copy-defaults.ts
|
|
186
|
+
var DEFAULT_COMMON_COPY = {
|
|
187
|
+
loading: "Loading\u2026",
|
|
188
|
+
saved: "Saved.",
|
|
189
|
+
unknown: "(unknown)"
|
|
190
|
+
};
|
|
191
|
+
var DEFAULT_JOIN_COPY = {
|
|
192
|
+
form: {
|
|
193
|
+
submit: "Submit application",
|
|
194
|
+
submitting: "Submitting\u2026",
|
|
195
|
+
submitFailed: "Your application could not be submitted.",
|
|
196
|
+
unexpectedFailure: "Something went wrong. Please try again."
|
|
197
|
+
},
|
|
198
|
+
booking: {
|
|
199
|
+
unavailable: "Scheduling is briefly unavailable. We'll reach out by email to arrange your call.",
|
|
200
|
+
loadFailed: "Times are briefly unavailable. Please try again.",
|
|
201
|
+
slotTaken: "That time was just taken. Here are the current openings.",
|
|
202
|
+
failed: "The booking could not be completed.",
|
|
203
|
+
loading: "Loading available times\u2026",
|
|
204
|
+
book: "Book this time",
|
|
205
|
+
booking: "Booking\u2026"
|
|
206
|
+
},
|
|
207
|
+
payment: {
|
|
208
|
+
setupFailed: "Payment could not be set up. Please try again.",
|
|
209
|
+
preparing: "Preparing secure payment\u2026",
|
|
210
|
+
pending: "Confirming your payment\u2026",
|
|
211
|
+
processing: "Processing\u2026",
|
|
212
|
+
payAndContinue: "Pay and continue",
|
|
213
|
+
incomplete: "The payment did not complete. Please try again."
|
|
214
|
+
},
|
|
215
|
+
done: {
|
|
216
|
+
label: "You're booked",
|
|
217
|
+
calendarInvite: "A calendar invitation with the video call link is on its way to your email.",
|
|
218
|
+
memberArea: "Go to your member area"
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
var DEFAULT_MEMBERS_COPY = {
|
|
222
|
+
loadFailed: "Sign in is briefly unavailable. Please refresh.",
|
|
223
|
+
account: {
|
|
224
|
+
signOut: "Sign out",
|
|
225
|
+
adminConsole: "Admin console"
|
|
226
|
+
},
|
|
227
|
+
provisional: {
|
|
228
|
+
cardLabel: "Your Application",
|
|
229
|
+
applicationNeeded: "One step remains",
|
|
230
|
+
applicationNeededBody: "Your account is ready, and the application that completes it takes a few minutes.",
|
|
231
|
+
apply: "Apply for membership",
|
|
232
|
+
refunded: "Membership refunded",
|
|
233
|
+
refundedBody: "Your fee has been refunded in full and your membership is canceled.",
|
|
234
|
+
active: "Your membership is active.",
|
|
235
|
+
renews: "Your membership is active and renews {date}.",
|
|
236
|
+
introductionCall: "Your introduction call",
|
|
237
|
+
calendarInvite: "A calendar invitation with the video call link is in your email.",
|
|
238
|
+
joinCall: "Join the video call",
|
|
239
|
+
bookCall: "Book your introduction call",
|
|
240
|
+
bookCallBody: "Your application is in. Choose a time below, and a calendar invitation will reach your email.",
|
|
241
|
+
chooseTime: "Choose a time"
|
|
242
|
+
},
|
|
243
|
+
full: {
|
|
244
|
+
welcome: "Welcome back."
|
|
245
|
+
},
|
|
246
|
+
reschedule: {
|
|
247
|
+
changeTime: "Change your time",
|
|
248
|
+
unavailable: "Scheduling is briefly unavailable. We'll reach out by email to arrange your call.",
|
|
249
|
+
loadFailed: "Times are briefly unavailable. Please try again.",
|
|
250
|
+
slotGone: "That time is no longer available. Please pick another.",
|
|
251
|
+
loading: "Loading available times\u2026",
|
|
252
|
+
noTimes: "No open times right now. Please check back soon.",
|
|
253
|
+
rescheduling: "Rescheduling\u2026",
|
|
254
|
+
keepTime: "Keep my current time"
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// src/copy-resolve.ts
|
|
259
|
+
var DEFAULT_CHAPTER_COPY = {
|
|
260
|
+
common: DEFAULT_COMMON_COPY,
|
|
261
|
+
join: DEFAULT_JOIN_COPY,
|
|
262
|
+
members: DEFAULT_MEMBERS_COPY,
|
|
263
|
+
admin: DEFAULT_ADMIN_COPY
|
|
264
|
+
};
|
|
265
|
+
var isObject = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
266
|
+
function mergeCopy(base, input, path) {
|
|
267
|
+
if (typeof base === "string") {
|
|
268
|
+
if (input === void 0) return base;
|
|
269
|
+
if (typeof input !== "string") throw new Error(`${path}: must be a string`);
|
|
270
|
+
return input;
|
|
271
|
+
}
|
|
272
|
+
if (Array.isArray(base)) {
|
|
273
|
+
if (input === void 0) return [...base];
|
|
274
|
+
if (!Array.isArray(input) || !input.every((item) => typeof item === "string")) {
|
|
275
|
+
throw new Error(`${path}: must be an array of strings`);
|
|
276
|
+
}
|
|
277
|
+
return [...input];
|
|
278
|
+
}
|
|
279
|
+
if (!isObject(base)) return base;
|
|
280
|
+
if (input !== void 0 && !isObject(input)) throw new Error(`${path}: must be an object`);
|
|
281
|
+
const overrides = input;
|
|
282
|
+
const result = {};
|
|
283
|
+
for (const [key, value] of Object.entries(base)) {
|
|
284
|
+
result[key] = mergeCopy(value, overrides?.[key], `${path}.${key}`);
|
|
285
|
+
}
|
|
286
|
+
return result;
|
|
287
|
+
}
|
|
288
|
+
function resolveChapterCopy(input) {
|
|
289
|
+
return mergeCopy(DEFAULT_CHAPTER_COPY, input, "defineChapter.copy");
|
|
290
|
+
}
|
|
291
|
+
function formatChapterCopy(template, values) {
|
|
292
|
+
return template.replace(
|
|
293
|
+
/\{([a-zA-Z][a-zA-Z0-9]*)\}/g,
|
|
294
|
+
(match, key) => Object.prototype.hasOwnProperty.call(values, key) ? String(values[key]) : match
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// src/ui/copy-context.tsx
|
|
299
|
+
import { jsx } from "preact/jsx-runtime";
|
|
300
|
+
var ChapterCopyContext = createContext(DEFAULT_CHAPTER_COPY);
|
|
301
|
+
function ChapterCopyProvider(props) {
|
|
302
|
+
return /* @__PURE__ */ jsx(ChapterCopyContext.Provider, { value: resolveChapterCopy(props.copy), children: props.children });
|
|
303
|
+
}
|
|
304
|
+
function useChapterCopy() {
|
|
305
|
+
return useContext(ChapterCopyContext);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// src/brand.ts
|
|
309
|
+
function paletteVar(key) {
|
|
310
|
+
return key.startsWith("--") ? key : `--${key}`;
|
|
311
|
+
}
|
|
312
|
+
function cleanValue(value) {
|
|
313
|
+
return value.replace(/[<>{};]/g, "").trim();
|
|
314
|
+
}
|
|
315
|
+
function paletteDecls(palette) {
|
|
316
|
+
const decls = [];
|
|
317
|
+
for (const [key, value] of Object.entries(palette ?? {})) {
|
|
318
|
+
if (typeof value === "string" && value.trim()) decls.push(`${paletteVar(key)}: ${cleanValue(value)};`);
|
|
319
|
+
}
|
|
320
|
+
return decls;
|
|
321
|
+
}
|
|
322
|
+
var TOKEN_VARS = {
|
|
323
|
+
background: "--ui-bg",
|
|
324
|
+
surface: "--ui-surface",
|
|
325
|
+
surface2: "--ui-surface-2",
|
|
326
|
+
text: "--ui-text",
|
|
327
|
+
textMuted: "--ui-text-muted",
|
|
328
|
+
textFaint: "--ui-text-faint",
|
|
329
|
+
border: "--ui-border",
|
|
330
|
+
borderStrong: "--ui-border-strong",
|
|
331
|
+
accent: "--ui-accent",
|
|
332
|
+
accentStrong: "--ui-accent-strong",
|
|
333
|
+
accentSoft: "--ui-accent-soft",
|
|
334
|
+
onAccent: "--ui-on-accent",
|
|
335
|
+
good: "--ui-good",
|
|
336
|
+
warn: "--ui-warn",
|
|
337
|
+
danger: "--ui-danger",
|
|
338
|
+
chart1: "--ui-chart-1",
|
|
339
|
+
chart2: "--ui-chart-2",
|
|
340
|
+
chart3: "--ui-chart-3",
|
|
341
|
+
chart4: "--ui-chart-4",
|
|
342
|
+
chart5: "--ui-chart-5",
|
|
343
|
+
chart6: "--ui-chart-6",
|
|
344
|
+
chartPositive: "--ui-chart-pos",
|
|
345
|
+
chartNegative: "--ui-chart-neg",
|
|
346
|
+
contentWidth: "--ui-content-width",
|
|
347
|
+
panelRadius: "--ui-radius-lg",
|
|
348
|
+
panelShadow: "--ui-shadow",
|
|
349
|
+
masterDetailColumns: "--ui-master-detail-columns",
|
|
350
|
+
masterDetailMinHeight: "--ui-master-detail-min-height",
|
|
351
|
+
pagePadding: "--chapter-admin-page-padding",
|
|
352
|
+
workspacePadding: "--chapter-admin-workspace-padding"
|
|
353
|
+
};
|
|
354
|
+
function semanticDecls(tokens) {
|
|
355
|
+
return Object.entries(tokens ?? {}).flatMap(
|
|
356
|
+
([key, value]) => typeof value === "string" && value.trim() ? [`${TOKEN_VARS[key]}: ${cleanValue(value)};`] : []
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
function brandTokens(brand, options = {}) {
|
|
360
|
+
if (!brand) return "";
|
|
361
|
+
const light = [...paletteDecls(brand.palette), ...semanticDecls(brand.tokens)];
|
|
362
|
+
const fonts = brand.fonts;
|
|
363
|
+
if (fonts?.display) light.push(`--ui-font-display: ${cleanValue(fonts.display)};`);
|
|
364
|
+
if (fonts?.body) light.push(`--ui-font-sans: ${cleanValue(fonts.body)};`);
|
|
365
|
+
if (fonts?.numeral) light.push(`--ui-font-numeral: ${cleanValue(fonts.numeral)};`);
|
|
366
|
+
const dark = [...paletteDecls(brand.paletteDark), ...semanticDecls(brand.tokensDark)];
|
|
367
|
+
const invert = paletteDecls(brand.paletteInvert);
|
|
368
|
+
const selector = options.selector ?? ":root";
|
|
369
|
+
const darkSelector = selector === ":root" ? ':root[data-theme="dark"]' : `${selector}[data-theme="dark"]`;
|
|
370
|
+
const systemSelector = selector === ":root" ? ':root:not([data-theme="light"])' : `${selector}:not([data-theme="light"])`;
|
|
371
|
+
let css = light.length ? `${selector} {
|
|
372
|
+
${light.join("\n ")}
|
|
373
|
+
}
|
|
374
|
+
` : "";
|
|
375
|
+
if (dark.length) {
|
|
376
|
+
const block = `{
|
|
377
|
+
${dark.join("\n ")}
|
|
378
|
+
}`;
|
|
379
|
+
css += `${darkSelector} ${block}
|
|
380
|
+
@media (prefers-color-scheme: dark) {
|
|
381
|
+
${systemSelector} ${block}
|
|
382
|
+
}
|
|
383
|
+
`;
|
|
384
|
+
}
|
|
385
|
+
if (invert.length) {
|
|
386
|
+
const invertSelector = selector === ":root" ? ".ui-invert" : `${selector}.ui-invert, ${selector} .ui-invert`;
|
|
387
|
+
css += `${invertSelector} {
|
|
388
|
+
color-scheme: dark;
|
|
389
|
+
${invert.join("\n ")}
|
|
390
|
+
}
|
|
391
|
+
`;
|
|
392
|
+
}
|
|
393
|
+
return css;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// src/ui/brand-style.tsx
|
|
397
|
+
import { jsx as jsx2 } from "preact/jsx-runtime";
|
|
398
|
+
function BrandStyle(props) {
|
|
399
|
+
const css = brandTokens(props.brand, { selector: props.selector });
|
|
400
|
+
if (!css) return null;
|
|
401
|
+
return /* @__PURE__ */ jsx2("style", { children: css });
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export {
|
|
405
|
+
DEFAULT_CHAPTER_COPY,
|
|
406
|
+
formatChapterCopy,
|
|
407
|
+
ChapterCopyProvider,
|
|
408
|
+
useChapterCopy,
|
|
409
|
+
BrandStyle
|
|
410
|
+
};
|
|
411
|
+
//# sourceMappingURL=chunk-NTZSXAFT.js.map
|