@odla-ai/chapter 0.58.0 → 0.59.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 CHANGED
@@ -283,6 +283,18 @@ membership and gift checkout.
283
283
 
284
284
  ### Join and member composition
285
285
 
286
+ The packaged journey (Chapter 0.59.0): inside `JourneyFrame`, every text
287
+ control renders at 16px or more on a touch device, above a host's own
288
+ `input[type=text]` rule, because iOS Safari zooms a page whose focused field is
289
+ smaller and keeps that zoom through the payment step. The booking step keeps
290
+ its button in view above the dates, inactive until a date and a time are both
291
+ chosen, with a line beneath it reading `copy.booking.pickDate` ("Pick a date"),
292
+ then `pickTime` ("Pick a time"), then nothing; the island starts the
293
+ `/api/schedule/slots` read while the member pays (`useJoinSlotsPreload`), so
294
+ the times are already on screen when payment confirms. The payment receipt
295
+ renders after each step's body, so a booked member sees their confirmation
296
+ first and the receipt beneath it.
297
+
286
298
  For a centrally managed membership page, validate a bounded presentation
287
299
  document against the live, server-authoritative tiers, then render it with
288
300
  `MembershipCards`:
@@ -478,6 +490,16 @@ webhook, and calendar booking journey before production.
478
490
 
479
491
  ### Authenticated member pages
480
492
 
493
+ A purchaser whose named-seat invitation is open (sent, not yet accepted, not
494
+ held for their own approval) can forward it themselves: `NamedSeatInvitationLink`
495
+ from `@odla-ai/chapter/ui/member` renders the same `/join?seat=…` link the
496
+ invitation email carries, with "Copy invitation link" and "Copy a ready-to-send
497
+ message" controls that name the recipient's email, which is the only address the
498
+ link opens for. `NamedSeatPurchase` includes it; a host with its own seat card
499
+ renders it under the recipient line and passes `joinPath` when its application
500
+ page is not `/join`. `namedSeatInvitationShareable`, `namedSeatInvitationUrl`
501
+ and `namedSeatInvitationMessage` are the pure pieces.
502
+
481
503
  Enable the standard member catalog explicitly:
482
504
 
