@odla-ai/chapter 0.59.1 → 0.60.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -0
- package/dist/{chunk-SDGVPIXF.js → chunk-D5UR5H6R.js} +2 -2
- package/dist/{chunk-P6X3W2XP.js → chunk-J7ES2OWN.js} +12 -2
- package/dist/{chunk-P6X3W2XP.js.map → chunk-J7ES2OWN.js.map} +1 -1
- package/dist/{chunk-GADLTNJF.js → chunk-QNP3UEIL.js} +2 -1
- package/dist/chunk-QNP3UEIL.js.map +1 -0
- package/dist/{copy-context-BuJBQafl.d.ts → copy-context-BP3G1iBd.d.ts} +1 -1
- package/dist/index.cjs +141 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +54 -2
- package/dist/index.d.ts +54 -2
- package/dist/index.js +141 -3
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +2 -2
- package/dist/ui/admin/index.js +2 -2
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.js +3 -3
- package/dist/ui/member/index.d.ts +2 -2
- package/dist/ui/member/index.js +2 -2
- package/dist/worker/index.cjs +143 -5
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +1 -1
- package/dist/worker/index.d.ts +1 -1
- package/dist/worker/index.js +143 -5
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-GADLTNJF.js.map +0 -1
- /package/dist/{chunk-SDGVPIXF.js.map → chunk-D5UR5H6R.js.map} +0 -0
|
@@ -110,6 +110,7 @@ var DEFAULT_ADMIN_COPY = {
|
|
|
110
110
|
replyTo: "Reply-to",
|
|
111
111
|
fromName: "From name",
|
|
112
112
|
fromNameHint: "Shown as the sender; the address stays the Worker's configured one.",
|
|
113
|
+
spamRisk: "Spam-filter risk",
|
|
113
114
|
debugInbox: "Debug inbox",
|
|
114
115
|
debugInboxHint: "Non-production sends redirect here.",
|
|
115
116
|
sendTest: "Send test",
|
|
@@ -526,4 +527,4 @@ export {
|
|
|
526
527
|
useChapterCopy,
|
|
527
528
|
BrandStyle
|
|
528
529
|
};
|
|
529
|
-
//# sourceMappingURL=chunk-
|
|
530
|
+
//# sourceMappingURL=chunk-QNP3UEIL.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 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"]}
|
|
@@ -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" | "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" | "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
|
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var src_exports = {};
|
|
22
22
|
__export(src_exports, {
|
|
23
|
+
ADMINISTRATOR_AUDIENCE: () => ADMINISTRATOR_AUDIENCE,
|
|
23
24
|
ADMISSION_GRANTS_PATH: () => ADMISSION_GRANTS_PATH,
|
|
24
25
|
ADMISSION_GRANTS_SCHEMA_VERSION: () => ADMISSION_GRANTS_SCHEMA_VERSION,
|
|
25
26
|
BUILDER_LIBRARY_ADMIN_PATH: () => BUILDER_LIBRARY_ADMIN_PATH,
|
|
@@ -113,9 +114,12 @@ __export(src_exports, {
|
|
|
113
114
|
dashboardMetricData: () => dashboardMetricData,
|
|
114
115
|
defaultCrm: () => defaultCrm,
|
|
115
116
|
defineChapter: () => defineChapter,
|
|
117
|
+
deliverabilityHeaders: () => deliverabilityHeaders,
|
|
116
118
|
drainMembershipEffects: () => drainMembershipEffects,
|
|
117
119
|
edgeUnpaired: () => edgeUnpaired,
|
|
118
120
|
emailGroupFrom: () => emailGroupFrom,
|
|
121
|
+
emailSenderWarnings: () => emailSenderWarnings,
|
|
122
|
+
emailSpamSignals: () => emailSpamSignals,
|
|
119
123
|
endForSlot: () => endForSlot,
|
|
120
124
|
exportChapterPromotion: () => exportChapterPromotion,
|
|
121
125
|
findAdmissionGrant: () => findAdmissionGrant,
|
|
@@ -134,6 +138,7 @@ __export(src_exports, {
|
|
|
134
138
|
introIdempotencyKey: () => introIdempotencyKey,
|
|
135
139
|
isAdminRole: () => isAdminRole,
|
|
136
140
|
isAlreadySent: () => isAlreadySent,
|
|
141
|
+
isRecipientFacing: () => isRecipientFacing,
|
|
137
142
|
isReconcilable: () => isReconcilable,
|
|
138
143
|
isSlotAvailable: () => isSlotAvailable,
|
|
139
144
|
isValidEmail: () => isValidEmail,
|
|
@@ -142,6 +147,7 @@ __export(src_exports, {
|
|
|
142
147
|
joinConfigTiers: () => joinConfigTiers,
|
|
143
148
|
leaderCrmConfig: () => leaderCrmConfig,
|
|
144
149
|
listStripeObjects: () => listStripeObjects,
|
|
150
|
+
listUnsubscribeHeader: () => listUnsubscribeHeader,
|
|
145
151
|
loadLatestAdmissionRevision: () => loadLatestAdmissionRevision,
|
|
146
152
|
loadOfferableTiers: () => loadOfferableTiers,
|
|
147
153
|
loadTiers: () => loadTiers,
|
|
@@ -259,6 +265,7 @@ __export(src_exports, {
|
|
|
259
265
|
submitApplication: () => submitApplication,
|
|
260
266
|
subscriptionIdempotencyKey: () => subscriptionIdempotencyKey,
|
|
261
267
|
syncApplicationToCrm: () => syncApplicationToCrm,
|
|
268
|
+
textToHtml: () => textToHtml,
|
|
262
269
|
tierIsFree: () => tierIsFree,
|
|
263
270
|
tierPayable: () => tierPayable,
|
|
264
271
|
updateClerkUserMetadata: () => updateClerkUserMetadata,
|
|
@@ -1680,6 +1687,7 @@ var DEFAULT_ADMIN_COPY = {
|
|
|
1680
1687
|
replyTo: "Reply-to",
|
|
1681
1688
|
fromName: "From name",
|
|
1682
1689
|
fromNameHint: "Shown as the sender; the address stays the Worker's configured one.",
|
|
1690
|
+
spamRisk: "Spam-filter risk",
|
|
1683
1691
|
debugInbox: "Debug inbox",
|
|
1684
1692
|
debugInboxHint: "Non-production sends redirect here.",
|
|
1685
1693
|
sendTest: "Send test",
|
|
@@ -7698,6 +7706,84 @@ function planDelivery(input) {
|
|
|
7698
7706
|
|
|
7699
7707
|
// src/email-readiness.ts
|
|
7700
7708
|
var import_crm6 = require("@odla-ai/crm");
|
|
7709
|
+
|
|
7710
|
+
// src/email-spam-signals.ts
|
|
7711
|
+
var TRIGGER_PHRASES = [
|
|
7712
|
+
"act now",
|
|
7713
|
+
"click here",
|
|
7714
|
+
"limited time",
|
|
7715
|
+
"guaranteed",
|
|
7716
|
+
"risk-free",
|
|
7717
|
+
"risk free",
|
|
7718
|
+
"no obligation",
|
|
7719
|
+
"100% free",
|
|
7720
|
+
"winner",
|
|
7721
|
+
"congratulations",
|
|
7722
|
+
"urgent",
|
|
7723
|
+
"double your",
|
|
7724
|
+
"earn money",
|
|
7725
|
+
"make money",
|
|
7726
|
+
"cash bonus",
|
|
7727
|
+
"once in a lifetime",
|
|
7728
|
+
"exclusive deal",
|
|
7729
|
+
"order now",
|
|
7730
|
+
"buy now",
|
|
7731
|
+
"this is not spam",
|
|
7732
|
+
"dear friend",
|
|
7733
|
+
"dear customer"
|
|
7734
|
+
];
|
|
7735
|
+
var SUBJECT_TRIGGERS = ["free", ...TRIGGER_PHRASES];
|
|
7736
|
+
var DEV_HOSTS = /(?:^|\.)(?:workers\.dev|pages\.dev|localhost|ngrok(?:-free)?\.app|trycloudflare\.com)$/i;
|
|
7737
|
+
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;
|
|
7739
|
+
var strip = (s) => s.replace(/\{\{\w+\}\}/g, " ");
|
|
7740
|
+
var words = (s) => strip(s).split(/\s+/).filter(Boolean);
|
|
7741
|
+
var phraseIn = (phrases, haystack) => phrases.find((phrase) => new RegExp(`(?:^|[^a-z0-9])${phrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![a-z0-9])`, "i").test(haystack));
|
|
7742
|
+
function emailSpamSignals(copy) {
|
|
7743
|
+
const subject = typeof copy.subject === "string" ? copy.subject : "";
|
|
7744
|
+
const text8 = typeof copy.text === "string" ? copy.text : "";
|
|
7745
|
+
const out = [];
|
|
7746
|
+
const letters = strip(subject).replace(/[^A-Za-z]/g, "");
|
|
7747
|
+
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, "")));
|
|
7748
|
+
if (shouting) out.push({ code: "subject-caps", message: "The subject shouts in capital letters. Filters read that as a spam signal; use sentence case." });
|
|
7749
|
+
if (/[!?]{2,}|!\s*$/.test(subject)) out.push({ code: "subject-punctuation", message: "Repeated or trailing exclamation marks in the subject are a classic spam signal." });
|
|
7750
|
+
if (/^\s*(?:re|fw|fwd)\s*:/i.test(subject)) out.push({ code: "subject-fake-reply", message: "A subject that starts like a reply or forward reads as a spoof when the recipient never wrote first." });
|
|
7751
|
+
const subjectHit = phraseIn(SUBJECT_TRIGGERS, strip(subject));
|
|
7752
|
+
if (subjectHit) out.push({ code: "subject-trigger-words", message: `The subject uses "${subjectHit}", a word filters weight against the sender.` });
|
|
7753
|
+
const bodyHit = phraseIn(TRIGGER_PHRASES, strip(text8));
|
|
7754
|
+
if (bodyHit) out.push({ code: "body-trigger-words", message: `The body uses "${bodyHit}", a phrase filters weight against the sender.` });
|
|
7755
|
+
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
|
+
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]);
|
|
7758
|
+
const hosts = urls.map((url) => {
|
|
7759
|
+
try {
|
|
7760
|
+
return new URL(url).hostname;
|
|
7761
|
+
} catch {
|
|
7762
|
+
return "";
|
|
7763
|
+
}
|
|
7764
|
+
});
|
|
7765
|
+
if (urls.some((url) => /^http:\/\//i.test(url))) out.push({ code: "link-insecure", message: "A link uses http:// instead of https://. Filters flag insecure links." });
|
|
7766
|
+
if (hosts.some((host) => DEV_HOSTS.test(host) || /^\d+\.\d+\.\d+\.\d+$/.test(host))) out.push({ code: "link-origin", message: "A link points at a development host (workers.dev, pages.dev, localhost or a raw IP). Link to the site's own domain." });
|
|
7767
|
+
if (hosts.some((host) => SHORTENERS.test(host))) out.push({ code: "link-shortener", message: "A link uses a URL shortener, which hides the destination and is heavily abused. Link to the real address." });
|
|
7768
|
+
if (new Set(urls).size > 5) out.push({ code: "link-many", message: "The body has more than five links. Keep lifecycle mail to the one or two the reader needs." });
|
|
7769
|
+
if (urls.length && words(text8).length < 20) out.push({ code: "body-link-only", message: "The body is little more than a link. Say what the reader is being sent and why before the address." });
|
|
7770
|
+
return out;
|
|
7771
|
+
}
|
|
7772
|
+
function emailSenderWarnings(input) {
|
|
7773
|
+
const out = [];
|
|
7774
|
+
const from = (input.fromEmail ?? "").trim().toLowerCase().replace(/^.*<([^>]+)>.*$/, "$1");
|
|
7775
|
+
const reply = (input.replyTo ?? "").trim().toLowerCase();
|
|
7776
|
+
const domain = (address) => address.split("@")[1] ?? "";
|
|
7777
|
+
if (/^(?:no-?reply|do-?not-?reply|donotreply)(?:[+.-][^@]*)?@/.test(from)) {
|
|
7778
|
+
out.push({ code: "sender-no-reply", message: "Mail is sent from a no-reply address. Gmail's sender guidelines advise against it; send from a monitored address so replies and unsubscribe requests reach someone." });
|
|
7779
|
+
}
|
|
7780
|
+
if (domain(from) && domain(reply) && domain(from) !== domain(reply)) {
|
|
7781
|
+
out.push({ code: "reply-to-domain", message: `Replies go to ${domain(reply)} while mail is sent from ${domain(from)}. A Reply-To on another domain is a mild spam signal; prefer an address on the sending domain.` });
|
|
7782
|
+
}
|
|
7783
|
+
return out;
|
|
7784
|
+
}
|
|
7785
|
+
|
|
7786
|
+
// src/email-readiness.ts
|
|
7701
7787
|
function validateEmailReadiness(input) {
|
|
7702
7788
|
const issues = [];
|
|
7703
7789
|
for (const key3 of Object.keys(input.templates)) if (!input.events[key3]) {
|
|
@@ -7722,9 +7808,56 @@ ${template.text ?? ""}`.matchAll(/\{\{(\w+)\}\}/g)].map((match) => match[1]);
|
|
|
7722
7808
|
}
|
|
7723
7809
|
if (event2.requiresTier && input.tiersUnavailable) problems.push({ code: "tiers-unavailable", message: "Tier names are unavailable; delivery checks will block unresolved memberships." });
|
|
7724
7810
|
else if (event2.requiresTier && noTierNames) problems.push({ code: "tiers-missing", message: "Configure the membership tier names needed by this event." });
|
|
7725
|
-
return {
|
|
7811
|
+
return {
|
|
7812
|
+
...event2,
|
|
7813
|
+
key: key3,
|
|
7814
|
+
status: template?.enabled === false ? "disabled" : problems.length ? "blocked" : "ready",
|
|
7815
|
+
issues: problems,
|
|
7816
|
+
warnings: template ? emailSpamSignals(template) : []
|
|
7817
|
+
};
|
|
7726
7818
|
});
|
|
7727
|
-
return { checkedAt: input.now ?? Date.now(), events: events2, issues };
|
|
7819
|
+
return { checkedAt: input.now ?? Date.now(), events: events2, issues, warnings: emailSenderWarnings(input.sender ?? {}) };
|
|
7820
|
+
}
|
|
7821
|
+
|
|
7822
|
+
// src/email-html.ts
|
|
7823
|
+
var URL_RE2 = /https?:\/\/[^\s<>"')\]]+[^\s<>"').,;:!?\]]/g;
|
|
7824
|
+
function escapeHtml(s) {
|
|
7825
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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
|
+
// src/email-deliverability.ts
|
|
7848
|
+
var ADMINISTRATOR_AUDIENCE = /^(?:administrator|admin|team|operator|owner|staff)s?$/i;
|
|
7849
|
+
function isRecipientFacing(event2) {
|
|
7850
|
+
return !event2 || !ADMINISTRATOR_AUDIENCE.test(event2.audience.trim());
|
|
7851
|
+
}
|
|
7852
|
+
function listUnsubscribeHeader(replyTo) {
|
|
7853
|
+
const address = replyTo.trim();
|
|
7854
|
+
if (!/^[^\s@,;<>?]+@[^\s@,;<>?]+\.[^\s@,;<>?]+$/.test(address)) return void 0;
|
|
7855
|
+
return `<mailto:${address}?subject=${encodeURIComponent("unsubscribe")}>`;
|
|
7856
|
+
}
|
|
7857
|
+
function deliverabilityHeaders(input) {
|
|
7858
|
+
if (!isRecipientFacing(input.event)) return void 0;
|
|
7859
|
+
const value = listUnsubscribeHeader(input.replyTo);
|
|
7860
|
+
return value ? { "List-Unsubscribe": value } : void 0;
|
|
7728
7861
|
}
|
|
7729
7862
|
|
|
7730
7863
|
// src/notify.ts
|
|
@@ -7892,11 +8025,16 @@ async function sendTemplated(deps, input) {
|
|
|
7892
8025
|
if (decision.transport === "log-only") return { sent: true };
|
|
7893
8026
|
let messageId;
|
|
7894
8027
|
try {
|
|
8028
|
+
const text8 = String(row2.body);
|
|
8029
|
+
const html = deps.renderHtml === false ? void 0 : (deps.renderHtml ?? textToHtml)(text8);
|
|
8030
|
+
const headers = deliverabilityHeaders({ event: event2, replyTo: input.group.replyTo });
|
|
7895
8031
|
const response = await deps.sender.send({
|
|
7896
8032
|
from: fromAddress(deps.from, input.group.fromName),
|
|
7897
8033
|
to: [decision.to],
|
|
7898
8034
|
subject: String(row2.subject),
|
|
7899
|
-
text:
|
|
8035
|
+
text: text8,
|
|
8036
|
+
...html ? { html } : {},
|
|
8037
|
+
...headers ? { headers } : {},
|
|
7900
8038
|
replyTo: input.group.replyTo
|
|
7901
8039
|
});
|
|
7902
8040
|
if (!response || typeof response.messageId !== "string" || !response.messageId) throw new Error("missing delivery receipt");
|