@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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/ui/copy-context.tsx","../src/copy-defaults-admin.ts","../src/copy-defaults.ts","../src/copy-resolve.ts","../src/brand.ts","../src/ui/brand-style.tsx"],"sourcesContent":["import { createContext, type ComponentChildren } from \"preact\";\nimport { useContext } from \"preact/hooks\";\nimport {\n DEFAULT_CHAPTER_COPY,\n resolveChapterCopy,\n type ChapterCopy,\n type ChapterCopyInput,\n} from \"../copy.js\";\n\nconst ChapterCopyContext = createContext<ChapterCopy>(DEFAULT_CHAPTER_COPY);\n\n/** Place a resolved or partial copy contract around any Chapter UI surface. */\nexport function ChapterCopyProvider(props: {\n copy?: ChapterCopy | ChapterCopyInput;\n children: ComponentChildren;\n}) {\n return (\n <ChapterCopyContext.Provider value={resolveChapterCopy(props.copy)}>\n {props.children}\n </ChapterCopyContext.Provider>\n );\n}\n\n/** Read the nearest complete Chapter copy contract. */\nexport function useChapterCopy(): ChapterCopy {\n return useContext(ChapterCopyContext);\n}\n","import type { ChapterCopy } from \"./copy.js\";\n\nexport const DEFAULT_ADMIN_COPY: ChapterCopy[\"admin\"] = {\n auth: {\n checking: \"Checking access…\",\n notAuthorized: \"Not authorized\",\n accountNotAuthorized: \"{account} isn't on the admin list.\",\n thisAccount: \"This account\",\n signOut: \"Sign out\",\n loading: \"Loading…\",\n signInNotConfigured: \"Sign-in not configured\",\n missingPublishableKey: \"No Clerk publishable key is set for this environment yet.\",\n noWorkspaces: \"No admin workspaces are configured.\",\n signInTagline: \"Admin sign-in, invite only\",\n },\n shell: {\n adminRole: \"admin\",\n adminConsole: \"Admin Console\",\n adminName: \"Admin\",\n navigationLabel: \"Admin navigation\",\n },\n workspaces: {\n dashboard: \"Dashboard\",\n overview: \"Overview\",\n billing: \"Billing\",\n people: \"People\",\n settings: \"Settings\",\n calendar: \"Calendar\",\n email: \"Email\",\n dashboardViewsLabel: \"Dashboard views\",\n settingsViewsLabel: \"Settings views\",\n collectionsLabel: \"CRM collections\",\n },\n dashboard: {\n loading: \"Loading dashboard…\",\n loadFailed: \"Couldn't load the dashboard\",\n upcomingCalls: \"Upcoming calls\",\n callsNeedAttention: \"{count} need attention\",\n noUpcomingCalls: \"No upcoming calls.\",\n drift: \"drift\",\n open: \"Open\",\n meet: \"Meet\",\n applications: \"Applications\",\n newMembers: \"New members\",\n newMembersUnavailable: \"Appears once billing is connected.\",\n revenueAdded: \"Revenue · annual run rate added\",\n revenue: \"Revenue\",\n revenueUnavailable: \"Appears once billing is connected.\",\n pipeline: \"Pipeline\",\n pipelineLabel: \"Membership pipeline\",\n thisWeek: \"+{count} this week\",\n noChange: \"no change\",\n activeMemberships: \"Active memberships\",\n annualRunRate: \"Annual run rate\",\n testMode: \"test mode\",\n },\n billing: {\n loading: \"Loading…\",\n loadFailed: \"Couldn't load billing\",\n notConfigured: \"Billing isn't configured. No Stripe key is vaulted for this group.\",\n active: \"Active\",\n annualized: \"Annualized\",\n renewingSoon: \"Renewing within 60 days\",\n pastDue: \"Past due\",\n subscriptions: \"Subscriptions\",\n testMode: \"Stripe test mode\",\n truncated: \"Showing the first 100 subscriptions. The list is truncated.\",\n name: \"Name\",\n email: \"Email\",\n application: \"Application\",\n subscription: \"Subscription\",\n cancelling: \"cancelling\",\n amount: \"Amount\",\n renews: \"Renews\",\n },\n availability: {\n loading: \"Loading…\",\n loadFailed: \"Couldn't load availability\",\n saved: \"Saved.\",\n title: \"Booking availability\",\n days: \"Days\",\n startHour: \"Start hour (0–24)\",\n endHour: \"End hour (0–24)\",\n slotMinutes: \"Slot length (minutes)\",\n minNoticeHours: \"Minimum notice (hours)\",\n windowDays: \"Booking window (days)\",\n timezone: \"Timezone\",\n summaryTemplate: \"Event summary template\",\n save: \"Save availability\",\n dayLabels: [\"Sun\", \"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\"],\n },\n email: {\n loading: \"Loading…\",\n loadFailed: \"Couldn't load email config\",\n saved: \"Saved.\",\n sent: \"Sent {template} to {address}{redirected}.\",\n testFailed: \"Test send did not deliver.\",\n delivery: \"Delivery\",\n notificationAddress: \"Notification address\",\n replyTo: \"Reply-to\",\n debugInbox: \"Debug inbox\",\n debugInboxHint: \"Non-production sends redirect here.\",\n sendTest: \"Send test\",\n enabled: \"Enabled\",\n subject: \"Subject\",\n body: \"Body\",\n save: \"Save changes\",\n sendLog: \"Send log\",\n sentColumn: \"Sent\",\n templateColumn: \"Template\",\n statusColumn: \"Status\",\n toColumn: \"To\",\n failed: \"failed\",\n redirected: \"redirected\",\n delivered: \"delivered\",\n },\n meetings: {\n loading: \"Loading…\",\n loadFailed: \"Couldn't load meetings\",\n cancelConfirm: \"Cancel this call? The calendar provider notifies the attendee.\",\n newStartPrompt: \"New start time (for example, 2026-08-01 14:00):\",\n parseFailed: \"Couldn't parse that time.\",\n agenda: \"Agenda\",\n empty: \"No meetings.\",\n unknown: \"(unknown)\",\n drift: \"drift\",\n meet: \"Meet\",\n reschedule: \"Reschedule\",\n cancel: \"Cancel\",\n },\n network: {\n title: \"Share to a follower\",\n allowlistDescription: \"Sends the target's allowlisted fields. Its pipeline and account state remain local.\",\n sharing: \"Sharing…\",\n shareWith: \"Share with {target}\",\n shared: \"Shared with {target}.\",\n shareFailed: \"Could not share with {target}.\",\n deliveryFailed: \"Delivery failed.\",\n },\n records: {\n workflowMissing: \"This record is not linked to an application workflow.\",\n comms: \"Comms\",\n commsHistory: \"Comms history\",\n scheduling: \"Scheduling\",\n billing: \"Billing\",\n notes: \"Notes\",\n access: \"Access\",\n sharing: \"Sharing\",\n lifecycle: \"Access & lifecycle\",\n role: \"Role\",\n superAdminHint: \"Super-admin, managed in odla Studio.\",\n noAccount: \"No linked account yet. The role is set once they sign in.\",\n approve: \"Approve\",\n refund: \"Refund\",\n approved: \"Approved.\",\n refunded: \"Refund issued.\",\n roleSet: \"Role set to {role}.\",\n messageSent: \"Message sent.\",\n messageNotSent: \"Message was not sent.\",\n loadingTemplates: \"Loading templates…\",\n template: \"Template\",\n chooseTemplate: \"Choose a template…\",\n sending: \"Sending…\",\n send: \"Send\",\n noMessages: \"No messages recorded.\",\n message: \"Message\",\n newStartPrompt: \"New start time:\",\n parseFailed: \"Couldn't parse that time.\",\n cancelConfirm: \"Cancel this call?\",\n callCancelled: \"Call cancelled.\",\n callRescheduled: \"Call rescheduled.\",\n noApplication: \"No application is linked to this record.\",\n loadingMeetings: \"Loading meetings…\",\n noMeetings: \"No meetings recorded.\",\n joinMeeting: \"Join meeting\",\n openCalendar: \"Open calendar\",\n reschedule: \"Reschedule\",\n cancel: \"Cancel\",\n },\n};\n","import type { ChapterCopy } from \"./copy.js\";\n\nexport const DEFAULT_COMMON_COPY: ChapterCopy[\"common\"] = {\n loading: \"Loading…\",\n saved: \"Saved.\",\n unknown: \"(unknown)\",\n};\n\nexport const DEFAULT_JOIN_COPY: ChapterCopy[\"join\"] = {\n form: {\n submit: \"Submit application\",\n submitting: \"Submitting…\",\n submitFailed: \"Your application could not be submitted.\",\n unexpectedFailure: \"Something went wrong. Please try again.\",\n },\n booking: {\n unavailable: \"Scheduling is briefly unavailable. We'll reach out by email to arrange your call.\",\n loadFailed: \"Times are briefly unavailable. Please try again.\",\n slotTaken: \"That time was just taken. Here are the current openings.\",\n failed: \"The booking could not be completed.\",\n loading: \"Loading available times…\",\n book: \"Book this time\",\n booking: \"Booking…\",\n },\n payment: {\n setupFailed: \"Payment could not be set up. Please try again.\",\n preparing: \"Preparing secure payment…\",\n pending: \"Confirming your payment…\",\n processing: \"Processing…\",\n payAndContinue: \"Pay and continue\",\n incomplete: \"The payment did not complete. Please try again.\",\n },\n done: {\n label: \"You're booked\",\n calendarInvite: \"A calendar invitation with the video call link is on its way to your email.\",\n memberArea: \"Go to your member area\",\n },\n};\n\nexport const DEFAULT_MEMBERS_COPY: ChapterCopy[\"members\"] = {\n loadFailed: \"Sign in is briefly unavailable. Please refresh.\",\n account: {\n signOut: \"Sign out\",\n adminConsole: \"Admin console\",\n },\n provisional: {\n cardLabel: \"Your Application\",\n applicationNeeded: \"One step remains\",\n applicationNeededBody: \"Your account is ready, and the application that completes it takes a few minutes.\",\n apply: \"Apply for membership\",\n refunded: \"Membership refunded\",\n refundedBody: \"Your fee has been refunded in full and your membership is canceled.\",\n active: \"Your membership is active.\",\n renews: \"Your membership is active and renews {date}.\",\n introductionCall: \"Your introduction call\",\n calendarInvite: \"A calendar invitation with the video call link is in your email.\",\n joinCall: \"Join the video call\",\n bookCall: \"Book your introduction call\",\n bookCallBody: \"Your application is in. Choose a time below, and a calendar invitation will reach your email.\",\n chooseTime: \"Choose a time\",\n },\n full: {\n welcome: \"Welcome back.\",\n },\n reschedule: {\n changeTime: \"Change your time\",\n unavailable: \"Scheduling is briefly unavailable. We'll reach out by email to arrange your call.\",\n loadFailed: \"Times are briefly unavailable. Please try again.\",\n slotGone: \"That time is no longer available. Please pick another.\",\n loading: \"Loading available times…\",\n noTimes: \"No open times right now. Please check back soon.\",\n rescheduling: \"Rescheduling…\",\n keepTime: \"Keep my current time\",\n },\n};\n","import { DEFAULT_ADMIN_COPY } from \"./copy-defaults-admin.js\";\nimport {\n DEFAULT_COMMON_COPY,\n DEFAULT_JOIN_COPY,\n DEFAULT_MEMBERS_COPY,\n} from \"./copy-defaults.js\";\nimport type { ChapterCopy, ChapterCopyInput } from \"./copy.js\";\n\n/** Complete default language resolved beneath partial Chapter copy overrides. */\nexport const DEFAULT_CHAPTER_COPY: ChapterCopy = {\n common: DEFAULT_COMMON_COPY,\n join: DEFAULT_JOIN_COPY,\n members: DEFAULT_MEMBERS_COPY,\n admin: DEFAULT_ADMIN_COPY,\n};\n\nconst isObject = (value: unknown): value is Record<string, unknown> =>\n Boolean(value) && typeof value === \"object\" && !Array.isArray(value);\n\nfunction mergeCopy<T>(base: T, input: unknown, path: string): T {\n if (typeof base === \"string\") {\n if (input === undefined) return base;\n if (typeof input !== \"string\") throw new Error(`${path}: must be a string`);\n return input as T;\n }\n if (Array.isArray(base)) {\n if (input === undefined) return [...base] as T;\n if (!Array.isArray(input) || !input.every((item) => typeof item === \"string\")) {\n throw new Error(`${path}: must be an array of strings`);\n }\n return [...input] as T;\n }\n if (!isObject(base)) return base;\n if (input !== undefined && !isObject(input)) throw new Error(`${path}: must be an object`);\n const overrides = input as Record<string, unknown> | undefined;\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(base)) {\n result[key] = mergeCopy(value, overrides?.[key], `${path}.${key}`);\n }\n return result as T;\n}\n\n/** Resolve a partial copy contract and validate every supplied leaf. */\nexport function resolveChapterCopy(input?: ChapterCopyInput): ChapterCopy {\n return mergeCopy(DEFAULT_CHAPTER_COPY, input, \"defineChapter.copy\");\n}\n\n/** Replace `{name}` placeholders without interpreting the resulting text as HTML. */\nexport function formatChapterCopy(\n template: string,\n values: Readonly<Record<string, string | number>>,\n): string {\n return template.replace(/\\{([a-zA-Z][a-zA-Z0-9]*)\\}/g, (match, key: string) =>\n Object.prototype.hasOwnProperty.call(values, key) ? String(values[key]) : match,\n );\n}\n","// Brand tokens (H4). defineChapter accepts a `brand` block; this turns it into a\n// `:root { --…: … }` CSS block, so a chapter re-skins the WHOLE UI — the\n// @odla-ai/ui components, the admin shell, and the member islands, which all read\n// --ui-* design tokens — from one config instead of hand-writing inline CSS.\n//\n// Pure string generation, so it is unit-testable and can be emitted at\n// build/SSR time into the page <head> (no flash of unstyled content), or via the\n// <BrandStyle> component from @odla-ai/chapter/ui.\nimport type { ChapterBrand, ChapterBrandTokens } from \"./types\";\n\n// A palette entry is either a direct custom property (already `--…`, e.g.\n// `--ui-accent` to retheme components) or a bare name we expose as `--<name>`\n// (e.g. `moss` → `--moss`, for a site to reference in its own CSS).\nfunction paletteVar(key: string): string {\n return key.startsWith(\"--\") ? key : `--${key}`;\n}\n\n// Strip characters that could break out of a `--var: value;` declaration or the\n// surrounding <style>. Brand config is trusted author input, so this is a\n// belt-and-suspenders guard, not a security boundary.\nfunction cleanValue(value: string): string {\n return value.replace(/[<>{};]/g, \"\").trim();\n}\n\nfunction paletteDecls(palette: Record<string, string> | undefined): string[] {\n const decls: string[] = [];\n for (const [key, value] of Object.entries(palette ?? {})) {\n if (typeof value === \"string\" && value.trim()) decls.push(`${paletteVar(key)}: ${cleanValue(value)};`);\n }\n return decls;\n}\n\nconst TOKEN_VARS: Record<keyof ChapterBrandTokens, string> = {\n background: \"--ui-bg\",\n surface: \"--ui-surface\",\n surface2: \"--ui-surface-2\",\n text: \"--ui-text\",\n textMuted: \"--ui-text-muted\",\n textFaint: \"--ui-text-faint\",\n border: \"--ui-border\",\n borderStrong: \"--ui-border-strong\",\n accent: \"--ui-accent\",\n accentStrong: \"--ui-accent-strong\",\n accentSoft: \"--ui-accent-soft\",\n onAccent: \"--ui-on-accent\",\n good: \"--ui-good\",\n warn: \"--ui-warn\",\n danger: \"--ui-danger\",\n chart1: \"--ui-chart-1\",\n chart2: \"--ui-chart-2\",\n chart3: \"--ui-chart-3\",\n chart4: \"--ui-chart-4\",\n chart5: \"--ui-chart-5\",\n chart6: \"--ui-chart-6\",\n chartPositive: \"--ui-chart-pos\",\n chartNegative: \"--ui-chart-neg\",\n contentWidth: \"--ui-content-width\",\n panelRadius: \"--ui-radius-lg\",\n panelShadow: \"--ui-shadow\",\n masterDetailColumns: \"--ui-master-detail-columns\",\n masterDetailMinHeight: \"--ui-master-detail-min-height\",\n pagePadding: \"--chapter-admin-page-padding\",\n workspacePadding: \"--chapter-admin-workspace-padding\",\n};\n\nfunction semanticDecls(tokens: ChapterBrandTokens | undefined): string[] {\n return Object.entries(tokens ?? {}).flatMap(([key, value]) =>\n typeof value === \"string\" && value.trim()\n ? [`${TOKEN_VARS[key as keyof ChapterBrandTokens]}: ${cleanValue(value)};`]\n : [],\n );\n}\n\n/**\n * Build the CSS that maps a chapter's brand onto the design tokens the UI reads:\n * each `palette` entry becomes a custom property (light, and dark unless\n * `paletteDark` overrides), and `fonts` (display/body/numeral) map to\n * `--ui-font-display` / `--ui-font-sans` / `--ui-font-numeral`. The dark block is\n * emitted under both `:root[data-theme=\"dark\"]` (the odla-ui theme toggle) and\n * `@media (prefers-color-scheme: dark)`. Returns \"\" when there is nothing to\n * theme. These are brand OVERRIDES on top of a base theme — they do not replace\n * the theme layer the components need (see {@link brandTokens} usage in the docs).\n */\nexport function brandTokens(\n brand: ChapterBrand | undefined,\n options: { selector?: string } = {},\n): string {\n if (!brand) return \"\";\n const light = [...paletteDecls(brand.palette), ...semanticDecls(brand.tokens)];\n const fonts = brand.fonts;\n if (fonts?.display) light.push(`--ui-font-display: ${cleanValue(fonts.display)};`);\n if (fonts?.body) light.push(`--ui-font-sans: ${cleanValue(fonts.body)};`);\n if (fonts?.numeral) light.push(`--ui-font-numeral: ${cleanValue(fonts.numeral)};`);\n const dark = [...paletteDecls(brand.paletteDark), ...semanticDecls(brand.tokensDark)];\n const invert = paletteDecls(brand.paletteInvert);\n const selector = options.selector ?? \":root\";\n const darkSelector = selector === \":root\" ? ':root[data-theme=\"dark\"]' : `${selector}[data-theme=\"dark\"]`;\n const systemSelector = selector === \":root\"\n ? ':root:not([data-theme=\"light\"])'\n : `${selector}:not([data-theme=\"light\"])`;\n\n let css = light.length ? `${selector} {\\n ${light.join(\"\\n \")}\\n}\\n` : \"\";\n if (dark.length) {\n const block = `{\\n ${dark.join(\"\\n \")}\\n}`;\n css += `${darkSelector} ${block}\\n@media (prefers-color-scheme: dark) {\\n ${systemSelector} ${block}\\n}\\n`;\n }\n if (invert.length) {\n const invertSelector = selector === \":root\"\n ? \".ui-invert\"\n : `${selector}.ui-invert, ${selector} .ui-invert`;\n css += `${invertSelector} {\\n color-scheme: dark;\\n ${invert.join(\"\\n \")}\\n}\\n`;\n }\n return css;\n}\n\n/** Structural output accepted from `@odla-ai/brand`'s pure token compiler. */\nexport interface CompiledChapterBrandTokens {\n light: Record<string, string>;\n dark: Record<string, string>;\n}\n\n/**\n * Turn a complete compiled token pair into Chapter's brand contract without\n * coupling Chapter's runtime to the brand-book/agent package. Explicit values in\n * `base` win, so a host can make final per-site adjustments after compilation.\n */\nexport function chapterBrandFromTokens(\n compiled: CompiledChapterBrandTokens,\n base: ChapterBrand = {},\n): ChapterBrand {\n return {\n ...base,\n palette: { ...compiled.light, ...base.palette },\n paletteDark: { ...compiled.dark, ...base.paletteDark },\n paletteInvert: { ...compiled.dark, ...base.paletteInvert },\n };\n}\n","// Client-side convenience for brand tokens: render the chapter's brand as a\n// <style> tag. Prefer emitting brandTokens() into the page <head> at build/SSR\n// time (no flash); use this when that isn't available (e.g. a pure SPA mount).\nimport { brandTokens } from \"../brand.js\";\nimport type { ChapterBrand } from \"../types\";\n\n/** Props for {@link BrandStyle}. */\nexport interface BrandStyleProps {\n brand: ChapterBrand | undefined;\n /** CSS selector receiving the variables. Default `:root`. */\n selector?: string;\n}\n\n/** Render a chapter's brand tokens as an inline <style> block (or nothing when\n * there is no brand to theme). */\nexport function BrandStyle(props: BrandStyleProps) {\n const css = brandTokens(props.brand, { selector: props.selector });\n if (!css) return null;\n return <style>{css}</style>;\n}\n"],"mappings":"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","names":["jsx"]}
@@ -1,3 +1,8 @@
1
+ import {
2
+ formatChapterCopy,
3
+ useChapterCopy
4
+ } from "./chunk-NTZSXAFT.js";
5
+
1
6
  // src/ui/slot-picker.tsx
