@odla-ai/chapter 0.22.0 → 0.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -94,7 +94,7 @@ secrets out of source and require development proof before production.
94
94
  billing state remain authoritative locally.
95
95
  - **The UI kit, adoptable in pieces.** Three entries, split on the dependency
96
96
  boundary so you never pay for what you don't use:
97
- - `@odla-ai/chapter/ui/member` — **react-only**: `SlotPicker`, the date/timezone
97
+ - `@odla-ai/chapter/ui/member` — **Preact-only**: `SlotPicker`, the date/timezone
98
98
  helpers, `JoinIsland` (form → payment → booking), `MembersArea`,
99
99
  `Rescheduler`, `PaymentStep`, `<BrandStyle>`. It never imports
100
100
  `@odla-ai/auth-clerk` or `@odla-ai/crm`, so you can adopt a single
@@ -108,8 +108,8 @@ secrets out of source and require development proof before production.
108
108
  that information architecture. This entry needs auth-clerk + crm/ui.
109
109
  - `@odla-ai/chapter/ui` — the full barrel, for back-compat.
110
110
 
111
- Authored against React, rendered as Preact via `preact/compat` in the reference
112
- sites. Brand tokens (`brandTokens`/`<BrandStyle>`) re-skin all of it from
111
+ Authored and shipped against Preact. Brand tokens
112
+ (`brandTokens`/`<BrandStyle>`) re-skin all of it from
113
113
  `brand` (now light **and** dark, via `brand.palette` + `brand.paletteDark`).
114
114
 
115
115
  ### Theme tokens and scoped branding
@@ -573,26 +573,24 @@ These bite silently — a smoke test won't catch them:
573
573
  route or provisioning seed; that is deliberate, so the running app cannot
574
574
  grant itself admin. Confirm the result by signing in, not merely by inspecting
575
575
  the row.
576
- - **`@odla-ai/auth-clerk` is not a chapter peer.** It is deliberately absent from
577
- this package's manifest, because only one entry imports it: the worker verifies
578
- JWTs with `jose` via `ctx.verifyUser`, and `@odla-ai/chapter/ui/member` is
579
- react-only. **Install `@odla-ai/auth-clerk` yourself if (and only if) you adopt
576
+ - **`@odla-ai/auth-clerk` is an optional Chapter peer.** Only one entry imports
577
+ it: the worker verifies JWTs with `jose` via `ctx.verifyUser`, and
578
+ `@odla-ai/chapter/ui/member` does not load the auth package. **Install
579
+ `@odla-ai/auth-clerk` yourself if (and only if) you adopt
580
580
  `@odla-ai/chapter/ui/admin`** or want the themed sign-in components; reach for
581
581
  `@odla-ai/auth-clerk/invitations` when you want to send your own branded
582
582
  invitation mail. Importing the full `@odla-ai/chapter/ui` barrel pulls the admin
583
583
  half, so prefer the narrower entry.
584
584
  - **Known-good application set** (installs clean, no flags):
585
- `@odla-ai/chapter` 0.22.0, `@odla-ai/ui` 0.12.0, `@odla-ai/crm` 0.3.0,
586
- `@odla-ai/db` 0.6.6,
585
+ `@odla-ai/chapter` 0.22.1, `@odla-ai/ui` 0.12.1, `@odla-ai/crm` 0.3.1,
586
+ `@odla-ai/db` 0.6.7,
587
587
  `@odla-ai/calendar` 0.2.0, `@odla-ai/email` 0.3.1,
588
- `@odla-ai/auth-clerk` 0.4.0, `@odla-ai/o11y` 2.2.2, `jose` 6.2.3, React
589
- 19.2.7, and `react-dom` 19.2.7.
590
- - **Known-good React host toolchain:** `@odla-ai/cli` 0.17.1,
588
+ `@odla-ai/auth-clerk` 0.4.1, `@odla-ai/o11y` 2.2.2, `jose` 6.2.3, and
589
+ Preact 10.29.7.
590
+ - **Known-good Preact host toolchain:** `@odla-ai/cli` 0.17.1,
591
591
  `@odla-ai/security` 0.3.1, `@cloudflare/workers-types` 4.20260702.1,
592
- `@types/react` 19.2.17, `@types/react-dom` 19.2.3,
593
- `@vitejs/plugin-react` 6.0.3, TypeScript 6.0.3, Vite 8.1.4, Vitest 4.1.10,
594
- and Wrangler 4.107.0. The greenfield runbook standardizes on this exact React
595
- matrix; a Preact host needs its own tested compatibility set.
592
+ TypeScript 6.0.3, Vite 8.1.4, Vitest 4.1.10, and Wrangler 4.107.0. The
593
+ greenfield runbook standardizes on this exact Preact matrix.
596
594
  - **`--legacy-peer-deps` is a diagnostic, not a setting.** It suppresses exactly
597
595
  the peer conflict that tells you a pair is unsupported. If you need it, find out
598
596
  why first.
@@ -78,7 +78,7 @@ function brandTokens(brand, options = {}) {
78
78
  }
79
79
 
80
80
  // src/ui/brand-style.tsx