483
505
  ```ts
@@ -1535,6 +1557,14 @@ worker to 6 lines and deleted ~2,500 lines. The order that worked:
1535
1557
 
1536
1558
  ### Behavior deltas to audit
1537
1559
 
1560
+ - Chapter 0.59.0 runs application emails, the CRM stage mirror and the booking
1561
+ prep email after the response through the Worker's execution context when
1562
+ `chapterWorker().fetch` receives one (every Cloudflare deployment does; a
1563
+ wrapper such as `withObservability` forwards it). A host that calls the
1564
+ handler directly without one still gets them awaited. Three sequential
1565
+ control reads on `/api/join-config` and four on `POST /api/applications` now
1566
+ leave together.
1567
+
1538
1568
  These bite silently — a smoke test won't catch them:
1539
1569
 
1540
1570
  - **Application fields generate the application schema.** The resolved
@@ -1784,6 +1814,30 @@ These bite silently — a smoke test won't catch them:
1784
1814
  durable, unfinished local approval work without starving later applicants.
1785
1815
  Neither helper installs a cron or provisions infrastructure by itself.
1786
1816
 
1817
+ **Card disputes (Chapter 0.59.0).** A chargeback is a provider fact, so it
1818
+ enters the hub the same way a payment does: subscribe the hub's Stripe
1819
+ webhook to `charge.dispute.created`, `charge.dispute.updated`,
1820
+ `charge.dispute.closed`, `charge.dispute.funds_withdrawn` and
1821
+ `charge.dispute.funds_reinstated`, and re-provision the authority schema
1822
+ (the membership row gains optional `dispute*`, `recharge*` attributes). On
1823
+ the deployment that hosts the authority, the hub webhook reads the disputed
1824
+ charge with the network's own Stripe credential (never trusting the webhook
1825
+ body for identity), binds it to the membership whose first invoice,
1826
+ subscription or earlier re-charge it paid, and moves that membership to
1827
+ `payment_disputed`; `readMembershipAccess` then refuses the member and the
1828
+ named seat until the dispute is won. A won or closed inquiry restores the
1829
+ status it interrupted; a lost one stays suspended. A chapter's own webhook
1830
+ relays the same event (`kind: "disputed"`) through the renewal edge so its
1831
+ application's `membershipStatus` projection follows the authority. The
1832
+ network owner mounts `GET /api/admin/membership-disputes` and
1833
+ `POST …/membership-disputes/:membershipId/recharge` beside the seat routes
1834
+ (super admin, `membershipAuthorityOperator`), and renders
1835
+ `MembershipDisputes` from `@odla-ai/chapter/ui/admin`: re-charging collects
1836
+ the lost amount again on the member's saved card, off-session and only on the
1837
+ operator's confirmed click, with one idempotency key kept on the row until
1838
+ Stripe answers so a lost response never charges twice. A seat's own charge or
1839
+ another customer's never touches the primary membership.
1840
+
1787
1841
  Starting with Chapter 0.55.0, a named-seat-enabled `JoinIsland` also offers the
1788
1842
  additional member in the initial payment step. Both journeys read
1789
1843
  `seatPriceCents` from the selected SKU's frozen offering: a positive amount is
@@ -1850,6 +1904,15 @@ These bite silently — a smoke test won't catch them:
1850
1904
  named and whether that person joined without folding the seat status machine
1851
1905
  itself; `NamedSeatRecipientLine` renders it.
1852
1906
 
1907
+ **Availability saves land on the seeded group (0.58.1).** A group's `id` is
1908
+ its public key, not its entity id: the CLI seeds the row under a fresh entity
1909
+ id and writes `id` as an attribute, which is how every live chapter's group is
1910
+ stored. `PUT /api/admin/scheduling` keyed its write by the `id` string, so
1911
+ odla-db targeted a phantom entity and the strict schema refused it
1912
+ (`required_attribute`) — a 500 on 0.57.0, a 503 with that `cause` on 0.58.0.
1913
+ The write now goes through the `groups.id` lookup like every other group
1914
+ write, and the test double seeds the group the way the CLI does.
1915
+
1853
1916
  **Availability saves keep what the editor does not show (0.58.0).**
1854
1917
  `PUT /api/admin/scheduling` now merges the fields it receives onto the
1855
1918
  stored config before validating, so an editor that sends only hours, days,
@@ -273,7 +273,9 @@ var DEFAULT_JOIN_COPY = {
273
273
  failed: "The booking could not be completed.",
274
274
  loading: "Loading available times\u2026",
275
275
  book: "Book this time",
276
- booking: "Booking\u2026"
276
+ booking: "Booking\u2026",
277
+ pickDate: "Pick a date",
278
+ pickTime: "Pick a time"
277
279
  },
278
280
  payment: {
279
281
  setupFailed: "Payment could not be set up. Please try again.",
@@ -524,4 +526,4 @@ export {
524
526
  useChapterCopy,
525
527
  BrandStyle
526
528
  };
527
- //# sourceMappingURL=chunk-FHKZVF5G.js.map
529
+ //# sourceMappingURL=chunk-GADLTNJF.js.map
@@ -1 +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 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 },\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"]}
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 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"]}
@@ -4,7 +4,7 @@ import {
4
4
  DEFAULT_CHAPTER_COPY,
5
5
  formatChapterCopy,
6
6
  useChapterCopy
7
- } from "./chunk-FHKZVF5G.js";
7
+ } from "./chunk-GADLTNJF.js";
8
8
 
9
9
  // src/ui/admin.tsx
10
10
  import { useEffect as useEffect12, useMemo as useMemo3, useRef as useRef3, useState as useState27 } from "preact/hooks";
@@ -773,7 +773,7 @@ function SeatPanel(props) {
773
773
  setBusy(false);
774
774
  }
775
775
  };
776
- const money5 = new Intl.NumberFormat("en-US", { style: "currency", currency: data.currency }).format(data.amountCents / 100);
776
+ const money6 = new Intl.NumberFormat("en-US", { style: "currency", currency: data.currency }).format(data.amountCents / 100);
777
777
  const refund = async () => {
778
778
  setBusy(true);
779
779
  setNote(null);
@@ -807,7 +807,7 @@ function SeatPanel(props) {
807
807
  seat.invitationHeld ? " (invitation waits for the purchaser's approval)" : ""
808
808
  ] }),
809
809
  /* @__PURE__ */ jsxs7("p", { className: "muted", style: { margin: 0 }, children: [
810
- data.amountCents === 0 ? "Included with the membership; nothing was charged." : `${money5} paid in full.`,
810
+ data.amountCents === 0 ? "Included with the membership; nothing was charged." : `${money6} paid in full.`,
811
811
  " The seat waits for its recipient and is never refunded automatically."
812
812
  ] }),
813
813
  data.refundable || data.regiftable ? /* @__PURE__ */ jsxs7("div", { style: { display: "flex", gap: 8 }, children: [
@@ -1944,7 +1944,7 @@ function RecordsExplorer(props) {
1944
1944
  `/api/admin/network/records?${query}`
1945
1945
  );
1946
1946
  const selected = page.data?.records.find((record) => record.id === ctx.route.recordId);
1947
- const columns3 = useMemo(() => [
1947
+ const columns4 = useMemo(() => [
1948
1948
  {
1949
1949
  key: "name",
1950
1950
  header: "Name",
@@ -1988,7 +1988,7 @@ function RecordsExplorer(props) {
1988
1988
  ctx.navigate({ recordId: null, detailTab: null });
1989
1989
  },
1990
1990
  rows: page.data?.records ?? [],
1991
- columns: columns3,
1991
+ columns: columns4,
1992
1992
  rowKey: (record) => record.id,
1993
1993
  loading: page.loading,
1994
1994
  emptyState: page.error ? `Could not load records: ${page.error}` : "No records found.",
@@ -3585,14 +3585,97 @@ function NetworkSeatBilling(props) {
3585
3585
  ] });
3586
3586
  }
3587
3587
 
3588
- // src/ui/membership-policy-versions.tsx
3589
- import { Button as Button20, DataTable as DataTable5 } from "@odla-ai/ui/components";
3588
+ // src/ui/membership-disputes.tsx
3590
3589
  import { useState as useState31 } from "preact/hooks";
3591
- import { Fragment as Fragment15, jsx as jsx38, jsxs as jsxs34 } from "preact/jsx-runtime";
3590
+ import { Button as Button20, DataTable as DataTable5 } from "@odla-ai/ui/components";
3591
+ import { jsx as jsx38, jsxs as jsxs34 } from "preact/jsx-runtime";
3592
+ var money4 = (row, cents) => new Intl.NumberFormat("en-US", { style: "currency", currency: row.currency }).format(cents / 100);
3593
+ var day = (at) => at ? new Date(at).toLocaleDateString() : "\u2014";
3594
+ var DISPUTE_STATUS_WORDS = {
3595
+ warning_needs_response: "inquiry, response needed",
3596
+ warning_under_review: "inquiry under review",
3597
+ warning_closed: "inquiry closed",
3598
+ needs_response: "open, response needed",
3599
+ under_review: "under review",
3600
+ won: "won",
3601
+ lost: "lost",
3602
+ recharged: "re-charged"
3603
+ };
3604
+ var columns3 = (begin, busy) => [
3605
+ { key: "member", header: "Member", cell: (row) => /* @__PURE__ */ jsxs34("span", { children: [
3606
+ row.memberNumber ? `#${row.memberNumber} \xB7 ` : "",
3607
+ /* @__PURE__ */ jsx38("code", { children: row.applicationId })
3608
+ ] }) },
3609
+ { key: "chapter", header: "Chapter / runtime", cell: (row) => `${row.chapterId} / ${row.runtime}` },
3610
+ { key: "status", header: "Membership", cell: (row) => row.status === "payment_disputed" ? "suspended by dispute" : row.status },
3611
+ { key: "dispute", header: "Dispute", cell: (row) => /* @__PURE__ */ jsxs34("span", { children: [
3612
+ DISPUTE_STATUS_WORDS[row.disputeStatus] ?? row.disputeStatus,
3613
+ row.disputeReason ? ` \xB7 ${row.disputeReason}` : ""
3614
+ ] }) },
3615
+ { key: "amount", header: "Amount", cell: (row) => money4(row, row.disputeAmountCents ?? row.amountCents) },
3616
+ { key: "opened", header: "Opened", cell: (row) => day(row.disputedAt) },
3617
+ { key: "closed", header: "Closed", cell: (row) => day(row.disputeClosedAt) },
3618
+ { key: "recharge", header: "Re-charge", cell: (row) => row.rechargePaymentIntentId ? /* @__PURE__ */ jsxs34("span", { children: [
3619
+ day(row.rechargedAt),
3620
+ " \xB7 ",
3621
+ /* @__PURE__ */ jsx38("code", { children: row.rechargePaymentIntentId })
3622
+ ] }) : row.status === "payment_disputed" && row.disputeStatus === "lost" ? /* @__PURE__ */ jsx38(Button20, { mini: true, disabled: busy !== null, onClick: () => begin(row), children: busy === row.id ? "Charging\u2026" : "Charge again" }) : row.status === "payment_disputed" ? "Respond to the dispute in Stripe" : "\u2014" }
3623
+ ];
3624
+ function MembershipDisputes(props) {
3625
+ const { data, loading, error, refresh } = useAdminResource(props.getToken, "/api/admin/membership-disputes");
3626
+ const [busy, setBusy] = useState31(null);
3627
+ const [confirming, setConfirming] = useState31(null);
3628
+ const [note, setNote] = useState31(null);
3629
+ const recharge = async (row) => {
3630
+ setBusy(row.id);
3631
+ setNote(null);
3632
+ setConfirming(null);
3633
+ try {
3634
+ const receipt = await adminFetch(
3635
+ props.getToken,
3636
+ `/api/admin/membership-disputes/${encodeURIComponent(row.id)}/recharge`,
3637
+ { method: "POST" }
3638
+ );
3639
+ setNote(receipt.replayed ? `Membership ${row.applicationId} was already re-charged (${receipt.paymentIntentId}).` : `Collected ${money4(row, receipt.amountCents)} again for ${row.applicationId}; the membership is ${receipt.status}.`);
3640
+ refresh();
3641
+ } catch (failure) {
3642
+ setNote(failure instanceof Error ? failure.message : "The charge could not be re-issued.");
3643
+ } finally {
3644
+ setBusy(null);
3645
+ }
3646
+ };
3647
+ const rows = data?.disputes ?? [];
3648
+ return /* @__PURE__ */ jsxs34(Panel, { title: "Card disputes", children: [
3649
+ /* @__PURE__ */ jsx38("p", { children: "A chargeback suspends the membership until the dispute is won or the member clears it with their bank. Re-charging collects the disputed amount again on the saved card, only after a lost dispute and only when you ask." }),
3650
+ loading ? /* @__PURE__ */ jsx38("p", { children: "Loading disputes\u2026" }) : error ? /* @__PURE__ */ jsxs34("p", { role: "alert", children: [
3651
+ "Disputes could not be loaded: ",
3652
+ error
3653
+ ] }) : rows.length ? /* @__PURE__ */ jsx38(DataTable5, { rows, columns: columns3(setConfirming, busy), rowKey: (row) => row.id }) : /* @__PURE__ */ jsx38("p", { children: "No membership carries a dispute." }),
3654
+ data?.truncated ? /* @__PURE__ */ jsx38("p", { role: "alert", children: "Only the most recent memberships were checked; older disputes may exist." }) : null,
3655
+ confirming ? /* @__PURE__ */ jsxs34("div", { role: "group", "aria-label": "Confirm re-charge", children: [
3656
+ /* @__PURE__ */ jsxs34("p", { children: [
3657
+ "Charge ",
3658
+ money4(confirming, confirming.disputeAmountCents ?? confirming.amountCents),
3659
+ " again to the card on file for ",
3660
+ confirming.applicationId,
3661
+ "?"
3662
+ ] }),
3663
+ /* @__PURE__ */ jsx38(Button20, { mini: true, onClick: () => void recharge(confirming), children: "Yes, charge the card" }),
3664
+ " ",
3665
+ /* @__PURE__ */ jsx38(Button20, { mini: true, onClick: () => setConfirming(null), children: "Cancel" })
3666
+ ] }) : null,
3667
+ note ? /* @__PURE__ */ jsx38("p", { role: "status", children: note }) : null
3668
+ ] });
3669
+ }
3670
+
3671
+ // src/ui/membership-policy-versions.tsx
3672
+ import { Button as Button21, DataTable as DataTable6 } from "@odla-ai/ui/components";
3673
+ import { useState as useState32 } from "preact/hooks";
3674
+ import { Fragment as Fragment15, jsx as jsx39, jsxs as jsxs35 } from "preact/jsx-runtime";
3592
3675
  var PATH = "/api/admin/membership-policy-versions";