2
7
  import { useMemo, useState } from "preact/hooks";
3
8
 
@@ -103,7 +108,9 @@ import { useEffect, useState as useState3 } from "preact/hooks";
103
108
  import { useState as useState2 } from "preact/hooks";
104
109
  import { jsx as jsx2, jsxs as jsxs2 } from "preact/jsx-runtime";
105
110
  function Rescheduler(props) {
106
- const { api, applicationId, timezone, onRescheduled, label = "Change your time" } = props;
111
+ const inheritedCopy = useChapterCopy();
112
+ const copy = props.copy ?? inheritedCopy.members.reschedule;
113
+ const { api, applicationId, timezone, onRescheduled, label = copy.changeTime } = props;
107
114
  const [open, setOpen] = useState2(false);
108
115
  const [slots, setSlots] = useState2(null);
109
116
  const [tz, setTz] = useState2(timezone);
@@ -117,14 +124,14 @@ function Rescheduler(props) {
117
124
  const r = await api("/api/schedule/slots");
118
125
  if (r.schedulingReady === false) {
119
126
  setSlots([]);
120
- setMsg("Scheduling is briefly unavailable. We'll reach out by email to arrange your call.");
127
+ setMsg(copy.unavailable);
121
128
  return;
122
129
  }
123
130
  setSlots(r.slots ?? []);
124
131
  if (r.timezone) setTz(r.timezone);
125
132
  } catch {
126
133
  setSlots([]);
127
- setMsg("Times are briefly unavailable. Please try again.");
134
+ setMsg(copy.loadFailed);
128
135
  }
129
136
  };
