@odla-ai/chapter 0.60.0 → 0.61.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.
@@ -117,6 +117,8 @@ var DEFAULT_ADMIN_COPY = {
117
117
  enabled: "Enabled",
118
118
  subject: "Subject",
119
119
  body: "Body",
120
+ htmlBody: "HTML body (optional)",
121
+ htmlBodyHint: "Sent beside the plain text when present, with the same {{placeholders}} and {{#name}} sections; values are escaped. Leave empty to send the plain text rendered as HTML.",
120
122
  save: "Save changes",
121
123
  sendLog: "Send log",
122
124
  sentColumn: "Sent",
@@ -527,4 +529,4 @@ export {
527
529
  useChapterCopy,
528
530
  BrandStyle
529
531
  };
530
- //# sourceMappingURL=chunk-QNP3UEIL.js.map
532
+ //# sourceMappingURL=chunk-PKVHXKGJ.js.map
@@ -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 admission: \"Admission\",\n network: \"Network\",\n portfolio: \"Portfolio\",\n settings: \"Settings\",\n calendar: \"Calendar\",\n email: \"Email\",\n chapters: \"Chapters\",\n formations: \"Formation applications\",\n dealFlow: \"Deal flow\",\n dashboardViewsLabel: \"Dashboard views\",\n settingsViewsLabel: \"Settings views\",\n collectionsLabel: \"CRM collections\",\n portfolioViewsLabel: \"Portfolio views\",\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 fromName: \"From name\",\n fromNameHint: \"Shown as the sender; the address stays the Worker's configured one.\",\n spamRisk: \"Spam-filter risk\",\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 htmlBody: \"HTML body (optional)\",\n htmlBodyHint: \"Sent beside the plain text when present, with the same {{placeholders}} and {{#name}} sections; values are escaped. Leave empty to send the plain text rendered as HTML.\",\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 admission: {\n title: \"Interview decisions\",\n description: \"Local decisions override Parent decisions. Requiring an interview locally suppresses a Parent waiver for the same email.\",\n email: \"Email\",\n decision: \"Decision\",\n expires: \"Expires\",\n note: \"Internal note\",\n waive: \"Waive interview\",\n require: \"Require interview\",\n add: \"Add decision\",\n ledger: \"Effective grant ledger\",\n loading: \"Loading…\",\n consumed: \"consumed\",\n revoked: \"revoked\",\n expired: \"expired\",\n active: \"active\",\n local: \"local\",\n parent: \"Parent\",\n revoke: \"Revoke\",\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 loadingRollup: \"Loading network overview…\",\n loadRollupFailed: \"Couldn't load the network overview\",\n overview: \"Network overview\",\n configuredFollowers: \"Configured followers\",\n reachableFollowers: \"Reachable followers\",\n recordsAcrossFollowers: \"Records across followers\",\n noFollowers: \"No follower sites are configured.\",\n followers: \"Followers\",\n available: \"available\",\n unavailable: \"unavailable\",\n transportReachable: \"transport reachable\",\n transportUnavailable: \"transport unavailable\",\n transportNotChecked: \"transport not checked\",\n edgeUnpaired: \"edge unpaired\",\n edgeUnpairedHint: \"This follower's shared secret is not in the leader vault, so nothing can be signed. Pair both vaults from an enrolled machine:\",\n bindingTransport: \"service binding {binding}\",\n urlTransport: \"public URL transport\",\n activeConfiguration: \"active configuration r{revision}\",\n noActiveConfiguration: \"no active managed configuration\",\n publicSiteAvailable: \"public site reachable\",\n publicSiteUnavailable: \"public site unavailable\",\n publicSiteNotChecked: \"public site not probed\",\n recordTotals: \"Portfolio records\",\n noRecordTotals: \"No aggregate records are available yet.\",\n },\n records: {\n workflowMissing: \"This record is not linked to an application workflow.\",\n invitation: \"Invitation\",\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 decline: \"Decline\",\n refund: \"Refund\",\n approved: \"Approved.\",\n declined: \"Declined.\",\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 namedSeat: {\n title: \"Your membership invitation\",\n from: \"{purchaserName} invited you.\",\n prepaid: \"Your {tierName} seat is already paid for.\",\n body: \"Accept your invitation, then complete your own application requirements. You will not be asked to pay for this seat.\",\n consent: \"I accept this membership for myself. It cannot be transferred and remains current while the purchaser's membership and my seat are current.\",\n accept: \"Accept my invitation\", accepting: \"Confirming your invitation…\", loading: \"Loading your invitation…\",\n unavailable: \"Your invitation is unavailable. Sign in with the invited email address, then try again.\",\n retry: \"Try again\", failure: \"We could not confirm acceptance. Retry this invitation; you do not need to purchase another membership.\",\n },\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 title: \"Book your introduction\",\n intro: \"Choose a time to meet your chapter and get to know each other.\",\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 pickDate: \"Pick a date\",\n pickTime: \"Pick a time\",\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 consent: \"I have read and agree to the pricing and refund policy above.\",\n foundingMember: \"Founding member rate\",\n foundingDiscount: \"Founding discount\",\n standardRate: \"Standard rate\",\n yourRate: \"Your rate\",\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 accepted: {\n label: \"You're approved\",\n body: \"Your membership is approved. No introduction call is required.\",\n },\n gift: {\n chooseSelf: \"Buy membership for myself\",\n optionsLabel: \"Or give access to someone else\",\n label: \"Your gift code\",\n body: \"Share this one-time code with the recipient. It becomes active after payment is confirmed.\",\n save: \"Save it now. For security, this code is shown only once.\",\n rotate: \"Generate a new code\",\n rotateFailed: \"A new code could not be generated.\",\n claimTitle: \"Have a gift code?\",\n claimBody: \"Use it here instead of paying by card.\",\n code: \"One-time code\",\n redeem: \"Use gift code\",\n redeemed: \"Gift code applied.\",\n redeemFailed: \"The code is invalid, unavailable, or does not match this membership.\",\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 gifts: {\n title: \"Give access\",\n body: \"Purchase access for someone else and share the one-time claim code.\",\n noneAvailable: \"No gift options are available right now.\",\n paid: \"Payment submitted. Save the code below while it is visible.\",\n saveCode: \"This code is shown once. Save it before leaving this page.\",\n purchases: \"Your gifts\",\n noPurchases: \"No gift purchases yet.\",\n pending: \"Pending\",\n redeemed: \"Redeemed\",\n expired: \"Expired\",\n revoked: \"Revoked\",\n rotate: \"Rotate code\",\n rotated: \"A new code was generated. Save it now.\",\n rotateFailed: \"The code could not be rotated.\",\n redeemTitle: \"Redeem a gift\",\n code: \"One-time code\",\n redeem: \"Redeem\",\n redeemFailed: \"The code is invalid or unavailable.\",\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"]}
@@ -2,7 +2,7 @@ import {
2
2
  DEFAULT_JOIN_COPY,
3
3
  formatChapterCopy,
4
4
  useChapterCopy
5
- } from "./chunk-QNP3UEIL.js";
5
+ } from "./chunk-PKVHXKGJ.js";
6
6
  import {
7
7
  collectFormFields
8
8
  } from "./chunk-IVXVECKR.js";