3593
3676
  var RETERM = "/api/admin/membership-discount/reterm";
3594
- var day = (ms) => new Date(ms).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", timeZone: "UTC" });
3595
- var money4 = (cents, currency) => new Intl.NumberFormat("en-US", { style: "currency", currency }).format(cents / 100);
3677
+ var day2 = (ms) => new Date(ms).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", timeZone: "UTC" });
3678
+ var money5 = (cents, currency) => new Intl.NumberFormat("en-US", { style: "currency", currency }).format(cents / 100);
3596
3679
  var STATE = { active: "Active", scheduled: "Scheduled", superseded: "Superseded", draft: "Draft", retired: "Retired" };
3597
3680
  var MECHANISM = {
3598
3681
  none: "None: cohort status only, no provider discount",
@@ -3600,62 +3683,62 @@ var MECHANISM = {
3600
3683
  stripe_coupon_forever: "Stripe coupon, applied while active",
3601
3684
  stripe_coupon_term_bound: "Stripe coupon, applied once; issuance refuses unless the offering's fixed term ends on the same date"
3602
3685
  };
3603
- var State = ({ row }) => /* @__PURE__ */ jsx38("span", { className: row.state === "active" ? "badge" : "muted", children: STATE[row.state] });
3604
- var superseded = (row) => row.supersededAt === void 0 ? "\u2014" : day(row.supersededAt);
3605
- var Source = ({ row }) => /* @__PURE__ */ jsxs34("span", { children: [
3606
- row.source ?? /* @__PURE__ */ jsx38("span", { className: "muted", children: "Unrecorded" }),
3607
- row.actor ? /* @__PURE__ */ jsxs34(Fragment15, { children: [
3686
+ var State = ({ row }) => /* @__PURE__ */ jsx39("span", { className: row.state === "active" ? "badge" : "muted", children: STATE[row.state] });
3687
+ var superseded = (row) => row.supersededAt === void 0 ? "\u2014" : day2(row.supersededAt);
3688
+ var Source = ({ row }) => /* @__PURE__ */ jsxs35("span", { children: [
3689
+ row.source ?? /* @__PURE__ */ jsx39("span", { className: "muted", children: "Unrecorded" }),
3690
+ row.actor ? /* @__PURE__ */ jsxs35(Fragment15, { children: [
3608
3691
  " \xB7 ",
3609
- /* @__PURE__ */ jsx38("code", { children: row.actor })
3692
+ /* @__PURE__ */ jsx39("code", { children: row.actor })
3610
3693
  ] }) : null,
3611
- row.recordedAt !== void 0 ? /* @__PURE__ */ jsxs34(Fragment15, { children: [
3694
+ row.recordedAt !== void 0 ? /* @__PURE__ */ jsxs35(Fragment15, { children: [
3612
3695
  " \xB7 ",
3613
- day(row.recordedAt)
3696
+ day2(row.recordedAt)
3614
3697
  ] }) : null
3615
3698
  ] });
3616
3699
  var offeringColumns = [
3617
- { key: "version", header: "Version", cell: (row) => /* @__PURE__ */ jsxs34(Fragment15, { children: [
3618
- /* @__PURE__ */ jsxs34("code", { children: [
3700
+ { key: "version", header: "Version", cell: (row) => /* @__PURE__ */ jsxs35(Fragment15, { children: [
3701
+ /* @__PURE__ */ jsxs35("code", { children: [
3619
3702
  "v",
3620
3703
  row.version
3621
3704
  ] }),
3622
3705
  " ",
3623
- /* @__PURE__ */ jsx38(State, { row })
3706
+ /* @__PURE__ */ jsx39(State, { row })
3624
3707
  ] }), sortAs: "number", sortValue: (row) => row.version },
3625
- { key: "sku", header: "SKU", cell: (row) => /* @__PURE__ */ jsx38("code", { children: row.baseSkuId }) },
3626
- { key: "list", header: "List price", cell: (row) => `${money4(row.basePriceCents, row.currency)} / ${row.interval}` },
3627
- { key: "term", header: "Term", cell: (row) => row.term.kind === "fixed_end" ? `Ends ${day(row.term.endsAt)}` : `Rolling ${row.term.months} months` },
3628
- { key: "seat", header: "Seat", cell: (row) => `${row.seatPriceCents === 0 ? "Included" : money4(row.seatPriceCents, row.currency)} \xB7 ${row.seatPurchaseWindowCalendarMonths === void 0 ? "no window" : `${row.seatPurchaseWindowCalendarMonths}-month window`}` },
3629
- { key: "effectiveFrom", header: "Effective from", cell: (row) => day(row.effectiveFrom), sortAs: "number", sortValue: (row) => row.effectiveFrom },
3630
- { key: "effectiveUntil", header: "Effective until", cell: (row) => row.effectiveUntil === void 0 ? "Open" : day(row.effectiveUntil) },
3708
+ { key: "sku", header: "SKU", cell: (row) => /* @__PURE__ */ jsx39("code", { children: row.baseSkuId }) },
3709
+ { key: "list", header: "List price", cell: (row) => `${money5(row.basePriceCents, row.currency)} / ${row.interval}` },
3710
+ { key: "term", header: "Term", cell: (row) => row.term.kind === "fixed_end" ? `Ends ${day2(row.term.endsAt)}` : `Rolling ${row.term.months} months` },
3711
+ { key: "seat", header: "Seat", cell: (row) => `${row.seatPriceCents === 0 ? "Included" : money5(row.seatPriceCents, row.currency)} \xB7 ${row.seatPurchaseWindowCalendarMonths === void 0 ? "no window" : `${row.seatPurchaseWindowCalendarMonths}-month window`}` },
3712
+ { key: "effectiveFrom", header: "Effective from", cell: (row) => day2(row.effectiveFrom), sortAs: "number", sortValue: (row) => row.effectiveFrom },
3713
+ { key: "effectiveUntil", header: "Effective until", cell: (row) => row.effectiveUntil === void 0 ? "Open" : day2(row.effectiveUntil) },
3631
3714
  { key: "supersededAt", header: "Superseded", cell: superseded },
3632
- { key: "source", header: "Source", cell: (row) => /* @__PURE__ */ jsx38(Source, { row }) }
3715
+ { key: "source", header: "Source", cell: (row) => /* @__PURE__ */ jsx39(Source, { row }) }
3633
3716
  ];
3634
3717
  var policyColumns = (getToken) => [
3635
- { key: "version", header: "Version", cell: (row) => /* @__PURE__ */ jsxs34(Fragment15, { children: [
3636
- /* @__PURE__ */ jsxs34("code", { children: [
3718
+ { key: "version", header: "Version", cell: (row) => /* @__PURE__ */ jsxs35(Fragment15, { children: [
3719
+ /* @__PURE__ */ jsxs35("code", { children: [
3637
3720
  "v",
3638
3721
  row.version
3639
3722
  ] }),
3640
3723
  " ",
3641
- /* @__PURE__ */ jsx38(State, { row })
3724
+ /* @__PURE__ */ jsx39(State, { row })
3642
3725
  ] }), sortAs: "number", sortValue: (row) => row.version },
3643
- { key: "cohort", header: "Cohort", cell: (row) => /* @__PURE__ */ jsxs34(Fragment15, { children: [
3726
+ { key: "cohort", header: "Cohort", cell: (row) => /* @__PURE__ */ jsxs35(Fragment15, { children: [
3644
3727
  row.cohortLabel,
3645
3728
  " ",
3646
- /* @__PURE__ */ jsxs34("span", { className: "muted", children: [
3729
+ /* @__PURE__ */ jsxs35("span", { className: "muted", children: [
3647
3730
  "\xB7 soft target ",
3648
3731
  row.softTarget
3649
3732
  ] })
3650
3733
  ] }) },
3651
3734
  { key: "percentOff", header: "Percent off", cell: (row) => `${row.percentOffBps / 100}%` },
3652
3735
  { key: "skus", header: "Discounted SKUs", cell: (row) => row.discountEligibleSkuIds.length ? row.discountEligibleSkuIds.join(", ") : "None" },
3653
- { key: "qualifying", header: "Qualifying window", cell: (row) => `${day(row.qualifyingStartsAt)} \u2192 ${day(row.qualifyingEndsAt)}` },
3654
- { key: "benefit", header: "Benefit duration", cell: (row) => row.benefitDuration.kind === "fixed_end" ? `Until ${day(row.benefitDuration.endsAt)}` : row.benefitDuration.kind === "first_term" ? "First term" : "While active" },
3736
+ { key: "qualifying", header: "Qualifying window", cell: (row) => `${day2(row.qualifyingStartsAt)} \u2192 ${day2(row.qualifyingEndsAt)}` },
3737
+ { key: "benefit", header: "Benefit duration", cell: (row) => row.benefitDuration.kind === "fixed_end" ? `Until ${day2(row.benefitDuration.endsAt)}` : row.benefitDuration.kind === "first_term" ? "First term" : "While active" },
3655
3738
  { key: "mechanism", header: "Provider mechanism", cell: (row) => MECHANISM[row.mechanism] },
3656
3739
  { key: "supersededAt", header: "Superseded", cell: superseded },
3657
- { key: "source", header: "Source", cell: (row) => /* @__PURE__ */ jsx38(Source, { row }) },
3658
- { key: "members", header: "Existing members", cell: (row) => /* @__PURE__ */ jsx38(RetermAction, { row, getToken }) }
3740
+ { key: "source", header: "Source", cell: (row) => /* @__PURE__ */ jsx39(Source, { row }) },
3741
+ { key: "members", header: "Existing members", cell: (row) => /* @__PURE__ */ jsx39(RetermAction, { row, getToken }) }
3659
3742
  ];
3660
3743
  var summary = (receipt) => {
3661
3744
  const skipped = receipt.skipped.reduce((acc, item) => ({ ...acc, [item.reason]: (acc[item.reason] ?? 0) + 1 }), {});
@@ -3667,9 +3750,9 @@ var summary = (receipt) => {
3667
3750
  return `${parts.join(" \xB7 ")}${receipt.more ? " \xB7 more remain, run again" : ""}`;
3668
3751
  };
3669
3752
  function RetermAction({ row, getToken }) {
3670
- const [busy, setBusy] = useState31(false);
3671
- const [result, setResult] = useState31(null);
3672
- if (row.state !== "active" || row.mechanism !== "stripe_coupon_forever") return /* @__PURE__ */ jsx38("span", { className: "muted", children: row.state !== "active" ? "Not the active version" : "Benefit is not while-active" });
3753
+ const [busy, setBusy] = useState32(false);
3754
+ const [result, setResult] = useState32(null);
3755
+ if (row.state !== "active" || row.mechanism !== "stripe_coupon_forever") return /* @__PURE__ */ jsx39("span", { className: "muted", children: row.state !== "active" ? "Not the active version" : "Benefit is not while-active" });
3673
3756
  const run = async () => {
3674
3757
  if (!globalThis.confirm?.(`Move every paid ${row.cohortLabel} member of ${row.chapterId} onto v${row.version}? Their next renewal invoice carries ${row.percentOffBps / 100}% off while they stay active. A member whose renewal this would raise is left alone.`)) return;
3675
3758
  setBusy(true);
@@ -3683,29 +3766,29 @@ function RetermAction({ row, getToken }) {
3683
3766
  setBusy(false);
3684
3767
  }
3685
3768
  };
3686
- return /* @__PURE__ */ jsxs34("span", { style: { display: "inline-flex", flexDirection: "column", gap: 4 }, children: [
3687
- /* @__PURE__ */ jsx38(Button20, { mini: true, disabled: busy, onClick: run, children: busy ? "Moving\u2026" : "Move members to this benefit" }),
3688
- result ? /* @__PURE__ */ jsx38("span", { role: result.tone === "good" ? "status" : "alert", className: result.tone === "danger" ? "danger" : "muted", children: result.text }) : null
3769
+ return /* @__PURE__ */ jsxs35("span", { style: { display: "inline-flex", flexDirection: "column", gap: 4 }, children: [
3770
+ /* @__PURE__ */ jsx39(Button21, { mini: true, disabled: busy, onClick: run, children: busy ? "Moving\u2026" : "Move members to this benefit" }),
3771
+ result ? /* @__PURE__ */ jsx39("span", { role: result.tone === "good" ? "status" : "alert", className: result.tone === "danger" ? "danger" : "muted", children: result.text }) : null
3689
3772
  ] });
3690
3773
  }
3691
3774
  function MembershipPolicyVersions(props) {
3692
3775
  const path = props.chapterId ? `${PATH}?chapterId=${encodeURIComponent(props.chapterId)}` : PATH;
3693
3776
  const { data, loading, error } = useAdminResource(props.getToken, path);
3694
3777
  const scope = props.chapterId ? `chapter ${props.chapterId}` : "any chapter";
3695
- if (loading) return /* @__PURE__ */ jsx38(Panel, { title: "Membership offering versions", children: /* @__PURE__ */ jsx38("p", { role: "status", children: "Loading offering versions\u2026" }) });
3696
- if (error || !data) return /* @__PURE__ */ jsx38(Panel, { title: "Membership offering versions", children: /* @__PURE__ */ jsxs34("p", { role: "alert", children: [
3778
+ if (loading) return /* @__PURE__ */ jsx39(Panel, { title: "Membership offering versions", children: /* @__PURE__ */ jsx39("p", { role: "status", children: "Loading offering versions\u2026" }) });
3779
+ if (error || !data) return /* @__PURE__ */ jsx39(Panel, { title: "Membership offering versions", children: /* @__PURE__ */ jsxs35("p", { role: "alert", children: [
3697
3780
  "Offering versions could not be loaded: ",
3698
3781
  error ?? "empty response"
3699
3782
  ] }) });
3700
- return /* @__PURE__ */ jsxs34(Fragment15, { children: [
3701
- /* @__PURE__ */ jsxs34(Panel, { title: "Membership offering versions", actions: data.truncated ? /* @__PURE__ */ jsx38("span", { className: "badge", children: "Truncated" }) : void 0, children: [
3702
- /* @__PURE__ */ jsxs34("p", { className: "muted", children: [
3783
+ return /* @__PURE__ */ jsxs35(Fragment15, { children: [
3784
+ /* @__PURE__ */ jsxs35(Panel, { title: "Membership offering versions", actions: data.truncated ? /* @__PURE__ */ jsx39("span", { className: "badge", children: "Truncated" }) : void 0, children: [
3785
+ /* @__PURE__ */ jsxs35("p", { className: "muted", children: [
3703
3786
  "The list price a card shows comes from the active offering; the payable price applies the active discount policy. State is judged as of ",
3704
- day(data.now),
3787
+ day2(data.now),
3705
3788
  ", exactly as checkout issuance judges it."
3706
3789
  ] }),
3707
- /* @__PURE__ */ jsx38(
3708
- DataTable5,
3790
+ /* @__PURE__ */ jsx39(
3791
+ DataTable6,
3709
3792
  {
3710
3793
  rows: data.offerings,
3711
3794
  columns: offeringColumns,
@@ -3715,9 +3798,9 @@ function MembershipPolicyVersions(props) {
3715
3798
  }
3716
3799
  )
3717
3800
  ] }),
3718
- /* @__PURE__ */ jsxs34(Panel, { title: "Discount policy versions", children: [
3719
- /* @__PURE__ */ jsx38(
3720
- DataTable5,
3801
+ /* @__PURE__ */ jsxs35(Panel, { title: "Discount policy versions", children: [
3802
+ /* @__PURE__ */ jsx39(
3803
+ DataTable6,
3721
3804
  {
3722
3805
  rows: data.discountPolicies,
3723
3806
  columns: policyColumns(props.getToken),
@@ -3726,7 +3809,7 @@ function MembershipPolicyVersions(props) {
3726
3809
  emptyState: `No discount policy versions are recorded for ${scope}.`
3727
3810
  }
3728
3811
  ),
3729
- data.invalid.length ? /* @__PURE__ */ jsxs34("p", { role: "alert", children: [
3812
+ data.invalid.length ? /* @__PURE__ */ jsxs35("p", { role: "alert", children: [
3730
3813
  data.invalid.length,
3731
3814
  " recorded version",
3732
3815
  data.invalid.length === 1 ? " no longer normalizes and blocks" : "s no longer normalize and block",
@@ -3782,6 +3865,8 @@ export {
3782
3865
  meetingsSection,
3783
3866
  NetworkRecordEditor,
3784
3867
  NetworkSeatBilling,
3868
+ DISPUTE_STATUS_WORDS,
3869
+ MembershipDisputes,
3785
3870
  MembershipPolicyVersions
3786
3871
  };
3787
- //# sourceMappingURL=chunk-L2IPS2FQ.js.map
3872
+ //# sourceMappingURL=chunk-WFTWT3V7.js.map