130
137
  const pick = async (slot) => {
@@ -135,7 +142,7 @@ function Rescheduler(props) {
135
142
  setOpen(false);
136
143
  await onRescheduled();
137
144
  } catch (e) {
138
- setMsg(e instanceof Error ? e.message : "That time is no longer available. Please pick another.");
145
+ setMsg(e instanceof Error ? e.message : copy.slotGone);
139
146
  } finally {
140
147
  setBusy(false);
141
148
  }
@@ -154,7 +161,7 @@ function Rescheduler(props) {
154
161
  ) });
155
162
  }
156
163
  return /* @__PURE__ */ jsxs2("div", { className: "msched", children: [
157
- slots === null ? /* @__PURE__ */ jsx2("p", { className: "meeting-note", children: "Loading available times\u2026" }) : slots.length === 0 ? /* @__PURE__ */ jsx2("p", { className: "meeting-note", children: msg ?? "No open times right now. Please check back soon." }) : /* @__PURE__ */ jsx2(
164
+ slots === null ? /* @__PURE__ */ jsx2("p", { className: "meeting-note", children: copy.loading }) : slots.length === 0 ? /* @__PURE__ */ jsx2("p", { className: "meeting-note", children: msg ?? copy.noTimes }) : /* @__PURE__ */ jsx2(
158
165
  SlotPicker,
159
166
  {
160
167
  slots,
@@ -163,7 +170,7 @@ function Rescheduler(props) {
163
170
  onPick: (s) => void pick(s)
164
171
  }
165
172
  ),
166
- busy ? /* @__PURE__ */ jsx2("p", { className: "meeting-note", children: "Rescheduling\u2026" }) : null,
173
+ busy ? /* @__PURE__ */ jsx2("p", { className: "meeting-note", children: copy.rescheduling }) : null,
167
174
  msg && slots && slots.length > 0 ? /* @__PURE__ */ jsx2("p", { className: "meeting-note error", children: msg }) : null,
168
175
  /* @__PURE__ */ jsx2("p", { className: "meeting-note", children: /* @__PURE__ */ jsx2(
169
176
  "a",
@@ -173,7 +180,7 @@ function Rescheduler(props) {
173
180
  e.preventDefault();
174
181
  setOpen(false);
175
182
  },
176
- children: "Keep my current time"
183
+ children: copy.keepTime
177
184
  }
178
185
  ) })
179
186
  ] });
@@ -181,9 +188,9 @@ function Rescheduler(props) {
181
188
 
182
189
  // src/ui/members.tsx
183
190
  import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "preact/jsx-runtime";
184
- function card(kicker, body) {
191
+ function card(copy, kicker, body) {
185
192
  return /* @__PURE__ */ jsxs3("div", { className: "card", children: [
186
- /* @__PURE__ */ jsx3("div", { className: "card-label", children: "Your Application" }),
193
+ /* @__PURE__ */ jsx3("div", { className: "card-label", children: copy.provisional.cardLabel }),
187
194
  /* @__PURE__ */ jsxs3("div", { className: "meeting-block", children: [
188
195
  /* @__PURE__ */ jsx3("div", { className: "meeting-kicker", children: kicker }),
189
196
  body
@@ -191,47 +198,78 @@ function card(kicker, body) {
191
198
  ] });
192
199
  }
193
200
  function ProvisionalCard(props) {
194
- const { api, application, applyHref, onReschedule } = props;
201
+ const { api, application, applyHref, copy, onReschedule } = props;
195
202
  if (!application) {
196
203
  return card(
197
- "One step remains",
204
+ copy,
205
+ copy.provisional.applicationNeeded,
198
206
  /* @__PURE__ */ jsxs3(Fragment2, { children: [
199
- /* @__PURE__ */ jsx3("div", { className: "meeting-note", children: "Your account is ready, and the application that completes it takes a few minutes." }),
200
- /* @__PURE__ */ jsx3("a", { className: "apply-link", href: applyHref, children: "Apply for membership" })
207
+ /* @__PURE__ */ jsx3("div", { className: "meeting-note", children: copy.provisional.applicationNeededBody }),
208
+ /* @__PURE__ */ jsx3("a", { className: "apply-link", href: applyHref, children: copy.provisional.apply })
201
209
  ] })
202
210
  );
203
211
  }
204
212
  if (application.status === "refunded") {
205
- return card("Membership refunded", /* @__PURE__ */ jsx3("div", { className: "meeting-note", children: "Your fee has been refunded in full and your membership is canceled." }));
213
+ return card(
214
+ copy,
215
+ copy.provisional.refunded,
216
+ /* @__PURE__ */ jsx3("div", { className: "meeting-note", children: copy.provisional.refundedBody })
217
+ );
206
218
  }
207
- const membership = application.paid ? /* @__PURE__ */ jsxs3("div", { className: "meeting-note", children: [
208
- "Your membership is active",
209
- application.renewalAt ? ` and renews ${fmtDate(application.renewalAt)}` : "",
210
- "."
211
- ] }) : null;
219
+ const membership = application.paid ? /* @__PURE__ */ jsx3("div", { className: "meeting-note", children: application.renewalAt ? formatChapterCopy(copy.provisional.renews, { date: fmtDate(application.renewalAt) }) : copy.provisional.active }) : null;
212
220
  if (application.meetingAt) {
213
221
  return card(
214
- "Your introduction call",
222
+ copy,
223
+ copy.provisional.introductionCall,
215
224
  /* @__PURE__ */ jsxs3(Fragment2, { children: [
216
225
  /* @__PURE__ */ jsx3("div", { className: "meeting-date", children: fullLabel(application.meetingAt, application.timezone) }),
217
- /* @__PURE__ */ jsx3("div", { className: "meeting-note", children: "A calendar invitation with the video call link is in your email." }),
218
- application.meetUrl ? /* @__PURE__ */ jsx3("div", { className: "meeting-note", children: /* @__PURE__ */ jsx3("a", { href: application.meetUrl, target: "_blank", rel: "noopener", children: "Join the video call" }) }) : null,
219
- /* @__PURE__ */ jsx3(Rescheduler, { api, applicationId: application.id, timezone: application.timezone, onRescheduled: onReschedule }),
226
+ /* @__PURE__ */ jsx3("div", { className: "meeting-note", children: copy.provisional.calendarInvite }),
227
+ application.meetUrl ? /* @__PURE__ */ jsx3("div", { className: "meeting-note", children: /* @__PURE__ */ jsx3("a", { href: application.meetUrl, target: "_blank", rel: "noopener", children: copy.provisional.joinCall }) }) : null,
228
+ /* @__PURE__ */ jsx3(
229
+ Rescheduler,
230
+ {
231
+ api,
232
+ applicationId: application.id,
233
+ timezone: application.timezone,
234
+ copy: copy.reschedule,
235
+ onRescheduled: onReschedule
236
+ }
237
+ ),
220
238
  membership
221
239
  ] })
222
240
  );
223
241
  }
224
242
  return card(
225
- "Book your introduction call",
243
+ copy,
244
+ copy.provisional.bookCall,
226
245
  /* @__PURE__ */ jsxs3(Fragment2, { children: [
227
- /* @__PURE__ */ jsx3("div", { className: "meeting-note", children: "Your application is in. Choose a time below, and a calendar invitation will reach your email." }),
228
- /* @__PURE__ */ jsx3(Rescheduler, { api, applicationId: application.id, timezone: application.timezone, onRescheduled: onReschedule, label: "Choose a time" }),
246
+ /* @__PURE__ */ jsx3("div", { className: "meeting-note", children: copy.provisional.bookCallBody }),
247
+ /* @__PURE__ */ jsx3(
248
+ Rescheduler,
249
+ {
250
+ api,
251
+ applicationId: application.id,
252
+ timezone: application.timezone,
253
+ copy: copy.reschedule,
254
+ onRescheduled: onReschedule,
255
+ label: copy.provisional.chooseTime
256
+ }
257
+ ),
229
258
  membership
230
259
  ] })
231
260
  );
232
261
  }
233
262
  function MembersArea(props) {
234
- const { api, signOut, adminHref = "/admin/", applyHref = "/join.html", memberContent } = props;
263
+ const inheritedCopy = useChapterCopy();
264
+ const {
265
+ api,
266
+ signOut,
267
+ adminHref = "/admin/",
268
+ applyHref = "/join.html",
269
+ memberContent,
270
+ renderProvisional
271
+ } = props;
272
+ const copy = props.copy ?? inheritedCopy.members;
235
273
  const [me, setMe] = useState3(null);
236
274
  const [error, setError] = useState3(false);
237
275
  const reload = async () => {
@@ -244,8 +282,8 @@ function MembersArea(props) {
244
282
  useEffect(() => {
245
283
  void reload();
246
284
  }, []);
247
- if (error) return /* @__PURE__ */ jsx3("p", { className: "meeting-note", children: "Sign in is briefly unavailable. Please refresh." });
248
- if (!me) return /* @__PURE__ */ jsx3("p", { className: "meeting-note", children: "Loading\u2026" });
285
+ if (error) return /* @__PURE__ */ jsx3("p", { className: "meeting-note", children: copy.loadFailed });
286
+ if (!me) return /* @__PURE__ */ jsx3("p", { className: "meeting-note", children: inheritedCopy.common.loading });
249
287
  const role = me.role || "provisional";
250
288
  return /* @__PURE__ */ jsxs3(Fragment2, { children: [
251
289
  /* @__PURE__ */ jsxs3("div", { className: "card", children: [
@@ -254,16 +292,34 @@ function MembersArea(props) {
254
292
  /* @__PURE__ */ jsx3("span", { className: "role-badge " + role, children: role })
255
293
  ] }),
256
294
  /* @__PURE__ */ jsxs3("div", { className: "account-actions", children: [
257
- /* @__PURE__ */ jsx3("button", { className: "btn secondary mini", onClick: signOut, children: "Sign out" }),
258
- role === "admin" ? /* @__PURE__ */ jsx3("a", { className: "admin-console-link", href: adminHref, children: "Admin console" }) : null
295
+ /* @__PURE__ */ jsx3("button", { className: "btn secondary mini", onClick: signOut, children: copy.account.signOut }),
296
+ role === "admin" ? /* @__PURE__ */ jsx3("a", { className: "admin-console-link", href: adminHref, children: copy.account.adminConsole }) : null
259
297
  ] })
260
298
  ] }),
261
- role === "provisional" ? /* @__PURE__ */ jsx3(ProvisionalCard, { api, application: me.application, applyHref, onReschedule: reload }) : memberContent ?? /* @__PURE__ */ jsx3("div", { className: "card", children: /* @__PURE__ */ jsx3("div", { className: "card-label", children: "Welcome back." }) })
299
+ role === "provisional" ? (() => {
300
+ const defaultContent = /* @__PURE__ */ jsx3(
301
+ ProvisionalCard,
302
+ {
303
+ api,
304
+ application: me.application,
305
+ applyHref,
306
+ copy,
307
+ onReschedule: reload
308
+ }
309
+ );
310
+ return renderProvisional?.({
311
+ api,
312
+ application: me.application,
313
+ applyHref,
314
+ reload,
315
+ defaultContent
316
+ }) ?? defaultContent;
317
+ })() : memberContent ?? /* @__PURE__ */ jsx3("div", { className: "card", children: /* @__PURE__ */ jsx3("div", { className: "card-label", children: copy.full.welcome }) })
262
318
  ] });
263
319
  }
264
320
 
265
321
  // src/ui/join.tsx
266
- import { useEffect as useEffect2, useState as useState5 } from "preact/hooks";
322
+ import { useEffect as useEffect3, useState as useState6 } from "preact/hooks";
267
323
 
268
324
  // src/ui/payment-step.tsx
269
325
  import { useRef, useState as useState4 } from "preact/hooks";
@@ -288,9 +344,20 @@ function loadStripe() {
288
344
  return loader;
289
345
  }
290
346
  function PaymentStep(props) {
291
- const { applicationId, refundPolicyText, onPaid } = props;
347
+ const inheritedCopy = useChapterCopy();
348
+ const {
349
+ applicationId,
350
+ refundPolicyText,
351
+ onPaid,
352
+ appearance,
353
+ fonts,
354
+ renderPriceLines,
355
+ children
356
+ } = props;
357
+ const copy = props.copy ?? inheritedCopy.join.payment;
292
358
  const [status, setStatus] = useState4("idle");
293
359
  const [error, setError] = useState4(null);
360
+ const [lineItems, setLineItems] = useState4(null);
294
361
  const mountRef = useRef(null);
295
362
  const stripeRef = useRef(null);
296
363
  const elementsRef = useRef(null);
@@ -304,17 +371,22 @@ function PaymentStep(props) {
304
371
  body: JSON.stringify({ applicationId, refundPolicyAck: true })
305
372
  });
306
373
  const data = await res.json();
307
- if (!res.ok || !data.clientSecret || !data.publishableKey) throw new Error("Payment could not be set up. Please try again.");
374
+ if (!res.ok || !data.clientSecret || !data.publishableKey) throw new Error(copy.setupFailed);
308
375
  const stripe = (await loadStripe())(data.publishableKey);
309
- const elements = stripe.elements({ clientSecret: data.clientSecret });
376
+ const elements = stripe.elements({
377
+ clientSecret: data.clientSecret,
378
+ ...appearance ? { appearance } : {},
379
+ ...fonts ? { fonts } : {}
380
+ });
310
381
  const element = elements.create("payment");
311
382
  if (mountRef.current) element.mount(mountRef.current);
312
383
  stripeRef.current = stripe;
313
384
  elementsRef.current = elements;
385
+ setLineItems(data.lineItems ?? null);
314
386
  setStatus("ready");
315
387
  } catch (e) {
316
388
  setStatus("idle");
317
- setError(e instanceof Error ? e.message : "Payment could not be set up. Please try again.");
389
+ setError(e instanceof Error ? e.message : copy.setupFailed);
318
390
  }
319
391
  };
320
392
  const pay = async () => {
@@ -323,17 +395,23 @@ function PaymentStep(props) {
323
395
  if (!stripe || !elements) return;
324
396
  setStatus("confirming");
325
397
  setError(null);
326
- const returnUrl = typeof window !== "undefined" ? `${window.location.origin}${window.location.pathname}?redirect_status=succeeded` : void 0;
398
+ let returnUrl;
399
+ if (typeof window !== "undefined") {
400
+ const next = new URL(window.location.href);
401
+ next.searchParams.set("application", applicationId);
402
+ next.searchParams.set("redirect_status", "succeeded");
403
+ returnUrl = next.toString();
404
+ }
327
405
  const result = await stripe.confirmPayment({ elements, confirmParams: { return_url: returnUrl }, redirect: "if_required" });
328
406
  if (result.error) {
329
- setError(result.error.message ?? "The payment could not be completed.");
407
+ setError(result.error.message ?? copy.incomplete);
330
408
  setStatus("ready");
331
409
  return;
332
410
  }
333
411
  const paid = result.paymentIntent?.status;
334
412
  if (paid === "succeeded" || paid === "processing") onPaid();
335
413
  else {
336
- setError("The payment did not complete. Please try again.");
414
+ setError(copy.incomplete);
337
415
  setStatus("ready");
338
416
  }
339
417
  };
@@ -351,17 +429,20 @@ function PaymentStep(props) {
351
429
  ),
352
430
  /* @__PURE__ */ jsx4("span", { children: refundPolicyText })
353
431
  ] }),
354
- status === "loading" ? /* @__PURE__ */ jsx4("p", { className: "pay-status", children: "Preparing secure payment\u2026" }) : null,
432
+ status === "loading" ? /* @__PURE__ */ jsx4("p", { className: "pay-status", children: copy.preparing }) : null,
433
+ lineItems && renderPriceLines ? renderPriceLines(lineItems) : null,
434
+ children,
355
435
  /* @__PURE__ */ jsx4("div", { ref: mountRef, hidden: status === "idle" || status === "loading" }),
356
- status === "ready" || status === "confirming" ? /* @__PURE__ */ jsx4("button", { className: "submit-btn", disabled: status === "confirming", onClick: () => void pay(), children: status === "confirming" ? "Processing\u2026" : "Pay and continue" }) : null,
436
+ status === "ready" || status === "confirming" ? /* @__PURE__ */ jsx4("button", { className: "submit-btn", disabled: status === "confirming", onClick: () => void pay(), children: status === "confirming" ? copy.processing : copy.payAndContinue }) : null,
357
437
  error ? /* @__PURE__ */ jsx4("p", { className: "pay-error", children: error }) : null
358
438
  ] });