81
- import { jsx } from "react/jsx-runtime";
81
+ import { jsx } from "preact/jsx-runtime";
82
82
  function BrandStyle(props) {
83
83
  const css = brandTokens(props.brand, { selector: props.selector });
84
84
  if (!css) return null;
@@ -88,4 +88,4 @@ function BrandStyle(props) {
88
88
  export {
89
89
  BrandStyle
90
90
  };
91
- //# sourceMappingURL=chunk-VZPBTTO3.js.map
91
+ //# sourceMappingURL=chunk-3QKGKUTX.js.map
@@ -1,5 +1,5 @@
1
1
  // src/ui/slot-picker.tsx
2
- import { useMemo, useState } from "react";
2
+ import { useMemo, useState } from "preact/hooks";
3
3
 
4
4
  // src/ui/datetime.ts
5
5
  function tzShort(tz) {
@@ -50,7 +50,7 @@ function groupSlotsByDay(slots, tz) {
50
50
  }
51
51
 
52
52
  // src/ui/slot-picker.tsx
53
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
53
+ import { Fragment, jsx, jsxs } from "preact/jsx-runtime";
54
54
  var DEFAULT_CLASSES = {
55
55
  days: "slot-days",
56
56
  day: "slot-day",
@@ -97,11 +97,11 @@ function SlotPicker(props) {
97
97
  }
98
98
 
99
99
  // src/ui/members.tsx
100
- import { useEffect, useState as useState3 } from "react";
100
+ import { useEffect, useState as useState3 } from "preact/hooks";
101
101
 
102
102
  // src/ui/reschedule.tsx
103
- import { useState as useState2 } from "react";
104
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
103
+ import { useState as useState2 } from "preact/hooks";
104
+ import { jsx as jsx2, jsxs as jsxs2 } from "preact/jsx-runtime";
105
105
  function Rescheduler(props) {
106
106
  const { api, applicationId, timezone, onRescheduled, label = "Change your time" } = props;
107
107
  const [open, setOpen] = useState2(false);
@@ -180,7 +180,7 @@ function Rescheduler(props) {
180
180
  }
181
181
 
182
182
  // src/ui/members.tsx
183
- import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
183
+ import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "preact/jsx-runtime";
184
184
  function card(kicker, body) {
185
185
  return /* @__PURE__ */ jsxs3("div", { className: "card", children: [
186
186
  /* @__PURE__ */ jsx3("div", { className: "card-label", children: "Your Application" }),
@@ -263,11 +263,11 @@ function MembersArea(props) {
263
263
  }
264
264
 
265
265
  // src/ui/join.tsx
266
- import { useEffect as useEffect2, useState as useState5 } from "react";
266
+ import { useEffect as useEffect2, useState as useState5 } from "preact/hooks";
267
267
 
268
268
  // src/ui/payment-step.tsx
269
- import { useRef, useState as useState4 } from "react";
270
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
269
+ import { useRef, useState as useState4 } from "preact/hooks";
270
+ import { jsx as jsx4, jsxs as jsxs4 } from "preact/jsx-runtime";
271
271
  var loader = null;
272
272
  function loadStripe() {
273
273
  const existing = globalThis.Stripe;
@@ -359,7 +359,7 @@ function PaymentStep(props) {
359
359
  }
360
360
 
361
361
  // src/ui/join.tsx
362
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
362
+ import { jsx as jsx5, jsxs as jsxs5 } from "preact/jsx-runtime";
363
363
  function JoinBooking(props) {
364
364
  const { applicationId, onBooked } = props;
365
365
  const [state, setState] = useState5(null);
@@ -502,4 +502,4 @@ export {
502
502
  PaymentStep,
503
503
  JoinIsland
504
504
  };
505
- //# sourceMappingURL=chunk-ILGUJXSB.js.map
505
+ //# sourceMappingURL=chunk-4COPR4SY.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/ui/slot-picker.tsx","../src/ui/datetime.ts","../src/ui/members.tsx","../src/ui/reschedule.tsx","../src/ui/join.tsx","../src/ui/payment-step.tsx"],"sourcesContent":["// SlotPicker — a pure presentational day-chips + time-grid picker, shared by the\n// join booking step and the member-area reschedule. Ported from the proven\n// reference Preact island; behaviour and DOM shape are unchanged.\n//\n// It holds only the active-day UI state; slot data, selection, and booking are\n// the caller's (the picker is `onPick`-driven). `classes` lets the composing\n// island supply its own CSS contract (join styles `slot-*`, reschedule\n// `msched-*`); the defaults work standalone against those class names.\nimport { useMemo, useState } from \"preact/hooks\";\nimport { dayLabel, groupSlotsByDay, timeLabel } from \"./datetime.js\";\nimport type { Slot } from \"./datetime.js\";\n\n/** CSS class names for the four picker parts — overridable so one component\n * serves several differently-styled surfaces. */\nexport interface SlotPickerClasses {\n days: string;\n day: string;\n times: string;\n time: string;\n}\n\nconst DEFAULT_CLASSES: SlotPickerClasses = {\n days: \"slot-days\",\n day: \"slot-day\",\n times: \"slot-grid\",\n time: \"slot-time\",\n};\n\n/** Props for {@link SlotPicker}. Generic over the slot type so a caller's richer\n * slot object survives through `onPick`. */\nexport interface SlotPickerProps<T extends Slot = Slot> {\n /** Bookable slots, ideally already chronological. */\n slots: readonly T[];\n /** The group's scheduling timezone; all labels render in it. */\n timezone: string;\n /** The currently-selected slot's start, for the pressed state. */\n selectedStartAt?: number;\n /** Called with the full slot object when a time is chosen. */\n onPick: (slot: T) => void;\n /** Called when the active day changes (callers clear their selection). */\n onDayChange?: () => void;\n classes?: SlotPickerClasses;\n}\n\n/** A day-chips + time-grid slot picker. Presentational and `onPick`-driven: it\n * owns only the active-day selection; the caller owns slot data, the chosen\n * slot, and booking. Shared by the join booking step and the member reschedule. */\nexport function SlotPicker<T extends Slot = Slot>(props: SlotPickerProps<T>) {\n const { slots, timezone, selectedStartAt, onPick, onDayChange, classes = DEFAULT_CLASSES } = props;\n const byDay = useMemo(() => groupSlotsByDay(slots, timezone), [slots, timezone]);\n const dayKeys = [...byDay.keys()];\n const [activeDay, setActiveDay] = useState<string | undefined>(dayKeys[0]);\n\n // Keep the active day valid if the slot set changed under us (e.g. a reload\n // after a 409): fall back to the first available day.\n const day = activeDay !== undefined && byDay.has(activeDay) ? activeDay : dayKeys[0];\n const times = (day !== undefined ? byDay.get(day) : undefined) ?? [];\n\n return (\n <>\n <div className={classes.days}>\n {dayKeys.map((key) => {\n const first = byDay.get(key)?.[0];\n return (\n <button\n key={key}\n type=\"button\"\n className={classes.day}\n aria-pressed={key === day}\n onClick={() => {\n setActiveDay(key);\n onDayChange?.();\n }}\n >\n {first ? dayLabel(first.startAt, timezone) : key}\n </button>\n );\n })}\n </div>\n <div className={classes.times}>\n {times.map((s) => (\n <button\n key={s.startAt}\n type=\"button\"\n className={classes.time}\n aria-pressed={selectedStartAt === s.startAt}\n onClick={() => onPick(s)}\n >\n {timeLabel(s.startAt, timezone)}\n </button>\n ))}\n </div>\n </>\n );\n}\n","// Date/timezone formatting for the chapter member islands (join booking, member\n// area). Ported from proven production helpers. Every time renders in the\n// group's SCHEDULING timezone with an explicit abbreviation — never the viewer's\n// unlabeled local zone — so an applicant always sees the time the chapter meant.\n// Pure and framework-free, so it is unit-testable without a DOM.\n\n/** A bookable slot: a start instant in epoch milliseconds. */\nexport interface Slot {\n startAt: number;\n}\n\n/** The timezone's short abbreviation (e.g. \"PST\"); falls back to the id if the\n * runtime can't resolve it. */\nexport function tzShort(tz: string): string {\n try {\n const parts = new Intl.DateTimeFormat(undefined, { timeZone: tz, timeZoneName: \"short\" }).formatToParts(\n new Date(),\n );\n return parts.find((p) => p.type === \"timeZoneName\")?.value ?? tz;\n } catch {\n return tz;\n }\n}\n\n/** DST-safe day bucket key (ISO date in the target zone). Groups slots by the\n * calendar day a viewer in `tz` would see, not by UTC midnight. */\nexport function dayKey(ms: number, tz: string): string {\n return new Date(ms).toLocaleDateString(\"en-CA\", { timeZone: tz });\n}\n\n/** Short day label for a day chip, e.g. \"Mon, Jun 3\". */\nexport function dayLabel(ms: number, tz: string): string {\n return new Date(ms).toLocaleDateString(undefined, { timeZone: tz, weekday: \"short\", month: \"short\", day: \"numeric\" });\n}\n\n/** Time-of-day label for a slot button, e.g. \"2:30 PM\". */\nexport function timeLabel(ms: number, tz: string): string {\n return new Date(ms).toLocaleTimeString(undefined, { timeZone: tz, hour: \"numeric\", minute: \"2-digit\" });\n}\n\n/** Full, human confirmation label, e.g. \"Monday, June 3, 2:30 PM PST\". */\nexport function fullLabel(ms: number, tz: string): string {\n return new Date(ms).toLocaleString(undefined, {\n timeZone: tz,\n weekday: \"long\",\n month: \"long\",\n day: \"numeric\",\n hour: \"numeric\",\n minute: \"2-digit\",\n timeZoneName: \"short\",\n });\n}\n\n/** Currency label from integer cents, e.g. 100000 → \"$1,000\". */\nexport function fmtMoney(cents: number): string {\n return \"$\" + Math.round(cents / 100).toLocaleString();\n}\n\n/** Short date label, e.g. \"Jun 3, 2026\" (used for renewal/refund copy). */\nexport function fmtDate(ms: number): string {\n return new Date(ms).toLocaleDateString(undefined, { month: \"short\", day: \"numeric\", year: \"numeric\" });\n}\n\n/** Bucket slots into an insertion-ordered `Map<dayKey, Slot[]>` for the picker.\n * Input order is preserved within each day, so a server that returns slots\n * chronologically yields chronological chips + times. */\nexport function groupSlotsByDay<T extends Slot>(slots: readonly T[], tz: string): Map<string, T[]> {\n const byDay = new Map<string, T[]>();\n for (const s of slots) {\n const k = dayKey(s.startAt, tz);\n const bucket = byDay.get(k);\n if (bucket) bucket.push(s);\n else byDay.set(k, [s]);\n }\n return byDay;\n}\n","// The member area island. Reads GET /api/me (role + the member's own\n// application, folded server-side) and renders the account header plus, for a\n// provisional member, their application card with book/reschedule; a full member\n// sees the caller-supplied `memberContent`. Ported from the reference members island to\n// Preact, driven by the injected `api` (no Clerk SDK dependency).\nimport type { ComponentChildren } from \"preact\";\nimport { useEffect, useState } from \"preact/hooks\";\nimport { Rescheduler } from \"./reschedule.js\";\nimport { fmtDate, fullLabel } from \"./datetime.js\";\nimport type { ApiFn } from \"./api.js\";\nimport type { MemberApplication } from \"../session.js\";\n\ninterface Me {\n email: string | null;\n role: string;\n application: MemberApplication | null;\n}\n\n/** Props for {@link MembersArea}. */\nexport interface MembersAreaProps {\n api: ApiFn;\n /** Sign the member out (the host owns Clerk's signOut). */\n signOut: () => void;\n /** Where the \"Admin console\" link points for admins. Default \"/admin/\". */\n adminHref?: string;\n /** Where an accountless member goes to apply. Default \"/join.html\". */\n applyHref?: string;\n /** What a full (non-provisional) member sees below the account header. */\n memberContent?: ComponentChildren;\n}\n\nfunction card(kicker: string, body: ComponentChildren): ComponentChildren {\n return (\n <div className=\"card\">\n <div className=\"card-label\">Your Application</div>\n <div className=\"meeting-block\">\n <div className=\"meeting-kicker\">{kicker}</div>\n {body}\n </div>\n </div>\n );\n}\n\nfunction ProvisionalCard(props: {\n api: ApiFn;\n application: MemberApplication | null;\n applyHref: string;\n onReschedule: () => void | Promise<void>;\n}): ComponentChildren {\n const { api, application, applyHref, onReschedule } = props;\n if (!application) {\n return card(\n \"One step remains\",\n <>\n <div className=\"meeting-note\">Your account is ready, and the application that completes it takes a few minutes.</div>\n <a className=\"apply-link\" href={applyHref}>\n Apply for membership\n </a>\n </>,\n );\n }\n if (application.status === \"refunded\") {\n return card(\"Membership refunded\", <div className=\"meeting-note\">Your fee has been refunded in full and your membership is canceled.</div>);\n }\n const membership = application.paid ? (\n <div className=\"meeting-note\">Your membership is active{application.renewalAt ? ` and renews ${fmtDate(application.renewalAt)}` : \"\"}.</div>\n ) : null;\n if (application.meetingAt) {\n return card(\n \"Your introduction call\",\n <>\n <div className=\"meeting-date\">{fullLabel(application.meetingAt, application.timezone)}</div>\n <div className=\"meeting-note\">A calendar invitation with the video call link is in your email.</div>\n {application.meetUrl ? (\n <div className=\"meeting-note\">\n <a href={application.meetUrl} target=\"_blank\" rel=\"noopener\">\n Join the video call\n </a>\n </div>\n ) : null}\n <Rescheduler api={api} applicationId={application.id} timezone={application.timezone} onRescheduled={onReschedule} />\n {membership}\n </>,\n );\n }\n return card(\n \"Book your introduction call\",\n <>\n <div className=\"meeting-note\">Your application is in. Choose a time below, and a calendar invitation will reach your email.</div>\n <Rescheduler api={api} applicationId={application.id} timezone={application.timezone} onRescheduled={onReschedule} label=\"Choose a time\" />\n {membership}\n </>,\n );\n}\n\n/** The signed-in member area. Loads /api/me on mount and renders the account\n * header plus the provisional application card or the full-member content. */\nexport function MembersArea(props: MembersAreaProps) {\n const { api, signOut, adminHref = \"/admin/\", applyHref = \"/join.html\", memberContent } = props;\n const [me, setMe] = useState<Me | null>(null);\n const [error, setError] = useState(false);\n\n const reload = async () => {\n try {\n setMe(await api<Me>(\"/api/me\"));\n } catch {\n setError(true);\n }\n };\n useEffect(() => {\n void reload();\n }, []);\n\n if (error) return <p className=\"meeting-note\">Sign in is briefly unavailable. Please refresh.</p>;\n if (!me) return <p className=\"meeting-note\">Loading…</p>;\n const role = me.role || \"provisional\";\n\n return (\n <>\n <div className=\"card\">\n <div className=\"member-row\">\n <span className=\"member-email\">{me.email}</span>\n <span className={\"role-badge \" + role}>{role}</span>\n </div>\n <div className=\"account-actions\">\n <button className=\"btn secondary mini\" onClick={signOut}>\n Sign out\n </button>\n {role === \"admin\" ? (\n <a className=\"admin-console-link\" href={adminHref}>\n Admin console\n </a>\n ) : null}\n </div>\n </div>\n {role === \"provisional\" ? (\n <ProvisionalCard api={api} application={me.application} applyHref={applyHref} onReschedule={reload} />\n ) : (\n (memberContent ?? (\n <div className=\"card\">\n <div className=\"card-label\">Welcome back.</div>\n </div>\n ))\n )}\n </>\n );\n}\n","// In-place reschedule: open a SlotPicker, rebook via /api/schedule/book (the same\n// capability the join flow uses; the application id is the credential). Ported\n// from the reference member-area Rescheduler, driven by the injected `api`.\nimport { useState } from \"preact/hooks\";\nimport { SlotPicker } from \"./slot-picker.js\";\nimport type { Slot } from \"./datetime.js\";\nimport type { ApiFn } from \"./api.js\";\n\ninterface SlotsResponse {\n schedulingReady?: boolean;\n slots?: Slot[];\n timezone?: string;\n}\n\n/** Props for {@link Rescheduler}. */\nexport interface RescheduleProps {\n api: ApiFn;\n applicationId: string;\n timezone: string;\n /** Called after a successful (re)booking so the caller can refresh /api/me. */\n onRescheduled: () => void | Promise<void>;\n /** Link text to open the picker (e.g. \"Choose a time\" for a first booking). */\n label?: string;\n}\n\n/** A collapsed \"change your time\" link that expands into a {@link SlotPicker} and\n * rebooks on pick. Degrades to a message when scheduling is unavailable. */\nexport function Rescheduler(props: RescheduleProps) {\n const { api, applicationId, timezone, onRescheduled, label = \"Change your time\" } = props;\n const [open, setOpen] = useState(false);\n const [slots, setSlots] = useState<Slot[] | null>(null);\n const [tz, setTz] = useState(timezone);\n const [busy, setBusy] = useState(false);\n const [msg, setMsg] = useState<string | null>(null);\n\n const start = async () => {\n setOpen(true);\n setSlots(null);\n setMsg(null);\n try {\n const r = await api<SlotsResponse>(\"/api/schedule/slots\");\n if (r.schedulingReady === false) {\n setSlots([]);\n setMsg(\"Scheduling is briefly unavailable. We'll reach out by email to arrange your call.\");\n return;\n }\n setSlots(r.slots ?? []);\n if (r.timezone) setTz(r.timezone);\n } catch {\n setSlots([]);\n setMsg(\"Times are briefly unavailable. Please try again.\");\n }\n };\n\n const pick = async (slot: Slot) => {\n setBusy(true);\n setMsg(null);\n try {\n await api(\"/api/schedule/book\", { method: \"POST\", body: JSON.stringify({ applicationId, startAt: slot.startAt }) });\n setOpen(false);\n await onRescheduled();\n } catch (e) {\n setMsg(e instanceof Error ? e.message : \"That time is no longer available. Please pick another.\");\n } finally {\n setBusy(false);\n }\n };\n\n if (!open) {\n return (\n <p className=\"meeting-note\">\n <a\n href=\"#\"\n onClick={(e) => {\n e.preventDefault();\n void start();\n }}\n >\n {label}\n </a>\n </p>\n );\n }\n return (\n <div className=\"msched\">\n {slots === null ? (\n <p className=\"meeting-note\">Loading available times…</p>\n ) : slots.length === 0 ? (\n <p className=\"meeting-note\">{msg ?? \"No open times right now. Please check back soon.\"}</p>\n ) : (\n <SlotPicker\n slots={slots}\n timezone={tz}\n classes={{ days: \"msched-days\", day: \"msched-day\", times: \"msched-times\", time: \"msched-time\" }}\n onPick={(s) => void pick(s)}\n />\n )}\n {busy ? <p className=\"meeting-note\">Rescheduling…</p> : null}\n {msg && slots && slots.length > 0 ? <p className=\"meeting-note error\">{msg}</p> : null}\n <p className=\"meeting-note\">\n <a\n href=\"#\"\n onClick={(e) => {\n e.preventDefault();\n setOpen(false);\n }}\n >\n Keep my current time\n </a>\n </p>\n </div>\n );\n}\n","// The join island — the signup flow orchestrator. The SITE provides the form\n// fields (as children); this owns the flow: submit → /api/applications →\n// (paymentsReady) the Stripe PaymentStep → (schedulingReady) the SlotPicker\n// booking → confirmation. Faithful to the reference join flow, but the flow +\n// payment + booking are packaged instead of hand-rolled per site.\nimport type { ComponentChildren, JSX } from \"preact\";\nimport { useEffect, useState } from \"preact/hooks\";\nimport { SlotPicker } from \"./slot-picker.js\";\nimport { PaymentStep } from \"./payment-step.js\";\nimport { fullLabel } from \"./datetime.js\";\nimport type { Slot } from \"./datetime.js\";\n\n/** The public join config (the shape `GET /api/join-config` returns). */\nexport interface JoinConfig {\n id: string;\n name: string;\n paymentsReady: boolean;\n refundPolicyText?: string;\n}\n\n/** Props for {@link JoinIsland}. */\nexport interface JoinIslandProps {\n config: JoinConfig;\n /** The site's application form fields — inputs with `name` attributes; their\n * values are collected via FormData and posted to /api/applications. */\n children: ComponentChildren;\n /** Where the confirmation links after booking. Default \"/members/\". */\n membersHref?: string;\n}\n\ntype Step = \"form\" | \"pay\" | \"book\" | \"done\";\n\ninterface SlotsResponse {\n schedulingReady?: boolean;\n slots?: Slot[];\n timezone?: string;\n}\n\nfunction JoinBooking(props: { applicationId: string; onBooked: (b: { startAt: number; timezone: string }) => void }): ComponentChildren {\n const { applicationId, onBooked } = props;\n const [state, setState] = useState<{ slots: Slot[]; timezone: string } | null>(null);\n const [msg, setMsg] = useState<string | null>(null);\n const [busy, setBusy] = useState(false);\n const [selected, setSelected] = useState<Slot | null>(null);\n\n const load = async () => {\n setMsg(null);\n try {\n const res = await fetch(\"/api/schedule/slots\");\n const data = (await res.json()) as SlotsResponse;\n if (data.schedulingReady === false || !data.slots?.length) {\n setState(null);\n setMsg(\"Scheduling is briefly unavailable — we'll reach out by email to arrange your call.\");\n return;\n }\n setState({ slots: data.slots, timezone: data.timezone ?? \"UTC\" });\n } catch {\n setMsg(\"Times are briefly unavailable. Please try again.\");\n }\n };\n useEffect(() => {\n void load();\n }, []);\n\n const book = async () => {\n if (!selected) return;\n setBusy(true);\n setMsg(null);\n try {\n const res = await fetch(\"/api/schedule/book\", {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ applicationId, startAt: selected.startAt }),\n });\n const data = (await res.json()) as { startAt?: number; error?: string; code?: string };\n if (res.status === 409 && data.code === \"calendar_slot_unavailable\") {\n setSelected(null);\n setMsg(\"That time was just taken. Here are the current openings.\");\n await load();\n return;\n }\n if (!res.ok) throw new Error(data.error ?? \"The booking could not be completed.\");\n onBooked({ startAt: data.startAt ?? selected.startAt, timezone: state?.timezone ?? \"UTC\" });\n } catch (e) {\n setMsg(e instanceof Error ? e.message : \"The booking could not be completed.\");\n } finally {\n setBusy(false);\n }\n };\n\n if (!state) return <p className=\"slots-status\">{msg ?? \"Loading available times…\"}</p>;\n return (\n <div className=\"join-book\">\n <SlotPicker\n slots={state.slots}\n timezone={state.timezone}\n selectedStartAt={selected?.startAt}\n onPick={setSelected}\n onDayChange={() => setSelected(null)}\n />\n <div className=\"slot-confirm\" hidden={!selected}>\n <button className=\"submit-btn\" disabled={busy} onClick={() => void book()}>\n {busy ? \"Booking…\" : \"Book this time\"}\n </button>\n {msg ? <p className=\"step2-note\">{msg}</p> : null}\n </div>\n </div>\n );\n}\n\n/** The signup island. Renders the site's form, then drives payment (when the\n * chapter charges) and booking (when a calendar is connected) to confirmation. */\nexport function JoinIsland(props: JoinIslandProps) {\n const { config, children, membersHref = \"/members/\" } = props;\n const [step, setStep] = useState<Step>(\"form\");\n const [applicationId, setApplicationId] = useState<string | null>(null);\n const [error, setError] = useState<string | null>(null);\n const [submitting, setSubmitting] = useState(false);\n const [booked, setBooked] = useState<{ startAt: number; timezone: string } | null>(null);\n\n const submit = async (e: JSX.TargetedSubmitEvent<HTMLFormElement>) => {\n e.preventDefault();\n setSubmitting(true);\n setError(null);\n try {\n const fields: Record<string, unknown> = {};\n for (const [k, v] of new FormData(e.currentTarget).entries()) fields[k] = v;\n fields.submissionId = crypto.randomUUID();\n const res = await fetch(\"/api/applications\", {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify(fields),\n });\n const data = (await res.json()) as { id?: string; error?: string };\n if (!res.ok || !data.id) throw new Error(data.error ?? \"Your application could not be submitted.\");\n setApplicationId(data.id);\n setStep(config.paymentsReady ? \"pay\" : \"book\");\n } catch (err) {\n setError(err instanceof Error ? err.message : \"Something went wrong. Please try again.\");\n } finally {\n setSubmitting(false);\n }\n };\n\n if (step === \"done\" && booked) {\n return (\n <div className=\"join-done card\">\n <div className=\"card-label\">You're booked</div>\n <p className=\"meeting-date\">{fullLabel(booked.startAt, booked.timezone)}</p>\n <p className=\"meeting-note\">A calendar invitation with the video call link is on its way to your email.</p>\n <a className=\"apply-link\" href={membersHref}>\n Go to your member area\n </a>\n </div>\n );\n }\n if (step === \"book\" && applicationId) {\n return (\n <JoinBooking\n applicationId={applicationId}\n onBooked={(b) => {\n setBooked(b);\n setStep(\"done\");\n }}\n />\n );\n }\n if (step === \"pay\" && applicationId) {\n return <PaymentStep applicationId={applicationId} refundPolicyText={config.refundPolicyText ?? \"\"} onPaid={() => setStep(\"book\")} />;\n }\n return (\n <form className=\"join-form\" onSubmit={(e) => void submit(e)}>\n {children}\n {error ? <p className=\"join-error\">{error}</p> : null}\n <button className=\"submit-btn\" type=\"submit\" disabled={submitting}>\n {submitting ? \"Submitting…\" : \"Submit application\"}\n </button>\n </form>\n );\n}\n","// The join flow's payment step. Ports the reference card entry into the island:\n// dynamically loads js.stripe.com (NO @stripe/react-stripe-js dep), creates a\n// subscription server-side, and mounts a Stripe Payment Element. The refund-policy\n// checkbox is the gate — checking it starts the (money-creating) subscription\n// call. Client success is advisory only; the webhook is the\n// authoritative writer of paid state (see payments.ts).\nimport { useRef, useState } from \"preact/hooks\";\n\n// Minimal shapes for the dynamically-loaded js.stripe.com global.\ninterface StripeElementsApi {\n create(type: string): { mount(target: HTMLElement): void };\n}\ninterface StripeApi {\n elements(opts: { clientSecret: string }): StripeElementsApi;\n confirmPayment(opts: {\n elements: StripeElementsApi;\n confirmParams?: { return_url?: string };\n redirect?: \"if_required\";\n }): Promise<{ error?: { message?: string }; paymentIntent?: { status?: string } }>;\n}\ntype StripeFactory = (publishableKey: string) => StripeApi;\n\nlet loader: Promise<StripeFactory> | null = null;\n\n/** Load js.stripe.com once and resolve the `Stripe` global (browser only). */\nfunction loadStripe(): Promise<StripeFactory> {\n const existing = (globalThis as { Stripe?: StripeFactory }).Stripe;\n if (existing) return Promise.resolve(existing);\n if (!loader) {\n loader = new Promise<StripeFactory>((resolve, reject) => {\n const s = document.createElement(\"script\");\n s.src = \"https://js.stripe.com/v3/\";\n s.onload = () => {\n const fn = (globalThis as { Stripe?: StripeFactory }).Stripe;\n if (fn) resolve(fn);\n else reject(new Error(\"stripe.js unavailable\"));\n };\n s.onerror = () => reject(new Error(\"stripe.js failed to load\"));\n document.head.appendChild(s);\n });\n }\n return loader;\n}\n\n/** Props for {@link PaymentStep}. */\nexport interface PaymentStepProps {\n /** The application to attach the subscription to (the capability). */\n applicationId: string;\n /** Refund-policy copy the applicant must acknowledge to start payment. */\n refundPolicyText: string;\n /** Called once the payment succeeds (or is processing) — advance to booking. */\n onPaid: () => void;\n}\n\ninterface SubResponse {\n clientSecret?: string;\n publishableKey?: string | null;\n}\n\n/** The card-entry step: acknowledge the refund policy → create the subscription\n * → mount Stripe Elements → confirm. */\nexport function PaymentStep(props: PaymentStepProps) {\n const { applicationId, refundPolicyText, onPaid } = props;\n const [status, setStatus] = useState<\"idle\" | \"loading\" | \"ready\" | \"confirming\">(\"idle\");\n const [error, setError] = useState<string | null>(null);\n const mountRef = useRef<HTMLDivElement | null>(null);\n const stripeRef = useRef<StripeApi | null>(null);\n const elementsRef = useRef<StripeElementsApi | null>(null);\n\n const begin = async () => {\n setStatus(\"loading\");\n setError(null);\n try {\n const res = await fetch(\"/api/payments/subscription\", {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ applicationId, refundPolicyAck: true }),\n });\n const data = (await res.json()) as SubResponse;\n if (!res.ok || !data.clientSecret || !data.publishableKey) throw new Error(\"Payment could not be set up. Please try again.\");\n const stripe = (await loadStripe())(data.publishableKey);\n const elements = stripe.elements({ clientSecret: data.clientSecret });\n const element = elements.create(\"payment\");\n if (mountRef.current) element.mount(mountRef.current);\n stripeRef.current = stripe;\n elementsRef.current = elements;\n setStatus(\"ready\");\n } catch (e) {\n setStatus(\"idle\");\n setError(e instanceof Error ? e.message : \"Payment could not be set up. Please try again.\");\n }\n };\n\n const pay = async () => {\n const stripe = stripeRef.current;\n const elements = elementsRef.current;\n if (!stripe || !elements) return;\n setStatus(\"confirming\");\n setError(null);\n const returnUrl =\n typeof window !== \"undefined\" ? `${window.location.origin}${window.location.pathname}?redirect_status=succeeded` : undefined;\n const result = await stripe.confirmPayment({ elements, confirmParams: { return_url: returnUrl }, redirect: \"if_required\" });\n if (result.error) {\n setError(result.error.message ?? \"The payment could not be completed.\");\n setStatus(\"ready\");\n return;\n }\n const paid = result.paymentIntent?.status;\n if (paid === \"succeeded\" || paid === \"processing\") onPaid();\n else {\n setError(\"The payment did not complete. Please try again.\");\n setStatus(\"ready\");\n }\n };\n\n return (\n <div className=\"join-pay\">\n <label className=\"compliance-box\">\n <input\n type=\"checkbox\"\n disabled={status !== \"idle\"}\n onChange={(e) => {\n if (e.currentTarget.checked) void begin();\n }}\n />\n <span>{refundPolicyText}</span>\n </label>\n {status === \"loading\" ? <p className=\"pay-status\">Preparing secure payment…</p> : null}\n <div ref={mountRef} hidden={status === \"idle\" || status === \"loading\"} />\n {status === \"ready\" || status === \"confirming\" ? (\n <button className=\"submit-btn\" disabled={status === \"confirming\"} onClick={() => void pay()}>\n {status === \"confirming\" ? \"Processing…\" : \"Pay and continue\"}\n </button>\n ) : null}\n {error ? <p className=\"pay-error\">{error}</p> : null}\n </div>\n );\n}\n"],"mappings":"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","names":["useState","useState","jsx","jsxs","useState","Fragment","jsx","jsxs","useState","useEffect","useState","useState","jsx","jsxs","jsx","jsxs","useState","useEffect"]}
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  BrandStyle
3
- } from "./chunk-VZPBTTO3.js";
3
+ } from "./chunk-3QKGKUTX.js";
4
4
 
5
5
  // src/ui/admin.tsx
6
- import { useEffect as useEffect7, useMemo, useState as useState11 } from "react";
6
+ import { useEffect as useEffect7, useMemo, useState as useState11 } from "preact/hooks";
7
7
  import {
8
8
  ClerkGate,
9
9
  SignedIn,
@@ -15,12 +15,12 @@ import {
15
15
  import { CrmClient } from "@odla-ai/crm";
16
16
 
17
17
  // src/ui/chrome.tsx
18
- import { useCallback, useEffect as useEffect2, useState as useState2 } from "react";
18
+ import { useCallback, useEffect as useEffect2, useState as useState2 } from "preact/hooks";
19
19
  import { Tabs, TopBar, TopBarLink } from "@odla-ai/ui/components";
20
20
 
21
21
  // src/ui/admin-layout.tsx
22
- import { useEffect, useState } from "react";
23
- import { jsx, jsxs } from "react/jsx-runtime";
22
+ import { useEffect, useState } from "preact/hooks";
23
+ import { jsx, jsxs } from "preact/jsx-runtime";
24
24
  var PAGE = {
25
25
  maxWidth: "var(--ui-content-width, 1120px)",
26
26
  margin: "0 auto",
@@ -84,7 +84,7 @@ function AdminNote({ children }) {
84
84
  }
85
85
 
86
86
  // src/ui/admin-brand.tsx
87
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
87
+ import { jsx as jsx2, jsxs as jsxs2 } from "preact/jsx-runtime";
88
88
  var badgeText = (brand) => brand.badge ?? brand.name.slice(0, 3).toUpperCase();
89
89
  var brandDisplay = (brand) => {
90
90
  const display = brand.wordmark ?? brand.name;
@@ -269,7 +269,7 @@ function adminSectionHref(current, basePath, id, routing = "fragment") {
269
269
  }
270
270
 
271
271
  // src/ui/chrome.tsx
272
- import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
272
+ import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "preact/jsx-runtime";
273
273
  var mergeRoute = (current, target) => {
274
274
  if (typeof target === "string") return { workspaceId: target };
275
275
  const changingWorkspace = target.workspaceId != null && target.workspaceId !== current.workspaceId;
@@ -432,11 +432,11 @@ function AdminShell({
432
432
  import { Tabs as Tabs2 } from "@odla-ai/ui/components";
433
433
 
434
434
  // src/ui/admin-availability.tsx
435
- import { useState as useState4 } from "react";
435
+ import { useState as useState4 } from "preact/hooks";
436
436
  import { Button, Checkbox, Field, Input } from "@odla-ai/ui/components";
437
437
 
438
438
  // src/ui/admin-api.ts
439
- import { useCallback as useCallback2, useEffect as useEffect3, useState as useState3 } from "react";
439
+ import { useCallback as useCallback2, useEffect as useEffect3, useState as useState3 } from "preact/hooks";
440
440
  async function adminFetch(getToken, path, init) {
441
441
  const token = await getToken();
442
442
  const headers = new Headers(init?.headers);
@@ -466,7 +466,7 @@ function useAdminResource(getToken, path) {
466
466
  }
467
467
 
468
468
  // src/ui/admin-availability.tsx
469
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
469
+ import { jsx as jsx4, jsxs as jsxs4 } from "preact/jsx-runtime";
470
470
  var DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
471
471
  function AvailabilityBody({ ctx }) {
472
472
  const { data, loading, error, refresh } = useAdminResource(ctx.getToken, "/api/admin/scheduling");
@@ -506,7 +506,7 @@ function AvailabilityBody({ ctx }) {
506
506
  setBusy(false);
507
507
  }
508
508
  };
509
- const numField = (key, label) => /* @__PURE__ */ jsx4(Field, { label, error: errors[key], children: /* @__PURE__ */ jsx4(Input, { type: "number", value: cfg[key], onChange: (e) => edit({ [key]: num(e.target.value, cfg[key]) }) }) });
509
+ const numField = (key, label) => /* @__PURE__ */ jsx4(Field, { label, error: errors[key], children: /* @__PURE__ */ jsx4(Input, { type: "number", value: cfg[key], onInput: (e) => edit({ [key]: num(e.target.value, cfg[key]) }) }) });
510
510
  return /* @__PURE__ */ jsx4(AdminPage, { children: /* @__PURE__ */ jsxs4(Panel, { title: "Booking availability", actions: note ? /* @__PURE__ */ jsx4("span", { className: "muted", children: note }) : void 0, children: [
511
511
  /* @__PURE__ */ jsx4(Field, { label: "Days", error: errors.days, children: /* @__PURE__ */ jsx4("div", { style: { display: "flex", gap: 14, flexWrap: "wrap" }, children: DAYS.map((d, i) => /* @__PURE__ */ jsx4(Checkbox, { label: d, checked: cfg.days.includes(i), onChange: () => toggleDay(i) }, d)) }) }),
512
512
  /* @__PURE__ */ jsxs4("div", { style: { display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(180px, 1fr))", gap: 14 }, children: [
@@ -515,9 +515,9 @@ function AvailabilityBody({ ctx }) {
515
515
  numField("slotMinutes", "Slot length (minutes)"),
516
516
  numField("minNoticeHours", "Minimum notice (hours)"),
517
517
  numField("windowDays", "Booking window (days)"),
518
- /* @__PURE__ */ jsx4(Field, { label: "Timezone", error: errors.timezone, children: /* @__PURE__ */ jsx4(Input, { value: cfg.timezone, onChange: (e) => edit({ timezone: e.target.value }) }) })
518
+ /* @__PURE__ */ jsx4(Field, { label: "Timezone", error: errors.timezone, children: /* @__PURE__ */ jsx4(Input, { value: cfg.timezone, onInput: (e) => edit({ timezone: e.target.value }) }) })
519
519
  ] }),
520
- /* @__PURE__ */ jsx4(Field, { label: "Event summary template", error: errors.summaryTemplate, children: /* @__PURE__ */ jsx4(Input, { value: cfg.summaryTemplate, onChange: (e) => edit({ summaryTemplate: e.target.value }) }) }),
520
+ /* @__PURE__ */ jsx4(Field, { label: "Event summary template", error: errors.summaryTemplate, children: /* @__PURE__ */ jsx4(Input, { value: cfg.summaryTemplate, onInput: (e) => edit({ summaryTemplate: e.target.value }) }) }),
521
521
  /* @__PURE__ */ jsx4("div", { children: /* @__PURE__ */ jsx4(Button, { disabled: !draft || busy, onClick: () => void save(), children: "Save availability" }) })
522
522
  ] }) });
523
523
  }
@@ -528,7 +528,7 @@ function availabilitySection(options = {}) {
528
528
 
529
529
  // src/ui/admin-billing.tsx
530
530
  import { DataTable, StatBand } from "@odla-ai/ui/components";
531
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
531
+ import { jsx as jsx5, jsxs as jsxs5 } from "preact/jsx-runtime";
532
532
  var money = (cents) => `$${(cents / 100).toLocaleString()}`;
533
533
  var date = (t) => t ? new Date(t).toLocaleDateString([], { dateStyle: "medium" }) : "\u2014";
534
534
  var COLUMNS = [
@@ -570,7 +570,7 @@ function billingSection(options = {}) {
570
570
 
571
571
  // src/ui/admin-dashboard.tsx
572
572
  import { MetricWidget, StepPipeline } from "@odla-ai/ui/components";
573
- import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
573
+ import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs6 } from "preact/jsx-runtime";
574
574
  var number = (value) => Math.round(value).toLocaleString();
575
575
  var money2 = (cents) => `$${Math.round(cents / 100).toLocaleString()}`;
576
576
  var when = (time) => new Date(time).toLocaleString([], { dateStyle: "medium", timeStyle: "short" });
@@ -654,9 +654,9 @@ function dashboardSection(options = {}) {
654
654
  }
655
655
 
656
656
  // src/ui/admin-email.tsx
657
- import { useState as useState5 } from "react";
657
+ import { useState as useState5 } from "preact/hooks";
658
658
  import { Button as Button2, Checkbox as Checkbox2, DataTable as DataTable2, Field as Field2, Input as Input2, Textarea } from "@odla-ai/ui/components";
659
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
659
+ import { jsx as jsx7, jsxs as jsxs7 } from "preact/jsx-runtime";
660
660
  var LOG_COLUMNS = [
661
661
  { key: "sentAt", header: "Sent", cell: (r) => new Date(r.sentAt).toLocaleString(), sortAs: "number", sortValue: (r) => r.sentAt },
662
662
  { key: "template", header: "Template", cell: (r) => r.template },
@@ -712,16 +712,16 @@ function EmailBody({ ctx }) {
712
712
  data.fromEmail ? ` \xB7 ${data.fromEmail}` : ""
713
713
  ] }),
714
714
  children: [
715
- /* @__PURE__ */ jsx7(Field2, { label: "Notification address", children: /* @__PURE__ */ jsx7(Input2, { value: data.notificationEmail, onChange: (e) => edit({ notificationEmail: e.target.value }) }) }),
716
- /* @__PURE__ */ jsx7(Field2, { label: "Reply-to", children: /* @__PURE__ */ jsx7(Input2, { value: data.replyTo, onChange: (e) => edit({ replyTo: e.target.value }) }) }),
717
- /* @__PURE__ */ jsx7(Field2, { label: "Debug inbox", hint: "Non-prod sends redirect here.", children: /* @__PURE__ */ jsx7(Input2, { value: data.debugEmail, onChange: (e) => edit({ debugEmail: e.target.value }) }) })
715
+ /* @__PURE__ */ jsx7(Field2, { label: "Notification address", children: /* @__PURE__ */ jsx7(Input2, { value: data.notificationEmail, onInput: (e) => edit({ notificationEmail: e.target.value }) }) }),
716
+ /* @__PURE__ */ jsx7(Field2, { label: "Reply-to", children: /* @__PURE__ */ jsx7(Input2, { value: data.replyTo, onInput: (e) => edit({ replyTo: e.target.value }) }) }),
717
+ /* @__PURE__ */ jsx7(Field2, { label: "Debug inbox", hint: "Non-prod sends redirect here.", children: /* @__PURE__ */ jsx7(Input2, { value: data.debugEmail, onInput: (e) => edit({ debugEmail: e.target.value }) }) })
718
718
  ]
719
719
  }
720
720
  ),
721
721
  Object.entries(data.emailTemplates).map(([key, t]) => /* @__PURE__ */ jsxs7(Panel, { title: key, actions: /* @__PURE__ */ jsx7(Button2, { variant: "secondary", mini: true, disabled: busy === `test:${key}`, onClick: () => void test(key), children: "Send test" }), children: [
722
722
  /* @__PURE__ */ jsx7(Checkbox2, { label: "Enabled", checked: t.enabled, onChange: (e) => editTemplate(key, { enabled: e.target.checked }) }),
723
- /* @__PURE__ */ jsx7(Field2, { label: "Subject", children: /* @__PURE__ */ jsx7(Input2, { value: t.subject, onChange: (e) => editTemplate(key, { subject: e.target.value }) }) }),
724
- /* @__PURE__ */ jsx7(Field2, { label: "Body", children: /* @__PURE__ */ jsx7(Textarea, { prose: true, rows: 5, value: t.text, onChange: (e) => editTemplate(key, { text: e.target.value }) }) })
723
+ /* @__PURE__ */ jsx7(Field2, { label: "Subject", children: /* @__PURE__ */ jsx7(Input2, { value: t.subject, onInput: (e) => editTemplate(key, { subject: e.target.value }) }) }),
724
+ /* @__PURE__ */ jsx7(Field2, { label: "Body", children: /* @__PURE__ */ jsx7(Textarea, { prose: true, rows: 5, value: t.text, onInput: (e) => editTemplate(key, { text: e.target.value }) }) })
725
725
  ] }, key)),
726
726
  /* @__PURE__ */ jsxs7("div", { style: { display: "flex", gap: 12, alignItems: "center" }, children: [
727
727
  /* @__PURE__ */ jsx7(Button2, { disabled: !draft || busy === "save", onClick: () => void save(), children: "Save changes" }),
@@ -739,9 +739,9 @@ function emailSection(options = {}) {
739
739
  import { BillingCard, CrmWorkspace } from "@odla-ai/crm/ui";
740
740
 
741
741
  // src/ui/admin-record-actions.tsx
742
- import { useEffect as useEffect4, useState as useState6 } from "react";
742
+ import { useEffect as useEffect4, useState as useState6 } from "preact/hooks";
743
743
  import { Button as Button3, Field as Field3, Select } from "@odla-ai/ui/components";
744
- import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
744
+ import { jsx as jsx8, jsxs as jsxs8 } from "preact/jsx-runtime";
745
745
  function RecordActions({ getToken, record, roles, onChanged }) {
746
746
  const userId = typeof record.clerkUserId === "string" ? record.clerkUserId : null;
747
747
  const applicationId = typeof record.fields?.applicationId === "string" ? record.fields.applicationId : null;
@@ -796,8 +796,8 @@ function RecordActions({ getToken, record, roles, onChanged }) {
796
796
  }
797
797
 
798
798
  // src/ui/admin-network.tsx
799
- import { useState as useState7 } from "react";
800
- import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
799
+ import { useState as useState7 } from "preact/hooks";
800
+ import { jsx as jsx9, jsxs as jsxs9 } from "preact/jsx-runtime";
801
801
  function NetworkShareActions(props) {
802
802
  const { recordId, recordType, getToken } = props;
803
803
  const targets = useAdminResource(getToken, "/api/admin/network/targets");
@@ -832,9 +832,9 @@ function NetworkShareActions(props) {
832
832
  }
833
833
 
834
834
  // src/ui/admin-record-activity.tsx
835
- import { useCallback as useCallback3, useEffect as useEffect5, useState as useState8 } from "react";
835
+ import { useCallback as useCallback3, useEffect as useEffect5, useState as useState8 } from "preact/hooks";
836
836
  import { ActivityFeed } from "@odla-ai/crm/ui";
837
- import { jsx as jsx10 } from "react/jsx-runtime";
837
+ import { jsx as jsx10 } from "preact/jsx-runtime";
838
838
  function RecordActivityPanel(props) {
839
839
  const { client, recordId } = props;
840
840
  const [activities, setActivities] = useState8([]);
@@ -873,9 +873,9 @@ function RecordActivityPanel(props) {
873
873
  }
874
874
 
875
875
  // src/ui/admin-record-comms.tsx
876
- import { useCallback as useCallback4, useEffect as useEffect6, useState as useState9 } from "react";
876
+ import { useCallback as useCallback4, useEffect as useEffect6, useState as useState9 } from "preact/hooks";
877
877
  import { Button as Button4, Field as Field4, Select as Select2 } from "@odla-ai/ui/components";
878
- import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
878
+ import { jsx as jsx11, jsxs as jsxs10 } from "preact/jsx-runtime";
879
879
  function RecordCommsPanel(props) {
880
880
  const { client, recordId, getToken } = props;
881
881
  const config = useAdminResource(getToken, "/api/admin/group/email");
@@ -940,9 +940,9 @@ function RecordCommsHistory(props) {
940
940
  }
941
941
 
942
942
  // src/ui/admin-record-scheduling.tsx
943
- import { useState as useState10 } from "react";
943
+ import { useState as useState10 } from "preact/hooks";
944
944
  import { Button as Button5 } from "@odla-ai/ui/components";
945
- import { Fragment as Fragment3, jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
945
+ import { Fragment as Fragment3, jsx as jsx12, jsxs as jsxs11 } from "preact/jsx-runtime";
946
946
  function RecordSchedulingPanel(props) {
947
947
  const { getToken, applicationId } = props;
948
948
  const resource = useAdminResource(
@@ -1003,7 +1003,7 @@ function RecordSchedulingPanel(props) {
1003
1003
  }
1004
1004
 
1005
1005
  // src/ui/admin-people-workspace.tsx
1006
- import { jsx as jsx13 } from "react/jsx-runtime";
1006
+ import { jsx as jsx13 } from "preact/jsx-runtime";
1007
1007
  function applicationLifecycleAdapter(ctx) {
1008
1008
  return {
1009
1009
  transitionStage: async ({ record, to }) => {
@@ -1139,7 +1139,7 @@ function PeopleBody(props) {
1139
1139
  }
1140
1140
 
1141
1141
  // src/ui/admin-people.tsx
1142
- import { jsx as jsx14 } from "react/jsx-runtime";
1142
+ import { jsx as jsx14 } from "preact/jsx-runtime";
1143
1143
  var DEFAULT_ROLES = ["provisional", "member", "admin"];
1144
1144
  function collectionSection(options) {
1145
1145
  const { crm, type } = options;
@@ -1224,7 +1224,7 @@ function peopleSection(options) {
1224
1224
  }
1225
1225
 
1226
1226
  // src/ui/admin-workspaces.tsx
1227
- import { jsx as jsx15 } from "react/jsx-runtime";
1227
+ import { jsx as jsx15 } from "preact/jsx-runtime";
1228
1228
  var adminViewContext = (ctx, viewId, active) => {
1229
1229
  const route = {
1230
1230
  workspaceId: ctx.route.workspaceId,
@@ -1356,7 +1356,7 @@ function defaultAdminWorkspaces(chapter) {
1356
1356
 
1357
1357
  // src/ui/admin-frame.tsx
1358
1358
  import { ThemeScope } from "@odla-ai/ui/components";
1359
- import { jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
1359
+ import { jsx as jsx16, jsxs as jsxs12 } from "preact/jsx-runtime";
1360
1360
  function AdminTheme(props) {
1361
1361
  const brand = props.chapter?.brand;
1362
1362
  return /* @__PURE__ */ jsxs12(
@@ -1410,7 +1410,7 @@ async function loadChapterAdminUser(adapter, apiBase, getToken) {
1410
1410
  }
1411
1411
 
1412
1412
  // src/ui/admin.tsx
1413
- import { jsx as jsx17, jsxs as jsxs13 } from "react/jsx-runtime";
1413
+ import { jsx as jsx17, jsxs as jsxs13 } from "preact/jsx-runtime";
1414
1414
  function Authed(props) {
1415
1415
  const {
1416
1416
  workspaces,
@@ -1570,9 +1570,9 @@ function defaultAdminSections(chapter) {
1570
1570
  }
1571
1571
 
1572
1572
  // src/ui/admin-meetings.tsx
1573
- import { useState as useState12 } from "react";
1573
+ import { useState as useState12 } from "preact/hooks";
1574
1574
  import { Button as Button6 } from "@odla-ai/ui/components";
1575
- import { Fragment as Fragment4, jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
1575
+ import { Fragment as Fragment4, jsx as jsx18, jsxs as jsxs14 } from "preact/jsx-runtime";
1576
1576
  var when2 = (t) => new Date(t).toLocaleString([], { dateStyle: "medium", timeStyle: "short" });
1577
1577
  function MeetingsBody({ ctx }) {
1578
1578
  const { data, loading, error, refresh } = useAdminResource(ctx.getToken, "/api/admin/meetings?all=1");
@@ -1663,4 +1663,4 @@ export {
1663
1663
  defaultAdminSections,
1664
1664
  meetingsSection
1665
1665
  };
1666
- //# sourceMappingURL=chunk-IWW5VAAC.js.map
1666
+ //# sourceMappingURL=chunk-C4XQSRW7.js.map