@@ -2698,4 +2698,4 @@ export {
2698
2698
  membershipApplicationHref,
2699
2699
  MembershipCards
2700
2700
  };
2701
- //# sourceMappingURL=chunk-D5UR5H6R.js.map
2701
+ //# sourceMappingURL=chunk-RTJKUOFE.js.map
@@ -95,7 +95,7 @@ interface ChapterCopy {
95
95
  availability: TextFields<"loading" | "loadFailed" | "saved" | "title" | "days" | "startHour" | "endHour" | "slotMinutes" | "minNoticeHours" | "windowDays" | "timezone" | "summaryTemplate" | "save"> & {
96
96
  dayLabels: readonly string[];
97
97
  };
98
- email: TextFields<"loading" | "loadFailed" | "saved" | "sent" | "testFailed" | "delivery" | "notificationAddress" | "replyTo" | "fromName" | "fromNameHint" | "spamRisk" | "debugInbox" | "debugInboxHint" | "sendTest" | "enabled" | "subject" | "body" | "save" | "sendLog" | "sentColumn" | "templateColumn" | "statusColumn" | "toColumn" | "failed" | "redirected" | "delivered">;
98
+ email: TextFields<"loading" | "loadFailed" | "saved" | "sent" | "testFailed" | "delivery" | "notificationAddress" | "replyTo" | "fromName" | "fromNameHint" | "spamRisk" | "debugInbox" | "debugInboxHint" | "sendTest" | "enabled" | "subject" | "body" | "htmlBody" | "htmlBodyHint" | "save" | "sendLog" | "sentColumn" | "templateColumn" | "statusColumn" | "toColumn" | "failed" | "redirected" | "delivered">;
99
99
  meetings: TextFields<"loading" | "loadFailed" | "cancelConfirm" | "newStartPrompt" | "parseFailed" | "agenda" | "empty" | "unknown" | "drift" | "meet" | "reschedule" | "cancel">;
100
100
  admission: TextFields<"title" | "description" | "email" | "decision" | "expires" | "note" | "waive" | "require" | "add" | "ledger" | "loading" | "consumed" | "revoked" | "expired" | "active" | "local" | "parent" | "revoke">;
101
101
  network: AdminNetworkCopy;
package/dist/index.cjs CHANGED
@@ -35,6 +35,7 @@ __export(src_exports, {
35
35
  DEFAULT_SHARE_FIELDS: () => DEFAULT_SHARE_FIELDS,
36
36
  EDGE_UNPAIRED: () => EDGE_UNPAIRED,
37
37
  GIFT_OFFER_CATALOG_VERSION: () => GIFT_OFFER_CATALOG_VERSION,
38
+ HTML_BODY_LIMIT: () => HTML_BODY_LIMIT,
38
39
  JOURNEY_ENDINGS: () => JOURNEY_ENDINGS,
39
40
  JOURNEY_STAGES: () => JOURNEY_STAGES,
40
41
  MAX_GIFT_OFFERS: () => MAX_GIFT_OFFERS,
@@ -87,6 +88,7 @@ __export(src_exports, {
87
88
  chapterDb: () => chapterDb,
88
89
  chapterEmailEvents: () => chapterEmailEvents,
89
90
  chapterFlowFieldContract: () => chapterFlowFieldContract,
91
+ checkoutVariables: () => checkoutVariables,
90
92
  clampArray: () => clampArray,
91
93
  classifyMemberStripeEvent: () => classifyMemberStripeEvent,
92
94
  classifyMemberSystemWrite: () => classifyMemberSystemWrite,
@@ -130,9 +132,11 @@ __export(src_exports, {
130
132
  formatChapterCopy: () => formatChapterCopy,
131
133
  formationFields: () => formationFields,
132
134
  formationFlowFields: () => formationFlowFields,
135
+ frozenCheckoutVariables: () => frozenCheckoutVariables,
133
136
  getVaultSecret: () => getVaultSecret,
134
137
  giftOfferActive: () => giftOfferActive,
135
138
  hasDisclaimerAck: () => hasDisclaimerAck,
139
+ htmlBodyIssues: () => htmlBodyIssues,
136
140
  hydrateSignupControlTheme: () => hydrateSignupControlTheme,
137
141
  injectSignupControlThemeHead: () => injectSignupControlThemeHead,
138
142
  introIdempotencyKey: () => introIdempotencyKey,
@@ -211,6 +215,7 @@ __export(src_exports, {
211
215
  refundNetworkMembership: () => refundNetworkMembership,
212
216
  refundedPatch: () => refundedPatch,
213
217
  render: () => render,
218
+ renderHtmlBody: () => renderHtmlBody,
214
219
  renderSignupControlThemeHead: () => renderSignupControlThemeHead,
215
220
  renderSummary: () => renderSummary,
216
221
  renderTemplateBody: () => renderTemplateBody,
@@ -1694,6 +1699,8 @@ var DEFAULT_ADMIN_COPY = {
1694
1699
  enabled: "Enabled",
1695
1700
  subject: "Subject",
1696
1701
  body: "Body",
1702
+ htmlBody: "HTML body (optional)",
1703
+ htmlBodyHint: "Sent beside the plain text when present, with the same {{placeholders}} and {{#name}} sections; values are escaped. Leave empty to send the plain text rendered as HTML.",
1697
1704
  save: "Save changes",
1698
1705
  sendLog: "Send log",
1699
1706
  sentColumn: "Sent",
@@ -7625,20 +7632,74 @@ function isRetiredEmailCopy(subject, text8) {
7625
7632
  }
7626
7633
 
7627
7634
  // src/email.ts
7635
+ var import_crm6 = require("@odla-ai/crm");
7636
+
7637
+ // src/email-render.ts
7628
7638
  var import_crm5 = require("@odla-ai/crm");
7629
7639
 
7640
+ // src/email-html.ts
7641
+ var URL_RE = /https?:\/\/[^\s<>"')\]]+[^\s<>"').,;:!?\]]/g;
7642
+ function escapeHtml(s) {
7643
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
7644
+ }
7645
+ function lineHtml(line) {
7646
+ let out = "", last = 0;
7647
+ for (const match of line.matchAll(URL_RE)) {
7648
+ out += escapeHtml(line.slice(last, match.index));
7649
+ const url = escapeHtml(match[0]);
7650
+ out += `<a href="${url}">${url}</a>`;
7651
+ last = match.index + match[0].length;
7652
+ }
7653
+ return out + escapeHtml(line.slice(last));
7654
+ }
7655
+ function textToHtml(text8) {
7656
+ const paragraphs = text8.replace(/\r\n/g, "\n").trim().split(/\n{2,}/).filter((paragraph) => paragraph.trim());
7657
+ const body = paragraphs.map((paragraph) => `<p>${paragraph.split("\n").map(lineHtml).join("<br>\n")}</p>`).join("\n");
7658
+ return `<!doctype html>
7659
+ <html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"></head>
7660
+ <body style="margin:0;padding:24px 16px;background:#ffffff;color:#1a1a1a;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;font-size:16px;line-height:1.5">
7661
+ ${body}
7662
+ </body></html>`;
7663
+ }
7664
+
7665
+ // src/email-render.ts
7666
+ function render(template, vars) {
7667
+ return (0, import_crm5.renderTemplate)(template, vars);
7668
+ }
7669
+ var HTML_BODY_LIMIT = 2e5;
7670
+ function renderHtmlBody(template, vars) {
7671
+ const escaped = Object.fromEntries(Object.entries(vars).map(([name, value]) => [name, escapeHtml(value).replace(/\r?\n/g, "<br>")]));
7672
+ return (0, import_crm5.renderTemplate)(template, escaped);
7673
+ }
7674
+ function htmlBodyIssues(html, options) {
7675
+ const issues = [];
7676
+ if (html.length > HTML_BODY_LIMIT) issues.push({ code: "html-invalid", message: `Keep the HTML body within ${HTML_BODY_LIMIT.toLocaleString("en-US")} characters.` });
7677
+ for (const message of (0, import_crm5.templateSectionIssues)(html)) issues.push({ code: "section-invalid", message: `${message} (HTML body)` });
7678
+ const unknown = new Set((0, import_crm5.templateVars)(html).filter((variable) => options.allowedVariables && !options.allowedVariables.includes(variable)));
7679
+ for (const variable of unknown) issues.push({ code: "variable-unknown", variable, message: `This event does not supply {{${variable}}} (HTML body).` });
7680
+ if (options.values) {
7681
+ for (const name of (0, import_crm5.templateSectionNames)(html)) if (typeof options.values[name] !== "string") issues.push({ code: "value-missing", variable: name, message: `Missing event value: ${name} (HTML body).` });
7682
+ for (const variable of (0, import_crm5.templateLiveVars)(html, options.values)) {
7683
+ if (unknown.has(variable)) continue;
7684
+ const value = options.values[variable];
7685
+ if (typeof value !== "string" || !value.trim() && !options.optionalVariables?.includes(variable)) issues.push({ code: "value-missing", variable, message: `Missing event value: ${variable} (HTML body).` });
7686
+ }
7687
+ }
7688
+ return issues;
7689
+ }
7690
+
7630
7691
  // src/email-events.ts
7631
7692
  var identity = ["firstName", "lastName", "email", "phone", "state"];
7632
7693
  var policy = ["refundPolicyText", "commitmentText", "normsText"];
7633
- var checkout = ["tier", "tierName", "paymentAmount", "paymentAmountSummary"];
7634
- var optionalVariables = ["phone", "state", "paymentAmount", "paymentAmountSummary"];
7694
+ var checkout = ["tier", "tierName", "paymentAmount", "paymentAmountSummary", "namedSeat", "namedSeatRecipientName"];
7695
+ var optionalVariables = ["phone", "state", "paymentAmount", "paymentAmountSummary", "namedSeat", "namedSeatRecipientName"];
7635
7696
  var event = (label, trigger, audience, variables, requiresTier = true) => ({ label, trigger, audience, variables: [...variables, ...policy], optionalVariables, requiresTier });
7636
7697
  var CHAPTER_EMAIL_EVENTS = {
7637
7698
  adminNotification: event("Application notification", "Application submitted or payment received, according to notification settings", "Administrator", [...identity, ...checkout, "adminUrl", "membersUrl"]),
7638
7699
  submitConfirmation: event("Application receipt", "Application submitted", "Applicant", [...identity, ...checkout, "submitNextStep", "membersUrl"]),
7639
7700
  paymentConfirmation: event("Payment confirmation", "Membership payment received", "Applicant", ["firstName", ...checkout, "paymentNextStep"]),
7640
7701
  prepEmail: event("Call confirmation", "Introduction call booked", "Applicant", [...identity, ...checkout, "startAt", "endAt", "timezone", "meetUrl", "htmlLink", "meetingTime", "meetingLink"]),
7641
- onboardingInvite: event("Membership welcome", "Membership approved", "Member", ["firstName", "membersUrl", "tierName"]),
7702
+ onboardingInvite: event("Membership welcome", "Membership approved", "Member", ["firstName", "membersUrl", "tierName", "namedSeat", "namedSeatRecipientName"]),
7642
7703
  namedSeatInvitation: event("Gift membership invitation", "A gift membership is ready to be accepted", "Gift recipient", ["firstName", "purchaserName", "chapterName", "acceptBy", "joinUrl", "tierName"]),
7643
7704
  seatAccepted: event("Gift accepted", "Gift recipient accepts their membership", "Purchaser", ["firstName", "purchaserName", "recipientName", "tierName", "chapterName", "membersUrl"])
7644
7705
  };
@@ -7653,9 +7714,6 @@ function chapterEmailEvents(chapter) {
7653
7714
  }
7654
7715
 
7655
7716
  // src/email.ts
7656
- function render(template, vars) {
7657
- return template.replace(/\{\{(\w+)\}\}/g, (_, key3) => vars[key3] ?? "");
7658
- }
7659
7717
  function groupVars(group, vars) {
7660
7718
  return {
7661
7719
  ...vars,
@@ -7681,14 +7739,16 @@ function planDelivery(input) {
7681
7739
  const vars = groupVars(input.group, input.vars);
7682
7740
  if (isRetiredEmailCopy(String(tpl.subject ?? ""), String(tpl.text ?? ""))) return { deliver: false, reason: "template-retired" };
7683
7741
  const event2 = input.event ?? CHAPTER_EMAIL_EVENTS[input.template];
7684
- const issues = (0, import_crm5.validateEmailCopy)(tpl, {
7742
+ const issues = (0, import_crm6.validateEmailCopy)(tpl, {
7685
7743
  allowedVariables: event2?.variables,
7686
7744
  optionalVariables: event2?.optionalVariables,
7687
7745
  requiredVariables: event2?.requiresTier ? ["tierName"] : [],
7688
7746
  values: vars,
7689
7747
  tierId: vars.tierId
7690
7748
  });
7691
- if (issues.length) return { deliver: false, reason: "template-invalid", issues: issues.map((issue) => issue.message) };
7749
+ const html = typeof tpl.html === "string" && tpl.html.trim() ? tpl.html : void 0;
7750
+ const htmlIssues = html ? htmlBodyIssues(html, { allowedVariables: event2?.variables, optionalVariables: event2?.optionalVariables, values: vars }) : [];
7751
+ if (issues.length || htmlIssues.length) return { deliver: false, reason: "template-invalid", issues: [...issues, ...htmlIssues].map((issue) => issue.message) };
7692
7752
  const isProd = input.envName === "prod";
7693
7753
  const redirect = !isProd && !!input.group.debugEmail;
7694
7754
  const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
@@ -7696,16 +7756,18 @@ function planDelivery(input) {
7696
7756
  const renderedSubject = render(tpl.subject, vars);
7697
7757
  const renderedText = render(tpl.text, vars);
7698
7758
  if (isRetiredEmailCopy(renderedSubject, renderedText)) return { deliver: false, reason: "template-retired" };
7699
- if ((0, import_crm5.validateEmailCopy)({ subject: renderedSubject, text: renderedText }, { allowedVariables: [] }).length) return { deliver: false, reason: "template-invalid" };
7759
+ if ((0, import_crm6.validateEmailCopy)({ subject: renderedSubject, text: renderedText }, { allowedVariables: [] }).length) return { deliver: false, reason: "template-invalid" };
7700
7760
  const subject = (redirect ? "[dev] " : "") + renderedSubject;
7701
7761
  const text8 = redirect ? `(dev redirect; original recipient: ${input.to})
7702
7762
 
7703
7763
  ` + renderedText : renderedText;
7704
- return { deliver: true, transport, to, subject, text: text8, redirected: redirect };
7764
+ const renderedHtml = html ? renderHtmlBody(html, vars) : void 0;
7765
+ const htmlOut = renderedHtml && redirect ? `<p style="font:13px monospace;color:#555;margin:0 0 12px">(dev redirect; original recipient: ${escapeHtml(input.to)})</p>` + renderedHtml : renderedHtml;
7766
+ return { deliver: true, transport, to, subject, text: text8, ...htmlOut ? { html: htmlOut } : {}, redirected: redirect };
7705
7767
  }
7706
7768
 
7707
7769
  // src/email-readiness.ts
7708
- var import_crm6 = require("@odla-ai/crm");
7770
+ var import_crm7 = require("@odla-ai/crm");
7709
7771
 
7710
7772
  // src/email-spam-signals.ts
7711
7773
  var TRIGGER_PHRASES = [
@@ -7735,13 +7797,14 @@ var TRIGGER_PHRASES = [
7735
7797
  var SUBJECT_TRIGGERS = ["free", ...TRIGGER_PHRASES];
7736
7798
  var DEV_HOSTS = /(?:^|\.)(?:workers\.dev|pages\.dev|localhost|ngrok(?:-free)?\.app|trycloudflare\.com)$/i;
7737
7799
  var SHORTENERS = /^(?:bit\.ly|tinyurl\.com|t\.co|goo\.gl|ow\.ly|is\.gd|buff\.ly|rebrand\.ly|cutt\.ly|tiny\.cc|shorturl\.at|rb\.gy)$/i;
7738
- var URL_RE = /https?:\/\/[^\s<>"')\]]+/g;
7800
+ var URL_RE2 = /https?:\/\/[^\s<>"')\]]+/g;
7739
7801
  var strip = (s) => s.replace(/\{\{\w+\}\}/g, " ");
7740
7802
  var words = (s) => strip(s).split(/\s+/).filter(Boolean);
7741
7803
  var phraseIn = (phrases, haystack) => phrases.find((phrase) => new RegExp(`(?:^|[^a-z0-9])${phrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![a-z0-9])`, "i").test(haystack));
7742
7804
  function emailSpamSignals(copy) {
7743
7805
  const subject = typeof copy.subject === "string" ? copy.subject : "";
7744
7806
  const text8 = typeof copy.text === "string" ? copy.text : "";
7807
+ const html = typeof copy.html === "string" ? copy.html : "";
7745
7808
  const out = [];
7746
7809
  const letters = strip(subject).replace(/[^A-Za-z]/g, "");
7747
7810
  const shouting = letters.length >= 6 && letters.replace(/[^A-Z]/g, "").length / letters.length > 0.5 || words(subject).some((word) => /^[A-Z]{4,}$/.test(word.replace(/[^A-Za-z]/g, "")));
@@ -7754,7 +7817,8 @@ function emailSpamSignals(copy) {
7754
7817
  if (bodyHit) out.push({ code: "body-trigger-words", message: `The body uses "${bodyHit}", a phrase filters weight against the sender.` });
7755
7818
  if (/(?:^|\s)[A-Z]{3,}(?:\s+[A-Z]{3,}){2,}(?=\s|$)/.test(strip(text8))) out.push({ code: "body-caps", message: "The body shouts a run of capitalised words; use sentence case." });
7756
7819
  if (/!{2,}/.test(text8)) out.push({ code: "body-punctuation", message: "Repeated exclamation marks in the body are a classic spam signal." });
7757
- const urls = [...text8.matchAll(URL_RE)].map((match) => match[0]);
7820
+ const urls = [...`${text8}
7821
+ ${html}`.matchAll(URL_RE2)].map((match) => match[0].replace(/&quot;.*$/, ""));
7758
7822
  const hosts = urls.map((url) => {
7759
7823
  try {
7760
7824
  return new URL(url).hostname;
@@ -7791,18 +7855,22 @@ function validateEmailReadiness(input) {
7791
7855
  }
7792
7856
  if (input.tiersUnavailable) issues.push({ code: "tiers-unavailable", message: "Tier names could not be checked. Membership emails cannot send until their tier is resolved." });
7793
7857
  for (const tier of input.tiers ?? []) {
7794
- if (!(0, import_crm6.isEmailTierName)(tier.name, tier.id)) issues.push({ code: "tier-name-invalid", message: `Tier ${tier.id} needs a recipient-facing display name.` });
7858
+ if (!(0, import_crm7.isEmailTierName)(tier.name, tier.id)) issues.push({ code: "tier-name-invalid", message: `Tier ${tier.id} needs a recipient-facing display name.` });
7795
7859
  }
7796
- const noTierNames = input.tiers !== void 0 && !input.tiers.some((tier) => (0, import_crm6.isEmailTierName)(tier.name, tier.id));
7860
+ const noTierNames = input.tiers !== void 0 && !input.tiers.some((tier) => (0, import_crm7.isEmailTierName)(tier.name, tier.id));
7797
7861
  if (noTierNames && !input.tiersUnavailable) issues.push({ code: "tiers-missing", message: "No membership tiers have a recipient-facing display name." });
7798
7862
  const events2 = Object.entries(input.events).map(([key3, event2]) => {
7799
7863
  const template = input.templates[key3];
7800
- const problems = !template ? [{ code: "template-missing", message: "No email is configured for this event." }] : (0, import_crm6.validateEmailCopy)(template, { allowedVariables: event2.variables });
7864
+ const problems = !template ? [{ code: "template-missing", message: "No email is configured for this event." }] : (0, import_crm7.validateEmailCopy)(template, { allowedVariables: event2.variables });
7801
7865
  if (template && typeof template.enabled !== "boolean") problems.push({ code: "enabled-missing", message: "Choose explicitly whether this email is enabled. Sending is blocked until then." });
7866
+ const html = typeof template?.html === "string" && template.html.trim() ? template.html : "";
7867
+ if (html) problems.push(...htmlBodyIssues(html, { allowedVariables: event2.variables }));
7802
7868
  if (template) {
7803
- const used = [...`${template.subject ?? ""}
7804
- ${template.text ?? ""}`.matchAll(/\{\{(\w+)\}\}/g)].map((match) => match[1]);
7805
- for (const name of used) if (name in input.groupValues && !input.groupValues[name]?.trim()) {
7869
+ const copy = `${template.subject ?? ""}
7870
+ ${template.text ?? ""}
7871
+ ${html}`;
7872
+ const sectionOnly = new Set((0, import_crm7.templateSectionNames)(copy));
7873
+ for (const name of (0, import_crm7.templateVars)(copy)) if (name in input.groupValues && !input.groupValues[name]?.trim() && !sectionOnly.has(name)) {
7806
7874
  problems.push({ code: "value-missing", variable: name, message: `Add the ${name} used by this email.` });
7807
7875
  }
7808
7876
  }
@@ -7819,31 +7887,6 @@ ${template.text ?? ""}`.matchAll(/\{\{(\w+)\}\}/g)].map((match) => match[1]);
7819
7887
  return { checkedAt: input.now ?? Date.now(), events: events2, issues, warnings: emailSenderWarnings(input.sender ?? {}) };
7820
7888
  }
7821
7889
 
7822
- // src/email-html.ts
7823
- var URL_RE2 = /https?:\/\/[^\s<>"')\]]+[^\s<>"').,;:!?\]]/g;
7824
- function escapeHtml(s) {
7825
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
7826
- }
7827
- function lineHtml(line) {
7828
- let out = "", last = 0;
7829
- for (const match of line.matchAll(URL_RE2)) {
7830
- out += escapeHtml(line.slice(last, match.index));
7831
- const url = escapeHtml(match[0]);
7832
- out += `<a href="${url}">${url}</a>`;
7833
- last = match.index + match[0].length;
7834
- }
7835
- return out + escapeHtml(line.slice(last));
7836
- }
7837
- function textToHtml(text8) {
7838
- const paragraphs = text8.replace(/\r\n/g, "\n").trim().split(/\n{2,}/).filter((paragraph) => paragraph.trim());
7839
- const body = paragraphs.map((paragraph) => `<p>${paragraph.split("\n").map(lineHtml).join("<br>\n")}</p>`).join("\n");
7840
- return `<!doctype html>
7841
- <html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"></head>
7842
- <body style="margin:0;padding:24px 16px;background:#ffffff;color:#1a1a1a;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;font-size:16px;line-height:1.5">
7843
- ${body}
7844
- </body></html>`;
7845
- }
7846
-
7847
7890
  // src/email-deliverability.ts
7848
7891
  var ADMINISTRATOR_AUDIENCE = /^(?:administrator|admin|team|operator|owner|staff)s?$/i;
7849
7892
  function isRecipientFacing(event2) {
@@ -7861,10 +7904,10 @@ function deliverabilityHeaders(input) {
7861
7904
  }
7862
7905
 
7863
7906
  // src/notify.ts
7864
- var import_crm8 = require("@odla-ai/crm");
7907
+ var import_crm9 = require("@odla-ai/crm");
7865
7908
 
7866
7909
  // src/email-event-values.ts
7867
- var import_crm7 = require("@odla-ai/crm");
7910
+ var import_crm8 = require("@odla-ai/crm");
7868
7911
 
7869
7912
  // src/tiers-store.ts
7870
7913
  function rowToTier(row2, groupId) {
@@ -7915,10 +7958,14 @@ async function frozenCheckoutVariables(db, app) {
7915
7958
  if (Number.isSafeInteger(amount) && Number(amount) > 0 && typeof currency === "string" && /^[a-z]{3}$/.test(currency)) {
7916
7959
  formatted = new Intl.NumberFormat("en-US", { style: "currency", currency: currency.toUpperCase() }).format(Number(amount) / 100);
7917
7960
  }
7961
+ const seat = quote?.namedSeat, seatCents = quote?.lineItems?.namedSeatCents;
7962
+ const namedSeatRecipientName = typeof seat?.recipientName === "string" ? seat.recipientName.trim() : "";
7918
7963
  return {
7919
7964
  paymentAmount: formatted,
7920
7965
  paymentAmountSummary: formatted ? ` of ${formatted}` : "",
7921
- tierName: typeof quote?.tierName === "string" ? quote.tierName : ""
7966
+ tierName: typeof quote?.tierName === "string" ? quote.tierName : "",
7967
+ namedSeat: namedSeatRecipientName ? Number.isSafeInteger(seatCents) && Number(seatCents) > 0 ? "paid" : "included" : "",
7968
+ namedSeatRecipientName
7922
7969
  };
7923
7970
  }
7924
7971
  async function catalogTierName(db, group, app) {
@@ -7945,7 +7992,7 @@ async function emailTierVariables(db, input) {
7945
7992
  const tierId = input.tierId ?? (typeof app.tierId === "string" ? app.tierId : "");
7946
7993
  if (!tierId) return { tierName: "", tier: "" };
7947
7994
  const name = input.tierId ? await catalogTierName(db, group, { ...app, tierId }) : (await checkoutVariables(db, group, app)).tierName;
7948
- const tierName = (0, import_crm7.isEmailTierName)(name, tierId) ? name.trim() : "";
7995
+ const tierName = (0, import_crm8.isEmailTierName)(name, tierId) ? name.trim() : "";
7949
7996
  return { tierName, tier: tierName, tierId };
7950
7997
  } catch {
7951
7998
  return { tierName: "", tier: "" };
@@ -8005,7 +8052,7 @@ async function sendTemplated(deps, input) {
8005
8052
  ...input.applicationId ? { applicationId: input.applicationId } : {}
8006
8053
  };
8007
8054
  if (isRetiredEmailCopy(String(row2.subject), String(row2.body))) return { sent: false, reason: "template-retired" };
8008
- if ((0, import_crm8.validateEmailCopy)({ subject: row2.subject, text: row2.body }, { allowedVariables: [] }).length) return { sent: false, reason: "template-invalid" };
8055
+ if ((0, import_crm9.validateEmailCopy)({ subject: row2.subject, text: row2.body }, { allowedVariables: [] }).length) return { sent: false, reason: "template-invalid" };
8009
8056
  try {
8010
8057
  const claim = await deps.db.transact([{ t: "update", ns: "emailLog", id: id11, attrs: row2 }], {
8011
8058
  mutationId: `email-claim:${id11}:${attempt}`,
@@ -8026,7 +8073,7 @@ async function sendTemplated(deps, input) {
8026
8073
  let messageId;
8027
8074
  try {
8028
8075
  const text8 = String(row2.body);
8029
- const html = deps.renderHtml === false ? void 0 : (deps.renderHtml ?? textToHtml)(text8);
8076
+ const html = deps.renderHtml === false ? void 0 : decision.html ?? (deps.renderHtml ?? textToHtml)(text8);
8030
8077
  const headers = deliverabilityHeaders({ event: event2, replyTo: input.group.replyTo });
8031
8078
  const response = await deps.sender.send({
8032
8079
  from: fromAddress(deps.from, input.group.fromName),
@@ -8569,10 +8616,10 @@ function findGiftOffer(catalog, offerId, now) {
8569
8616
  }
8570
8617
 
8571
8618
  // src/network.ts
8572
- var import_crm11 = require("@odla-ai/crm");
8619
+ var import_crm12 = require("@odla-ai/crm");
8573
8620
 
8574
8621
  // src/crm-person-projection.ts
8575
- var import_crm9 = require("@odla-ai/crm");
8622
+ var import_crm10 = require("@odla-ai/crm");
8576
8623
  async function ensureProjectedPerson(deps, input, options = {}) {
8577
8624
  const email = String(input.email ?? "").trim().toLowerCase();
8578
8625
  if (!email) throw new Error("crm_projection_email_required");
@@ -8590,7 +8637,7 @@ async function ensureProjectedPerson(deps, input, options = {}) {
8590
8637
  const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`chapter:person:v1:${email}`)));
8591
8638
  const id11 = `person-${Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
8592
8639
  try {
8593
- const created = await (0, import_crm9.createRecord)({ ...deps, db: deps.db }, {
8640
+ const created = await (0, import_crm10.createRecord)({ ...deps, db: deps.db }, {
8594
8641
  id: id11,
8595
8642
  type: "person",
8596
8643
  input: { ...input, email },
@@ -8606,7 +8653,7 @@ async function ensureProjectedPerson(deps, input, options = {}) {
8606
8653
  }
8607
8654
 
8608
8655
  // src/applicant-projection.ts
8609
- var import_crm10 = require("@odla-ai/crm");
8656
+ var import_crm11 = require("@odla-ai/crm");
8610
8657
 
8611
8658
  // src/crm-application-fields.ts
8612
8659
  function applicationCrmFields(crm, app) {
@@ -8625,10 +8672,10 @@ async function upsertPerson(deps, opts) {
8625
8672
  const email = opts.email.toLowerCase();
8626
8673
  const crmDeps = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
8627
8674
  const { record: record4, created } = await ensureProjectedPerson(deps, { ...opts.input, email });
8628
- if (!created) await (0, import_crm10.updateRecord)(crmDeps, { id: String(record4.id), input: opts.input, mutationId: opts.mutationId });
8675
+ if (!created) await (0, import_crm11.updateRecord)(crmDeps, { id: String(record4.id), input: opts.input, mutationId: opts.mutationId });
8629
8676
  const initial = deps.crm.initialStage("person");
8630
8677
  if (!created && record4.stage == null && initial) {
8631
- await (0, import_crm10.setStage)(crmDeps, {
8678
+ await (0, import_crm11.setStage)(crmDeps, {
8632
8679
  id: String(record4.id),
8633
8680
  to: initial,
8634
8681
  authorId: "system",
@@ -8751,7 +8798,7 @@ function sharedRecordFromCrm(crm, record4, target, sourceId) {
8751
8798
  return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record4.id }, type: record4.type, input } : { version: 1, type: record4.type, hubRecordId: record4.id, input };
8752
8799
  }
8753
8800
  async function findSharedRecord(deps, record4, tag) {
8754
- const structured = await (0, import_crm11.getRecordByOrigin)(
8801
+ const structured = await (0, import_crm12.getRecordByOrigin)(
8755
8802
  { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId },
8756
8803
  record4.sourceId,
8757
8804
  record4.sourceRecordId
@@ -8803,15 +8850,15 @@ async function projectSharedRecord(deps, shared, options = {}) {
8803
8850
  const existing = await findSharedRecord(deps, record4, tag);
8804
8851
  let recordId;
8805
8852
  if (existing && typeof existing.id === "string") {
8806
- await (0, import_crm11.updateRecord)(crmDeps, { id: existing.id, input: record4.input });
8853
+ await (0, import_crm12.updateRecord)(crmDeps, { id: existing.id, input: record4.input });
8807
8854
  recordId = existing.id;
8808
8855
  } else if (record4.type === "person" && typeof record4.input.email === "string") {
8809
8856
  const projected = await ensureProjectedPerson(deps, record4.input);
8810
8857
  recordId = String(projected.record.id);
8811
- if (!projected.created) await (0, import_crm11.updateRecord)(crmDeps, { id: recordId, input: record4.input });
8858
+ if (!projected.created) await (0, import_crm12.updateRecord)(crmDeps, { id: recordId, input: record4.input });
8812
8859
  } else {
8813
8860
  recordId = `network_${shortHash(`${record4.sourceId}\0${record4.type}\0${record4.sourceRecordId}`)}`;
8814
- await (0, import_crm11.createRecord)({ ...crmDeps, newId: () => recordId }, {
8861
+ await (0, import_crm12.createRecord)({ ...crmDeps, newId: () => recordId }, {
8815
8862
  type: record4.type,
8816
8863
  input: record4.input,
8817
8864
  mutationId: `share-create:${tag}`
@@ -8821,7 +8868,7 @@ async function projectSharedRecord(deps, shared, options = {}) {
8821
8868
  [{ t: "update", ns: "crm_tag", id: tag, attrs: { key: `${recordId}:${tag}`, recordId, tag, createdAt: deps.now() } }],
8822
8869
  { mutationId: `share-map:${tag}:${recordId}` }
8823
8870
  );
8824
- await (0, import_crm11.upsertRecordOrigin)(crmDeps, {
8871
+ await (0, import_crm12.upsertRecordOrigin)(crmDeps, {
8825
8872
  recordId,
8826
8873
  sourceId: record4.sourceId,
8827
8874
  sourceRecordId: record4.sourceRecordId,
@@ -8832,7 +8879,7 @@ async function projectSharedRecord(deps, shared, options = {}) {
8832
8879
  }
8833
8880
 
8834
8881
  // src/crm-sync.ts
8835
- var import_crm12 = require("@odla-ai/crm");
8882
+ var import_crm13 = require("@odla-ai/crm");
8836
8883
 
8837
8884
  // src/crm-account-projection.ts
8838
8885
  async function syncAccountToCrm(deps, userId) {
@@ -8917,9 +8964,9 @@ async function syncApplicationToCrm(deps, opts) {
8917
8964
  const stage = opts.stage || void 0;
8918
8965
  const { record: existing, created } = await ensureProjectedPerson(deps, input, { stage });
8919
8966
  const recordId = String(existing.id);
8920
- if (!created) await (0, import_crm12.updateRecord)(recordDeps, { id: recordId, input });
8967
+ if (!created) await (0, import_crm13.updateRecord)(recordDeps, { id: recordId, input });
8921
8968
  if (!created && stage && existing.stage !== stage) {
8922
- await (0, import_crm12.setStage)(recordDeps, { id: recordId, to: stage, authorId: "system", mutationId: `crm:stage:${recordId}:${existing.updatedAt}:${stage}` });
8969
+ await (0, import_crm13.setStage)(recordDeps, { id: recordId, to: stage, authorId: "system", mutationId: `crm:stage:${recordId}:${existing.updatedAt}:${stage}` });
8923
8970
  }
8924
8971
  const billing = billingColumns(opts.app);
8925
8972
  await deps.db.transact([
@@ -8927,7 +8974,7 @@ async function syncApplicationToCrm(deps, opts) {
8927
8974
  // Optional typed dates are removed with a retract, never an invalid null.
8928
8975
  ...!("renewalAt" in billing) ? [{ t: "retract", ns: "crm_record", id: recordId, attrs: ["renewalAt"] }] : []
8929
8976
  ]);
8930
- await (0, import_crm12.linkIdentity)(recordDeps, { recordId, email: emailKey, mutationId: `crm:link:${recordId}:${emailKey}` }).catch(() => void 0);
8977
+ await (0, import_crm13.linkIdentity)(recordDeps, { recordId, email: emailKey, mutationId: `crm:link:${recordId}:${emailKey}` }).catch(() => void 0);
8931
8978
  return recordId;
8932
8979
  }
8933
8980
  async function backfillCrm(deps) {