359
439
  }
360
440
 
361
- // src/ui/join.tsx
441
+ // src/ui/join-booking.tsx
442
+ import { useEffect as useEffect2, useState as useState5 } from "preact/hooks";
362
443
  import { jsx as jsx5, jsxs as jsxs5 } from "preact/jsx-runtime";
363
444
  function JoinBooking(props) {
364
- const { applicationId, onBooked } = props;
445
+ const { applicationId, copy, onBooked } = props;
365
446
  const [state, setState] = useState5(null);
366
447
  const [msg, setMsg] = useState5(null);
367
448
  const [busy, setBusy] = useState5(false);
@@ -373,12 +454,12 @@ function JoinBooking(props) {
373
454
  const data = await res.json();
374
455
  if (data.schedulingReady === false || !data.slots?.length) {
375
456
  setState(null);
376
- setMsg("Scheduling is briefly unavailable \u2014 we'll reach out by email to arrange your call.");
457
+ setMsg(copy.unavailable);
377
458
  return;
378
459
  }
379
460
  setState({ slots: data.slots, timezone: data.timezone ?? "UTC" });
380
461
  } catch {
381
- setMsg("Times are briefly unavailable. Please try again.");
462
+ setMsg(copy.loadFailed);
382
463
  }
383
464
  };
384
465
  useEffect2(() => {
@@ -397,19 +478,19 @@ function JoinBooking(props) {
397
478
  const data = await res.json();
398
479
  if (res.status === 409 && data.code === "calendar_slot_unavailable") {
399
480
  setSelected(null);
400
- setMsg("That time was just taken. Here are the current openings.");
481
+ setMsg(copy.slotTaken);
401
482
  await load();
402
483
  return;
403
484
  }
404
- if (!res.ok) throw new Error(data.error ?? "The booking could not be completed.");
485
+ if (!res.ok) throw new Error(data.error ?? copy.failed);
405
486
  onBooked({ startAt: data.startAt ?? selected.startAt, timezone: state?.timezone ?? "UTC" });
406
487
  } catch (e) {
407
- setMsg(e instanceof Error ? e.message : "The booking could not be completed.");
488
+ setMsg(e instanceof Error ? e.message : copy.failed);
408
489
  } finally {
409
490
  setBusy(false);
410
491
  }
411
492
  };
412
- if (!state) return /* @__PURE__ */ jsx5("p", { className: "slots-status", children: msg ?? "Loading available times\u2026" });
493
+ if (!state) return /* @__PURE__ */ jsx5("p", { className: "slots-status", children: msg ?? copy.loading });
413
494
  return /* @__PURE__ */ jsxs5("div", { className: "join-book", children: [
414
495
  /* @__PURE__ */ jsx5(
415
496
  SlotPicker,
@@ -422,20 +503,82 @@ function JoinBooking(props) {
422
503
  }
423
504
  ),
424
505
  /* @__PURE__ */ jsxs5("div", { className: "slot-confirm", hidden: !selected, children: [
425
- /* @__PURE__ */ jsx5("button", { className: "submit-btn", disabled: busy, onClick: () => void book(), children: busy ? "Booking\u2026" : "Book this time" }),
506
+ /* @__PURE__ */ jsx5("button", { className: "submit-btn", disabled: busy, onClick: () => void book(), children: busy ? copy.booking : copy.book }),
426
507
  msg ? /* @__PURE__ */ jsx5("p", { className: "step2-note", children: msg }) : null
427
508
  ] })
428
509
  ] });
429
510
  }
511
+
512
+ // src/ui/join.tsx
513
+ import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs6 } from "preact/jsx-runtime";
514
+ async function loadJoinResume(applicationId, fetcher = fetch) {
515
+ const res = await fetcher(`/api/join/resume?application=${encodeURIComponent(applicationId)}`);
516
+ const data = await res.json();
517
+ if (!res.ok) throw new Error(data.error ?? "The application could not be resumed.");
518
+ if (data.state === "done" && data.booked) {
519
+ return { step: "done", applicationId: data.applicationId, booked: data.booked };
520
+ }
521
+ if (data.state === "paymentPending") {
522
+ return { step: "paymentPending", applicationId: data.applicationId };
523
+ }
524
+ if (data.state === "booking") {
525
+ return { step: "booking", applicationId: data.applicationId };
526
+ }
527
+ return { step: "payment", applicationId: data.applicationId };
528
+ }
430
529
  function JoinIsland(props) {
431
- const { config, children, membersHref = "/members/" } = props;
432
- const [step, setStep] = useState5("form");
433
- const [applicationId, setApplicationId] = useState5(null);
434
- const [error, setError] = useState5(null);
435
- const [submitting, setSubmitting] = useState5(false);
436
- const [booked, setBooked] = useState5(null);
530
+ const inheritedCopy = useChapterCopy();
531
+ const {
532
+ config,
533
+ children,
534
+ membersHref = "/members/",
535
+ renderStepHeader,
536
+ renderDone,
537
+ renderSubmit
538
+ } = props;
539
+ const copy = props.copy ?? config.copy ?? inheritedCopy.join;
540
+ const [state, setState] = useState6(props.initialState ?? { step: "form" });
541
+ const [error, setError] = useState6(null);
542
+ const [submitting, setSubmitting] = useState6(false);
543
+ useEffect3(() => {
544
+ const resolver = props.loadResume ?? (() => {
545
+ if (typeof window === "undefined") return Promise.resolve(null);
546
+ const applicationId = new URL(window.location.href).searchParams.get("application");
547
+ return applicationId ? loadJoinResume(applicationId) : Promise.resolve(null);
548
+ });
549
+ let live = true;
550
+ void resolver().then((resumed) => {
551
+ if (live && resumed) setState(resumed);
552
+ }).catch(() => {
553
+ if (live) setError(copy.form.unexpectedFailure);
554
+ });
555
+ return () => {
556
+ live = false;
557
+ };
558
+ }, [props.loadResume]);
559
+ useEffect3(() => {
560
+ if (state.step !== "paymentPending") return;
561
+ let live = true;
562
+ let timer;
563
+ const poll = async () => {
564
+ try {
565
+ const resumed = props.loadResume ? await props.loadResume() : await loadJoinResume(state.applicationId);
566
+ if (!live || !resumed) return;
567
+ setState(resumed);
568
+ if (resumed.step === "paymentPending") timer = setTimeout(() => void poll(), 1500);
569
+ } catch {
570
+ if (live) timer = setTimeout(() => void poll(), 2500);
571
+ }
572
+ };
573
+ timer = setTimeout(() => void poll(), 1e3);
574
+ return () => {
575
+ live = false;
576
+ if (timer) clearTimeout(timer);
577
+ };
578
+ }, [state, props.loadResume]);
437
579
  const submit = async (e) => {
438
580
  e.preventDefault();
581
+ if (submitting || props.submitDisabled === true) return;
439
582
  setSubmitting(true);
440
583
  setError(null);
441
584
  try {
@@ -448,42 +591,80 @@ function JoinIsland(props) {
448
591
  body: JSON.stringify(fields)
449
592
  });
450
593
  const data = await res.json();
451
- if (!res.ok || !data.id) throw new Error(data.error ?? "Your application could not be submitted.");
452
- setApplicationId(data.id);
453
- setStep(config.paymentsReady ? "pay" : "book");
594
+ if (!res.ok || !data.id) throw new Error(data.error ?? copy.form.submitFailed);
595
+ setState({
596
+ step: config.paymentsReady ? "payment" : "booking",
597
+ applicationId: data.id
598
+ });
454
599
  } catch (err) {
455
- setError(err instanceof Error ? err.message : "Something went wrong. Please try again.");
600
+ setError(err instanceof Error ? err.message : copy.form.unexpectedFailure);
456
601
  } finally {
457
602
  setSubmitting(false);
458
603
  }
459
604
  };
460
- if (step === "done" && booked) {
461
- return /* @__PURE__ */ jsxs5("div", { className: "join-done card", children: [
462
- /* @__PURE__ */ jsx5("div", { className: "card-label", children: "You're booked" }),
463
- /* @__PURE__ */ jsx5("p", { className: "meeting-date", children: fullLabel(booked.startAt, booked.timezone) }),
464
- /* @__PURE__ */ jsx5("p", { className: "meeting-note", children: "A calendar invitation with the video call link is on its way to your email." }),
465
- /* @__PURE__ */ jsx5("a", { className: "apply-link", href: membersHref, children: "Go to your member area" })
605
+ const header = renderStepHeader?.({
606
+ state,
607
+ ..."applicationId" in state ? { applicationId: state.applicationId } : {}
608
+ });
609
+ if (state.step === "done") {
610
+ const body = renderDone?.({ ...state, membersHref }) ?? /* @__PURE__ */ jsxs6("div", { className: "join-done card", children: [
611
+ /* @__PURE__ */ jsx6("div", { className: "card-label", children: copy.done.label }),
612
+ /* @__PURE__ */ jsx6("p", { className: "meeting-date", children: fullLabel(state.booked.startAt, state.booked.timezone) }),
613
+ /* @__PURE__ */ jsx6("p", { className: "meeting-note", children: copy.done.calendarInvite }),
614
+ /* @__PURE__ */ jsx6("a", { className: "apply-link", href: membersHref, children: copy.done.memberArea })
615
+ ] });
616
+ return /* @__PURE__ */ jsxs6(Fragment3, { children: [
617
+ header,
618
+ body
466
619
  ] });
467
620
  }
468
- if (step === "book" && applicationId) {
469
- return /* @__PURE__ */ jsx5(
470
- JoinBooking,
471
- {
472
- applicationId,
473
- onBooked: (b) => {
474
- setBooked(b);
475
- setStep("done");
621
+ if (state.step === "booking") {
622
+ return /* @__PURE__ */ jsxs6(Fragment3, { children: [
623
+ header,
624
+ /* @__PURE__ */ jsx6(
625
+ JoinBooking,
626
+ {
627
+ applicationId: state.applicationId,
628
+ copy: copy.booking,
629
+ onBooked: (booked) => {
630
+ setState({ step: "done", applicationId: state.applicationId, booked });
631
+ }
476
632
  }
477
- }
478
- );
633
+ )
634
+ ] });
635
+ }
636
+ if (state.step === "payment") {
637
+ return /* @__PURE__ */ jsxs6(Fragment3, { children: [
638
+ header,
639
+ /* @__PURE__ */ jsx6(
640
+ PaymentStep,
641
+ {
642
+ applicationId: state.applicationId,
643
+ refundPolicyText: config.refundPolicyText ?? "",
644
+ copy: copy.payment,
645
+ appearance: props.payment?.appearance,
646
+ fonts: props.payment?.fonts,
647
+ renderPriceLines: props.payment?.renderPriceLines,
648
+ onPaid: () => setState({ step: "booking", applicationId: state.applicationId }),
649
+ children: props.payment?.children
650
+ }
651
+ )
652
+ ] });
479
653
  }
480
- if (step === "pay" && applicationId) {
481
- return /* @__PURE__ */ jsx5(PaymentStep, { applicationId, refundPolicyText: config.refundPolicyText ?? "", onPaid: () => setStep("book") });
654
+ if (state.step === "paymentPending") {
655
+ return /* @__PURE__ */ jsxs6(Fragment3, { children: [
656
+ header,
657
+ /* @__PURE__ */ jsx6("p", { className: "pay-status", role: "status", children: copy.payment.pending })
658
+ ] });
482
659
  }
483
- return /* @__PURE__ */ jsxs5("form", { className: "join-form", onSubmit: (e) => void submit(e), children: [
484
- children,
485
- error ? /* @__PURE__ */ jsx5("p", { className: "join-error", children: error }) : null,
486
- /* @__PURE__ */ jsx5("button", { className: "submit-btn", type: "submit", disabled: submitting, children: submitting ? "Submitting\u2026" : "Submit application" })
660
+ const submitDisabled = submitting || props.submitDisabled === true;
661
+ return /* @__PURE__ */ jsxs6(Fragment3, { children: [
662
+ header,
663
+ /* @__PURE__ */ jsxs6("form", { className: "join-form", onSubmit: (e) => void submit(e), children: [
664
+ children,
665
+ error ? /* @__PURE__ */ jsx6("p", { className: "join-error", children: error }) : null,
666
+ renderSubmit?.({ submitting, disabled: submitDisabled }) ?? /* @__PURE__ */ jsx6("button", { className: "submit-btn", type: "submit", disabled: submitDisabled, children: submitting ? copy.form.submitting : copy.form.submit })
667
+ ] })
487
668
  ] });
488
669
  }
489
670
 
@@ -500,6 +681,7 @@ export {
500
681
  Rescheduler,
501
682
  MembersArea,
502
683
  PaymentStep,
684
+ loadJoinResume,
503
685
  JoinIsland
504
686
  };
505
- //# sourceMappingURL=chunk-4COPR4SY.js.map
687
+ //# sourceMappingURL=chunk-OYXNI6VR.js.map