@riverbankcms/sdk 0.136.0 → 0.137.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.
Files changed (160) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/dist/{PreviewEditorSidebar-BU5ZlhQR.mjs → PreviewEditorSidebar-Bn6cONPa.mjs} +8 -8
  3. package/dist/{PreviewEditorUI-9XkHL-bC.mjs → PreviewEditorUI-LAwXZPSx.mjs} +3 -3
  4. package/dist/{PreviewQueryProvider-wbEpXryy.mjs → PreviewQueryProvider-ztQUdsNo.mjs} +1 -1
  5. package/dist/{SdkPreviewModeRuntime-CnWmj-2B.mjs → SdkPreviewModeRuntime-BbbGsI_v.mjs} +596 -164
  6. package/dist/{SiteChromeCustomizeContext-B6d5zkog.mjs → SiteChromeCustomizeContext-BbkNSt5a.mjs} +2888 -1404
  7. package/dist/_dts/ai/src/contracts/proposals.d.ts +12 -298
  8. package/dist/_dts/ai/src/contracts/siteStyleProposalSchemas.d.ts +21 -14
  9. package/dist/_dts/ai/src/designer/themePatch.d.ts +5 -10
  10. package/dist/_dts/api/src/accessAdmin.d.ts +1 -9
  11. package/dist/_dts/api/src/accountApi.d.ts +49 -2
  12. package/dist/_dts/api/src/contentRuntime.d.ts +1 -4
  13. package/dist/_dts/api/src/domainConnection.d.ts +41 -0
  14. package/dist/_dts/api/src/endpoints.d.ts +194 -181
  15. package/dist/_dts/api/src/index.d.ts +4 -3
  16. package/dist/_dts/api/src/media.d.ts +19 -2
  17. package/dist/_dts/api/src/sdkContracts.d.ts +2 -15
  18. package/dist/_dts/api/src/sdkMediaContracts.d.ts +40 -0
  19. package/dist/_dts/api/src/sdkMediaEndpointDefinitions.d.ts +61 -0
  20. package/dist/_dts/api/src/sdkThemeWire.d.ts +15 -4
  21. package/dist/_dts/api/src/siteInfrastructure.d.ts +17 -23
  22. package/dist/_dts/api/src/siteManagementEndpoints.d.ts +7 -21
  23. package/dist/_dts/api/src/sitePlatformEndpoints.d.ts +27 -3
  24. package/dist/_dts/api/src/siteRuntimeEndpoints.d.ts +37 -17
  25. package/dist/_dts/api/src/types.d.ts +1 -1
  26. package/dist/_dts/blocks/src/PageRenderer.d.ts +2 -2
  27. package/dist/_dts/blocks/src/index.d.ts +1 -1
  28. package/dist/_dts/blocks/src/pageRenderAdmission.d.ts +9 -0
  29. package/dist/_dts/blocks/src/pageRendererBlockSupport.d.ts +75 -0
  30. package/dist/_dts/blocks/src/pageRendererBoundaries.d.ts +2 -0
  31. package/dist/_dts/blocks/src/server.d.ts +1 -1
  32. package/dist/_dts/blocks/src/system/blocks/event-registration.d.ts +1 -1
  33. package/dist/_dts/blocks/src/system/runtime/api/client.d.ts +2 -1
  34. package/dist/_dts/blocks/src/system/runtime/hooks/useCalendarEvents.d.ts +3 -3
  35. package/dist/_dts/blocks/src/system/runtime/hooks/useCalendarEvents.state.d.ts +34 -22
  36. package/dist/_dts/blocks/src/system/runtime/nodes/event-registration/ErrorStep.d.ts +3 -5
  37. package/dist/_dts/blocks/src/system/runtime/nodes/event-registration/event-registration.headless.contract.d.ts +2 -0
  38. package/dist/_dts/blocks/src/system/runtime/nodes/event-registration/event-registration.headless.planning.d.ts +1 -1
  39. package/dist/_dts/blocks/src/system/runtime/nodes/event-registration/hooks.d.ts +2 -0
  40. package/dist/_dts/blocks/src/system/runtime/nodes/event-registration/registrationErrorPresentation.d.ts +5 -0
  41. package/dist/_dts/blocks/src/system/runtime/nodes/event-registration/registrationErrorRecovery.d.ts +25 -0
  42. package/dist/_dts/blocks/src/system/runtime/nodes/event-registration/registrationWizardContext.d.ts +4 -0
  43. package/dist/_dts/blocks/src/system/runtime/nodes/event-registration/useEventRegistrationStripeReturn.d.ts +7 -3
  44. package/dist/_dts/blocks/src/system/runtime/nodes/event-registration/useEventRegistrationSubmit.d.ts +2 -0
  45. package/dist/_dts/blocks/src/system/runtime/nodes/event-registration/useEventRegistrationWizard.d.ts +5 -2
  46. package/dist/_dts/blocks/src/system/runtime/nodes/event-registration/useWaitlistClaimRestoration.d.ts +2 -0
  47. package/dist/_dts/blocks/src/system/runtime/nodes/events/domain/capacity.d.ts +164 -0
  48. package/dist/_dts/blocks/src/system/runtime/nodes/events/domain/cta.d.ts +29 -0
  49. package/dist/_dts/blocks/src/system/runtime/nodes/events/domain/dates.d.ts +47 -0
  50. package/dist/_dts/blocks/src/system/runtime/nodes/events/domain/index.d.ts +14 -0
  51. package/dist/_dts/blocks/src/system/runtime/nodes/events/domain/people.d.ts +6 -0
  52. package/dist/_dts/blocks/src/system/runtime/nodes/events/domain/pricing.d.ts +31 -0
  53. package/dist/_dts/blocks/src/system/runtime/nodes/events/shared/OfferingTitle.d.ts +11 -0
  54. package/dist/_dts/blocks/src/system/runtime/nodes/events/shared/WeekAgendaView.d.ts +1 -1
  55. package/dist/_dts/blocks/src/system/runtime/nodes/events/shared/WeekBoardView.d.ts +1 -1
  56. package/dist/_dts/blocks/src/system/runtime/nodes/events/shared/index.d.ts +3 -1
  57. package/dist/_dts/blocks/src/system/runtime/nodes/events/shared/listingLayout.d.ts +23 -0
  58. package/dist/_dts/commerce/src/purchase/event-registration-errors.d.ts +57 -0
  59. package/dist/_dts/commerce/src/purchase/index.d.ts +1 -0
  60. package/dist/_dts/db/src/generated/supabase/database.types.d.ts +8 -40
  61. package/dist/_dts/db/src/index.d.ts +1 -0
  62. package/dist/_dts/db/src/utils/postgrestFilter.d.ts +2 -0
  63. package/dist/_dts/media-core/src/bulkDeletePlanning.d.ts +4 -0
  64. package/dist/_dts/media-core/src/fileDomain.d.ts +3 -0
  65. package/dist/_dts/preview-next/src/client/preview/PreviewShell.d.ts +1 -0
  66. package/dist/_dts/preview-next/src/client/preview/PreviewShellLayout.d.ts +1 -0
  67. package/dist/_dts/preview-next/src/client/preview/SiteChromeCustomizeContext.d.ts +3 -2
  68. package/dist/_dts/preview-next/src/client/preview/siteChromeBrandPreferences.d.ts +1 -1
  69. package/dist/_dts/preview-next/src/client/preview/siteChromeQuickControlHistoryState.d.ts +2 -0
  70. package/dist/_dts/preview-next/src/client/preview/siteChromeQuickControlSaveRequest.d.ts +51 -0
  71. package/dist/_dts/preview-next/src/client/preview/siteChromeSavePlan.d.ts +24 -2
  72. package/dist/_dts/preview-next/src/client/preview/siteStylePreviewPlanner.d.ts +23 -1
  73. package/dist/_dts/preview-next/src/client/preview/usePersistedHeaderPreferences.d.ts +8 -0
  74. package/dist/_dts/sdk/src/public-api/contracts.d.ts +2 -1
  75. package/dist/_dts/sdk/src/version.d.ts +1 -1
  76. package/dist/_dts/theme-core/src/generated/containerResponsiveThemeCss.d.ts +1 -1
  77. package/dist/_dts/theme-core/src/gradients/types.d.ts +5 -0
  78. package/dist/_dts/theme-core/src/header/interactionPalette.d.ts +25 -2
  79. package/dist/_dts/theme-core/src/mock-themes/all.d.ts +3 -3
  80. package/dist/_dts/theme-core/src/schema.d.ts +12 -11
  81. package/dist/_dts/theme-core/src/site-styles/compilationPlan.d.ts +43 -3
  82. package/dist/_dts/theme-core/src/site-styles/curatedSiteStyles.d.ts +10 -0
  83. package/dist/_dts/theme-core/src/site-styles/headerBrandPresentation.d.ts +63 -0
  84. package/dist/_dts/theme-core/src/site-styles/headerBytesComparison.d.ts +30 -0
  85. package/dist/_dts/theme-core/src/site-styles/headerInteractionPalette.d.ts +50 -1
  86. package/dist/_dts/theme-core/src/site-styles/headerLayoutCatalog.d.ts +48 -9
  87. package/dist/_dts/theme-core/src/site-styles/headerPreferences.d.ts +162 -0
  88. package/dist/_dts/theme-core/src/site-styles/headerPreferencesPersistence.d.ts +73 -0
  89. package/dist/_dts/theme-core/src/site-styles/headerRecompile.d.ts +118 -0
  90. package/dist/_dts/theme-core/src/site-styles/headerStyleCompiler.d.ts +10 -1
  91. package/dist/_dts/theme-core/src/site-styles/headerStyleDefaults.d.ts +1 -1
  92. package/dist/_dts/theme-core/src/site-styles/headerStyleRecipe.d.ts +7 -0
  93. package/dist/_dts/theme-core/src/site-styles/index.d.ts +5 -0
  94. package/dist/_dts/theme-core/src/site-styles/previewCompiler.d.ts +1 -1
  95. package/dist/_dts/theme-core/src/site-styles/siteChromeBackgrounds.d.ts +20 -1
  96. package/dist/_dts/theme-core/src/site-styles/siteChromeNavPreferences.d.ts +43 -2
  97. package/dist/client/{BookingConsentAgreement-DCch21a6.mjs → BookingConsentAgreement--_fUCZe-.mjs} +2 -2
  98. package/dist/client/{DiscountCodeDisclosure-DIj1T59K.mjs → DiscountCodeDisclosure-BMipSoDJ.mjs} +1 -1
  99. package/dist/client/{EventCombined.client-BWVNlXTx.mjs → EventCombined.client-CwJOxn7A.mjs} +154 -178
  100. package/dist/client/{EventListing.client-DVFCDe3i.mjs → EventListing.client-eVwiylPW.mjs} +2 -2
  101. package/dist/client/{LeafletStaticMap-BY5mIHw9.mjs → LeafletStaticMap-IKenna5u.mjs} +1 -1
  102. package/dist/client/{accordion-CK29E8rG.mjs → accordion-MGAO3PuE.mjs} +1 -1
  103. package/dist/client/{bookingForm-DFvnet-N.mjs → bookingForm-CudlQPUu.mjs} +5 -5
  104. package/dist/client/bookings.mjs +418 -79
  105. package/dist/client/capacity-BW00PO7s.mjs +271 -0
  106. package/dist/client/{carousel-BVU5NQlb.mjs → carousel-BrbGJ2Jl.mjs} +1 -1
  107. package/dist/client/{cart-BNItcZVa.mjs → cart-Bq5eU0xZ.mjs} +3 -3
  108. package/dist/client/{checkout-Bu6iCdkP.mjs → checkout-BAFVRdrK.mjs} +3 -3
  109. package/dist/client/client.mjs +2919 -1918
  110. package/dist/client/{context-BImarOAq.mjs → context-DCkiHVdD.mjs} +8 -5
  111. package/dist/client/dates-2RPXHQy4.mjs +264 -0
  112. package/dist/client/{eventCalendar-DS2h-UXD.mjs → eventCalendar-B0O-hPF7.mjs} +10 -17
  113. package/dist/client/{eventCombined-Drt4Oq9p.mjs → eventCombined-DCkDaoDI.mjs} +3 -3
  114. package/dist/client/{eventDetails-Bl2vWgTX.mjs → eventDetails-D8GG2dnD.mjs} +5 -3
  115. package/dist/client/{eventListing-CS7PHY7v.mjs → eventListing-3ssV_bxz.mjs} +3 -3
  116. package/dist/client/{eventRegistration-D-j8eNy2.mjs → eventRegistration-bPQo3kVw.mjs} +562 -309
  117. package/dist/client/{form-RMpoLTyr.mjs → form-qMv8RDkk.mjs} +2 -2
  118. package/dist/client/{headerSection-BYE3Lb3k.mjs → headerSection-CH0Sc5hX.mjs} +2 -2
  119. package/dist/client/hooks.mjs +289 -32
  120. package/dist/client/{locationMap-BxSr7Kkh.mjs → locationMap-0_xlBnNI.mjs} +2 -2
  121. package/dist/client/{newsletterForm-DerbXwFC.mjs → newsletterForm-NUW7m9RI.mjs} +2 -2
  122. package/dist/client/{productDetail-4SZCLhAf.mjs → productDetail-FBEOM7jW.mjs} +3 -3
  123. package/dist/client/{productList-D7AP4GNq.mjs → productList-R0OSGhnn.mjs} +3 -3
  124. package/dist/client/{propsDecoder-pzBeCh14.mjs → propsDecoder-pYvx_Srl.mjs} +88 -380
  125. package/dist/client/rendering/client.mjs +2616 -1637
  126. package/dist/client/rendering/islands.mjs +17 -17
  127. package/dist/client/rendering.mjs +2710 -1702
  128. package/dist/client/{scrollToBlock-CduAKf8T.mjs → scrollToBlock-ByQv96M2.mjs} +1 -1
  129. package/dist/client/{shop-Cp5p_kGe.mjs → shop-RUG9DmRE.mjs} +6 -6
  130. package/dist/client/{shop-commerce-DicXYaYX.mjs → shop-commerce-B76Jz23B.mjs} +5 -5
  131. package/dist/client/{shop-commerce-store-CGWnpY4K.mjs → shop-commerce-store-QuQNdDsD.mjs} +1 -1
  132. package/dist/client/{shopShared-BDf9ljkm.mjs → shopShared-vXU1omwJ.mjs} +3 -3
  133. package/dist/client/{useEventFilters-DeeyGYuk.mjs → useEventFilters-ByuU6HLA.mjs} +33 -18
  134. package/dist/preview-next/before-render.mjs +79 -51
  135. package/dist/preview-next/client/runtime.mjs +4 -4
  136. package/dist/preview-next/keepAlive.mjs +79 -51
  137. package/dist/preview-next/middleware.mjs +79 -51
  138. package/dist/{previewAuthStore-DbqBol0V.mjs → previewAuthStore-L_cKW8Sb.mjs} +79 -51
  139. package/dist/{sdk-runtime-19MO27F0.mjs → sdk-runtime-sUMr5d70.mjs} +2 -2
  140. package/dist/{separator-DaPLtJ1f.mjs → separator-BIHkie5i.mjs} +1 -1
  141. package/dist/server/components.mjs +2077 -1274
  142. package/dist/server/config-validation.mjs +289 -32
  143. package/dist/server/config.mjs +289 -32
  144. package/dist/server/data.mjs +289 -32
  145. package/dist/server/index.mjs +80 -52
  146. package/dist/server/next.mjs +1822 -1111
  147. package/dist/server/page-converter.mjs +167 -31
  148. package/dist/server/prebuild.mjs +1 -1
  149. package/dist/server/rendering/server.mjs +1988 -1286
  150. package/dist/server/rendering.mjs +1943 -1182
  151. package/dist/server/routing.mjs +351 -86
  152. package/dist/server/server.mjs +293 -128
  153. package/dist/server/theme-bridge.mjs +52 -8
  154. package/dist/server/theme.mjs +27 -8
  155. package/dist/styles/index.css +15 -0
  156. package/package.json +1 -1
  157. package/dist/_dts/blocks/src/system/runtime/nodes/events/shared/eventCapacity.d.ts +0 -48
  158. package/dist/_dts/blocks/src/system/runtime/nodes/events/shared/pricing.d.ts +0 -17
  159. package/dist/_dts/blocks/src/system/runtime/nodes/events/shared/utils.d.ts +0 -100
  160. /package/dist/_dts/blocks/src/system/runtime/nodes/events/{shared → domain}/destination.d.ts +0 -0
@@ -7664,6 +7664,11 @@ const gradientConfigSchema = z$1.object({
7664
7664
  position: z$1.string().optional(),
7665
7665
  stops: z$1.array(gradientStopSchema).min(2)
7666
7666
  });
7667
+ const gradientsSchema = z$1.object({
7668
+ button: z$1.string().min(1).nullable().optional(),
7669
+ hero: z$1.string().min(1).nullable().optional(),
7670
+ background: z$1.string().min(1).nullable().optional()
7671
+ });
7667
7672
  //#endregion
7668
7673
  //#region ../theme-core/src/interactive/baseSchema.ts
7669
7674
  /**
@@ -8664,11 +8669,30 @@ const headerContainerSchema = z$1.object({
8664
8669
  tint: z$1.string().nullable().optional(),
8665
8670
  opacity: z$1.number().min(0).max(1).default(.12)
8666
8671
  }).optional();
8667
- const headerInteractionPaletteSchema = z$1.enum([
8672
+ //#endregion
8673
+ //#region ../theme-core/src/header/interactionPalette.ts
8674
+ /**
8675
+ * The palettes that fill the nav link with a colour on hover and focus. Each
8676
+ * one names a background/foreground token pair.
8677
+ */
8678
+ const headerInteractionPillPaletteValues = [
8668
8679
  "primary",
8669
8680
  "secondary",
8670
8681
  "accent"
8671
- ]);
8682
+ ];
8683
+ /**
8684
+ * "No pill": the nav link keeps whatever hover treatment its recipe compiled,
8685
+ * and the palette step writes nothing at all (#1318 Phase 4c decision 2).
8686
+ *
8687
+ * It exists because "a colour change on hover, no filled background" was not
8688
+ * expressible in the vocabulary, and it is what
8689
+ * `anna-raven-therapy-and-yoga` and `kirtan-lucy` have rendered since before
8690
+ * the palette existed. Without it those two rows can only be classified as a
8691
+ * conscious visual change.
8692
+ */
8693
+ const headerInteractionQuietPalette = "quiet";
8694
+ const headerInteractionPaletteValues = [...headerInteractionPillPaletteValues, headerInteractionQuietPalette];
8695
+ const headerInteractionPaletteSchema = z$1.enum(headerInteractionPaletteValues);
8672
8696
  //#endregion
8673
8697
  //#region ../theme-core/src/boxBackgroundSchema.ts
8674
8698
  const boxBackgroundOverlaySchema = z$1.preprocess(normalizeAuthoringOpacity, z$1.discriminatedUnion("type", [
@@ -9050,11 +9074,12 @@ const inputStyle = z$1.object({
9050
9074
  ]),
9051
9075
  label: z$1.enum(["inside", "above"])
9052
9076
  });
9053
- const headerPositioning = z$1.enum([
9077
+ const headerPositioningValues = [
9054
9078
  "static",
9055
9079
  "sticky",
9056
9080
  "fixed"
9057
- ]);
9081
+ ];
9082
+ const headerPositioning = z$1.enum(headerPositioningValues);
9058
9083
  const headerNavStyle = z$1.enum([
9059
9084
  "minimal",
9060
9085
  "underline",
@@ -9189,7 +9214,7 @@ const headerSchema = z$1.object({
9189
9214
  variant: headerVariantWithLegacyAliasSchema,
9190
9215
  positioning: headerPositioning,
9191
9216
  shrinkOnScroll: z$1.boolean(),
9192
- maxWidth: headerMaxWidth,
9217
+ maxWidth: headerMaxWidth.default("container"),
9193
9218
  scrollTreatment: headerScrollTreatmentSchema,
9194
9219
  background: headerBackgroundSchema.default({
9195
9220
  type: "color",
@@ -9388,11 +9413,6 @@ const bodyTextSchema = z$1.object({ intro: z$1.object({
9388
9413
  paragraphSize: bodyTextIntroTypographySizeSchema.optional(),
9389
9414
  headingSize: bodyTextIntroTypographySizeSchema.optional()
9390
9415
  }).optional() }).optional();
9391
- const gradientsSchema = z$1.object({
9392
- button: z$1.string().min(1).nullable().optional(),
9393
- hero: z$1.string().min(1).nullable().optional(),
9394
- background: z$1.string().min(1).nullable().optional()
9395
- });
9396
9416
  /**
9397
9417
  * Testimonial quote-mark treatment, owned by the site style (analogous to the
9398
9418
  * accordion icon). `style` is the coarse selector; the optional fields fine-tune
@@ -13359,12 +13379,12 @@ function allGroupChildPlans(plan) {
13359
13379
  }
13360
13380
  function selectedGroupChildPlans(plan, value) {
13361
13381
  if (plan.childSelection.kind === "all") return plan.childSelection.fields;
13362
- if (!isPlainObject$6(value)) return [];
13382
+ if (!isPlainObject$7(value)) return [];
13363
13383
  const selectedType = value[plan.childSelection.activeTabField];
13364
13384
  return plan.childSelection.tabs.find((tab) => tab.typeId === selectedType)?.fields ?? [];
13365
13385
  }
13366
13386
  function canonicalizeObjectForWrite(plans, value) {
13367
- if (!isPlainObject$6(value)) return value;
13387
+ if (!isPlainObject$7(value)) return value;
13368
13388
  const canonical = { ...value };
13369
13389
  for (const plan of plans) {
13370
13390
  if (!(plan.fieldId in canonical)) continue;
@@ -13388,7 +13408,7 @@ function canonicalizeFieldValue(plan, value) {
13388
13408
  }
13389
13409
  }
13390
13410
  function canonicalizeMedia(value) {
13391
- if (!isPlainObject$6(value)) return value;
13411
+ if (!isPlainObject$7(value)) return value;
13392
13412
  const canonical = { ...value };
13393
13413
  if (mediaIdentifierSchema.safeParse(canonical.identifier).success) deleteEnvSpecificKeys(canonical);
13394
13414
  return canonical;
@@ -13396,13 +13416,13 @@ function canonicalizeMedia(value) {
13396
13416
  function canonicalizeRepeater(plan, value) {
13397
13417
  if (!isUnknownArray$3(value)) return value;
13398
13418
  return value.map((item) => {
13399
- if (!isPlainObject$6(item)) return item;
13419
+ if (!isPlainObject$7(item)) return item;
13400
13420
  const childPlans = plan.repeatedItemVariants ? plan.repeatedItemVariants.find((variant) => variant.typeId === item._type)?.fields : plan.repeatedItemPlan;
13401
13421
  return childPlans ? canonicalizeObjectForWrite(childPlans, item) : item;
13402
13422
  });
13403
13423
  }
13404
13424
  function canonicalizeDiscriminatedGroup(plan, value) {
13405
- if (!isPlainObject$6(value) || plan.childSelection.kind !== "discriminatedTabs") return value;
13425
+ if (!isPlainObject$7(value) || plan.childSelection.kind !== "discriminatedTabs") return value;
13406
13426
  const selection = plan.childSelection;
13407
13427
  const selectedPlans = selectedGroupChildPlans(plan, value);
13408
13428
  const selectedIds = rootFieldIds(selectedPlans);
@@ -13417,10 +13437,10 @@ function rootFieldIds(plans) {
13417
13437
  }
13418
13438
  function omitUndefined(value) {
13419
13439
  if (isUnknownArray$3(value)) return value.map(omitUndefined);
13420
- if (!isPlainObject$6(value)) return value;
13440
+ if (!isPlainObject$7(value)) return value;
13421
13441
  return Object.fromEntries(Object.entries(value).filter(([, field]) => field !== void 0).map(([key, field]) => [key, omitUndefined(field)]));
13422
13442
  }
13423
- function isPlainObject$6(value) {
13443
+ function isPlainObject$7(value) {
13424
13444
  return typeof value === "object" && value !== null && !Array.isArray(value);
13425
13445
  }
13426
13446
  function isUnknownArray$3(value) {
@@ -13816,7 +13836,7 @@ function validateNormalizedFieldValue(plan, value, ctx) {
13816
13836
  case "select": return validateSelectPlan(plan, value);
13817
13837
  case "richText": return isValidRichTextDoc(value) ? [] : [issue(plan, "invalidRichText")];
13818
13838
  case "inlineText": return validateInlineTextPlan(plan, value);
13819
- case "media": return isPlainObject$5(value) && mediaIdentifierSchema.safeParse(value.identifier).success && (value.type === void 0 || typeof value.type === "string" && plan.allowedMediaTypes.includes(value.type)) ? [] : [issue(plan, "invalidMedia")];
13839
+ case "media": return isPlainObject$6(value) && mediaIdentifierSchema.safeParse(value.identifier).success && (value.type === void 0 || typeof value.type === "string" && plan.allowedMediaTypes.includes(value.type)) ? [] : [issue(plan, "invalidMedia")];
13820
13840
  case "link": return linkSchema.safeParse(value).success ? [] : [issue(plan, "invalidLink")];
13821
13841
  case "repeater": return validateRepeaterPlan(plan, value, ctx);
13822
13842
  case "group": return validateGroupPlan(plan, value, ctx);
@@ -13909,7 +13929,7 @@ function validateRepeaterPlan(plan, value, ctx) {
13909
13929
  }), ...value.flatMap((item, index) => validateRepeaterItem(plan, item, index, ctx))];
13910
13930
  }
13911
13931
  function validateRepeaterItem(plan, item, index, ctx) {
13912
- if (!isPlainObject$5(item)) return [{
13932
+ if (!isPlainObject$6(item)) return [{
13913
13933
  kind: "wrongType",
13914
13934
  path: [...plan.path, index],
13915
13935
  fieldId: plan.fieldId,
@@ -13935,7 +13955,7 @@ function validateRepeaterItem(plan, item, index, ctx) {
13935
13955
  });
13936
13956
  }
13937
13957
  function validateGroupPlan(plan, value, ctx) {
13938
- if (!isPlainObject$5(value)) return [issue(plan, "wrongType", { expected: "an object" })];
13958
+ if (!isPlainObject$6(value)) return [issue(plan, "wrongType", { expected: "an object" })];
13939
13959
  if (plan.childSelection.kind === "discriminatedTabs") {
13940
13960
  const selectedType = value[plan.childSelection.activeTabField];
13941
13961
  if (selectedType !== void 0 && selectedType !== null && !plan.childSelection.tabs.some((tab) => tab.typeId === selectedType)) return [{
@@ -14238,7 +14258,7 @@ function isEmptyValue$1(value) {
14238
14258
  if (isEmptyRichTextDoc(value)) return true;
14239
14259
  return false;
14240
14260
  }
14241
- function isPlainObject$5(value) {
14261
+ function isPlainObject$6(value) {
14242
14262
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
14243
14263
  }
14244
14264
  function isUnknownArray$2(value) {
@@ -14248,7 +14268,7 @@ function isValidRichTextDoc(value) {
14248
14268
  return isRichTextDoc(value);
14249
14269
  }
14250
14270
  function isEmptyRichTextDoc(value) {
14251
- if (!isPlainObject$5(value)) return false;
14271
+ if (!isPlainObject$6(value)) return false;
14252
14272
  if (value.type !== "doc") return false;
14253
14273
  const content = value.content;
14254
14274
  if (!Array.isArray(content) || content.length === 0) return true;
@@ -14300,26 +14320,6 @@ function compileManifestContentContract(manifestOrFields) {
14300
14320
  findNonPortableMediaPaths: (content) => findNonPortableMediaPaths(plans, content)
14301
14321
  }) });
14302
14322
  }
14303
- /** Strictly admits already-persisted content without repairing it. */
14304
- function admitStoredManifestContent(contract, content) {
14305
- const state = contract[compiledManifestContentContract];
14306
- const nonPortablePaths = state.findNonPortableMediaPaths(content);
14307
- if (nonPortablePaths.length > 0) return {
14308
- ok: false,
14309
- error: {
14310
- kind: "non_portable_media",
14311
- paths: nonPortablePaths
14312
- }
14313
- };
14314
- const parsed = state.strictStoredSchema.safeParse(content);
14315
- return parsed.success ? {
14316
- ok: true,
14317
- value: content
14318
- } : {
14319
- ok: false,
14320
- error: zodAdmissionError(parsed.error)
14321
- };
14322
- }
14323
14323
  /** Validates raw editor state without granting a stored-content proof. */
14324
14324
  function validateManifestDraft(contract, data) {
14325
14325
  const parsed = contract[compiledManifestContentContract].draftSchema.safeParse(data);
@@ -14337,21 +14337,6 @@ function validateManifestDraft(contract, data) {
14337
14337
  };
14338
14338
  }
14339
14339
  /**
14340
- * Applies a pure runtime projection to admitted stored content and validates
14341
- * the resulting render shape before granting the renderable proof.
14342
- */
14343
- function deriveRenderableManifestContent(contract, stored, project) {
14344
- const projected = project(stored);
14345
- const parsed = contract[compiledManifestContentContract].renderSchema.safeParse(projected);
14346
- return parsed.success ? {
14347
- ok: true,
14348
- value: projected
14349
- } : {
14350
- ok: false,
14351
- error: zodAdmissionError(parsed.error)
14352
- };
14353
- }
14354
- /**
14355
14340
  * Re-admits renderable content after a transport boundary has erased its
14356
14341
  * compile-time proof. Unlike write admission, this never canonicalizes or
14357
14342
  * grants a stored proof, so hydrated media cannot flow back to persistence.
@@ -14507,6 +14492,23 @@ function defineLookOptionGroup(group) {
14507
14492
  defaultOption(group);
14508
14493
  return group;
14509
14494
  }
14495
+ function resolveLookOption(group, requestedOptionId) {
14496
+ const fallback = defaultOption(group);
14497
+ if (requestedOptionId === null || requestedOptionId === void 0) return {
14498
+ kind: "defaulted",
14499
+ option: fallback
14500
+ };
14501
+ const selected = group.options.find((option) => option.id === requestedOptionId);
14502
+ return selected ? {
14503
+ kind: "selected",
14504
+ option: selected
14505
+ } : {
14506
+ kind: "repaired",
14507
+ requestedOptionId,
14508
+ option: fallback,
14509
+ reason: "unsupported-option"
14510
+ };
14511
+ }
14510
14512
  function defaultOption(group) {
14511
14513
  const option = group.options.find((candidate) => candidate.id === group.defaultOptionId);
14512
14514
  if (option) return option;
@@ -15397,6 +15399,12 @@ function cardRoleFromLegacyVocabulary(value) {
15397
15399
  */
15398
15400
  const cardRoleWithLegacyAliasSchema = z$1.preprocess((value) => cardRoleFromLegacyVocabulary(value) ?? value, cardRoleSchema);
15399
15401
  //#endregion
15402
+ //#region ../theme-core/src/site-styles/compilationPlan.ts
15403
+ const defaultHeaderBrandPreferences = {
15404
+ brandDisplay: "auto",
15405
+ logoSize: "auto"
15406
+ };
15407
+ //#endregion
15400
15408
  //#region ../theme-core/src/site-styles/sectionTransitions.ts
15401
15409
  function boundaryRuntimeMetrics(family, size, heightExpression) {
15402
15410
  return {
@@ -17778,6 +17786,12 @@ const overlayLogoAccommodations = [
17778
17786
  inlineMarkWithTitleLogoAccommodation,
17779
17787
  unbackedOverlayLogoAccommodation
17780
17788
  ];
17789
+ const allCuratedLogoAccommodations = [
17790
+ ...inlineClassicLogoAccommodations,
17791
+ ...centeredLogoAccommodations,
17792
+ ...overlayLogoAccommodations
17793
+ ];
17794
+ new Set(allCuratedLogoAccommodations.filter((option) => option.providesBacking === true).map((option) => option.pattern));
17781
17795
  const headerLayoutCatalog = defineExactCatalog()([
17782
17796
  {
17783
17797
  id: asHeaderLayoutId("classic"),
@@ -17927,9 +17941,50 @@ defineLookOptionGroup({
17927
17941
  {
17928
17942
  id: "secondary",
17929
17943
  label: "Secondary"
17944
+ },
17945
+ {
17946
+ id: headerInteractionQuietPalette,
17947
+ label: "Text only"
17930
17948
  }
17931
17949
  ]
17932
17950
  });
17951
+ /**
17952
+ * Key-sorted serialisation, because one side of the comparison is built here
17953
+ * and the other comes back out of `jsonb` in whatever order it was stored.
17954
+ * `JSON.stringify` alone reported two identical effect lists as different,
17955
+ * which is the same key-order trap the `logoText` derivation hit in Phase 4a.
17956
+ */
17957
+ function canonicalJson(value) {
17958
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
17959
+ if (typeof value === "object" && value !== null) return `{${Object.entries(value).filter(([, member]) => member !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, member]) => `${JSON.stringify(key)}:${canonicalJson(member)}`).join(",")}}`;
17960
+ return value === void 0 ? "undefined" : JSON.stringify(value);
17961
+ }
17962
+ //#endregion
17963
+ //#region ../theme-core/src/site-styles/headerNavStyles.ts
17964
+ function applyNavTypography(style, typography) {
17965
+ switch (typography) {
17966
+ case "plain": return style;
17967
+ case "heading": return {
17968
+ ...style,
17969
+ typography: "heading",
17970
+ fontWeight: "medium",
17971
+ letterSpacing: "wide"
17972
+ };
17973
+ case "quiet-caps": return {
17974
+ ...style,
17975
+ textTransform: "uppercase",
17976
+ textSize: "sm",
17977
+ letterSpacing: "wide"
17978
+ };
17979
+ case "brand-caps": return {
17980
+ ...style,
17981
+ textTransform: "uppercase",
17982
+ textSize: "sm",
17983
+ letterSpacing: "wider"
17984
+ };
17985
+ default: return assertNever$24(typography, "Unhandled header nav typography");
17986
+ }
17987
+ }
17933
17988
  function asFooterLookId(value) {
17934
17989
  return value;
17935
17990
  }
@@ -18638,6 +18693,37 @@ function uniformSectionForeground(token) {
18638
18693
  supportingColor: token
18639
18694
  };
18640
18695
  }
18696
+ function compileTransitionRuntime(input) {
18697
+ return {
18698
+ treatment: input.treatment,
18699
+ from: input.from,
18700
+ to: input.to,
18701
+ cssVars: {
18702
+ from: transitionEndpointCssValue(input.from),
18703
+ to: transitionEndpointCssValue(input.to)
18704
+ }
18705
+ };
18706
+ }
18707
+ function transitionEndpointCssValue(endpoint) {
18708
+ switch (endpoint.kind) {
18709
+ case "color-token": return themeColorTokenCssValue(endpoint.token);
18710
+ case "surface": return themeColorTokenCssValue(tokenForTransitionSurface(endpoint.surface));
18711
+ default: return assertNever$24(endpoint);
18712
+ }
18713
+ }
18714
+ function tokenForTransitionSurface(surface) {
18715
+ switch (surface) {
18716
+ case "base":
18717
+ case "hero-image": return "background";
18718
+ case "muted-band": return "muted";
18719
+ case "accent-band": return "accent";
18720
+ case "brand-depth-band": return "primary-800";
18721
+ default: return assertNever$24(surface);
18722
+ }
18723
+ }
18724
+ function themeColorTokenCssValue(token) {
18725
+ return `rgb(var(--tb-${token}))`;
18726
+ }
18641
18727
  //#endregion
18642
18728
  //#region ../theme-core/src/site-styles/curatedSiteStyles.ts
18643
18729
  const SOFT_EARTH_SITE_STYLE_ID_VALUE = "site-style:soft-earth";
@@ -20345,175 +20431,6 @@ const blockDesignFallback = {
20345
20431
  };
20346
20432
  blockDesignFallback.sectionSurface, blockDesignFallback.contentFrame, blockDesignFallback.transitionAfter, blockDesignFallback.sectionHeight, blockDesignFallback.heroLegibility, blockDesignFallback.imageSurfaceTreatment, blockDesignFallback.imageSurfaceMotion, blockDesignFallback.tileTreatment, blockDesignFallback.eventListTreatment, blockDesignFallback.eyebrowTreatment, blockDesignFallback.accordionIconTreatment, blockDesignFallback.composition;
20347
20433
  //#endregion
20348
- //#region ../theme-core/src/site-styles/generatedSchemas.ts
20349
- function uniqueNonEmpty(values) {
20350
- const uniqueValues = [...new Set(values)];
20351
- const firstValue = uniqueValues[0];
20352
- if (!firstValue) throw new Error("Expected at least one generated schema option.");
20353
- return [firstValue, ...uniqueValues.slice(1)];
20354
- }
20355
- const generatedPanelTreatments = uniqueNonEmpty(generatedDesignOptionCatalog.contentFrames.filter((frame) => frame.kind === "panel").map((frame) => frame.treatment));
20356
- const generatedFlushTreatments = uniqueNonEmpty(generatedDesignOptionCatalog.contentFrames.filter((frame) => frame.kind === "flush-panel").map((frame) => frame.treatment));
20357
- const generatedFlushBleeds = uniqueNonEmpty(generatedDesignOptionCatalog.contentFrames.filter((frame) => frame.kind === "flush-panel").map((frame) => frame.bleed));
20358
- const generatedPanelFrameSchema = z$1.object({
20359
- kind: z$1.literal("panel"),
20360
- treatment: z$1.enum(generatedPanelTreatments)
20361
- }).strict();
20362
- const generatedFlushPanelFrameSchema = z$1.object({
20363
- kind: z$1.literal("flush-panel"),
20364
- treatment: z$1.enum(generatedFlushTreatments),
20365
- bleed: z$1.enum(generatedFlushBleeds)
20366
- }).strict();
20367
- const generatedContentFrameIntentSchema = z$1.discriminatedUnion("kind", [
20368
- z$1.object({ kind: z$1.literal("plain") }).strict(),
20369
- generatedPanelFrameSchema,
20370
- generatedFlushPanelFrameSchema
20371
- ]);
20372
- const generatedSiteStyleIdSchema = z$1.enum(curatedSiteStyleIdValues).transform(asSiteStyleId);
20373
- function generatedChoiceSchema(valueSchema) {
20374
- return z$1.discriminatedUnion("kind", [z$1.object({
20375
- source: z$1.literal("generated"),
20376
- kind: z$1.literal("auto")
20377
- }).strict(), z$1.object({
20378
- source: z$1.literal("generated"),
20379
- kind: z$1.literal("explicit"),
20380
- value: valueSchema
20381
- }).strict()]);
20382
- }
20383
- const generatedBlockDesignIntentSchema = z$1.object({
20384
- sectionSurface: generatedChoiceSchema(z$1.enum(generatedDesignOptionCatalog.sectionSurfaces)),
20385
- contentFrame: generatedChoiceSchema(generatedContentFrameIntentSchema),
20386
- cardRole: generatedChoiceSchema(cardRoleWithLegacyAliasSchema),
20387
- transitionAfter: generatedChoiceSchema(z$1.enum(generatedDesignOptionCatalog.transitions)),
20388
- emphasis: generatedChoiceSchema(z$1.enum(generatedDesignOptionCatalog.emphases))
20389
- }).strict();
20390
- const generatedMediaCompositionIntentSchema = z$1.enum([
20391
- "none",
20392
- "supporting",
20393
- "dominant"
20394
- ]);
20395
- const generatedDecorativeCompositionIntentSchema = z$1.enum(["none", "supporting"]);
20396
- const generatedHeroLegibilityStrategySchema = z$1.enum(["none", "scrim-gradient"]);
20397
- const generatedBlockMediaIntentSchema = z$1.object({
20398
- media: generatedMediaCompositionIntentSchema,
20399
- decorative: generatedDecorativeCompositionIntentSchema,
20400
- heroLegibility: generatedChoiceSchema(generatedHeroLegibilityStrategySchema)
20401
- }).strict();
20402
- const generatedBoundaryIntentSchema = z$1.object({ transitionAfter: generatedChoiceSchema(z$1.enum(generatedDesignOptionCatalog.transitions)) }).strict();
20403
- const generatedPageDesignBlockIntentSchema = z$1.object({
20404
- blockId: z$1.string().min(1),
20405
- blockKind: z$1.string().min(1),
20406
- purpose: z$1.string().min(1),
20407
- design: generatedBlockDesignIntentSchema,
20408
- mediaIntent: generatedBlockMediaIntentSchema,
20409
- boundaryIntent: generatedBoundaryIntentSchema
20410
- }).strict();
20411
- z$1.object({
20412
- siteStyleId: generatedSiteStyleIdSchema,
20413
- blocks: z$1.array(generatedPageDesignBlockIntentSchema)
20414
- }).strict();
20415
- //#endregion
20416
- //#region ../theme-core/src/site-styles/pageDesignAuthority.ts
20417
- const nonEmptyStringSchema = z$1.string().min(1);
20418
- const blockContentVersionIdSchema = nonEmptyStringSchema.transform(asBlockContentVersionId);
20419
- const designBlockIdSchema = nonEmptyStringSchema.transform(asDesignBlockId);
20420
- const designPageIdSchema = nonEmptyStringSchema.transform(asDesignPageId);
20421
- const designEntryIdSchema = nonEmptyStringSchema.transform(asDesignEntryId);
20422
- const blockOrderIndexSchema = z$1.number().int().nonnegative().transform(asBlockOrderIndex);
20423
- const publishedPageBlockSnapshotSchema = z$1.object({
20424
- kind: z$1.literal("published-block-content"),
20425
- blockId: designBlockIdSchema,
20426
- contentVersionId: blockContentVersionIdSchema,
20427
- identifier: z$1.string().nullable(),
20428
- blockKind: nonEmptyStringSchema,
20429
- orderIndex: blockOrderIndexSchema
20430
- }).strict();
20431
- const draftPageBlockSnapshotSchema = z$1.object({
20432
- kind: z$1.literal("draft-block-content"),
20433
- blockId: designBlockIdSchema,
20434
- contentVersionId: blockContentVersionIdSchema.nullable(),
20435
- identifier: z$1.string().nullable(),
20436
- blockKind: nonEmptyStringSchema,
20437
- orderIndex: blockOrderIndexSchema
20438
- }).strict();
20439
- const missingPageBlockSnapshotSchema = z$1.object({
20440
- kind: z$1.literal("missing-block-content"),
20441
- blockId: designBlockIdSchema,
20442
- identifier: z$1.string().nullable(),
20443
- blockKind: nonEmptyStringSchema,
20444
- orderIndex: blockOrderIndexSchema,
20445
- reason: z$1.enum([
20446
- "new-block",
20447
- "deleted-content-version",
20448
- "unpublished-block"
20449
- ])
20450
- }).strict();
20451
- const pageBlockSnapshotSchema = z$1.discriminatedUnion("kind", [
20452
- publishedPageBlockSnapshotSchema,
20453
- draftPageBlockSnapshotSchema,
20454
- missingPageBlockSnapshotSchema
20455
- ]);
20456
- const publishedPageBlockSequenceSchema = z$1.array(publishedPageBlockSnapshotSchema).superRefine(validateBlockSequence);
20457
- const draftPageBlockSequenceSchema = z$1.array(pageBlockSnapshotSchema).superRefine(validateBlockSequence);
20458
- const publishedPageDesignVersionDataV1Schema = z$1.object({
20459
- schemaVersion: z$1.literal(1),
20460
- stage: z$1.literal("published"),
20461
- pageId: designPageIdSchema,
20462
- blockSequence: publishedPageBlockSequenceSchema
20463
- }).strict();
20464
- const draftPageDesignVersionDataV1Schema = z$1.object({
20465
- schemaVersion: z$1.literal(1),
20466
- stage: z$1.literal("draft"),
20467
- pageId: designPageIdSchema,
20468
- blockSequence: draftPageBlockSequenceSchema
20469
- }).strict();
20470
- z$1.discriminatedUnion("stage", [publishedPageDesignVersionDataV1Schema, draftPageDesignVersionDataV1Schema]);
20471
- z$1.discriminatedUnion("kind", [
20472
- z$1.object({
20473
- kind: z$1.literal("block"),
20474
- blockId: designBlockIdSchema
20475
- }).strict(),
20476
- z$1.object({
20477
- kind: z$1.literal("entry"),
20478
- entryId: designEntryIdSchema
20479
- }).strict(),
20480
- z$1.object({
20481
- kind: z$1.literal("page"),
20482
- pageId: designPageIdSchema
20483
- }).strict()
20484
- ]);
20485
- function validateBlockSequence(snapshots, ctx) {
20486
- for (const issue of collectBlockSequenceIssues(snapshots)) ctx.addIssue(issue);
20487
- }
20488
- function collectBlockSequenceIssues(snapshots) {
20489
- const issues = [];
20490
- const seenBlockIds = /* @__PURE__ */ new Set();
20491
- const seenOrderIndexes = /* @__PURE__ */ new Set();
20492
- let previousOrderIndex = null;
20493
- snapshots.forEach((snapshot, index) => {
20494
- if (seenBlockIds.has(snapshot.blockId)) issues.push({
20495
- code: z$1.ZodIssueCode.custom,
20496
- message: `Duplicate blockId in page design blockSequence: ${snapshot.blockId}`,
20497
- path: [index, "blockId"]
20498
- });
20499
- seenBlockIds.add(snapshot.blockId);
20500
- const orderIndex = snapshot.orderIndex;
20501
- if (seenOrderIndexes.has(orderIndex)) issues.push({
20502
- code: z$1.ZodIssueCode.custom,
20503
- message: `Duplicate orderIndex in page design blockSequence: ${String(orderIndex)}`,
20504
- path: [index, "orderIndex"]
20505
- });
20506
- seenOrderIndexes.add(orderIndex);
20507
- if (previousOrderIndex !== null && orderIndex <= previousOrderIndex) issues.push({
20508
- code: z$1.ZodIssueCode.custom,
20509
- message: `Expected orderIndex greater than ${String(previousOrderIndex)} for blockSequence position ${String(index)}; received ${String(orderIndex)}.`,
20510
- path: [index, "orderIndex"]
20511
- });
20512
- previousOrderIndex = orderIndex;
20513
- });
20514
- return issues;
20515
- }
20516
- //#endregion
20517
20434
  //#region ../theme-core/src/site-styles/siteChromeBackgrounds.ts
20518
20435
  const siteChromeBackgroundColorTokens = toSiteChromeBackgroundColorTokens(backgroundAppearanceColorTokenNames);
20519
20436
  const quickSiteChromeBackgroundColorTokens = toSiteChromeBackgroundColorTokens(backgroundAppearanceQuickColorTokenNames);
@@ -20528,6 +20445,25 @@ function toSiteChromeBackgroundColorTokens(tokenNames) {
20528
20445
  return token ? [token] : [];
20529
20446
  });
20530
20447
  }
20448
+ //#endregion
20449
+ //#region ../theme-core/src/site-styles/siteChromeNavPreferences.ts
20450
+ const headerNavTypographyPreferenceValues = [
20451
+ "inherit",
20452
+ "body",
20453
+ "heading"
20454
+ ];
20455
+ const headerNavFontSizePreferenceValues = [
20456
+ "inherit",
20457
+ "sm",
20458
+ "base",
20459
+ "lg",
20460
+ "xl"
20461
+ ];
20462
+ const defaultHeaderNavPreferences = {
20463
+ typography: "inherit",
20464
+ fontSize: "inherit",
20465
+ interactionPalette: "recommended"
20466
+ };
20531
20467
  /**
20532
20468
  * Applies the user's nav font-role/size overrides on top of a style-resolved
20533
20469
  * nav link style. This is where the precedence contract lands:
@@ -20554,6 +20490,322 @@ function readHeaderNavFontSizePreference(value) {
20554
20490
  return value === "sm" || value === "base" || value === "lg" || value === "xl" ? value : "inherit";
20555
20491
  }
20556
20492
  //#endregion
20493
+ //#region ../theme-core/src/site-styles/headerBrandPresentation.ts
20494
+ /**
20495
+ * Whether the header wears a brand presentation at all (#1318 Phase 4c).
20496
+ *
20497
+ * `applyHeaderBrandPresentation` is unconditional today: given a logo context
20498
+ * it always writes a `logo` object carrying a resolved display, size and
20499
+ * accommodation. That makes "this header has no brand presentation block" —
20500
+ * which is what several live sites persist, including one whose site *does*
20501
+ * have a logo asset — a state the preference record could not express, so a
20502
+ * recompile would silently give the site a logo treatment it has never
20503
+ * rendered.
20504
+ *
20505
+ * This is the third field in this phase with that shape, after the interaction
20506
+ * palette and the nav text size: the persisted bytes carry intent the reader
20507
+ * cannot say, so the recompile "corrects" it. The fix is the same one — widen
20508
+ * the closed vocabulary with a named member and derive it by baseline
20509
+ * comparison — and it is a *widening*, not a restyle: `'recommended'` is the
20510
+ * default and reproduces today's behaviour exactly, so no row whose bytes carry
20511
+ * a presentation compiles differently for `'plain'` existing.
20512
+ *
20513
+ * `'plain'` is deliberately not "preserve whatever bytes are there". It is a
20514
+ * constructive instruction — *write no presentation block* — so Phase 4e's
20515
+ * compiler, which builds the header from nothing, can produce it.
20516
+ */
20517
+ const headerBrandPresentationValues = ["recommended", "plain"];
20518
+ /**
20519
+ * Key-order-safe structural equality over the persisted `logo` object.
20520
+ *
20521
+ * The persisted value comes back from `jsonb`, whose key order is not the order
20522
+ * the applier builds; a `JSON.stringify` comparison would read two equal logo
20523
+ * blocks as different and quietly classify a `'recommended'` row as
20524
+ * unrecognised — the same trap the interaction-palette transcription test
20525
+ * exists to catch.
20526
+ */
20527
+ function jsonEquals(left, right) {
20528
+ if (left === right) return true;
20529
+ if (left === null || right === null) return false;
20530
+ if (typeof left !== "object" || typeof right !== "object") return false;
20531
+ if (Array.isArray(left) !== Array.isArray(right)) return false;
20532
+ if (Array.isArray(left) && Array.isArray(right)) return left.length === right.length && left.every((value, index) => jsonEquals(value, right[index]));
20533
+ const leftRecord = left;
20534
+ const rightRecord = right;
20535
+ const keys = /* @__PURE__ */ new Set([...Object.keys(leftRecord), ...Object.keys(rightRecord)]);
20536
+ for (const key of keys) if (!jsonEquals(leftRecord[key], rightRecord[key])) return false;
20537
+ return true;
20538
+ }
20539
+ defaultHeaderBrandPreferences.brandDisplay, defaultHeaderBrandPreferences.logoSize, defaultHeaderNavPreferences.typography, defaultHeaderNavPreferences.fontSize, defaultHeaderNavPreferences.interactionPalette;
20540
+ function retargetCompiledBoundaryRuntimeForContentAdjacency(boundary, adjacency) {
20541
+ const fromSurface = contentOwnedBackgroundEndpoint(adjacency.currentContent) ?? boundary.from;
20542
+ const toSurface = contentOwnedBackgroundEndpoint(adjacency.nextContent) ?? boundary.to;
20543
+ if (fromSurface === boundary.from && toSurface === boundary.to) return boundary;
20544
+ const transition = compileTransitionRuntime({
20545
+ treatment: boundary.treatment,
20546
+ from: fromSurface,
20547
+ to: toSurface
20548
+ });
20549
+ return {
20550
+ ...boundary,
20551
+ from: transition.from,
20552
+ to: transition.to,
20553
+ cssVars: transition.cssVars
20554
+ };
20555
+ }
20556
+ function contentOwnedBackgroundEndpoint(content) {
20557
+ const background = readRecord(readRecord(content?._sectionStyles)?.background);
20558
+ if (background?.contentOwner !== "page-design-editor" || background.type !== "color") return null;
20559
+ const token = parseThemeColorToken(background.color);
20560
+ if (!token) return null;
20561
+ return {
20562
+ kind: "color-token",
20563
+ token
20564
+ };
20565
+ }
20566
+ function readRecord(value) {
20567
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
20568
+ }
20569
+ //#endregion
20570
+ //#region ../theme-core/src/site-styles/generatedSchemas.ts
20571
+ function uniqueNonEmpty(values) {
20572
+ const uniqueValues = [...new Set(values)];
20573
+ const firstValue = uniqueValues[0];
20574
+ if (!firstValue) throw new Error("Expected at least one generated schema option.");
20575
+ return [firstValue, ...uniqueValues.slice(1)];
20576
+ }
20577
+ const generatedPanelTreatments = uniqueNonEmpty(generatedDesignOptionCatalog.contentFrames.filter((frame) => frame.kind === "panel").map((frame) => frame.treatment));
20578
+ const generatedFlushTreatments = uniqueNonEmpty(generatedDesignOptionCatalog.contentFrames.filter((frame) => frame.kind === "flush-panel").map((frame) => frame.treatment));
20579
+ const generatedFlushBleeds = uniqueNonEmpty(generatedDesignOptionCatalog.contentFrames.filter((frame) => frame.kind === "flush-panel").map((frame) => frame.bleed));
20580
+ const generatedPanelFrameSchema = z$1.object({
20581
+ kind: z$1.literal("panel"),
20582
+ treatment: z$1.enum(generatedPanelTreatments)
20583
+ }).strict();
20584
+ const generatedFlushPanelFrameSchema = z$1.object({
20585
+ kind: z$1.literal("flush-panel"),
20586
+ treatment: z$1.enum(generatedFlushTreatments),
20587
+ bleed: z$1.enum(generatedFlushBleeds)
20588
+ }).strict();
20589
+ const generatedContentFrameIntentSchema = z$1.discriminatedUnion("kind", [
20590
+ z$1.object({ kind: z$1.literal("plain") }).strict(),
20591
+ generatedPanelFrameSchema,
20592
+ generatedFlushPanelFrameSchema
20593
+ ]);
20594
+ const generatedSiteStyleIdSchema = z$1.enum(curatedSiteStyleIdValues).transform(asSiteStyleId);
20595
+ function generatedChoiceSchema(valueSchema) {
20596
+ return z$1.discriminatedUnion("kind", [z$1.object({
20597
+ source: z$1.literal("generated"),
20598
+ kind: z$1.literal("auto")
20599
+ }).strict(), z$1.object({
20600
+ source: z$1.literal("generated"),
20601
+ kind: z$1.literal("explicit"),
20602
+ value: valueSchema
20603
+ }).strict()]);
20604
+ }
20605
+ const generatedBlockDesignIntentSchema = z$1.object({
20606
+ sectionSurface: generatedChoiceSchema(z$1.enum(generatedDesignOptionCatalog.sectionSurfaces)),
20607
+ contentFrame: generatedChoiceSchema(generatedContentFrameIntentSchema),
20608
+ cardRole: generatedChoiceSchema(cardRoleWithLegacyAliasSchema),
20609
+ transitionAfter: generatedChoiceSchema(z$1.enum(generatedDesignOptionCatalog.transitions)),
20610
+ emphasis: generatedChoiceSchema(z$1.enum(generatedDesignOptionCatalog.emphases))
20611
+ }).strict();
20612
+ const generatedMediaCompositionIntentSchema = z$1.enum([
20613
+ "none",
20614
+ "supporting",
20615
+ "dominant"
20616
+ ]);
20617
+ const generatedDecorativeCompositionIntentSchema = z$1.enum(["none", "supporting"]);
20618
+ const generatedHeroLegibilityStrategySchema = z$1.enum(["none", "scrim-gradient"]);
20619
+ const generatedBlockMediaIntentSchema = z$1.object({
20620
+ media: generatedMediaCompositionIntentSchema,
20621
+ decorative: generatedDecorativeCompositionIntentSchema,
20622
+ heroLegibility: generatedChoiceSchema(generatedHeroLegibilityStrategySchema)
20623
+ }).strict();
20624
+ const generatedBoundaryIntentSchema = z$1.object({ transitionAfter: generatedChoiceSchema(z$1.enum(generatedDesignOptionCatalog.transitions)) }).strict();
20625
+ const generatedPageDesignBlockIntentSchema = z$1.object({
20626
+ blockId: z$1.string().min(1),
20627
+ blockKind: z$1.string().min(1),
20628
+ purpose: z$1.string().min(1),
20629
+ design: generatedBlockDesignIntentSchema,
20630
+ mediaIntent: generatedBlockMediaIntentSchema,
20631
+ boundaryIntent: generatedBoundaryIntentSchema
20632
+ }).strict();
20633
+ z$1.object({
20634
+ siteStyleId: generatedSiteStyleIdSchema,
20635
+ blocks: z$1.array(generatedPageDesignBlockIntentSchema)
20636
+ }).strict();
20637
+ //#endregion
20638
+ //#region ../theme-core/src/site-styles/headerPreferencesPersistence.ts
20639
+ const backgroundColorTokenSchema = z$1.unknown().transform((value, ctx) => {
20640
+ const token = parseSiteChromeBackgroundColorToken(value);
20641
+ if (token === null) {
20642
+ ctx.addIssue({
20643
+ code: "custom",
20644
+ message: "Unknown site chrome background colour token"
20645
+ });
20646
+ return z$1.NEVER;
20647
+ }
20648
+ return token;
20649
+ });
20650
+ const backgroundOverrideSchema = z$1.discriminatedUnion("kind", [z$1.object({ kind: z$1.literal("default") }).strict(), z$1.object({
20651
+ kind: z$1.literal("color"),
20652
+ token: backgroundColorTokenSchema
20653
+ }).strict()]);
20654
+ const logoTextIntentSchema = z$1.object({
20655
+ mobileMaxWidth: z$1.enum([
20656
+ "xs",
20657
+ "sm",
20658
+ "md"
20659
+ ]).optional(),
20660
+ mobileWrapLines: z$1.union([z$1.literal(1), z$1.literal(2)]).optional(),
20661
+ hideOnShrink: z$1.boolean().optional()
20662
+ }).strict();
20663
+ const structureSchema = z$1.object({
20664
+ positioning: z$1.union([z$1.literal("inherit"), z$1.enum(headerPositioningValues)]),
20665
+ shrinkOnScroll: z$1.union([z$1.literal("inherit"), z$1.boolean()]),
20666
+ logoText: z$1.union([
20667
+ z$1.literal("inherit"),
20668
+ z$1.literal("none"),
20669
+ logoTextIntentSchema
20670
+ ])
20671
+ }).strict();
20672
+ const headerPreferencesShape = {
20673
+ brandPresentation: z$1.enum(headerBrandPresentationValues),
20674
+ brandDisplay: z$1.enum(brandDisplayModeValues),
20675
+ logoSize: z$1.enum(logoSizePolicyValues),
20676
+ navFontRole: z$1.enum(headerNavTypographyPreferenceValues),
20677
+ navTextSize: z$1.enum(headerNavFontSizePreferenceValues),
20678
+ interactionPalette: z$1.union([z$1.literal("recommended"), z$1.enum(headerInteractionPaletteValues)]),
20679
+ background: backgroundOverrideSchema,
20680
+ structure: structureSchema
20681
+ };
20682
+ z$1.object(headerPreferencesShape).strict();
20683
+ z$1.object(headerPreferencesShape).omit({
20684
+ brandPresentation: true,
20685
+ structure: true
20686
+ }).strict();
20687
+ //#endregion
20688
+ //#region ../theme-core/src/site-styles/headerBytesComparison.ts
20689
+ function isPlainObject$5(value) {
20690
+ return typeof value === "object" && value !== null && !Array.isArray(value);
20691
+ }
20692
+ /**
20693
+ * Structural diff by JSON path. Values are compared through
20694
+ * `JSON.stringify`, so a key present with the value `undefined` and an absent
20695
+ * key read as the same thing — which is what `jsonb` does to them anyway
20696
+ * (#1318 Correction 5).
20697
+ */
20698
+ function diffJsonPaths(left, right, path = "") {
20699
+ if (isPlainObject$5(left) && isPlainObject$5(right)) return [.../* @__PURE__ */ new Set([...Object.keys(left), ...Object.keys(right)])].sort().flatMap((key) => diffJsonPaths(left[key], right[key], path ? `${path}.${key}` : key));
20700
+ if (JSON.stringify(left) === JSON.stringify(right)) return [];
20701
+ return [{
20702
+ path,
20703
+ left,
20704
+ right
20705
+ }];
20706
+ }
20707
+ //#endregion
20708
+ //#region ../theme-core/src/site-styles/pageDesignAuthority.ts
20709
+ const nonEmptyStringSchema = z$1.string().min(1);
20710
+ const blockContentVersionIdSchema = nonEmptyStringSchema.transform(asBlockContentVersionId);
20711
+ const designBlockIdSchema = nonEmptyStringSchema.transform(asDesignBlockId);
20712
+ const designPageIdSchema = nonEmptyStringSchema.transform(asDesignPageId);
20713
+ const designEntryIdSchema = nonEmptyStringSchema.transform(asDesignEntryId);
20714
+ const blockOrderIndexSchema = z$1.number().int().nonnegative().transform(asBlockOrderIndex);
20715
+ const publishedPageBlockSnapshotSchema = z$1.object({
20716
+ kind: z$1.literal("published-block-content"),
20717
+ blockId: designBlockIdSchema,
20718
+ contentVersionId: blockContentVersionIdSchema,
20719
+ identifier: z$1.string().nullable(),
20720
+ blockKind: nonEmptyStringSchema,
20721
+ orderIndex: blockOrderIndexSchema
20722
+ }).strict();
20723
+ const draftPageBlockSnapshotSchema = z$1.object({
20724
+ kind: z$1.literal("draft-block-content"),
20725
+ blockId: designBlockIdSchema,
20726
+ contentVersionId: blockContentVersionIdSchema.nullable(),
20727
+ identifier: z$1.string().nullable(),
20728
+ blockKind: nonEmptyStringSchema,
20729
+ orderIndex: blockOrderIndexSchema
20730
+ }).strict();
20731
+ const missingPageBlockSnapshotSchema = z$1.object({
20732
+ kind: z$1.literal("missing-block-content"),
20733
+ blockId: designBlockIdSchema,
20734
+ identifier: z$1.string().nullable(),
20735
+ blockKind: nonEmptyStringSchema,
20736
+ orderIndex: blockOrderIndexSchema,
20737
+ reason: z$1.enum([
20738
+ "new-block",
20739
+ "deleted-content-version",
20740
+ "unpublished-block"
20741
+ ])
20742
+ }).strict();
20743
+ const pageBlockSnapshotSchema = z$1.discriminatedUnion("kind", [
20744
+ publishedPageBlockSnapshotSchema,
20745
+ draftPageBlockSnapshotSchema,
20746
+ missingPageBlockSnapshotSchema
20747
+ ]);
20748
+ const publishedPageBlockSequenceSchema = z$1.array(publishedPageBlockSnapshotSchema).superRefine(validateBlockSequence);
20749
+ const draftPageBlockSequenceSchema = z$1.array(pageBlockSnapshotSchema).superRefine(validateBlockSequence);
20750
+ const publishedPageDesignVersionDataV1Schema = z$1.object({
20751
+ schemaVersion: z$1.literal(1),
20752
+ stage: z$1.literal("published"),
20753
+ pageId: designPageIdSchema,
20754
+ blockSequence: publishedPageBlockSequenceSchema
20755
+ }).strict();
20756
+ const draftPageDesignVersionDataV1Schema = z$1.object({
20757
+ schemaVersion: z$1.literal(1),
20758
+ stage: z$1.literal("draft"),
20759
+ pageId: designPageIdSchema,
20760
+ blockSequence: draftPageBlockSequenceSchema
20761
+ }).strict();
20762
+ z$1.discriminatedUnion("stage", [publishedPageDesignVersionDataV1Schema, draftPageDesignVersionDataV1Schema]);
20763
+ z$1.discriminatedUnion("kind", [
20764
+ z$1.object({
20765
+ kind: z$1.literal("block"),
20766
+ blockId: designBlockIdSchema
20767
+ }).strict(),
20768
+ z$1.object({
20769
+ kind: z$1.literal("entry"),
20770
+ entryId: designEntryIdSchema
20771
+ }).strict(),
20772
+ z$1.object({
20773
+ kind: z$1.literal("page"),
20774
+ pageId: designPageIdSchema
20775
+ }).strict()
20776
+ ]);
20777
+ function validateBlockSequence(snapshots, ctx) {
20778
+ for (const issue of collectBlockSequenceIssues(snapshots)) ctx.addIssue(issue);
20779
+ }
20780
+ function collectBlockSequenceIssues(snapshots) {
20781
+ const issues = [];
20782
+ const seenBlockIds = /* @__PURE__ */ new Set();
20783
+ const seenOrderIndexes = /* @__PURE__ */ new Set();
20784
+ let previousOrderIndex = null;
20785
+ snapshots.forEach((snapshot, index) => {
20786
+ if (seenBlockIds.has(snapshot.blockId)) issues.push({
20787
+ code: z$1.ZodIssueCode.custom,
20788
+ message: `Duplicate blockId in page design blockSequence: ${snapshot.blockId}`,
20789
+ path: [index, "blockId"]
20790
+ });
20791
+ seenBlockIds.add(snapshot.blockId);
20792
+ const orderIndex = snapshot.orderIndex;
20793
+ if (seenOrderIndexes.has(orderIndex)) issues.push({
20794
+ code: z$1.ZodIssueCode.custom,
20795
+ message: `Duplicate orderIndex in page design blockSequence: ${String(orderIndex)}`,
20796
+ path: [index, "orderIndex"]
20797
+ });
20798
+ seenOrderIndexes.add(orderIndex);
20799
+ if (previousOrderIndex !== null && orderIndex <= previousOrderIndex) issues.push({
20800
+ code: z$1.ZodIssueCode.custom,
20801
+ message: `Expected orderIndex greater than ${String(previousOrderIndex)} for blockSequence position ${String(index)}; received ${String(orderIndex)}.`,
20802
+ path: [index, "orderIndex"]
20803
+ });
20804
+ previousOrderIndex = orderIndex;
20805
+ });
20806
+ return issues;
20807
+ }
20808
+ //#endregion
20557
20809
  //#region ../theme-core/src/site-styles/tokenRecipes.ts
20558
20810
  const themeTokenRecipeOptions = {
20559
20811
  palette: [
@@ -32377,7 +32629,7 @@ const eventDisplayFields = [
32377
32629
  {
32378
32630
  id: "showCapacity",
32379
32631
  type: "boolean",
32380
- label: "Show available spots",
32632
+ label: "Show available places",
32381
32633
  description: "Display remaining capacity",
32382
32634
  required: false,
32383
32635
  defaultValue: true,
@@ -32702,7 +32954,272 @@ function planEventRegistrationScope(input) {
32702
32954
  };
32703
32955
  }
32704
32956
  //#endregion
32705
- //#region ../blocks/src/system/runtime/nodes/events/shared/destination.ts
32957
+ //#region ../blocks/src/system/runtime/nodes/events/domain/capacity.ts
32958
+ /**
32959
+ * Compute remaining spots for an event from `availableSpots` with a fallback
32960
+ * to `capacity - registeredCount`. Returns `{ available: null, isFull: false }`
32961
+ * for uncapped events so call sites can render no capacity indicator at all.
32962
+ *
32963
+ * The single source of truth for capacity math across event cards, agenda
32964
+ * rows, and timetable cells — keep wording variations in the call sites.
32965
+ */
32966
+ function getEventAvailability(event) {
32967
+ if (event.capacity == null) return {
32968
+ available: null,
32969
+ isFull: false
32970
+ };
32971
+ const available = event.availableSpots ?? event.capacity - event.registeredCount;
32972
+ if (available <= 0) return {
32973
+ available: 0,
32974
+ isFull: true
32975
+ };
32976
+ return {
32977
+ available,
32978
+ isFull: false
32979
+ };
32980
+ }
32981
+ /**
32982
+ * Pick the capacity row a booking decision should read: the selected
32983
+ * attendance mode's availability when the visitor has chosen one and the
32984
+ * event publishes it, otherwise the event-level aggregate.
32985
+ *
32986
+ * The mode row is used wholesale. A mode with `capacity: null` on a capped
32987
+ * event is uncapped *by design*: the registration write path applies the
32988
+ * same rule via `getEffectiveOccurrenceCapacityForAttendanceMode`, so falling
32989
+ * back to the event's `availableSpots` here would block bookings the server
32990
+ * accepts. (Waitlist claims are still occurrence-scoped; see issue 1377.)
32991
+ */
32992
+ function selectEventCapacitySource(event, attendanceMode) {
32993
+ if (!attendanceMode) return event;
32994
+ return (event.attendanceModes ?? []).find((mode) => mode.mode === attendanceMode) ?? event;
32995
+ }
32996
+ /**
32997
+ * Whether the visitor's next step is the waitlist: the source is full *and*
32998
+ * a waitlist is open. The one place this is decided — cards, rows, the
32999
+ * wizard's steps and its action bar all route through here so they agree,
33000
+ * including for producers that omit `availableSpots` and rely on the
33001
+ * `capacity - registeredCount` fallback.
33002
+ */
33003
+ function canJoinEventWaitlist(event, options) {
33004
+ const source = selectEventCapacitySource(event, options?.attendanceMode);
33005
+ return getEventAvailability(source).isFull && source.waitlistEnabled === true;
33006
+ }
33007
+ /**
33008
+ * Remaining places at or below which an event reads as scarce and the exact
33009
+ * count is worth showing. Above it, no number is shown at all: "147 places
33010
+ * left" is not something a visitor acts on, and it is the figure most likely
33011
+ * to be stale on a cached page.
33012
+ *
33013
+ * max( min(3, ceil(capacity * 0.25)), ceil(capacity * 0.05) )
33014
+ *
33015
+ * The 25% arm stops small groups shouting early (3 of 4 is not urgency); the
33016
+ * 5% floor stops large events staying silent until three remain.
33017
+ * capacity 4 -> 1, 12 -> 3, 40 -> 3, 100 -> 5, 200 -> 10.
33018
+ */
33019
+ function lowCapacityThreshold(capacity) {
33020
+ return Math.max(Math.min(3, Math.ceil(capacity * .25)), Math.ceil(capacity * .05));
33021
+ }
33022
+ function getRemainingPlaces(source) {
33023
+ const { available, isFull } = getEventAvailability(source);
33024
+ if (available === null || source.capacity === null) return { kind: "unlimited" };
33025
+ if (isFull) return {
33026
+ kind: "full",
33027
+ waitlist: canJoinEventWaitlist(source)
33028
+ };
33029
+ if (available <= lowCapacityThreshold(source.capacity)) return {
33030
+ kind: "low",
33031
+ remaining: available
33032
+ };
33033
+ return { kind: "open" };
33034
+ }
33035
+ /**
33036
+ * The one place a remaining-places count becomes text. Returns `null` when
33037
+ * nothing should be shown: uncapped events, and capped events above the
33038
+ * scarcity threshold. Surfaces that need their own full-state wording pass
33039
+ * it in rather than re-deriving the count.
33040
+ */
33041
+ function formatRemainingPlaces(source, wording = {}) {
33042
+ const remaining = getRemainingPlaces(source);
33043
+ switch (remaining.kind) {
33044
+ case "unlimited":
33045
+ case "open": return null;
33046
+ case "low": return formatPlacesLeft(remaining.remaining, wording.compact === true);
33047
+ case "full": return remaining.waitlist ? wording.waitlist ?? "Waitlist" : wording.full ?? "Full";
33048
+ }
33049
+ }
33050
+ function formatPlacesLeft(count, compact) {
33051
+ if (compact) return `${count} left`;
33052
+ return count === 1 ? "1 place left" : `${count} places left`;
33053
+ }
33054
+ /** Total capacity as copy ("12 places"), for detail surfaces that state the size of an event. */
33055
+ function formatTotalCapacity(capacity) {
33056
+ if (capacity === null) return null;
33057
+ return capacity === 1 ? "1 place" : `${capacity} places`;
33058
+ }
33059
+ function getEventAvailabilityStatus(event) {
33060
+ const source = selectMostOpenCapacitySource(event);
33061
+ const remaining = getRemainingPlaces(source);
33062
+ const label = formatEventRemainingPlaces(event, { compact: true });
33063
+ switch (remaining.kind) {
33064
+ case "unlimited": return {
33065
+ status: "available",
33066
+ available: null,
33067
+ isFull: false,
33068
+ label
33069
+ };
33070
+ case "open": return {
33071
+ status: "available",
33072
+ available: getEventAvailability(source).available,
33073
+ isFull: false,
33074
+ label
33075
+ };
33076
+ case "low": return {
33077
+ status: "filling",
33078
+ available: remaining.remaining,
33079
+ isFull: false,
33080
+ label
33081
+ };
33082
+ case "full": return {
33083
+ status: "waitlist",
33084
+ available: 0,
33085
+ isFull: true,
33086
+ label
33087
+ };
33088
+ }
33089
+ }
33090
+ /**
33091
+ * Which capacity figure wins when they disagree.
33092
+ *
33093
+ * For a hybrid event that publishes both attendance-mode rows, **the mode
33094
+ * rows are authoritative and the event-level aggregate is only a summary**.
33095
+ * The registration write path admits or refuses per mode
33096
+ * (`getEffectiveOccurrenceCapacityForAttendanceMode` on the server), so the
33097
+ * aggregate can read full while a mode still has room, and can read open
33098
+ * while the mode the visitor wants is full. Believing the aggregate on a
33099
+ * card is how a "places available" event turns into a dead end at step two.
33100
+ *
33101
+ * Everything else — single-mode events, events that publish no rows, and the
33102
+ * aggregate-only helpers {@link getEventAvailability} /
33103
+ * {@link getRemainingPlaces} that existing callers rely on — keeps reading
33104
+ * the aggregate, unchanged.
33105
+ */
33106
+ function findHybridModeRows(event) {
33107
+ if (event.deliveryMode !== "hybrid") return null;
33108
+ const rows = event.attendanceModes ?? [];
33109
+ const inPerson = rows.find((row) => row.mode === "in_person");
33110
+ const online = rows.find((row) => row.mode === "online");
33111
+ return inPerson && online ? {
33112
+ inPerson,
33113
+ online
33114
+ } : null;
33115
+ }
33116
+ /** Openness rank; higher means more room. A full mode with a waitlist beats one without. */
33117
+ function remainingPlacesRank(remaining) {
33118
+ switch (remaining.kind) {
33119
+ case "unlimited": return 4;
33120
+ case "open": return 3;
33121
+ case "low": return 2;
33122
+ case "full": return remaining.waitlist ? 1 : 0;
33123
+ }
33124
+ }
33125
+ function isMoreOpen(candidate, than) {
33126
+ const rank = remainingPlacesRank(candidate);
33127
+ const otherRank = remainingPlacesRank(than);
33128
+ if (rank !== otherRank) return rank > otherRank;
33129
+ return candidate.kind === "low" && than.kind === "low" ? candidate.remaining > than.remaining : false;
33130
+ }
33131
+ /** True when `candidate` has less room left than `than`. */
33132
+ function isScarcer(candidate, than) {
33133
+ return isMoreOpen(than, candidate);
33134
+ }
33135
+ /**
33136
+ * The capacity row a whole-event decision should read: the most open
33137
+ * attendance mode of a hybrid event, otherwise the aggregate. A hybrid event
33138
+ * is sold out only when both of its modes are, and its waitlist is reachable
33139
+ * when either full mode has one open.
33140
+ */
33141
+ function selectMostOpenCapacitySource(event) {
33142
+ const rows = findHybridModeRows(event);
33143
+ if (!rows) return event;
33144
+ return isMoreOpen(getRemainingPlaces(rows.online), getRemainingPlaces(rows.inPerson)) ? rows.online : rows.inPerson;
33145
+ }
33146
+ /** True when no attendance mode has room. Hybrid-aware; use this for sold-out decisions. */
33147
+ function isEventFull(event) {
33148
+ return getEventAvailability(selectMostOpenCapacitySource(event)).isFull;
33149
+ }
33150
+ /**
33151
+ * True when the event is full on every mode and at least one of those full
33152
+ * modes has a waitlist open. An event with a mode still bookable is not a
33153
+ * waitlist prospect: it should send the visitor to that mode instead.
33154
+ */
33155
+ function canJoinEventWaitlistOnAnyMode(event) {
33156
+ return canJoinEventWaitlist(selectMostOpenCapacitySource(event));
33157
+ }
33158
+ const ATTENDANCE_MODE_LABEL = {
33159
+ in_person: "In person",
33160
+ online: "Online"
33161
+ };
33162
+ /** Wording for a mode with room to spare, where the count is deliberately withheld. */
33163
+ const PLACES_AVAILABLE = "Places available";
33164
+ /**
33165
+ * The browse-surface label for an event's remaining places. Single-mode
33166
+ * events get exactly the aggregate label {@link formatRemainingPlaces}
33167
+ * produces, and so does a hybrid event whose two modes tell the same story.
33168
+ * When the modes differ, both sides are named: "available" on a card is a
33169
+ * promise the registration wizard would otherwise have to break.
33170
+ *
33171
+ * Hybrid examples, in person full with 3 online places left:
33172
+ * default -> "Online: 3 places left · In person: Full"
33173
+ * { full: 'Sold out' } -> "Online: 3 places left · In person: Sold out"
33174
+ * { compact: true } -> "Online: 3 left"
33175
+ */
33176
+ function formatEventRemainingPlaces(event, wording = {}) {
33177
+ const rows = findHybridModeRows(event);
33178
+ if (!rows) return formatRemainingPlaces(event, wording);
33179
+ const inPerson = formatRemainingPlaces(rows.inPerson, wording);
33180
+ const online = formatRemainingPlaces(rows.online, wording);
33181
+ if (inPerson === online) return inPerson;
33182
+ if (wording.compact === true) return formatCompactHybridPlaces(rows, wording);
33183
+ return formatBothHybridSides(rows, {
33184
+ inPerson,
33185
+ online
33186
+ });
33187
+ }
33188
+ /**
33189
+ * Both sides, the one with room first: that is the half a visitor can act on,
33190
+ * and it should not arrive after a full stop's worth of bad news.
33191
+ */
33192
+ function formatBothHybridSides(rows, labels) {
33193
+ return (isMoreOpen(getRemainingPlaces(rows.online), getRemainingPlaces(rows.inPerson)) ? [["online", labels.online], ["in_person", labels.inPerson]] : [["in_person", labels.inPerson], ["online", labels.online]]).map(([mode, label]) => namedModePlaces(mode, label)).join(" · ");
33194
+ }
33195
+ function namedModePlaces(mode, label) {
33196
+ return `${ATTENDANCE_MODE_LABEL[mode]}: ${label ?? PLACES_AVAILABLE}`;
33197
+ }
33198
+ /**
33199
+ * Pills and timetable cells have room for one short phrase, so they name the
33200
+ * single mode that carries news rather than listing both: the scarce side
33201
+ * when there is a count to show, and otherwise the side that has closed —
33202
+ * "In person: Full", never "Online only", which reads as a statement about
33203
+ * how the event is delivered rather than about its remaining places.
33204
+ */
33205
+ function formatCompactHybridPlaces(rows, wording) {
33206
+ const inPerson = getRemainingPlaces(rows.inPerson);
33207
+ const online = getRemainingPlaces(rows.online);
33208
+ if (inPerson.kind === "full" && online.kind === "full") return formatRemainingPlaces(inPerson.waitlist ? rows.inPerson : rows.online, wording);
33209
+ if (inPerson.kind === "full" || online.kind === "full") {
33210
+ const fullSide = inPerson.kind === "full" ? "in_person" : "online";
33211
+ const openSide = inPerson.kind === "full" ? "online" : "in_person";
33212
+ const openLabel = formatRemainingPlaces(rowFor(rows, openSide), wording);
33213
+ return openLabel !== null ? namedModePlaces(openSide, openLabel) : namedModePlaces(fullSide, formatRemainingPlaces(rowFor(rows, fullSide), wording));
33214
+ }
33215
+ const scarcerSide = isScarcer(online, inPerson) ? "online" : "in_person";
33216
+ return namedModePlaces(scarcerSide, formatRemainingPlaces(rowFor(rows, scarcerSide), wording));
33217
+ }
33218
+ function rowFor(rows, mode) {
33219
+ return mode === "online" ? rows.online : rows.inPerson;
33220
+ }
33221
+ //#endregion
33222
+ //#region ../blocks/src/system/runtime/nodes/events/domain/destination.ts
32706
33223
  function isRecord$24(value) {
32707
33224
  return typeof value === "object" && value !== null && !Array.isArray(value);
32708
33225
  }
@@ -32720,7 +33237,701 @@ function resolveRuntimeEventDestination(event) {
32720
33237
  };
32721
33238
  }
32722
33239
  //#endregion
32723
- //#region ../blocks/src/system/runtime/nodes/events/shared/pricing.ts
33240
+ //#region ../blocks/src/system/runtime/nodes/events/domain/cta.ts
33241
+ /**
33242
+ * Booking call-to-action resolution for events: where an event links to and
33243
+ * what the button says. Sold-out and waitlist decisions come from capacity.ts
33244
+ * so cards, rows and the wizard never disagree.
33245
+ */
33246
+ function closedEventBookingLabel(reason) {
33247
+ switch (reason) {
33248
+ case "occurrence_cancelled":
33249
+ case "series_cancelled": return "Cancelled";
33250
+ case "occurrence_completed":
33251
+ case "series_completed": return "Completed";
33252
+ case "series_draft":
33253
+ case "series_archived":
33254
+ case "parent_archived":
33255
+ case "parent_inconsistent":
33256
+ case "not_public":
33257
+ case "already_started":
33258
+ case "enrollment_closed": return "Registration closed";
33259
+ default: return assertNever$23(reason);
33260
+ }
33261
+ }
33262
+ function resolveEventHref(event) {
33263
+ const destination = resolveRuntimeEventDestination(event);
33264
+ return destination.kind === "external" ? destination.url : destination.path;
33265
+ }
33266
+ function resolveEventLinkProps(event) {
33267
+ const href = resolveEventHref(event);
33268
+ return resolveRuntimeEventDestination(event).kind === "external" ? {
33269
+ href,
33270
+ target: "_blank",
33271
+ rel: "noopener noreferrer"
33272
+ } : { href };
33273
+ }
33274
+ /**
33275
+ * Resolve the CTA state for an event.
33276
+ * Shared by EventCard and EventCompactRow to keep sold-out, registration-closed,
33277
+ * and course-specific defaults consistent.
33278
+ */
33279
+ function resolveEventCta(event, buttonText) {
33280
+ const isSoldOut = isEventFull(event);
33281
+ const destination = resolveRuntimeEventDestination(event);
33282
+ const isExternal = destination.kind === "external";
33283
+ const defaultButtonText = isExternal ? destination.label : event.bookingScope === "whole_series" ? "Book course" : "Book";
33284
+ const effectiveButtonText = buttonText ?? defaultButtonText;
33285
+ const href = resolveEventHref(event);
33286
+ if (event.bookingAvailability.kind === "closed") {
33287
+ const linksToClosedWholeSeriesDetails = event.bookingAvailability.reason === "enrollment_closed" && !isExternal;
33288
+ return {
33289
+ label: closedEventBookingLabel(event.bookingAvailability.reason),
33290
+ href,
33291
+ external: isExternal,
33292
+ hidden: !linksToClosedWholeSeriesDetails
33293
+ };
33294
+ }
33295
+ if (!isExternal && isSoldOut) {
33296
+ if (canJoinEventWaitlistOnAnyMode(event)) return {
33297
+ label: "Join waitlist",
33298
+ href,
33299
+ external: false,
33300
+ hidden: false
33301
+ };
33302
+ return {
33303
+ label: "Sold out",
33304
+ href,
33305
+ external: false,
33306
+ hidden: true
33307
+ };
33308
+ }
33309
+ return {
33310
+ label: effectiveButtonText,
33311
+ href,
33312
+ external: isExternal,
33313
+ hidden: false
33314
+ };
33315
+ }
33316
+ //#endregion
33317
+ //#region ../calendar/src/grid.ts
33318
+ const DAY_NAMES = [
33319
+ "Sun",
33320
+ "Mon",
33321
+ "Tue",
33322
+ "Wed",
33323
+ "Thu",
33324
+ "Fri",
33325
+ "Sat"
33326
+ ];
33327
+ const MONTH_NAMES = [
33328
+ "January",
33329
+ "February",
33330
+ "March",
33331
+ "April",
33332
+ "May",
33333
+ "June",
33334
+ "July",
33335
+ "August",
33336
+ "September",
33337
+ "October",
33338
+ "November",
33339
+ "December"
33340
+ ];
33341
+ function formatDateKey(date) {
33342
+ return `${String(date.year).padStart(4, "0")}-${String(date.month).padStart(2, "0")}-${String(date.day).padStart(2, "0")}`;
33343
+ }
33344
+ function isToday(date, today) {
33345
+ return isSameCalendarDay(date, today);
33346
+ }
33347
+ function isSameCalendarDay(left, right) {
33348
+ return left.year === right.year && left.month === right.month && left.day === right.day;
33349
+ }
33350
+ function addDays(date, days) {
33351
+ const result = new Date(Date.UTC(date.year, date.month - 1, date.day + days, 12));
33352
+ return {
33353
+ year: result.getUTCFullYear(),
33354
+ month: result.getUTCMonth() + 1,
33355
+ day: result.getUTCDate()
33356
+ };
33357
+ }
33358
+ function getMonthCalendarDays(year, month, startOfWeek) {
33359
+ const firstDay = {
33360
+ year,
33361
+ month,
33362
+ day: 1
33363
+ };
33364
+ const dayOfWeek = getDayOfWeek(firstDay);
33365
+ const gridStart = addDays(firstDay, -(startOfWeek === "monday" ? dayOfWeek === 0 ? 6 : dayOfWeek - 1 : dayOfWeek));
33366
+ return Array.from({ length: 42 }, (_, index) => addDays(gridStart, index));
33367
+ }
33368
+ function getWeekCalendarDays(baseDate, startOfWeek) {
33369
+ const dayOfWeek = getDayOfWeek(baseDate);
33370
+ const weekStart = addDays(baseDate, -(startOfWeek === "monday" ? dayOfWeek === 0 ? 6 : dayOfWeek - 1 : dayOfWeek));
33371
+ return Array.from({ length: 7 }, (_, index) => addDays(weekStart, index));
33372
+ }
33373
+ function getDayOfWeek(date) {
33374
+ return new Date(Date.UTC(date.year, date.month - 1, date.day, 12)).getUTCDay();
33375
+ }
33376
+ function getWeekCalendarDayTuple(baseDate, startOfWeek) {
33377
+ const days = getWeekCalendarDays(baseDate, startOfWeek);
33378
+ assertWeekCalendarDays(days);
33379
+ return days;
33380
+ }
33381
+ function assertWeekCalendarDays(days) {
33382
+ if (days.length !== 7) throw new Error("Expected exactly 7 calendar days");
33383
+ }
33384
+ function getOrderedDayNames(startOfWeek, dayNames = DAY_NAMES) {
33385
+ if (startOfWeek === "monday") {
33386
+ const sunday = dayNames[0] ?? "Sun";
33387
+ return [...dayNames.slice(1), sunday];
33388
+ }
33389
+ return [...dayNames];
33390
+ }
33391
+ function formatCalendarTitle(viewDate, view, days, monthNames = MONTH_NAMES) {
33392
+ if (view === "week") {
33393
+ const weekStart = days[0];
33394
+ const weekEnd = days[6];
33395
+ if (weekStart === void 0 || weekEnd === void 0) return `${monthNames[viewDate.month - 1] ?? ""} ${viewDate.year}`;
33396
+ const startMonth = monthNames[weekStart.month - 1] ?? "";
33397
+ const endMonth = monthNames[weekEnd.month - 1] ?? "";
33398
+ if (weekStart.month === weekEnd.month) return `${startMonth} ${weekStart.day} - ${weekEnd.day}, ${weekEnd.year}`;
33399
+ return `${startMonth} ${weekStart.day} - ${endMonth} ${weekEnd.day}, ${weekEnd.year}`;
33400
+ }
33401
+ return `${monthNames[viewDate.month - 1] ?? ""} ${viewDate.year}`;
33402
+ }
33403
+ function buildCalendarViewport(input) {
33404
+ const monthNames = input.monthNames ?? MONTH_NAMES;
33405
+ if (input.view === "week") {
33406
+ const days = getWeekCalendarDayTuple(input.anchorDate, input.startOfWeek);
33407
+ return {
33408
+ kind: "week",
33409
+ anchorDate: input.anchorDate,
33410
+ days,
33411
+ title: formatCalendarTitle(input.anchorDate, "week", days, monthNames)
33412
+ };
33413
+ }
33414
+ const days = getMonthCalendarDays(input.anchorDate.year, input.anchorDate.month, input.startOfWeek);
33415
+ return {
33416
+ kind: "month",
33417
+ anchorDate: input.anchorDate,
33418
+ days,
33419
+ title: formatCalendarTitle(input.anchorDate, "month", days, monthNames)
33420
+ };
33421
+ }
33422
+ //#endregion
33423
+ //#region ../calendar/src/lane-packing.ts
33424
+ /**
33425
+ * Assign lanes to a list of intervals.
33426
+ *
33427
+ * Returns one result per input in the same order as the input. Internally
33428
+ * sorts a copy by `(start, end, orderKey)` to ensure determinism, then
33429
+ * permutes results back to input order.
33430
+ */
33431
+ function assignLanes(intervals) {
33432
+ if (intervals.length === 0) return [];
33433
+ const indexed = intervals.map((interval, index) => ({
33434
+ interval,
33435
+ index
33436
+ }));
33437
+ indexed.sort((left, right) => left.interval.start - right.interval.start || left.interval.end - right.interval.end || left.interval.orderKey.localeCompare(right.interval.orderKey));
33438
+ const results = new Array(intervals.length);
33439
+ let cluster = [];
33440
+ let activeLaneEnds = [];
33441
+ let clusterMaxEnd = -Infinity;
33442
+ const closeCluster = () => {
33443
+ if (cluster.length === 0) return;
33444
+ const laneCount = Math.max(1, cluster.reduce((max, entry) => Math.max(max, entry.laneIndex + 1), 0));
33445
+ for (const entry of cluster) results[entry.index] = {
33446
+ laneIndex: entry.laneIndex,
33447
+ laneCount
33448
+ };
33449
+ cluster = [];
33450
+ activeLaneEnds = [];
33451
+ clusterMaxEnd = -Infinity;
33452
+ };
33453
+ for (const { interval, index } of indexed) {
33454
+ if (interval.start >= clusterMaxEnd) closeCluster();
33455
+ const laneIndex = firstAvailableLane(activeLaneEnds, interval.start);
33456
+ activeLaneEnds[laneIndex] = interval.end;
33457
+ clusterMaxEnd = Math.max(clusterMaxEnd, interval.end);
33458
+ cluster.push({
33459
+ index,
33460
+ laneIndex
33461
+ });
33462
+ }
33463
+ closeCluster();
33464
+ return results;
33465
+ }
33466
+ function firstAvailableLane(activeLaneEnds, start) {
33467
+ const laneIndex = activeLaneEnds.findIndex((end) => end <= start);
33468
+ return laneIndex >= 0 ? laneIndex : activeLaneEnds.length;
33469
+ }
33470
+ //#endregion
33471
+ //#region ../calendar/src/timezone.ts
33472
+ /**
33473
+ * Timezone boundary helpers — internal.
33474
+ *
33475
+ * Both functions read an ISO instant in a given IANA timezone and project it
33476
+ * onto the wall clock of that zone. They use Intl.DateTimeFormat with
33477
+ * hourCycle: 'h23' to avoid 12/24-hour ambiguity. Used internally by the
33478
+ * timetable model and `groupItemsByDate`; not exported from the package's
33479
+ * public surface.
33480
+ */
33481
+ const MINUTES_PER_HOUR = 60;
33482
+ /**
33483
+ * Local minute-of-day [0, 24*60) for an ISO instant in the given zone.
33484
+ * Returns NaN if the inputs cannot be parsed (caller should drop the item).
33485
+ */
33486
+ function getLocalMinuteOfDay(isoTimestamp, timeZone) {
33487
+ const date = new Date(isoTimestamp);
33488
+ if (Number.isNaN(date.getTime())) return NaN;
33489
+ if (!isValidTimeZone(timeZone)) return NaN;
33490
+ const parts = new Intl.DateTimeFormat("en-GB", {
33491
+ timeZone,
33492
+ hour: "2-digit",
33493
+ minute: "2-digit",
33494
+ hourCycle: "h23"
33495
+ }).formatToParts(date);
33496
+ const hourPart = Number(parts.find((part) => part.type === "hour")?.value);
33497
+ const minute = Number(parts.find((part) => part.type === "minute")?.value);
33498
+ const hour = hourPart === 24 ? 0 : hourPart;
33499
+ if (Number.isNaN(hour) || Number.isNaN(minute)) return NaN;
33500
+ return hour * MINUTES_PER_HOUR + minute;
33501
+ }
33502
+ /**
33503
+ * Local YYYY-MM-DD for an ISO instant in the given zone.
33504
+ * Returns null if the inputs cannot be parsed.
33505
+ */
33506
+ function getLocalDateKey(isoTimestamp, timeZone) {
33507
+ const date = new Date(isoTimestamp);
33508
+ if (Number.isNaN(date.getTime())) return null;
33509
+ if (!isValidTimeZone(timeZone)) return null;
33510
+ const parts = new Intl.DateTimeFormat("en-CA", {
33511
+ timeZone,
33512
+ year: "numeric",
33513
+ month: "2-digit",
33514
+ day: "2-digit"
33515
+ }).formatToParts(date);
33516
+ const year = parts.find((part) => part.type === "year")?.value;
33517
+ const month = parts.find((part) => part.type === "month")?.value;
33518
+ const day = parts.find((part) => part.type === "day")?.value;
33519
+ if (year === void 0 || year.length === 0) return null;
33520
+ if (month === void 0 || month.length === 0) return null;
33521
+ if (day === void 0 || day.length === 0) return null;
33522
+ return `${year}-${month}-${day}`;
33523
+ }
33524
+ const validTimeZoneCache = /* @__PURE__ */ new Map();
33525
+ function isValidTimeZone(timeZone) {
33526
+ if (typeof timeZone !== "string" || timeZone.length === 0) return false;
33527
+ const cached = validTimeZoneCache.get(timeZone);
33528
+ if (cached !== void 0) return cached;
33529
+ try {
33530
+ new Intl.DateTimeFormat("en-US", { timeZone });
33531
+ validTimeZoneCache.set(timeZone, true);
33532
+ return true;
33533
+ } catch {
33534
+ validTimeZoneCache.set(timeZone, false);
33535
+ return false;
33536
+ }
33537
+ }
33538
+ //#endregion
33539
+ //#region ../calendar/src/timetable.ts
33540
+ /**
33541
+ * Timetable model — the package's core surface.
33542
+ *
33543
+ * Generic over `T`. Takes a list of timed items + a viewport; returns a
33544
+ * day-by-day layout with overlap-packed lanes and percent-based positions
33545
+ * plus a diagnostics list for items dropped during processing.
33546
+ *
33547
+ * The function is pure: given identical input (including `now`), it returns
33548
+ * the same output. The model has no opinion on what an item is — pass
33549
+ * accessors.
33550
+ */
33551
+ const DEFAULT_BOUNDS = {
33552
+ minStartHour: 5,
33553
+ maxEndHour: 23,
33554
+ defaultStartHour: 6,
33555
+ defaultEndHour: 21,
33556
+ minHeightPercent: 2.5
33557
+ };
33558
+ function buildTimetableModel$1(input) {
33559
+ const bounds = resolveBounds(input.bounds);
33560
+ const viewportDays = getTimetableDays(input.viewDate, input.view ?? "week", input.startOfWeek);
33561
+ const viewportKeys = new Set(viewportDays.map(formatDateKey));
33562
+ const parsed = parseAllItems(input.items, input);
33563
+ const inViewport = deriveVisibleSegments(parsed.drafts, viewportKeys);
33564
+ const hourRange = resolveHourRange(inViewport, bounds);
33565
+ const laid = layoutCellsByDate(inViewport, hourRange, bounds);
33566
+ return {
33567
+ days: assembleDays(viewportDays, laid.cellsByDate, input.today),
33568
+ hourTicks: buildHourTicks(hourRange.startHour, hourRange.endHour),
33569
+ startHour: hourRange.startHour,
33570
+ endHour: hourRange.endHour,
33571
+ rangeMinutes: (hourRange.endHour - hourRange.startHour) * 60,
33572
+ hasItems: inViewport.length > 0,
33573
+ diagnostics: [...parsed.diagnostics, ...laid.diagnostics]
33574
+ };
33575
+ }
33576
+ function parseAllItems(items, accessors) {
33577
+ const drafts = [];
33578
+ const diagnostics = [];
33579
+ for (const item of items) {
33580
+ const result = parseDraft(item, accessors);
33581
+ if (result.ok) drafts.push(result.draft);
33582
+ else diagnostics.push({
33583
+ item,
33584
+ reason: result.reason
33585
+ });
33586
+ }
33587
+ return {
33588
+ drafts,
33589
+ diagnostics
33590
+ };
33591
+ }
33592
+ function layoutCellsByDate(segments, hourRange, bounds) {
33593
+ const rangeMinutes = (hourRange.endHour - hourRange.startHour) * 60;
33594
+ const rangeStartMinutes = hourRange.startHour * 60;
33595
+ const rangeEndMinutes = hourRange.endHour * 60;
33596
+ const segmentsByDate = groupSegmentsByDate(segments);
33597
+ const cellsByDate = /* @__PURE__ */ new Map();
33598
+ const diagnostics = [];
33599
+ for (const [dateKey, daySegments] of segmentsByDate) {
33600
+ const clipped = [];
33601
+ for (const segment of daySegments) {
33602
+ const result = clipDraft(segment, rangeStartMinutes, rangeEndMinutes);
33603
+ if (result.ok) clipped.push(result.clipped);
33604
+ else diagnostics.push({
33605
+ item: segment.draft.item,
33606
+ reason: result.reason
33607
+ });
33608
+ }
33609
+ const laneResults = assignLanes(clipped.map((entry) => ({
33610
+ start: entry.visibleStart,
33611
+ end: entry.visibleEnd,
33612
+ orderKey: entry.segment.draft.orderKey
33613
+ })));
33614
+ cellsByDate.set(dateKey, clipped.map((entry, idx) => buildCell(entry, laneResults[idx] ?? {
33615
+ laneIndex: 0,
33616
+ laneCount: 1
33617
+ }, rangeStartMinutes, rangeMinutes, bounds.minHeightPercent)));
33618
+ }
33619
+ return {
33620
+ cellsByDate,
33621
+ diagnostics
33622
+ };
33623
+ }
33624
+ function assembleDays(viewportDays, cellsByDate, today) {
33625
+ return viewportDays.map((date) => {
33626
+ const dateKey = formatDateKey(date);
33627
+ return {
33628
+ date,
33629
+ dateKey,
33630
+ isToday: isSameCalendarDay(date, today),
33631
+ cells: cellsByDate.get(dateKey) ?? []
33632
+ };
33633
+ });
33634
+ }
33635
+ function parseDraft(item, accessors) {
33636
+ const startsAtIso = accessors.getStartsAt(item);
33637
+ const endsAtIso = accessors.getEndsAt(item);
33638
+ const timeZone = accessors.getTimeZone(item);
33639
+ const startsAtMinutes = getLocalMinuteOfDay(startsAtIso, timeZone);
33640
+ if (Number.isNaN(startsAtMinutes)) return {
33641
+ ok: false,
33642
+ reason: "invalid-start"
33643
+ };
33644
+ const startDateKey = getLocalDateKey(startsAtIso, timeZone);
33645
+ if (startDateKey == null) return {
33646
+ ok: false,
33647
+ reason: "invalid-timezone"
33648
+ };
33649
+ const endDateKey = getLocalDateKey(endsAtIso, timeZone);
33650
+ if (endDateKey == null) return {
33651
+ ok: false,
33652
+ reason: "invalid-end"
33653
+ };
33654
+ const endsAtMinutesRaw = getLocalMinuteOfDay(endsAtIso, timeZone);
33655
+ if (Number.isNaN(endsAtMinutesRaw)) return {
33656
+ ok: false,
33657
+ reason: "invalid-end"
33658
+ };
33659
+ const spansMultipleDays = startDateKey < endDateKey;
33660
+ const endsAtMinutes = spansMultipleDays || endsAtMinutesRaw > startsAtMinutes ? endsAtMinutesRaw : Math.min(1440, startsAtMinutes + 60);
33661
+ if (!spansMultipleDays && endsAtMinutes <= startsAtMinutes) return {
33662
+ ok: false,
33663
+ reason: "non-positive-duration"
33664
+ };
33665
+ return {
33666
+ ok: true,
33667
+ draft: {
33668
+ item,
33669
+ startDateKey,
33670
+ endDateKey,
33671
+ startsAtMinutes,
33672
+ endsAtMinutes,
33673
+ color: accessors.getColor?.(item) ?? null,
33674
+ orderKey: accessors.getOrderKey?.(item) ?? accessors.getId(item)
33675
+ }
33676
+ };
33677
+ }
33678
+ function deriveVisibleSegments(drafts, viewportKeys) {
33679
+ const segments = [];
33680
+ for (const draft of drafts) for (const dateKey of enumerateDateKeys(draft.startDateKey, draft.endDateKey)) {
33681
+ if (!viewportKeys.has(dateKey)) continue;
33682
+ const isStartDay = dateKey === draft.startDateKey;
33683
+ const isEndDay = dateKey === draft.endDateKey;
33684
+ const startsAtMinutes = isStartDay ? draft.startsAtMinutes : 0;
33685
+ const endsAtMinutes = isEndDay ? draft.endsAtMinutes : 1440;
33686
+ if (endsAtMinutes <= startsAtMinutes) continue;
33687
+ segments.push({
33688
+ draft,
33689
+ dateKey,
33690
+ startsAtMinutes,
33691
+ endsAtMinutes,
33692
+ continuesBefore: !isStartDay,
33693
+ continuesAfter: !isEndDay
33694
+ });
33695
+ }
33696
+ return segments;
33697
+ }
33698
+ function clipDraft(segment, rangeStartMinutes, rangeEndMinutes) {
33699
+ const visibleStart = Math.max(rangeStartMinutes, segment.startsAtMinutes);
33700
+ const visibleEnd = Math.min(rangeEndMinutes, segment.endsAtMinutes);
33701
+ if (visibleEnd <= visibleStart) return {
33702
+ ok: false,
33703
+ reason: "out-of-visible-hours"
33704
+ };
33705
+ return {
33706
+ ok: true,
33707
+ clipped: {
33708
+ segment,
33709
+ visibleStart,
33710
+ visibleEnd
33711
+ }
33712
+ };
33713
+ }
33714
+ function buildCell(entry, lane, rangeStartMinutes, rangeMinutes, minHeightPercent) {
33715
+ const heightPercent = Math.max(minHeightPercent, toRangePercent(entry.visibleEnd - entry.visibleStart, rangeMinutes));
33716
+ const rawTopPercent = toRangePercent(entry.visibleStart - rangeStartMinutes, rangeMinutes);
33717
+ const topPercent = Math.min(rawTopPercent, 100 - heightPercent);
33718
+ return {
33719
+ item: entry.segment.draft.item,
33720
+ dateKey: entry.segment.dateKey,
33721
+ startsAtMinutes: entry.segment.startsAtMinutes,
33722
+ endsAtMinutes: entry.segment.endsAtMinutes,
33723
+ visibleStartsAtMinutes: entry.visibleStart,
33724
+ visibleEndsAtMinutes: entry.visibleEnd,
33725
+ topPercent,
33726
+ heightPercent,
33727
+ isClippedStart: entry.segment.continuesBefore || entry.segment.startsAtMinutes < entry.visibleStart,
33728
+ isClippedEnd: entry.segment.continuesAfter || entry.segment.endsAtMinutes > entry.visibleEnd,
33729
+ laneIndex: lane.laneIndex,
33730
+ laneCount: lane.laneCount,
33731
+ color: entry.segment.draft.color
33732
+ };
33733
+ }
33734
+ function resolveBounds(bounds) {
33735
+ return {
33736
+ minStartHour: bounds?.minStartHour ?? DEFAULT_BOUNDS.minStartHour,
33737
+ maxEndHour: bounds?.maxEndHour ?? DEFAULT_BOUNDS.maxEndHour,
33738
+ defaultStartHour: bounds?.defaultStartHour ?? DEFAULT_BOUNDS.defaultStartHour,
33739
+ defaultEndHour: bounds?.defaultEndHour ?? DEFAULT_BOUNDS.defaultEndHour,
33740
+ minHeightPercent: bounds?.minHeightPercent ?? DEFAULT_BOUNDS.minHeightPercent
33741
+ };
33742
+ }
33743
+ function resolveHourRange(segments, bounds) {
33744
+ if (segments.length === 0) return {
33745
+ startHour: bounds.defaultStartHour,
33746
+ endHour: bounds.defaultEndHour
33747
+ };
33748
+ const earliestStart = Math.min(...segments.map((segment) => segment.startsAtMinutes));
33749
+ const latestEnd = Math.max(...segments.map((segment) => segment.endsAtMinutes));
33750
+ const startHour = Math.max(bounds.minStartHour, Math.floor(earliestStart / 60));
33751
+ const endHour = Math.min(bounds.maxEndHour, Math.ceil(latestEnd / 60));
33752
+ return {
33753
+ startHour,
33754
+ endHour: Math.max(startHour + 1, endHour)
33755
+ };
33756
+ }
33757
+ function buildHourTicks(startHour, endHour) {
33758
+ const ticks = [];
33759
+ for (let hour = startHour; hour <= endHour; hour += 1) ticks.push(hour);
33760
+ return ticks;
33761
+ }
33762
+ function groupSegmentsByDate(segments) {
33763
+ const grouped = /* @__PURE__ */ new Map();
33764
+ for (const segment of segments) {
33765
+ const existing = grouped.get(segment.dateKey) ?? [];
33766
+ existing.push(segment);
33767
+ grouped.set(segment.dateKey, existing);
33768
+ }
33769
+ return grouped;
33770
+ }
33771
+ function toRangePercent(minutes, rangeMinutes) {
33772
+ if (rangeMinutes <= 0) return 0;
33773
+ return minutes / rangeMinutes * 100;
33774
+ }
33775
+ function getTimetableDays(viewDate, view, startOfWeek) {
33776
+ if (view === "day") return [viewDate];
33777
+ return getWeekCalendarDays(viewDate, startOfWeek);
33778
+ }
33779
+ function enumerateDateKeys(startDateKey, endDateKey) {
33780
+ if (startDateKey > endDateKey) throw new Error(`Invalid timetable date range: ${startDateKey} is after ${endDateKey}`);
33781
+ const start = parseDateKey(startDateKey);
33782
+ parseDateKey(endDateKey);
33783
+ const keys = [];
33784
+ let cursor = start;
33785
+ while (formatDateKey(cursor) <= endDateKey) {
33786
+ keys.push(formatDateKey(cursor));
33787
+ cursor = addDays(cursor, 1);
33788
+ }
33789
+ return keys;
33790
+ }
33791
+ function parseDateKey(dateKey) {
33792
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(dateKey);
33793
+ if (match === null) throw new Error(`Invalid timetable date key: ${dateKey}`);
33794
+ const [, year, month, day] = match;
33795
+ return {
33796
+ year: Number(year),
33797
+ month: Number(month),
33798
+ day: Number(day)
33799
+ };
33800
+ }
33801
+ //#endregion
33802
+ //#region ../calendar/src/group.ts
33803
+ /**
33804
+ * Group items by their local date (YYYY-MM-DD in the item's own timezone).
33805
+ *
33806
+ * Generic over `T`. Items with un-parseable startsAt or invalid timezone are
33807
+ * silently dropped — call sites that need diagnostics should validate upstream.
33808
+ *
33809
+ * The output map preserves insertion order of dates (ES2015 spec) and the
33810
+ * relative order of items within each date matches input order.
33811
+ */
33812
+ function groupItemsByDate(items, accessors) {
33813
+ const grouped = /* @__PURE__ */ new Map();
33814
+ for (const item of items) {
33815
+ const dateKey = getLocalDateKey(accessors.getStartsAt(item), accessors.getTimeZone(item));
33816
+ if (dateKey == null) continue;
33817
+ const existing = grouped.get(dateKey);
33818
+ if (existing !== void 0) existing.push(item);
33819
+ else grouped.set(dateKey, [item]);
33820
+ }
33821
+ return grouped;
33822
+ }
33823
+ //#endregion
33824
+ //#region ../blocks/src/system/runtime/nodes/events/domain/dates.ts
33825
+ /**
33826
+ * Date and time presentation for events: the single place an event's
33827
+ * timestamps become strings or date buckets. Calendar-grid primitives are
33828
+ * re-exported from @riverbankcms/calendar so block code has one import.
33829
+ */
33830
+ /**
33831
+ * Format date for display.
33832
+ * Returns: "Monday, January 15, 2025"
33833
+ */
33834
+ function formatDate$2(isoTimestamp, options) {
33835
+ return formatBookingDate(toScheduledBookingDateTime({
33836
+ startsAt: isoTimestamp,
33837
+ timeZone: toBookingTimeZone(options?.timeZone ?? "UTC"),
33838
+ locale: options?.locale ?? "en-US"
33839
+ }), {
33840
+ weekday: "long",
33841
+ year: "numeric",
33842
+ month: "long",
33843
+ day: "numeric"
33844
+ });
33845
+ }
33846
+ /**
33847
+ * Format time for display.
33848
+ * Returns: "10:00 AM"
33849
+ */
33850
+ function formatTime(isoTimestamp, options) {
33851
+ return formatBookingTime(toScheduledBookingDateTime({
33852
+ startsAt: isoTimestamp,
33853
+ timeZone: toBookingTimeZone(options?.timeZone ?? "UTC"),
33854
+ locale: options?.locale ?? "en-US"
33855
+ }), {
33856
+ hour: "numeric",
33857
+ minute: "2-digit",
33858
+ hour12: true
33859
+ });
33860
+ }
33861
+ function formatEventDateParts(args) {
33862
+ const scheduledRange = toScheduledBookingDateRange({
33863
+ startsAt: args.startsAt,
33864
+ endsAt: args.endsAt,
33865
+ timeZone: toBookingTimeZone(args.timeZone ?? "UTC"),
33866
+ locale: args.locale ?? "en-US"
33867
+ });
33868
+ const { date, time: start } = formatBookingDateParts(scheduledRange, { dateFormat: {
33869
+ weekday: "long",
33870
+ year: "numeric",
33871
+ month: "long",
33872
+ day: "numeric"
33873
+ } });
33874
+ return {
33875
+ date,
33876
+ timeRange: `${start} – ${formatBookingTime(toScheduledBookingDateTime({
33877
+ startsAt: args.endsAt,
33878
+ timeZone: scheduledRange.timeZone,
33879
+ locale: args.locale ?? "en-US"
33880
+ }))}`
33881
+ };
33882
+ }
33883
+ function formatEventCompactDateRail(isoTimestamp, options) {
33884
+ const dateTime = toScheduledBookingDateTime({
33885
+ startsAt: isoTimestamp,
33886
+ timeZone: toBookingTimeZone(options?.timeZone ?? "UTC"),
33887
+ locale: options?.locale ?? "en-US"
33888
+ });
33889
+ const weekday = formatBookingDate(dateTime, { weekday: "short" }).toLocaleUpperCase(options?.locale ?? "en-US");
33890
+ const month = formatBookingDate(dateTime, { month: "short" }).toLocaleUpperCase(options?.locale ?? "en-US");
33891
+ return {
33892
+ weekday,
33893
+ day: formatBookingDate(dateTime, { day: "numeric" }),
33894
+ month,
33895
+ label: formatBookingDate(dateTime, {
33896
+ weekday: "long",
33897
+ year: "numeric",
33898
+ month: "long",
33899
+ day: "numeric"
33900
+ })
33901
+ };
33902
+ }
33903
+ /**
33904
+ * Group events by their local date (YYYY-MM-DD key in each event's own
33905
+ * timezone). Thin domain wrapper over `groupItemsByDate` from the calendar
33906
+ * package; preserves the historical `Map<string, PublicEvent[]>` shape.
33907
+ */
33908
+ function groupEventsByDate(events, calendarTimeZone) {
33909
+ const grouped = groupItemsByDate(events, {
33910
+ getStartsAt: (event) => event.startsAt,
33911
+ getTimeZone: () => calendarTimeZone
33912
+ });
33913
+ const map = /* @__PURE__ */ new Map();
33914
+ for (const [dateKey, bucket] of grouped) map.set(dateKey, [...bucket]);
33915
+ return map;
33916
+ }
33917
+ //#endregion
33918
+ //#region ../blocks/src/system/runtime/nodes/events/domain/people.ts
33919
+ function formatTeamMemberNames(names) {
33920
+ const normalized = names.map((name) => name.trim()).filter(Boolean);
33921
+ if (normalized.length === 0) return null;
33922
+ const [firstName, secondName] = normalized;
33923
+ if (!firstName) return null;
33924
+ if (normalized.length === 1) return firstName;
33925
+ if (normalized.length === 2 && secondName) return `${firstName} and ${secondName}`;
33926
+ const finalName = normalized[normalized.length - 1] ?? "";
33927
+ return `${normalized.slice(0, -1).join(", ")}, and ${finalName}`;
33928
+ }
33929
+ function formatEventTeacherLine(event) {
33930
+ const names = formatTeamMemberNames((event.teamMembers ?? []).map((teamMember) => teamMember.name));
33931
+ return names ? `with ${names}` : null;
33932
+ }
33933
+ //#endregion
33934
+ //#region ../blocks/src/system/runtime/nodes/events/domain/pricing.ts
32724
33935
  const DEFAULT_EVENT_CURRENCY = "USD";
32725
33936
  function resolveRuntimeEventPricing(event) {
32726
33937
  if (isPublicEventPricing(event.pricing)) return event.pricing;
@@ -32887,6 +34098,17 @@ function readCoursePriceDisplay(value, currency) {
32887
34098
  function readAmount(value) {
32888
34099
  return typeof value === "number" && Number.isFinite(value) ? value : null;
32889
34100
  }
34101
+ /**
34102
+ * Format event price for display.
34103
+ * @returns "Free", "£25", "£12 - £25", "Free / £12 - £25", "Course (From £40)", etc.
34104
+ *
34105
+ * Events render compact amounts (no trailing ".00"); the actual ADT→string
34106
+ * mapping lives in `formatBookingPriceDisplay` in @riverbankcms/core so all
34107
+ * surfaces stay consistent.
34108
+ */
34109
+ function formatEventPrice(event) {
34110
+ return formatEventPricing(resolveRuntimeEventPricing(event), { decimals: "auto" });
34111
+ }
32890
34112
  const eventCalendarDisplayModeValues = [
32891
34113
  "combined",
32892
34114
  "calendar",
@@ -33411,7 +34633,7 @@ const eventRegistrationBlockDefinition = {
33411
34633
  {
33412
34634
  id: "showCapacity",
33413
34635
  type: "boolean",
33414
- label: "Show available spots",
34636
+ label: "Show available places",
33415
34637
  description: "Display remaining capacity",
33416
34638
  required: false,
33417
34639
  defaultValue: true,
@@ -33956,591 +35178,26 @@ const eventSpotlightBlockDefinition = {
33956
35178
  dataLoaders: { events: {
33957
35179
  endpoint: "listPublicEvents",
33958
35180
  params: {
33959
- siteId: { $bind: { from: "$root.siteId" } },
33960
- limit: { $bind: {
33961
- from: "content.maxEvents",
33962
- fallback: "3"
33963
- } },
33964
- stage: { $bind: {
33965
- from: "$root.previewStage",
33966
- fallback: "published"
33967
- } },
33968
- detailLevel: "listing",
33969
- readProfile: "spotlight"
33970
- },
33971
- mode: "server"
33972
- } }
33973
- };
33974
- //#endregion
33975
- //#region ../calendar/src/grid.ts
33976
- const DAY_NAMES = [
33977
- "Sun",
33978
- "Mon",
33979
- "Tue",
33980
- "Wed",
33981
- "Thu",
33982
- "Fri",
33983
- "Sat"
33984
- ];
33985
- const MONTH_NAMES = [
33986
- "January",
33987
- "February",
33988
- "March",
33989
- "April",
33990
- "May",
33991
- "June",
33992
- "July",
33993
- "August",
33994
- "September",
33995
- "October",
33996
- "November",
33997
- "December"
33998
- ];
33999
- function formatDateKey(date) {
34000
- return `${String(date.year).padStart(4, "0")}-${String(date.month).padStart(2, "0")}-${String(date.day).padStart(2, "0")}`;
34001
- }
34002
- function isToday(date, today) {
34003
- return isSameCalendarDay(date, today);
34004
- }
34005
- function isSameCalendarDay(left, right) {
34006
- return left.year === right.year && left.month === right.month && left.day === right.day;
34007
- }
34008
- function addDays(date, days) {
34009
- const result = new Date(Date.UTC(date.year, date.month - 1, date.day + days, 12));
34010
- return {
34011
- year: result.getUTCFullYear(),
34012
- month: result.getUTCMonth() + 1,
34013
- day: result.getUTCDate()
34014
- };
34015
- }
34016
- function getMonthCalendarDays(year, month, startOfWeek) {
34017
- const firstDay = {
34018
- year,
34019
- month,
34020
- day: 1
34021
- };
34022
- const dayOfWeek = getDayOfWeek(firstDay);
34023
- const gridStart = addDays(firstDay, -(startOfWeek === "monday" ? dayOfWeek === 0 ? 6 : dayOfWeek - 1 : dayOfWeek));
34024
- return Array.from({ length: 42 }, (_, index) => addDays(gridStart, index));
34025
- }
34026
- function getWeekCalendarDays(baseDate, startOfWeek) {
34027
- const dayOfWeek = getDayOfWeek(baseDate);
34028
- const weekStart = addDays(baseDate, -(startOfWeek === "monday" ? dayOfWeek === 0 ? 6 : dayOfWeek - 1 : dayOfWeek));
34029
- return Array.from({ length: 7 }, (_, index) => addDays(weekStart, index));
34030
- }
34031
- function getDayOfWeek(date) {
34032
- return new Date(Date.UTC(date.year, date.month - 1, date.day, 12)).getUTCDay();
34033
- }
34034
- function getWeekCalendarDayTuple(baseDate, startOfWeek) {
34035
- const days = getWeekCalendarDays(baseDate, startOfWeek);
34036
- assertWeekCalendarDays(days);
34037
- return days;
34038
- }
34039
- function assertWeekCalendarDays(days) {
34040
- if (days.length !== 7) throw new Error("Expected exactly 7 calendar days");
34041
- }
34042
- function getOrderedDayNames(startOfWeek, dayNames = DAY_NAMES) {
34043
- if (startOfWeek === "monday") {
34044
- const sunday = dayNames[0] ?? "Sun";
34045
- return [...dayNames.slice(1), sunday];
34046
- }
34047
- return [...dayNames];
34048
- }
34049
- function formatCalendarTitle(viewDate, view, days, monthNames = MONTH_NAMES) {
34050
- if (view === "week") {
34051
- const weekStart = days[0];
34052
- const weekEnd = days[6];
34053
- if (weekStart === void 0 || weekEnd === void 0) return `${monthNames[viewDate.month - 1] ?? ""} ${viewDate.year}`;
34054
- const startMonth = monthNames[weekStart.month - 1] ?? "";
34055
- const endMonth = monthNames[weekEnd.month - 1] ?? "";
34056
- if (weekStart.month === weekEnd.month) return `${startMonth} ${weekStart.day} - ${weekEnd.day}, ${weekEnd.year}`;
34057
- return `${startMonth} ${weekStart.day} - ${endMonth} ${weekEnd.day}, ${weekEnd.year}`;
34058
- }
34059
- return `${monthNames[viewDate.month - 1] ?? ""} ${viewDate.year}`;
34060
- }
34061
- function buildCalendarViewport(input) {
34062
- const monthNames = input.monthNames ?? MONTH_NAMES;
34063
- if (input.view === "week") {
34064
- const days = getWeekCalendarDayTuple(input.anchorDate, input.startOfWeek);
34065
- return {
34066
- kind: "week",
34067
- anchorDate: input.anchorDate,
34068
- days,
34069
- title: formatCalendarTitle(input.anchorDate, "week", days, monthNames)
34070
- };
34071
- }
34072
- const days = getMonthCalendarDays(input.anchorDate.year, input.anchorDate.month, input.startOfWeek);
34073
- return {
34074
- kind: "month",
34075
- anchorDate: input.anchorDate,
34076
- days,
34077
- title: formatCalendarTitle(input.anchorDate, "month", days, monthNames)
34078
- };
34079
- }
34080
- //#endregion
34081
- //#region ../calendar/src/lane-packing.ts
34082
- /**
34083
- * Assign lanes to a list of intervals.
34084
- *
34085
- * Returns one result per input in the same order as the input. Internally
34086
- * sorts a copy by `(start, end, orderKey)` to ensure determinism, then
34087
- * permutes results back to input order.
34088
- */
34089
- function assignLanes(intervals) {
34090
- if (intervals.length === 0) return [];
34091
- const indexed = intervals.map((interval, index) => ({
34092
- interval,
34093
- index
34094
- }));
34095
- indexed.sort((left, right) => left.interval.start - right.interval.start || left.interval.end - right.interval.end || left.interval.orderKey.localeCompare(right.interval.orderKey));
34096
- const results = new Array(intervals.length);
34097
- let cluster = [];
34098
- let activeLaneEnds = [];
34099
- let clusterMaxEnd = -Infinity;
34100
- const closeCluster = () => {
34101
- if (cluster.length === 0) return;
34102
- const laneCount = Math.max(1, cluster.reduce((max, entry) => Math.max(max, entry.laneIndex + 1), 0));
34103
- for (const entry of cluster) results[entry.index] = {
34104
- laneIndex: entry.laneIndex,
34105
- laneCount
34106
- };
34107
- cluster = [];
34108
- activeLaneEnds = [];
34109
- clusterMaxEnd = -Infinity;
34110
- };
34111
- for (const { interval, index } of indexed) {
34112
- if (interval.start >= clusterMaxEnd) closeCluster();
34113
- const laneIndex = firstAvailableLane(activeLaneEnds, interval.start);
34114
- activeLaneEnds[laneIndex] = interval.end;
34115
- clusterMaxEnd = Math.max(clusterMaxEnd, interval.end);
34116
- cluster.push({
34117
- index,
34118
- laneIndex
34119
- });
34120
- }
34121
- closeCluster();
34122
- return results;
34123
- }
34124
- function firstAvailableLane(activeLaneEnds, start) {
34125
- const laneIndex = activeLaneEnds.findIndex((end) => end <= start);
34126
- return laneIndex >= 0 ? laneIndex : activeLaneEnds.length;
34127
- }
34128
- //#endregion
34129
- //#region ../calendar/src/timezone.ts
34130
- /**
34131
- * Timezone boundary helpers — internal.
34132
- *
34133
- * Both functions read an ISO instant in a given IANA timezone and project it
34134
- * onto the wall clock of that zone. They use Intl.DateTimeFormat with
34135
- * hourCycle: 'h23' to avoid 12/24-hour ambiguity. Used internally by the
34136
- * timetable model and `groupItemsByDate`; not exported from the package's
34137
- * public surface.
34138
- */
34139
- const MINUTES_PER_HOUR = 60;
34140
- /**
34141
- * Local minute-of-day [0, 24*60) for an ISO instant in the given zone.
34142
- * Returns NaN if the inputs cannot be parsed (caller should drop the item).
34143
- */
34144
- function getLocalMinuteOfDay(isoTimestamp, timeZone) {
34145
- const date = new Date(isoTimestamp);
34146
- if (Number.isNaN(date.getTime())) return NaN;
34147
- if (!isValidTimeZone(timeZone)) return NaN;
34148
- const parts = new Intl.DateTimeFormat("en-GB", {
34149
- timeZone,
34150
- hour: "2-digit",
34151
- minute: "2-digit",
34152
- hourCycle: "h23"
34153
- }).formatToParts(date);
34154
- const hourPart = Number(parts.find((part) => part.type === "hour")?.value);
34155
- const minute = Number(parts.find((part) => part.type === "minute")?.value);
34156
- const hour = hourPart === 24 ? 0 : hourPart;
34157
- if (Number.isNaN(hour) || Number.isNaN(minute)) return NaN;
34158
- return hour * MINUTES_PER_HOUR + minute;
34159
- }
34160
- /**
34161
- * Local YYYY-MM-DD for an ISO instant in the given zone.
34162
- * Returns null if the inputs cannot be parsed.
34163
- */
34164
- function getLocalDateKey(isoTimestamp, timeZone) {
34165
- const date = new Date(isoTimestamp);
34166
- if (Number.isNaN(date.getTime())) return null;
34167
- if (!isValidTimeZone(timeZone)) return null;
34168
- const parts = new Intl.DateTimeFormat("en-CA", {
34169
- timeZone,
34170
- year: "numeric",
34171
- month: "2-digit",
34172
- day: "2-digit"
34173
- }).formatToParts(date);
34174
- const year = parts.find((part) => part.type === "year")?.value;
34175
- const month = parts.find((part) => part.type === "month")?.value;
34176
- const day = parts.find((part) => part.type === "day")?.value;
34177
- if (year === void 0 || year.length === 0) return null;
34178
- if (month === void 0 || month.length === 0) return null;
34179
- if (day === void 0 || day.length === 0) return null;
34180
- return `${year}-${month}-${day}`;
34181
- }
34182
- const validTimeZoneCache = /* @__PURE__ */ new Map();
34183
- function isValidTimeZone(timeZone) {
34184
- if (typeof timeZone !== "string" || timeZone.length === 0) return false;
34185
- const cached = validTimeZoneCache.get(timeZone);
34186
- if (cached !== void 0) return cached;
34187
- try {
34188
- new Intl.DateTimeFormat("en-US", { timeZone });
34189
- validTimeZoneCache.set(timeZone, true);
34190
- return true;
34191
- } catch {
34192
- validTimeZoneCache.set(timeZone, false);
34193
- return false;
34194
- }
34195
- }
34196
- //#endregion
34197
- //#region ../calendar/src/timetable.ts
34198
- /**
34199
- * Timetable model — the package's core surface.
34200
- *
34201
- * Generic over `T`. Takes a list of timed items + a viewport; returns a
34202
- * day-by-day layout with overlap-packed lanes and percent-based positions
34203
- * plus a diagnostics list for items dropped during processing.
34204
- *
34205
- * The function is pure: given identical input (including `now`), it returns
34206
- * the same output. The model has no opinion on what an item is — pass
34207
- * accessors.
34208
- */
34209
- const DEFAULT_BOUNDS = {
34210
- minStartHour: 5,
34211
- maxEndHour: 23,
34212
- defaultStartHour: 6,
34213
- defaultEndHour: 21,
34214
- minHeightPercent: 2.5
35181
+ siteId: { $bind: { from: "$root.siteId" } },
35182
+ limit: { $bind: {
35183
+ from: "content.maxEvents",
35184
+ fallback: "3"
35185
+ } },
35186
+ stage: { $bind: {
35187
+ from: "$root.previewStage",
35188
+ fallback: "published"
35189
+ } },
35190
+ detailLevel: "listing",
35191
+ readProfile: "spotlight"
35192
+ },
35193
+ mode: "server"
35194
+ } }
34215
35195
  };
34216
- function buildTimetableModel$1(input) {
34217
- const bounds = resolveBounds(input.bounds);
34218
- const viewportDays = getTimetableDays(input.viewDate, input.view ?? "week", input.startOfWeek);
34219
- const viewportKeys = new Set(viewportDays.map(formatDateKey));
34220
- const parsed = parseAllItems(input.items, input);
34221
- const inViewport = deriveVisibleSegments(parsed.drafts, viewportKeys);
34222
- const hourRange = resolveHourRange(inViewport, bounds);
34223
- const laid = layoutCellsByDate(inViewport, hourRange, bounds);
34224
- return {
34225
- days: assembleDays(viewportDays, laid.cellsByDate, input.today),
34226
- hourTicks: buildHourTicks(hourRange.startHour, hourRange.endHour),
34227
- startHour: hourRange.startHour,
34228
- endHour: hourRange.endHour,
34229
- rangeMinutes: (hourRange.endHour - hourRange.startHour) * 60,
34230
- hasItems: inViewport.length > 0,
34231
- diagnostics: [...parsed.diagnostics, ...laid.diagnostics]
34232
- };
34233
- }
34234
- function parseAllItems(items, accessors) {
34235
- const drafts = [];
34236
- const diagnostics = [];
34237
- for (const item of items) {
34238
- const result = parseDraft(item, accessors);
34239
- if (result.ok) drafts.push(result.draft);
34240
- else diagnostics.push({
34241
- item,
34242
- reason: result.reason
34243
- });
34244
- }
34245
- return {
34246
- drafts,
34247
- diagnostics
34248
- };
34249
- }
34250
- function layoutCellsByDate(segments, hourRange, bounds) {
34251
- const rangeMinutes = (hourRange.endHour - hourRange.startHour) * 60;
34252
- const rangeStartMinutes = hourRange.startHour * 60;
34253
- const rangeEndMinutes = hourRange.endHour * 60;
34254
- const segmentsByDate = groupSegmentsByDate(segments);
34255
- const cellsByDate = /* @__PURE__ */ new Map();
34256
- const diagnostics = [];
34257
- for (const [dateKey, daySegments] of segmentsByDate) {
34258
- const clipped = [];
34259
- for (const segment of daySegments) {
34260
- const result = clipDraft(segment, rangeStartMinutes, rangeEndMinutes);
34261
- if (result.ok) clipped.push(result.clipped);
34262
- else diagnostics.push({
34263
- item: segment.draft.item,
34264
- reason: result.reason
34265
- });
34266
- }
34267
- const laneResults = assignLanes(clipped.map((entry) => ({
34268
- start: entry.visibleStart,
34269
- end: entry.visibleEnd,
34270
- orderKey: entry.segment.draft.orderKey
34271
- })));
34272
- cellsByDate.set(dateKey, clipped.map((entry, idx) => buildCell(entry, laneResults[idx] ?? {
34273
- laneIndex: 0,
34274
- laneCount: 1
34275
- }, rangeStartMinutes, rangeMinutes, bounds.minHeightPercent)));
34276
- }
34277
- return {
34278
- cellsByDate,
34279
- diagnostics
34280
- };
34281
- }
34282
- function assembleDays(viewportDays, cellsByDate, today) {
34283
- return viewportDays.map((date) => {
34284
- const dateKey = formatDateKey(date);
34285
- return {
34286
- date,
34287
- dateKey,
34288
- isToday: isSameCalendarDay(date, today),
34289
- cells: cellsByDate.get(dateKey) ?? []
34290
- };
34291
- });
34292
- }
34293
- function parseDraft(item, accessors) {
34294
- const startsAtIso = accessors.getStartsAt(item);
34295
- const endsAtIso = accessors.getEndsAt(item);
34296
- const timeZone = accessors.getTimeZone(item);
34297
- const startsAtMinutes = getLocalMinuteOfDay(startsAtIso, timeZone);
34298
- if (Number.isNaN(startsAtMinutes)) return {
34299
- ok: false,
34300
- reason: "invalid-start"
34301
- };
34302
- const startDateKey = getLocalDateKey(startsAtIso, timeZone);
34303
- if (startDateKey == null) return {
34304
- ok: false,
34305
- reason: "invalid-timezone"
34306
- };
34307
- const endDateKey = getLocalDateKey(endsAtIso, timeZone);
34308
- if (endDateKey == null) return {
34309
- ok: false,
34310
- reason: "invalid-end"
34311
- };
34312
- const endsAtMinutesRaw = getLocalMinuteOfDay(endsAtIso, timeZone);
34313
- if (Number.isNaN(endsAtMinutesRaw)) return {
34314
- ok: false,
34315
- reason: "invalid-end"
34316
- };
34317
- const spansMultipleDays = startDateKey < endDateKey;
34318
- const endsAtMinutes = spansMultipleDays || endsAtMinutesRaw > startsAtMinutes ? endsAtMinutesRaw : Math.min(1440, startsAtMinutes + 60);
34319
- if (!spansMultipleDays && endsAtMinutes <= startsAtMinutes) return {
34320
- ok: false,
34321
- reason: "non-positive-duration"
34322
- };
34323
- return {
34324
- ok: true,
34325
- draft: {
34326
- item,
34327
- startDateKey,
34328
- endDateKey,
34329
- startsAtMinutes,
34330
- endsAtMinutes,
34331
- color: accessors.getColor?.(item) ?? null,
34332
- orderKey: accessors.getOrderKey?.(item) ?? accessors.getId(item)
34333
- }
34334
- };
34335
- }
34336
- function deriveVisibleSegments(drafts, viewportKeys) {
34337
- const segments = [];
34338
- for (const draft of drafts) for (const dateKey of enumerateDateKeys(draft.startDateKey, draft.endDateKey)) {
34339
- if (!viewportKeys.has(dateKey)) continue;
34340
- const isStartDay = dateKey === draft.startDateKey;
34341
- const isEndDay = dateKey === draft.endDateKey;
34342
- const startsAtMinutes = isStartDay ? draft.startsAtMinutes : 0;
34343
- const endsAtMinutes = isEndDay ? draft.endsAtMinutes : 1440;
34344
- if (endsAtMinutes <= startsAtMinutes) continue;
34345
- segments.push({
34346
- draft,
34347
- dateKey,
34348
- startsAtMinutes,
34349
- endsAtMinutes,
34350
- continuesBefore: !isStartDay,
34351
- continuesAfter: !isEndDay
34352
- });
34353
- }
34354
- return segments;
34355
- }
34356
- function clipDraft(segment, rangeStartMinutes, rangeEndMinutes) {
34357
- const visibleStart = Math.max(rangeStartMinutes, segment.startsAtMinutes);
34358
- const visibleEnd = Math.min(rangeEndMinutes, segment.endsAtMinutes);
34359
- if (visibleEnd <= visibleStart) return {
34360
- ok: false,
34361
- reason: "out-of-visible-hours"
34362
- };
34363
- return {
34364
- ok: true,
34365
- clipped: {
34366
- segment,
34367
- visibleStart,
34368
- visibleEnd
34369
- }
34370
- };
34371
- }
34372
- function buildCell(entry, lane, rangeStartMinutes, rangeMinutes, minHeightPercent) {
34373
- const heightPercent = Math.max(minHeightPercent, toRangePercent(entry.visibleEnd - entry.visibleStart, rangeMinutes));
34374
- const rawTopPercent = toRangePercent(entry.visibleStart - rangeStartMinutes, rangeMinutes);
34375
- const topPercent = Math.min(rawTopPercent, 100 - heightPercent);
34376
- return {
34377
- item: entry.segment.draft.item,
34378
- dateKey: entry.segment.dateKey,
34379
- startsAtMinutes: entry.segment.startsAtMinutes,
34380
- endsAtMinutes: entry.segment.endsAtMinutes,
34381
- visibleStartsAtMinutes: entry.visibleStart,
34382
- visibleEndsAtMinutes: entry.visibleEnd,
34383
- topPercent,
34384
- heightPercent,
34385
- isClippedStart: entry.segment.continuesBefore || entry.segment.startsAtMinutes < entry.visibleStart,
34386
- isClippedEnd: entry.segment.continuesAfter || entry.segment.endsAtMinutes > entry.visibleEnd,
34387
- laneIndex: lane.laneIndex,
34388
- laneCount: lane.laneCount,
34389
- color: entry.segment.draft.color
34390
- };
34391
- }
34392
- function resolveBounds(bounds) {
34393
- return {
34394
- minStartHour: bounds?.minStartHour ?? DEFAULT_BOUNDS.minStartHour,
34395
- maxEndHour: bounds?.maxEndHour ?? DEFAULT_BOUNDS.maxEndHour,
34396
- defaultStartHour: bounds?.defaultStartHour ?? DEFAULT_BOUNDS.defaultStartHour,
34397
- defaultEndHour: bounds?.defaultEndHour ?? DEFAULT_BOUNDS.defaultEndHour,
34398
- minHeightPercent: bounds?.minHeightPercent ?? DEFAULT_BOUNDS.minHeightPercent
34399
- };
34400
- }
34401
- function resolveHourRange(segments, bounds) {
34402
- if (segments.length === 0) return {
34403
- startHour: bounds.defaultStartHour,
34404
- endHour: bounds.defaultEndHour
34405
- };
34406
- const earliestStart = Math.min(...segments.map((segment) => segment.startsAtMinutes));
34407
- const latestEnd = Math.max(...segments.map((segment) => segment.endsAtMinutes));
34408
- const startHour = Math.max(bounds.minStartHour, Math.floor(earliestStart / 60));
34409
- const endHour = Math.min(bounds.maxEndHour, Math.ceil(latestEnd / 60));
34410
- return {
34411
- startHour,
34412
- endHour: Math.max(startHour + 1, endHour)
34413
- };
34414
- }
34415
- function buildHourTicks(startHour, endHour) {
34416
- const ticks = [];
34417
- for (let hour = startHour; hour <= endHour; hour += 1) ticks.push(hour);
34418
- return ticks;
34419
- }
34420
- function groupSegmentsByDate(segments) {
34421
- const grouped = /* @__PURE__ */ new Map();
34422
- for (const segment of segments) {
34423
- const existing = grouped.get(segment.dateKey) ?? [];
34424
- existing.push(segment);
34425
- grouped.set(segment.dateKey, existing);
34426
- }
34427
- return grouped;
34428
- }
34429
- function toRangePercent(minutes, rangeMinutes) {
34430
- if (rangeMinutes <= 0) return 0;
34431
- return minutes / rangeMinutes * 100;
34432
- }
34433
- function getTimetableDays(viewDate, view, startOfWeek) {
34434
- if (view === "day") return [viewDate];
34435
- return getWeekCalendarDays(viewDate, startOfWeek);
34436
- }
34437
- function enumerateDateKeys(startDateKey, endDateKey) {
34438
- if (startDateKey > endDateKey) throw new Error(`Invalid timetable date range: ${startDateKey} is after ${endDateKey}`);
34439
- const start = parseDateKey(startDateKey);
34440
- parseDateKey(endDateKey);
34441
- const keys = [];
34442
- let cursor = start;
34443
- while (formatDateKey(cursor) <= endDateKey) {
34444
- keys.push(formatDateKey(cursor));
34445
- cursor = addDays(cursor, 1);
34446
- }
34447
- return keys;
34448
- }
34449
- function parseDateKey(dateKey) {
34450
- const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(dateKey);
34451
- if (match === null) throw new Error(`Invalid timetable date key: ${dateKey}`);
34452
- const [, year, month, day] = match;
34453
- return {
34454
- year: Number(year),
34455
- month: Number(month),
34456
- day: Number(day)
34457
- };
34458
- }
34459
- //#endregion
34460
- //#region ../calendar/src/group.ts
34461
- /**
34462
- * Group items by their local date (YYYY-MM-DD in the item's own timezone).
34463
- *
34464
- * Generic over `T`. Items with un-parseable startsAt or invalid timezone are
34465
- * silently dropped — call sites that need diagnostics should validate upstream.
34466
- *
34467
- * The output map preserves insertion order of dates (ES2015 spec) and the
34468
- * relative order of items within each date matches input order.
34469
- */
34470
- function groupItemsByDate(items, accessors) {
34471
- const grouped = /* @__PURE__ */ new Map();
34472
- for (const item of items) {
34473
- const dateKey = getLocalDateKey(accessors.getStartsAt(item), accessors.getTimeZone(item));
34474
- if (dateKey == null) continue;
34475
- const existing = grouped.get(dateKey);
34476
- if (existing !== void 0) existing.push(item);
34477
- else grouped.set(dateKey, [item]);
34478
- }
34479
- return grouped;
34480
- }
34481
- //#endregion
34482
- //#region ../blocks/src/system/runtime/nodes/events/shared/eventCapacity.ts
34483
- /**
34484
- * Compute remaining spots for an event from `availableSpots` with a fallback
34485
- * to `capacity - registeredCount`. Returns `{ available: null, isFull: false }`
34486
- * for uncapped events so call sites can render no capacity indicator at all.
34487
- *
34488
- * The single source of truth for capacity math across event cards, agenda
34489
- * rows, and timetable cells — keep wording variations in the call sites.
34490
- */
34491
- function getEventAvailability(event) {
34492
- if (event.capacity == null) return {
34493
- available: null,
34494
- isFull: false
34495
- };
34496
- const available = event.availableSpots ?? event.capacity - event.registeredCount;
34497
- if (available <= 0) return {
34498
- available: 0,
34499
- isFull: true
34500
- };
34501
- return {
34502
- available,
34503
- isFull: false
34504
- };
34505
- }
34506
- /**
34507
- * Derive the three-state availability status (available / filling / waitlist)
34508
- * plus a short pill label. Builds on {@link getEventAvailability} so the
34509
- * capacity math (availableSpots with a `capacity - registeredCount` fallback)
34510
- * stays consistent with the binary helper used elsewhere.
34511
- */
34512
- function getEventAvailabilityStatus(event, options) {
34513
- const threshold = options?.fillingThreshold ?? 2;
34514
- const { available, isFull } = getEventAvailability(event);
34515
- if (available == null) return {
34516
- status: "available",
34517
- available: null,
34518
- isFull: false,
34519
- label: null
34520
- };
34521
- if (isFull) return {
34522
- status: "waitlist",
34523
- available: 0,
34524
- isFull: true,
34525
- label: event.waitlistEnabled ? "Waitlist" : "Full"
34526
- };
34527
- if (available <= threshold) return {
34528
- status: "filling",
34529
- available,
34530
- isFull: false,
34531
- label: `${available} left`
34532
- };
34533
- return {
34534
- status: "available",
34535
- available,
34536
- isFull: false,
34537
- label: `${available} places`
34538
- };
34539
- }
34540
35196
  //#endregion
34541
- //#region ../blocks/src/system/runtime/nodes/events/shared/utils.ts
35197
+ //#region ../blocks/src/system/runtime/nodes/events/shared/listingLayout.ts
34542
35198
  /**
34543
- * Shared utility functions for event blocks
35199
+ * Layout classes for event listing containers and cards. Presentation
35200
+ * plumbing rather than event domain logic, so it lives beside the components.
34544
35201
  */
34545
35202
  /**
34546
35203
  * Column class mappings for grid layouts, derived from the canonical
@@ -34575,180 +35232,6 @@ function getCardOrientation(layout) {
34575
35232
  case "compact": return "vertical";
34576
35233
  }
34577
35234
  }
34578
- /**
34579
- * Format date for display.
34580
- * Returns: "Monday, January 15, 2025"
34581
- */
34582
- function formatDate$2(isoTimestamp, options) {
34583
- return formatBookingDate(toScheduledBookingDateTime({
34584
- startsAt: isoTimestamp,
34585
- timeZone: toBookingTimeZone(options?.timeZone ?? "UTC"),
34586
- locale: options?.locale ?? "en-US"
34587
- }), {
34588
- weekday: "long",
34589
- year: "numeric",
34590
- month: "long",
34591
- day: "numeric"
34592
- });
34593
- }
34594
- /**
34595
- * Format time for display.
34596
- * Returns: "10:00 AM"
34597
- */
34598
- function formatTime(isoTimestamp, options) {
34599
- return formatBookingTime(toScheduledBookingDateTime({
34600
- startsAt: isoTimestamp,
34601
- timeZone: toBookingTimeZone(options?.timeZone ?? "UTC"),
34602
- locale: options?.locale ?? "en-US"
34603
- }), {
34604
- hour: "numeric",
34605
- minute: "2-digit",
34606
- hour12: true
34607
- });
34608
- }
34609
- function formatEventDateParts(args) {
34610
- const scheduledRange = toScheduledBookingDateRange({
34611
- startsAt: args.startsAt,
34612
- endsAt: args.endsAt,
34613
- timeZone: toBookingTimeZone(args.timeZone ?? "UTC"),
34614
- locale: args.locale ?? "en-US"
34615
- });
34616
- const { date, time: start } = formatBookingDateParts(scheduledRange, { dateFormat: {
34617
- weekday: "long",
34618
- year: "numeric",
34619
- month: "long",
34620
- day: "numeric"
34621
- } });
34622
- return {
34623
- date,
34624
- timeRange: `${start} – ${formatBookingTime(toScheduledBookingDateTime({
34625
- startsAt: args.endsAt,
34626
- timeZone: scheduledRange.timeZone,
34627
- locale: args.locale ?? "en-US"
34628
- }))}`
34629
- };
34630
- }
34631
- function formatEventCompactDateRail(isoTimestamp, options) {
34632
- const dateTime = toScheduledBookingDateTime({
34633
- startsAt: isoTimestamp,
34634
- timeZone: toBookingTimeZone(options?.timeZone ?? "UTC"),
34635
- locale: options?.locale ?? "en-US"
34636
- });
34637
- const weekday = formatBookingDate(dateTime, { weekday: "short" }).toLocaleUpperCase(options?.locale ?? "en-US");
34638
- const month = formatBookingDate(dateTime, { month: "short" }).toLocaleUpperCase(options?.locale ?? "en-US");
34639
- return {
34640
- weekday,
34641
- day: formatBookingDate(dateTime, { day: "numeric" }),
34642
- month,
34643
- label: formatBookingDate(dateTime, {
34644
- weekday: "long",
34645
- year: "numeric",
34646
- month: "long",
34647
- day: "numeric"
34648
- })
34649
- };
34650
- }
34651
- function formatTeamMemberNames(names) {
34652
- const normalized = names.map((name) => name.trim()).filter(Boolean);
34653
- if (normalized.length === 0) return null;
34654
- const [firstName, secondName] = normalized;
34655
- if (!firstName) return null;
34656
- if (normalized.length === 1) return firstName;
34657
- if (normalized.length === 2 && secondName) return `${firstName} and ${secondName}`;
34658
- const finalName = normalized[normalized.length - 1] ?? "";
34659
- return `${normalized.slice(0, -1).join(", ")}, and ${finalName}`;
34660
- }
34661
- function formatEventTeacherLine(event) {
34662
- const names = formatTeamMemberNames((event.teamMembers ?? []).map((teamMember) => teamMember.name));
34663
- return names ? `with ${names}` : null;
34664
- }
34665
- /**
34666
- * Group events by their local date (YYYY-MM-DD key in each event's own
34667
- * timezone). Thin domain wrapper over `groupItemsByDate` from the calendar
34668
- * package; preserves the historical `Map<string, PublicEvent[]>` shape.
34669
- */
34670
- function groupEventsByDate(events, calendarTimeZone) {
34671
- const grouped = groupItemsByDate(events, {
34672
- getStartsAt: (event) => event.startsAt,
34673
- getTimeZone: () => calendarTimeZone
34674
- });
34675
- const map = /* @__PURE__ */ new Map();
34676
- for (const [dateKey, bucket] of grouped) map.set(dateKey, [...bucket]);
34677
- return map;
34678
- }
34679
- /**
34680
- * Format event price for display.
34681
- * @returns "Free", "£25", "£12 - £25", "Free / £12 - £25", "Course (From £40)", etc.
34682
- *
34683
- * Events render compact amounts (no trailing ".00"); the actual ADT→string
34684
- * mapping lives in `formatBookingPriceDisplay` in @riverbankcms/core so all
34685
- * surfaces stay consistent.
34686
- */
34687
- function formatEventPrice(event) {
34688
- return formatEventPricing(resolveRuntimeEventPricing(event), { decimals: "auto" });
34689
- }
34690
- function closedEventBookingLabel(reason) {
34691
- switch (reason) {
34692
- case "occurrence_cancelled":
34693
- case "series_cancelled": return "Cancelled";
34694
- case "occurrence_completed":
34695
- case "series_completed": return "Completed";
34696
- case "series_draft":
34697
- case "series_archived":
34698
- case "parent_archived":
34699
- case "parent_inconsistent":
34700
- case "not_public":
34701
- case "already_started":
34702
- case "enrollment_closed": return "Registration closed";
34703
- default: return assertNever$23(reason);
34704
- }
34705
- }
34706
- function resolveEventHref(event) {
34707
- const destination = resolveRuntimeEventDestination(event);
34708
- return destination.kind === "external" ? destination.url : destination.path;
34709
- }
34710
- function resolveEventLinkProps(event) {
34711
- const href = resolveEventHref(event);
34712
- return resolveRuntimeEventDestination(event).kind === "external" ? {
34713
- href,
34714
- target: "_blank",
34715
- rel: "noopener noreferrer"
34716
- } : { href };
34717
- }
34718
- /**
34719
- * Resolve the CTA state for an event.
34720
- * Shared by EventCard and EventCompactRow to keep sold-out, registration-closed,
34721
- * and course-specific defaults consistent.
34722
- */
34723
- function resolveEventCta(event, buttonText) {
34724
- const { isFull: isSoldOut } = getEventAvailability(event);
34725
- const destination = resolveRuntimeEventDestination(event);
34726
- const isExternal = destination.kind === "external";
34727
- const defaultButtonText = isExternal ? destination.label : event.bookingScope === "whole_series" ? "Book course" : "Book";
34728
- const effectiveButtonText = buttonText ?? defaultButtonText;
34729
- const href = resolveEventHref(event);
34730
- if (event.bookingAvailability.kind === "closed") {
34731
- const linksToClosedWholeSeriesDetails = event.bookingAvailability.reason === "enrollment_closed" && !isExternal;
34732
- return {
34733
- label: closedEventBookingLabel(event.bookingAvailability.reason),
34734
- href,
34735
- external: isExternal,
34736
- hidden: !linksToClosedWholeSeriesDetails
34737
- };
34738
- }
34739
- if (!isExternal && isSoldOut) return {
34740
- label: "Sold out",
34741
- href,
34742
- external: false,
34743
- hidden: true
34744
- };
34745
- return {
34746
- label: effectiveButtonText,
34747
- href,
34748
- external: isExternal,
34749
- hidden: false
34750
- };
34751
- }
34752
35235
  //#endregion
34753
35236
  //#region ../blocks/src/system/media/slotContracts.ts
34754
35237
  const DEFAULT_MIN_SOURCE_WIDTH = 1200;
@@ -36044,7 +36527,6 @@ const EventCard = ({ event, cardRole = "default", buttonVariant = "primary", but
36044
36527
  const formattedDate = formatDate$2(event.startsAt, { timeZone });
36045
36528
  const formattedTime = `${formatTime(event.startsAt, { timeZone })} - ${formatTime(event.endsAt, { timeZone })}`;
36046
36529
  const cta = resolveEventCta(event, buttonText);
36047
- const { available: spotsLeft, isFull: isSoldOut } = getEventAvailability(event);
36048
36530
  const cardClass = boxStyleClassNames({ role: cardRole }, { baseClass: "event-card" });
36049
36531
  const buttonClass = themeButtonClassName({
36050
36532
  variant: buttonVariant,
@@ -36060,10 +36542,7 @@ const EventCard = ({ event, cardRole = "default", buttonVariant = "primary", but
36060
36542
  } : null;
36061
36543
  const hasImage = Boolean(showImage && image);
36062
36544
  const teacherLine = formatEventTeacherLine(event);
36063
- const capacityText = showCapacity ? formatCapacityText(event, {
36064
- isSoldOut,
36065
- spotsLeft
36066
- }) : null;
36545
+ const capacityText = showCapacity ? formatEventRemainingPlaces(event, { full: "Sold out" }) : null;
36067
36546
  return /* @__PURE__ */ jsxs("article", {
36068
36547
  className: `${cardClass} group rb-flex rb-flex-col rb-h-full ${eventCardOrientationClass(orientation)}`,
36069
36548
  "data-orientation": orientation,
@@ -36129,7 +36608,11 @@ function EventCardContent(props) {
36129
36608
  }),
36130
36609
  /* @__PURE__ */ jsx("h3", {
36131
36610
  className: "card-title rb-mb-3",
36132
- children: props.title
36611
+ children: /* @__PURE__ */ jsx("a", {
36612
+ className: "event-title-link",
36613
+ ...resolveEventLinkProps(event),
36614
+ children: props.title
36615
+ })
36133
36616
  }),
36134
36617
  /* @__PURE__ */ jsx("div", {
36135
36618
  className: "rb-mb-2 rb-caption",
@@ -36206,13 +36689,6 @@ function eventCardOrientationClass(orientation) {
36206
36689
  case "vertical": return "event-card--vertical";
36207
36690
  }
36208
36691
  }
36209
- function formatCapacityText(event, options) {
36210
- if (event.capacity == null) return null;
36211
- if (options.isSoldOut) return "Sold out";
36212
- if (options.spotsLeft == null) return null;
36213
- const count = options.spotsLeft;
36214
- return `${count} ${count === 1 ? "spot" : "spots"} available`;
36215
- }
36216
36692
  function getCategoryBadgeStyle(background) {
36217
36693
  const normalized = background?.trim() ?? "";
36218
36694
  if (!normalized) return {
@@ -36281,7 +36757,11 @@ const EventCompactRow = ({ event, buttonVariant = "primary", buttonText }) => {
36281
36757
  className: "event-compact-row-content",
36282
36758
  children: [/* @__PURE__ */ jsx("h3", {
36283
36759
  className: "event-compact-row-title",
36284
- children: event.title
36760
+ children: /* @__PURE__ */ jsx("a", {
36761
+ className: "event-title-link",
36762
+ ...resolveEventLinkProps(event),
36763
+ children: event.title
36764
+ })
36285
36765
  }), /* @__PURE__ */ jsxs("div", {
36286
36766
  className: "event-compact-row-meta",
36287
36767
  children: [
@@ -36370,6 +36850,23 @@ function DateTimeTile({ startsAt, endsAt, timeZone }) {
36370
36850
  });
36371
36851
  }
36372
36852
  //#endregion
36853
+ //#region ../blocks/src/system/runtime/nodes/events/shared/OfferingTitle.tsx
36854
+ /**
36855
+ * Heading for an offering card or row. Links to the offering's page when one
36856
+ * exists so a sold-out or closed offering stays reachable (issue 1372); the
36857
+ * `.event-title-link` rule lives in theme-core's event-list.css.
36858
+ */
36859
+ function OfferingTitle({ className, path, title }) {
36860
+ return /* @__PURE__ */ jsx("h3", {
36861
+ className,
36862
+ children: path ? /* @__PURE__ */ jsx("a", {
36863
+ className: "event-title-link",
36864
+ href: path,
36865
+ children: title
36866
+ }) : title
36867
+ });
36868
+ }
36869
+ //#endregion
36373
36870
  //#region ../blocks/src/system/runtime/nodes/events/shared/listOptions.ts
36374
36871
  const eventPageSizeValues = [
36375
36872
  3,
@@ -37714,7 +38211,7 @@ const eventCalendarAdditionalFields = [
37714
38211
  {
37715
38212
  id: "showCapacity",
37716
38213
  type: "boolean",
37717
- label: "Show available spots",
38214
+ label: "Show available places",
37718
38215
  description: "Display remaining capacity on event cards and timetable cells",
37719
38216
  required: false,
37720
38217
  defaultValue: true,
@@ -47464,7 +47961,7 @@ function EventFacts({ selection, selected, venue, teamMembers, pricing, showCapa
47464
47961
  children: [/* @__PURE__ */ jsx("div", {
47465
47962
  className: "rb-label rb-mb-2",
47466
47963
  children: "Places"
47467
- }), /* @__PURE__ */ jsxs("p", { children: [selected.capacity, " places"] })]
47964
+ }), /* @__PURE__ */ jsx("p", { children: formatTotalCapacity(selected.capacity) })]
47468
47965
  }) : null,
47469
47966
  pricing,
47470
47967
  selected.bookingScope === "whole_series" ? /* @__PURE__ */ jsxs("div", {
@@ -48308,9 +48805,10 @@ function CourseCompactRow({ course, path }) {
48308
48805
  }),
48309
48806
  /* @__PURE__ */ jsxs("div", {
48310
48807
  className: "event-compact-row-content",
48311
- children: [/* @__PURE__ */ jsx("h3", {
48808
+ children: [/* @__PURE__ */ jsx(OfferingTitle, {
48312
48809
  className: "event-compact-row-title",
48313
- children: course.title
48810
+ path,
48811
+ title: course.title
48314
48812
  }), /* @__PURE__ */ jsxs("div", {
48315
48813
  className: "event-compact-row-meta",
48316
48814
  children: [
@@ -48367,9 +48865,10 @@ function CourseFeatureCard({ course, path, buttonVariant, buttonText }) {
48367
48865
  children: /* @__PURE__ */ jsxs("div", {
48368
48866
  className: "card-content rb-flex rb-flex-1 rb-flex-col",
48369
48867
  children: [
48370
- /* @__PURE__ */ jsx("h3", {
48868
+ /* @__PURE__ */ jsx(OfferingTitle, {
48371
48869
  className: "card-title rb-mb-3",
48372
- children: course.title
48870
+ path,
48871
+ title: course.title
48373
48872
  }),
48374
48873
  /* @__PURE__ */ jsxs("div", {
48375
48874
  className: "rb-mb-2 rb-caption",
@@ -49225,8 +49724,8 @@ const AgendaEventCard = ({ event, showCapacity, onClick }) => {
49225
49724
  const eventLinkProps = resolveEventLinkProps(event);
49226
49725
  const categoryColor = event.eventCategory?.color || "rgb(var(--tb-accent))";
49227
49726
  const price = formatEventPrice(event);
49228
- const { available, isFull } = getEventAvailability(event);
49229
- const capacity = showCapacity && available != null ? isFull ? "Full" : `${available} spots left` : null;
49727
+ const capacity = showCapacity ? formatEventRemainingPlaces(event) : null;
49728
+ const isFull = isEventFull(event);
49230
49729
  const title = event.title;
49231
49730
  const timeZone = event.timeZone ?? null;
49232
49731
  const teacherLine = formatEventTeacherLine(event);
@@ -49668,7 +50167,7 @@ function buildTimetableModel(input) {
49668
50167
  getEndsAt: (event) => event.endsAt,
49669
50168
  getTimeZone: (event) => event.timeZone ?? "UTC",
49670
50169
  getColor: (event) => event.eventCategory?.color ?? null,
49671
- getOrderKey: (event) => `${event.title}${event.id}`
50170
+ getOrderKey: (event) => `${event.title}\u0000${event.id}`
49672
50171
  });
49673
50172
  return {
49674
50173
  hourTicks: [...generic.hourTicks],
@@ -49705,18 +50204,9 @@ function toEventCell(cell) {
49705
50204
  };
49706
50205
  }
49707
50206
  function formatTimetableCapacity(event) {
49708
- const { available, isFull } = getEventAvailability(event);
49709
- if (available == null) return {
49710
- label: null,
49711
- isFull: false
49712
- };
49713
- if (isFull) return {
49714
- label: "Full",
49715
- isFull: true
49716
- };
49717
50207
  return {
49718
- label: `${available} spots left`,
49719
- isFull: false
50208
+ label: formatEventRemainingPlaces(event, { compact: true }),
50209
+ isFull: isEventFull(event)
49720
50210
  };
49721
50211
  }
49722
50212
  function formatEventTimeRange(event) {
@@ -50192,7 +50682,7 @@ function EventRegistrationSSR({ blockId, subject, children, ...sourceProps }) {
50192
50682
  className: `event-registration ${className ?? ""}`.trim(),
50193
50683
  "data-block": "event-registration",
50194
50684
  "data-rb-island-required": "true",
50195
- "data-rb-island-error-message": "Booking options failed to load. Please refresh the page.",
50685
+ "data-rb-island-error-message": "We couldn’t load the booking options for this event. Please refresh this page and try again. If this keeps happening, contact the organiser for help.",
50196
50686
  "data-rb-island-error-class": "event-registration__hydration-error",
50197
50687
  ...getIslandRootProps("event-registration", blockId),
50198
50688
  children: [
@@ -56102,6 +56592,32 @@ function isSiteStyleTransitionStyleGroup(value) {
56102
56592
  function boundaryMapForAdjacentBlockIds(boundaries) {
56103
56593
  return new Map(boundaries.map((boundary) => [boundaryKey(boundary.previousBlockId, boundary.nextBlockId), boundary]));
56104
56594
  }
56595
+ /** Keep a surviving block's outgoing treatment when omitted blocks change its neighbor. */
56596
+ function boundariesForSurvivingBlocks(boundaries, blocks) {
56597
+ const outgoing = new Map(boundaries.map((boundary) => [boundary.previousBlockId, boundary]));
56598
+ const incoming = new Map(boundaries.map((boundary) => [boundary.nextBlockId, boundary]));
56599
+ return blocks.flatMap((block, index) => {
56600
+ const next = blocks[index + 1];
56601
+ if (!block.id || !next?.id) return [];
56602
+ const boundary = outgoing.get(block.id);
56603
+ if (!boundary) return [];
56604
+ if (boundary.nextBlockId === next.id) return [boundary];
56605
+ const destination = incoming.get(next.id);
56606
+ return [retargetCompiledBoundaryRuntimeForContentAdjacency({
56607
+ ...boundary,
56608
+ boundaryId: `boundary:${block.id}:${next.id}`,
56609
+ nextBlockId: next.id,
56610
+ to: destination?.to ?? boundary.to,
56611
+ cssVars: {
56612
+ ...boundary.cssVars,
56613
+ to: destination?.cssVars.to ?? boundary.cssVars.to
56614
+ }
56615
+ }, {
56616
+ currentContent: block.content,
56617
+ nextContent: next.content
56618
+ })];
56619
+ });
56620
+ }
56105
56621
  function boundaryBetweenBlocks(previousBlock, nextBlock, boundaries) {
56106
56622
  const previousBlockId = previousBlock?.id ?? null;
56107
56623
  const nextBlockId = nextBlock?.id ?? null;
@@ -56203,48 +56719,187 @@ function toEditorPresentationPage(page) {
56203
56719
  };
56204
56720
  }
56205
56721
  //#endregion
56722
+ //#region ../blocks/src/pageRendererBlockSupport.tsx
56723
+ /**
56724
+ * Support for rendering one block section: what a block that cannot render
56725
+ * leaves behind, how an override is found, and the props both override paths
56726
+ * hand to a component. `PageRenderer` owns the page-level composition; this
56727
+ * module owns the pieces it reaches for per block.
56728
+ */
56729
+ function fallback(message) {
56730
+ return /* @__PURE__ */ jsx("div", {
56731
+ className: "rb-p-6 rb-caption",
56732
+ style: textColorStyle("mutedText"),
56733
+ children: message
56734
+ });
56735
+ }
56736
+ /**
56737
+ * A page with nothing to show. Authoring surfaces get the diagnostic; a public
56738
+ * page renders nothing at all rather than telling a visitor about the outline.
56739
+ */
56740
+ function emptyPageNotice(audience) {
56741
+ if (audience === "public") return null;
56742
+ return /* @__PURE__ */ jsx("div", {
56743
+ className: "rb-p-6 rb-caption",
56744
+ style: textColorStyle("mutedText"),
56745
+ children: "No blocks found."
56746
+ });
56747
+ }
56748
+ /**
56749
+ * A public page dropped a block, so nothing in the rendered HTML says the
56750
+ * content is missing. Say it in the logs instead.
56751
+ */
56752
+ function warnOmittedFromPublicPage(input) {
56753
+ try {
56754
+ console.warn("[blocks:PageRenderer] omitted a non-renderable block from a public page", {
56755
+ blockId: input.block.id ?? null,
56756
+ blockKind: input.block.kind,
56757
+ outcome: input.outcome,
56758
+ ...input.admissionError ? { error: input.admissionError } : {}
56759
+ });
56760
+ } catch {}
56761
+ }
56762
+ function shouldDebugCustomBlocks() {
56763
+ try {
56764
+ return globalThis.process?.env?.DEBUG_CUSTOM_BLOCKS === "1";
56765
+ } catch {
56766
+ return false;
56767
+ }
56768
+ }
56769
+ /**
56770
+ * Look up a block override component by block kind.
56771
+ * Supports both full ("block.hero") and short ("hero") keys.
56772
+ */
56773
+ function getBlockOverride(blockKind, overrides) {
56774
+ if (!overrides) return void 0;
56775
+ if (overrides[blockKind]) return overrides[blockKind];
56776
+ const shortKind = blockKind.replace(/^block\./, "");
56777
+ if (overrides[shortKind]) return overrides[shortKind];
56778
+ }
56779
+ /**
56780
+ * Resolves block content based on stage priority.
56781
+ * For draft/preview: draftContent takes priority over content.
56782
+ * For published: content is the source of truth.
56783
+ * Returns empty object if no content exists (validator will handle placeholders if needed).
56784
+ */
56785
+ function getRenderableContent(block, previewStage = "published") {
56786
+ const source = previewStage === "preview" && block.draftContent ? block.draftContent : block.content ?? {};
56787
+ if (!isObjectRecord$1(source)) return {};
56788
+ return source;
56789
+ }
56790
+ function resolvePageRendererImageResolveContext(dataContext) {
56791
+ const input = dataContext?.imageResolveContext;
56792
+ return buildResolveImageUrlContext({
56793
+ siteId: input?.siteId ?? dataContext?.siteId,
56794
+ supabaseUrl: input?.supabaseUrl ?? dataContext?.supabaseUrl,
56795
+ mediaAssetIndex: input?.mediaAssetIndex,
56796
+ mediaAssets: input?.mediaAssets ?? dataContext?.mediaAssets
56797
+ });
56798
+ }
56799
+ /**
56800
+ * The RSC-safe props every override component receives, whether the block
56801
+ * resolved a manifest or is being rendered from an override alone.
56802
+ */
56803
+ function buildBlockOverrideProps(input) {
56804
+ return {
56805
+ content: input.content,
56806
+ ...overrideThemeProps(input.themeResources),
56807
+ ...blockOverrideContextProps(input.dataContext),
56808
+ data: input.data,
56809
+ imageResolveContext: input.imageResolveContext,
56810
+ blockId: input.block.id ?? null,
56811
+ blockKind: input.block.kind
56812
+ };
56813
+ }
56814
+ /** The render-context half of the override props, absent context included. */
56815
+ function blockOverrideContextProps(dataContext) {
56816
+ const context = dataContext ?? {};
56817
+ return {
56818
+ siteId: context.siteId,
56819
+ apiBaseUrl: context.apiBaseUrl,
56820
+ bookingTimeZone: context.bookingTimeZone,
56821
+ portalToken: context.portalToken ?? null,
56822
+ previewToken: context.previewToken ?? null,
56823
+ stage: context.previewStage,
56824
+ occurrenceContext: context.occurrenceContext ?? null,
56825
+ entrySubrouteContext: context.entrySubrouteContext ?? null,
56826
+ contentEntry: context.contentEntry ?? null,
56827
+ pageSurface: context.pageSurface ?? null,
56828
+ subject: context.subject ?? null,
56829
+ supabaseUrl: context.supabaseUrl
56830
+ };
56831
+ }
56832
+ const blockComponentCache = /* @__PURE__ */ new WeakMap();
56833
+ const DEFAULT_PERF_SLOW_BLOCK_RENDER_THRESHOLD_MS = 25;
56834
+ let cachedPerfSlowBlockRenderThresholdMs = null;
56835
+ function getPerfSlowBlockRenderThresholdMs() {
56836
+ if (cachedPerfSlowBlockRenderThresholdMs !== null) return cachedPerfSlowBlockRenderThresholdMs;
56837
+ const raw = globalThis.process?.env?.DEBUG_PERF_BLOCK_MS;
56838
+ const parsed = raw ? Number.parseInt(raw, 10) : NaN;
56839
+ cachedPerfSlowBlockRenderThresholdMs = Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_PERF_SLOW_BLOCK_RENDER_THRESHOLD_MS;
56840
+ return cachedPerfSlowBlockRenderThresholdMs;
56841
+ }
56842
+ function getCachedBlockComponent(manifest) {
56843
+ const cached = blockComponentCache.get(manifest);
56844
+ if (cached) return cached;
56845
+ const created = makeDefaultBlockComponent({ manifest });
56846
+ blockComponentCache.set(manifest, created);
56847
+ return created;
56848
+ }
56849
+ //#endregion
56206
56850
  //#region ../blocks/src/PageRenderer.tsx
56207
56851
  function PageRenderer$1(props) {
56208
56852
  const page = React$1.useMemo(() => toPresentationPage(props.page), [props.page]);
56209
56853
  return /* @__PURE__ */ jsx(PageRendererCore, {
56210
56854
  ...props,
56211
- page
56855
+ page,
56856
+ audience: "public"
56212
56857
  });
56213
56858
  }
56214
56859
  function EditorPageRenderer$1(props) {
56215
56860
  const page = React$1.useMemo(() => toEditorPresentationPage(props.page), [props.page]);
56216
56861
  return /* @__PURE__ */ jsx(PageRendererCore, {
56217
56862
  ...props,
56218
- page
56863
+ page,
56864
+ audience: "authoring"
56219
56865
  });
56220
56866
  }
56221
56867
  function PageRendererCore(props) {
56222
- const { page, runtimeBoundaries, dataContext, wrapBlock, wrapBoundary, registry } = props;
56868
+ const { page, audience, runtimeBoundaries, dataContext, wrapBlock, wrapBoundary, registry } = props;
56223
56869
  const imageResolveContext = React$1.useMemo(() => resolvePageRendererImageResolveContext(dataContext), [
56224
56870
  dataContext?.imageResolveContext,
56225
56871
  dataContext?.mediaAssets,
56226
56872
  dataContext?.siteId,
56227
56873
  dataContext?.supabaseUrl
56228
56874
  ]);
56229
- const boundaryByAdjacentBlockIds = React$1.useMemo(() => boundaryMapForAdjacentBlockIds(runtimeBoundaries ?? []), [runtimeBoundaries]);
56230
56875
  const usesRuntimeBoundaries = runtimeBoundaries !== void 0;
56231
56876
  const themeResources = React$1.useMemo(() => resolvePageThemeResources(props), [
56232
56877
  props.rendering,
56233
56878
  props.theme,
56234
56879
  props.themeTokens
56235
56880
  ]);
56236
- if (!page || page.blocks.length === 0) return /* @__PURE__ */ jsx("div", {
56237
- className: "rb-p-6 rb-caption",
56238
- style: textColorStyle("mutedText"),
56239
- children: "No blocks found."
56240
- });
56241
56881
  const resolvedRegistry = registry ?? getDefaultComponentRegistry();
56242
56882
  setContextResolveImageUrlContext(imageResolveContext);
56243
- return /* @__PURE__ */ jsx("div", { children: page.blocks.map((block, index) => {
56244
- const blockPosition = pageBlockPosition(index, page.blocks.length);
56245
- const blockKey = block.id ?? `${block.kind}-${index}`;
56246
- const previousBlock = page.blocks[index - 1] ?? null;
56247
- const nextBlock = page.blocks[index + 1] ?? null;
56883
+ const sections = usePageSections(props, {
56884
+ page,
56885
+ themeResources,
56886
+ imageResolveContext,
56887
+ registry: resolvedRegistry
56888
+ });
56889
+ const boundaryByAdjacentBlockIds = React$1.useMemo(() => boundaryMapForAdjacentBlockIds(boundariesForSurvivingBlocks(runtimeBoundaries ?? [], sections.map(({ block }) => ({
56890
+ ...block,
56891
+ content: getRenderableContent(block, dataContext?.previewStage)
56892
+ })))), [
56893
+ runtimeBoundaries,
56894
+ sections,
56895
+ dataContext?.previewStage
56896
+ ]);
56897
+ if (sections.length === 0) return emptyPageNotice(audience);
56898
+ return /* @__PURE__ */ jsx("div", { children: sections.map((section, index) => {
56899
+ const { block, blockKey, node } = section;
56900
+ const blockPosition = pageBlockPosition(index, sections.length);
56901
+ const previousBlock = sections[index - 1]?.block ?? null;
56902
+ const nextBlock = sections[index + 1]?.block ?? null;
56248
56903
  const boundaryBefore = boundaryBetweenBlocks(previousBlock, block, boundaryByAdjacentBlockIds);
56249
56904
  const boundaryAfter = boundaryBetweenBlocks(block, nextBlock, boundaryByAdjacentBlockIds);
56250
56905
  const boundaryElement = renderBoundaryAfterBlock({
@@ -56259,19 +56914,7 @@ function PageRendererCore(props) {
56259
56914
  const managesOwnWrapper = definition?.manifest.behaviours?.managesOwnWrapper;
56260
56915
  const renderableContent = getRenderableContent(block, dataContext?.previewStage);
56261
56916
  const variantKey = definition?.manifest ? resolveVariantKey(definition.manifest, renderableContent) : null;
56262
- const shouldUseWrapper = !managesOwnWrapper || Boolean(wrapBlock);
56263
- const blockSectionProps = buildBlockSectionProps({
56264
- props,
56265
- themeResources,
56266
- block,
56267
- blockKey,
56268
- imageResolveContext,
56269
- registry: resolvedRegistry
56270
- });
56271
- if (!shouldUseWrapper) {
56272
- const rendered = /* @__PURE__ */ jsx(MemoBlockSection, { ...blockSectionProps }, `${blockKey}:block`);
56273
- return boundaryElement ? /* @__PURE__ */ jsxs(React$1.Fragment, { children: [rendered, boundaryElement] }, blockKey) : rendered;
56274
- }
56917
+ if (!(!managesOwnWrapper || Boolean(wrapBlock))) return /* @__PURE__ */ jsxs(React$1.Fragment, { children: [node, boundaryElement] }, blockKey);
56275
56918
  const wrapperClassName = [`block-${cleanKind}`, wrapBlock ? "group relative" : null].filter(Boolean).join(" ");
56276
56919
  const styleGroup = typeof renderableContent.styleGroup === "string" ? renderableContent.styleGroup : void 0;
56277
56920
  const blockLook = typeof renderableContent.blockLook === "string" ? renderableContent.blockLook : void 0;
@@ -56296,7 +56939,7 @@ function PageRendererCore(props) {
56296
56939
  "data-site-style-boundary-after": boundaryAfter?.metrics.size,
56297
56940
  "data-style-group": styleGroup,
56298
56941
  style: blockStyle,
56299
- children: /* @__PURE__ */ jsx(MemoBlockSection, { ...blockSectionProps })
56942
+ children: node
56300
56943
  }, blockKey);
56301
56944
  if (boundaryElement) return /* @__PURE__ */ jsxs(React$1.Fragment, { children: [blockElement, boundaryElement] }, blockKey);
56302
56945
  if (usesRuntimeBoundaries || !isSiteStyleTransitionStyleGroup(styleGroup)) return blockElement;
@@ -56309,18 +56952,69 @@ function PageRendererCore(props) {
56309
56952
  })] }, blockKey);
56310
56953
  }) });
56311
56954
  }
56312
- function resolvePageRendererImageResolveContext(dataContext) {
56313
- const input = dataContext?.imageResolveContext;
56314
- return buildResolveImageUrlContext({
56315
- siteId: input?.siteId ?? dataContext?.siteId,
56316
- supabaseUrl: input?.supabaseUrl ?? dataContext?.supabaseUrl,
56317
- mediaAssetIndex: input?.mediaAssetIndex,
56318
- mediaAssets: input?.mediaAssets ?? dataContext?.mediaAssets
56955
+ /**
56956
+ * Memoised so that a re-render with unchanged inputs reuses the same section
56957
+ * elements and React can skip the block subtrees, exactly as the per-block memo
56958
+ * did before public pages started resolving their bodies up front.
56959
+ */
56960
+ function usePageSections(props, input) {
56961
+ const { page, themeResources, imageResolveContext, registry } = input;
56962
+ return React$1.useMemo(() => admitPageSections({
56963
+ props,
56964
+ blocks: page?.blocks ?? [],
56965
+ audience: props.audience,
56966
+ themeResources,
56967
+ imageResolveContext,
56968
+ registry
56969
+ }), [
56970
+ page,
56971
+ props.audience,
56972
+ themeResources,
56973
+ imageResolveContext,
56974
+ registry,
56975
+ props.dataContext,
56976
+ props.routeMap,
56977
+ props.wrapBlock,
56978
+ props.blockOverrides,
56979
+ props.sdkConfig,
56980
+ props.customBlocks,
56981
+ props.previewAuthoring
56982
+ ]);
56983
+ }
56984
+ /**
56985
+ * Resolves each block's rendered body before the page decides what to wrap.
56986
+ *
56987
+ * On a public page the body is resolved eagerly so a block that cannot render
56988
+ * is dropped along with its wrapper, its data attributes, its boundary reserve
56989
+ * space and its style-group sibling — the section body is the single source of
56990
+ * truth for whether the block is on the page (issue 1378). Authoring surfaces
56991
+ * keep the memoised component so the editor's render-loop guards still hold.
56992
+ */
56993
+ function admitPageSections(input) {
56994
+ const sections = [];
56995
+ input.blocks.forEach((block, index) => {
56996
+ const blockKey = block.id ?? `${block.kind}-${index}`;
56997
+ const blockSectionProps = buildBlockSectionProps({
56998
+ props: input.props,
56999
+ themeResources: input.themeResources,
57000
+ block,
57001
+ blockKey,
57002
+ imageResolveContext: input.imageResolveContext,
57003
+ registry: input.registry
57004
+ });
57005
+ const node = input.audience === "public" ? renderBlockSection(blockSectionProps) : /* @__PURE__ */ jsx(MemoBlockSection, { ...blockSectionProps }, `${blockKey}:block`);
57006
+ if (node === null || node === void 0) return;
57007
+ sections.push({
57008
+ block,
57009
+ blockKey,
57010
+ node
57011
+ });
56319
57012
  });
57013
+ return sections;
56320
57014
  }
56321
- const resolveBookingTimeZone = (dataContext) => dataContext?.bookingTimeZone;
56322
57015
  function buildBlockSectionProps(input) {
56323
57016
  return {
57017
+ audience: input.props.audience,
56324
57018
  themeResources: input.themeResources,
56325
57019
  block: input.block,
56326
57020
  dataContext: input.props.dataContext,
@@ -56336,40 +57030,24 @@ function buildBlockSectionProps(input) {
56336
57030
  };
56337
57031
  }
56338
57032
  /**
56339
- * Look up a block override component by block kind.
56340
- * Supports both full ("block.hero") and short ("hero") keys.
56341
- */
56342
- function getBlockOverride(blockKind, overrides) {
56343
- if (!overrides) return void 0;
56344
- if (overrides[blockKind]) return overrides[blockKind];
56345
- const shortKind = blockKind.replace(/^block\./, "");
56346
- if (overrides[shortKind]) return overrides[shortKind];
56347
- }
56348
- const blockComponentCache = /* @__PURE__ */ new WeakMap();
56349
- const DEFAULT_PERF_SLOW_BLOCK_RENDER_THRESHOLD_MS = 25;
56350
- let cachedPerfSlowBlockRenderThresholdMs = null;
56351
- function getPerfSlowBlockRenderThresholdMs() {
56352
- if (cachedPerfSlowBlockRenderThresholdMs !== null) return cachedPerfSlowBlockRenderThresholdMs;
56353
- const raw = globalThis.process?.env?.DEBUG_PERF_BLOCK_MS;
56354
- const parsed = raw ? Number.parseInt(raw, 10) : NaN;
56355
- cachedPerfSlowBlockRenderThresholdMs = Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_PERF_SLOW_BLOCK_RENDER_THRESHOLD_MS;
56356
- return cachedPerfSlowBlockRenderThresholdMs;
56357
- }
56358
- function getCachedBlockComponent(manifest) {
56359
- const cached = blockComponentCache.get(manifest);
56360
- if (cached) return cached;
56361
- const created = makeDefaultBlockComponent({ manifest });
56362
- blockComponentCache.set(manifest, created);
56363
- return created;
56364
- }
56365
- /**
56366
57033
  * Memoized block section component.
56367
57034
  *
56368
57035
  * Custom memoization prevents render loops during editing by explicitly checking
56369
57036
  * referential equality of all props that affect rendering. Standard React.memo
56370
57037
  * wouldn't catch all the render triggers from the editor store.
56371
57038
  */
56372
- const MemoBlockSection = React$1.memo(function MemoBlockSection({ themeResources, block, dataContext, registry, routeMap, wrapBlock, blockOverrides, sdkConfig, customBlocks, imageResolveContext, previewAuthoring }) {
57039
+ const MemoBlockSection = React$1.memo(function MemoBlockSection(props) {
57040
+ return renderBlockSection(props);
57041
+ }, (prev, next) => {
57042
+ return prev.audience === next.audience && prev.block === next.block && prev.registry === next.registry && prev.dataContext === next.dataContext && prev.themeResources === next.themeResources && prev.routeMap === next.routeMap && prev.block.presentationAdmission === next.block.presentationAdmission && prev.wrapBlock === next.wrapBlock && prev.blockOverrides === next.blockOverrides && prev.sdkConfig === next.sdkConfig && prev.customBlocks === next.customBlocks && prev.imageResolveContext === next.imageResolveContext && prev.previewAuthoring === next.previewAuthoring;
57043
+ });
57044
+ /**
57045
+ * Renders a single block body.
57046
+ *
57047
+ * Returns `null` when the block cannot render and the audience is `public`;
57048
+ * the caller treats that as "this block is not on the page".
57049
+ */
57050
+ function renderBlockSection({ audience, themeResources, block, dataContext, registry, routeMap, wrapBlock, blockOverrides, sdkConfig, customBlocks, imageResolveContext, previewAuthoring }) {
56373
57051
  const perfStart = globalThis.process?.env?.DEBUG_PERF === "1" && typeof window === "undefined" ? Date.now() : 0;
56374
57052
  const fallbackImageResolveContext = getCurrentResolveImageUrlContext();
56375
57053
  const resolvedImageResolveContext = imageResolveContext ?? fallbackImageResolveContext;
@@ -56389,19 +57067,30 @@ const MemoBlockSection = React$1.memo(function MemoBlockSection({ themeResources
56389
57067
  } catch {}
56390
57068
  return node;
56391
57069
  }
56392
- if (block.presentationAdmission.kind === "invalid") {
56393
- try {
56394
- if (globalThis.process?.env?.NODE_ENV !== "production") console.warn("[blocks:PageRenderer] block admission failed", {
56395
- blockId: block.id ?? null,
56396
- blockKind: block.kind,
56397
- error: block.presentationAdmission.error
56398
- });
56399
- } catch {}
56400
- return finish(fallback(`Invalid content for block kind "${block.kind}".`), {
56401
- outcome: "invalidContent",
56402
- override: false
57070
+ /**
57071
+ * The one place that decides what a block that cannot render leaves behind.
57072
+ * A public page gets nothing (and a log line, since dropping content on a
57073
+ * 200 is otherwise invisible); authoring surfaces get the diagnostic.
57074
+ */
57075
+ function diagnostic(message, detail) {
57076
+ if (audience !== "public") return fallback(message);
57077
+ warnOmittedFromPublicPage({
57078
+ block,
57079
+ ...detail
56403
57080
  });
57081
+ return null;
57082
+ }
57083
+ function withBlockWrapper(node) {
57084
+ if (node === null) return null;
57085
+ return wrapBlock && block.id ? /* @__PURE__ */ jsx(Fragment, { children: wrapBlock(block.id, node) }) : /* @__PURE__ */ jsx(Fragment, { children: node });
56404
57086
  }
57087
+ if (block.presentationAdmission.kind === "invalid") return finish(diagnostic(`Invalid content for block kind "${block.kind}".`, {
57088
+ outcome: "invalidContent",
57089
+ admissionError: block.presentationAdmission.error
57090
+ }), {
57091
+ outcome: "invalidContent",
57092
+ override: false
57093
+ });
56405
57094
  const OverrideComponent = getBlockOverride(block.kind, blockOverrides);
56406
57095
  requireBlockOverride(themeResources, OverrideComponent, block.kind);
56407
57096
  let definition = getBlockDefinition(block.kind);
@@ -56414,25 +57103,14 @@ const MemoBlockSection = React$1.memo(function MemoBlockSection({ themeResources
56414
57103
  }).findManifest(block.kind)?.descriptor.definition;
56415
57104
  if (!definition) {
56416
57105
  if (OverrideComponent) {
56417
- const rendered = /* @__PURE__ */ jsx(OverrideComponent, {
57106
+ const rendered = /* @__PURE__ */ jsx(OverrideComponent, { ...buildBlockOverrideProps({
57107
+ block,
56418
57108
  content: getRenderableContent(block, dataContext?.previewStage),
56419
- ...overrideThemeProps(themeResources),
56420
57109
  data: void 0,
56421
- siteId: dataContext?.siteId,
56422
- apiBaseUrl: dataContext?.apiBaseUrl,
56423
- portalToken: dataContext?.portalToken ?? null,
56424
- previewToken: dataContext?.previewToken ?? null,
56425
- stage: dataContext?.previewStage,
56426
- occurrenceContext: dataContext?.occurrenceContext ?? null,
56427
- entrySubrouteContext: dataContext?.entrySubrouteContext ?? null,
56428
- contentEntry: dataContext?.contentEntry ?? null,
56429
- pageSurface: dataContext?.pageSurface ?? null,
56430
- subject: dataContext?.subject ?? null,
56431
- supabaseUrl: dataContext?.supabaseUrl,
56432
- imageResolveContext: resolvedImageResolveContext,
56433
- blockId: block.id ?? null,
56434
- blockKind: block.kind
56435
- });
57110
+ themeResources,
57111
+ dataContext,
57112
+ imageResolveContext: resolvedImageResolveContext
57113
+ }) });
56436
57114
  return finish(wrapBlock && block.id ? /* @__PURE__ */ jsx(Fragment, { children: wrapBlock(block.id, rendered) }) : /* @__PURE__ */ jsx(Fragment, { children: rendered }), {
56437
57115
  outcome: "overrideWithoutDefinition",
56438
57116
  override: true,
@@ -56448,8 +57126,7 @@ const MemoBlockSection = React$1.memo(function MemoBlockSection({ themeResources
56448
57126
  dbCustomBlocksCount: customBlocks?.length ?? 0
56449
57127
  });
56450
57128
  } catch {}
56451
- const rendered = fallback(block.kind.startsWith("custom.") ? `No renderer available for block kind "${block.kind}". The custom block definition was not found (missing manifest).` : `No renderer available for block kind "${block.kind}".`);
56452
- return finish(wrapBlock && block.id ? /* @__PURE__ */ jsx(Fragment, { children: wrapBlock(block.id, rendered) }) : /* @__PURE__ */ jsx(Fragment, { children: rendered }), {
57129
+ return finish(withBlockWrapper(diagnostic(block.kind.startsWith("custom.") ? `No renderer available for block kind "${block.kind}". The custom block definition was not found (missing manifest).` : `No renderer available for block kind "${block.kind}".`, { outcome: "missingDefinition" })), {
56453
57130
  outcome: "missingDefinition",
56454
57131
  override: false
56455
57132
  });
@@ -56464,8 +57141,7 @@ const MemoBlockSection = React$1.memo(function MemoBlockSection({ themeResources
56464
57141
  manifestKeys: Object.keys(definition.manifest ?? {}).sort()
56465
57142
  });
56466
57143
  } catch {}
56467
- const rendered = fallback(block.kind.startsWith("custom.") ? `No renderer available for block kind "${block.kind}". This custom block has no layout/variants, so it must be rendered via blockOverrides["${block.kind}"].` : `No renderer available for block kind "${block.kind}".`);
56468
- return finish(wrapBlock && block.id ? /* @__PURE__ */ jsx(Fragment, { children: wrapBlock(block.id, rendered) }) : /* @__PURE__ */ jsx(Fragment, { children: rendered }), {
57144
+ return finish(withBlockWrapper(diagnostic(block.kind.startsWith("custom.") ? `No renderer available for block kind "${block.kind}". This custom block has no layout/variants, so it must be rendered via blockOverrides["${block.kind}"].` : `No renderer available for block kind "${block.kind}".`, { outcome: "missingLayout" })), {
56469
57145
  outcome: "missingLayout",
56470
57146
  override: false
56471
57147
  });
@@ -56475,39 +57151,24 @@ const MemoBlockSection = React$1.memo(function MemoBlockSection({ themeResources
56475
57151
  if (block.presentationAdmission.kind === "editor") {
56476
57152
  const draft = validateManifestDraft(compileManifestContentContract(definition.manifest), content);
56477
57153
  if (draft.kind === "previewable") data = draft.data;
56478
- else {
56479
- const rendered = fallback(`Unable to render block kind "${block.kind}".`);
56480
- return finish(wrapBlock && block.id ? /* @__PURE__ */ jsx(Fragment, { children: wrapBlock(block.id, rendered) }) : /* @__PURE__ */ jsx(Fragment, { children: rendered }), {
56481
- outcome: "invalidPlaceholder",
56482
- override: false
56483
- });
56484
- }
57154
+ else return finish(withBlockWrapper(diagnostic(`Unable to render block kind "${block.kind}".`, { outcome: "invalidPlaceholder" })), {
57155
+ outcome: "invalidPlaceholder",
57156
+ override: false
57157
+ });
56485
57158
  }
56486
57159
  try {
56487
57160
  const routes = routeMap ?? dataContext?.routes;
56488
57161
  const hydratedContent = routes ? hydrateManifestLinks(definition.manifest, data, routes, dataContext?.pagesByIdentifier, dataContext?.entriesByIdentifier) : data;
56489
57162
  const resolved = resolveBlockData$1(block.id, definition.dataLoaders, dataContext, routes);
56490
57163
  const hasResolvedData = !!resolved && typeof resolved === "object" && Object.keys(resolved).length > 0;
56491
- const baseProps = {
57164
+ const baseProps = buildBlockOverrideProps({
57165
+ block,
56492
57166
  content: hydratedContent,
56493
- ...overrideThemeProps(themeResources),
56494
57167
  data: resolved,
56495
- siteId: dataContext?.siteId,
56496
- apiBaseUrl: dataContext?.apiBaseUrl,
56497
- portalToken: dataContext?.portalToken ?? null,
56498
- previewToken: dataContext?.previewToken ?? null,
56499
- stage: dataContext?.previewStage,
56500
- occurrenceContext: dataContext?.occurrenceContext ?? null,
56501
- entrySubrouteContext: dataContext?.entrySubrouteContext ?? null,
56502
- contentEntry: dataContext?.contentEntry ?? null,
56503
- pageSurface: dataContext?.pageSurface ?? null,
56504
- subject: dataContext?.subject ?? null,
56505
- supabaseUrl: dataContext?.supabaseUrl,
56506
- imageResolveContext: resolvedImageResolveContext,
56507
- blockId: block.id ?? null,
56508
- blockKind: block.kind
56509
- };
56510
- baseProps.bookingTimeZone = resolveBookingTimeZone(dataContext);
57168
+ themeResources,
57169
+ dataContext,
57170
+ imageResolveContext: resolvedImageResolveContext
57171
+ });
56511
57172
  let rendered;
56512
57173
  if (OverrideComponent) rendered = /* @__PURE__ */ jsx(OverrideComponent, { ...baseProps });
56513
57174
  else {
@@ -56554,39 +57215,11 @@ const MemoBlockSection = React$1.memo(function MemoBlockSection({ themeResources
56554
57215
  hasResolvedData
56555
57216
  });
56556
57217
  } catch {
56557
- const rendered = block.presentationAdmission.kind === "editor" ? fallback(`Unable to render block kind "${block.kind}".`) : fallback(`Invalid content for block kind "${block.kind}".`);
56558
- return finish(wrapBlock && block.id ? /* @__PURE__ */ jsx(Fragment, { children: wrapBlock(block.id, rendered) }) : /* @__PURE__ */ jsx(Fragment, { children: rendered }), {
57218
+ return finish(withBlockWrapper(diagnostic(block.presentationAdmission.kind === "editor" ? `Unable to render block kind "${block.kind}".` : `Invalid content for block kind "${block.kind}".`, { outcome: "exception" })), {
56559
57219
  outcome: "exception",
56560
57220
  override: false
56561
57221
  });
56562
57222
  }
56563
- }, (prev, next) => {
56564
- return prev.block === next.block && prev.registry === next.registry && prev.dataContext === next.dataContext && prev.themeResources === next.themeResources && prev.routeMap === next.routeMap && prev.block.presentationAdmission === next.block.presentationAdmission && prev.wrapBlock === next.wrapBlock && prev.blockOverrides === next.blockOverrides && prev.sdkConfig === next.sdkConfig && prev.customBlocks === next.customBlocks && prev.imageResolveContext === next.imageResolveContext && prev.previewAuthoring === next.previewAuthoring;
56565
- });
56566
- function fallback(message) {
56567
- return /* @__PURE__ */ jsx("div", {
56568
- className: "rb-p-6 rb-caption",
56569
- style: textColorStyle("mutedText"),
56570
- children: message
56571
- });
56572
- }
56573
- function shouldDebugCustomBlocks() {
56574
- try {
56575
- return globalThis.process?.env?.DEBUG_CUSTOM_BLOCKS === "1";
56576
- } catch {
56577
- return false;
56578
- }
56579
- }
56580
- /**
56581
- * Resolves block content based on stage priority.
56582
- * For draft/preview: draftContent takes priority over content.
56583
- * For published: content is the source of truth.
56584
- * Returns empty object if no content exists (validator will handle placeholders if needed).
56585
- */
56586
- function getRenderableContent(block, previewStage = "published") {
56587
- const source = previewStage === "preview" && block.draftContent ? block.draftContent : block.content ?? {};
56588
- if (!isObjectRecord$1(source)) return {};
56589
- return source;
56590
57223
  }
56591
57224
  //#endregion
56592
57225
  //#region ../theme-core/src/generated/containerResponsiveThemeCss.ts
@@ -65512,6 +66145,21 @@ const containerResponsiveThemeCss = `/*
65512
66145
  outline-offset: 2px;
65513
66146
  }
65514
66147
 
66148
+ /* Event titles always link to the event page, so a card or row whose CTA is
66149
+ hidden (sold out, closed) is never a dead end (issue 1372). The link inherits
66150
+ the heading's colour and only reveals itself on hover or keyboard focus.
66151
+ Shared by EventCard, EventCompactRow and the offerings-preview course cards via \`.event-title-link\`. */
66152
+ :where(.theme-scope) .event-title-link {
66153
+ color: inherit;
66154
+ text-decoration: none;
66155
+ }
66156
+
66157
+ :where(.theme-scope) .event-title-link:hover,
66158
+ :where(.theme-scope) .event-title-link:focus-visible {
66159
+ text-decoration: underline;
66160
+ text-underline-offset: 0.12em;
66161
+ }
66162
+
65515
66163
  /* =============================================================================
65516
66164
  * Block: Carousel Navigation (Phase 3 client component migration)
65517
66165
  * =============================================================================
@@ -73838,6 +74486,83 @@ const SDK_ENDPOINT_DEFINITIONS = defineEndpointDefinitions({
73838
74486
  errors: ["validation:invalid_input", "server:internal_error"]
73839
74487
  }
73840
74488
  });
74489
+ //#endregion
74490
+ //#region ../api/src/sdkMediaEndpointDefinitions.ts
74491
+ const SDK_MEDIA_ENDPOINT_DEFINITIONS = {
74492
+ sdkMediaExists: {
74493
+ path: "/sdk/{siteId}/media/exists",
74494
+ method: "POST",
74495
+ errors: ["validation:invalid_input"],
74496
+ auth: "service",
74497
+ responseKind: "json"
74498
+ },
74499
+ sdkMediaFindByChecksum: {
74500
+ path: "/sdk/{siteId}/media/find-by-checksum",
74501
+ method: "POST",
74502
+ errors: ["server:internal_error"],
74503
+ auth: "service",
74504
+ responseKind: "json"
74505
+ },
74506
+ sdkMediaResolve: {
74507
+ path: "/sdk/{siteId}/media/resolve",
74508
+ method: "POST",
74509
+ errors: ["server:internal_error"],
74510
+ auth: "service",
74511
+ responseKind: "json"
74512
+ },
74513
+ sdkMediaList: {
74514
+ path: "/sdk/{siteId}/media",
74515
+ method: "GET",
74516
+ auth: "service",
74517
+ responseKind: "json"
74518
+ },
74519
+ sdkMediaDownload: {
74520
+ path: "/sdk/{siteId}/media/{identifier}/download",
74521
+ method: "GET",
74522
+ auth: "service",
74523
+ responseKind: "json"
74524
+ },
74525
+ sdkMediaDeletePreview: {
74526
+ path: "/sdk/{siteId}/media/{identifier}/delete-preview",
74527
+ method: "POST",
74528
+ auth: "service",
74529
+ responseKind: "json"
74530
+ },
74531
+ sdkMediaDeleteApply: {
74532
+ path: "/sdk/{siteId}/media/{identifier}",
74533
+ method: "DELETE",
74534
+ auth: "service",
74535
+ responseKind: "json"
74536
+ },
74537
+ sdkMediaUploadUrl: {
74538
+ path: "/sdk/{siteId}/media/upload-url",
74539
+ method: "POST",
74540
+ errors: [
74541
+ "resource:not_found",
74542
+ "validation:invalid_input",
74543
+ "resource:conflict",
74544
+ "validation:payload_too_large",
74545
+ "billing:plan_limit_exceeded",
74546
+ "server:internal_error"
74547
+ ],
74548
+ auth: "service",
74549
+ responseKind: "json"
74550
+ },
74551
+ sdkMediaUploadFinalize: {
74552
+ path: "/sdk/{siteId}/media/upload",
74553
+ method: "POST",
74554
+ errors: [
74555
+ "resource:not_found",
74556
+ "validation:invalid_input",
74557
+ "resource:conflict",
74558
+ "validation:payload_too_large",
74559
+ "billing:plan_limit_exceeded",
74560
+ "server:internal_error"
74561
+ ],
74562
+ auth: "service",
74563
+ responseKind: "json"
74564
+ }
74565
+ };
73841
74566
  ({
73842
74567
  ...ACCOUNT_ENDPOINT_DEFINITIONS,
73843
74568
  ...PUBLIC_EVENT_ENDPOINT_DEFINITIONS,
@@ -73846,7 +74571,7 @@ const SDK_ENDPOINT_DEFINITIONS = defineEndpointDefinitions({
73846
74571
  ...NEWSLETTER_ENDPOINT_DEFINITIONS,
73847
74572
  ...APPOINTMENT_ENDPOINT_DEFINITIONS,
73848
74573
  ...EVENT_SERIES_REGISTRATION_ENDPOINT_DEFINITIONS
73849
- }, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.siteById, TAG_TEMPLATES.siteBySlug, TAG_TEMPLATES.siteByDomain, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.siteById, TAG_TEMPLATES.siteBySlug, TAG_TEMPLATES.siteByDomain, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.pageSurfaceById, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.pageSurfaceById, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.siteRoutableContent, TAG_TEMPLATES.pageSurfaceById, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.siteRoutableContent, TAG_TEMPLATES.pageSurfaceById, TAG_TEMPLATES.workflowRunById, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.siteRoutableContent, TAG_TEMPLATES.routeById, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.site, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.siteNavigation, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.siteNavigation, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.siteNavigation, TAG_TEMPLATES.navigationMenuById, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.siteNavigation, TAG_TEMPLATES.navigationMenuById, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.siteNavigation, TAG_TEMPLATES.navigationMenuById, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.siteNavigation, TAG_TEMPLATES.navigationMenuById, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.siteNavigation, TAG_TEMPLATES.navigationMenuById, TAG_TEMPLATES.navigationItemById, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.siteNavigation, TAG_TEMPLATES.navigationMenuById, TAG_TEMPLATES.navigationItemById, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.siteNavigation, TAG_TEMPLATES.navigationMenuById, TAG_TEMPLATES.siteRoutableContent, TAG_TEMPLATES.siteRoutableContent, TAG_TEMPLATES.siteRoutableContent, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.siteTheme, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.siteTheme, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.siteTheme, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.pageSurfaceById, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.pageSurfaceById, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.pageSurfaceById, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.pageSurfaceById, TAG_TEMPLATES.siteRoutableContent, TAG_TEMPLATES.pageById, TAG_TEMPLATES.siteRoutableContent, TAG_TEMPLATES.pageById, TAG_TEMPLATES.siteRoutableContent, TAG_TEMPLATES.pageById, TAG_TEMPLATES.siteRoutableContent, TAG_TEMPLATES.pageById, TAG_TEMPLATES.siteRoutableContent, TAG_TEMPLATES.pageById, TAG_TEMPLATES.siteRoutableContent, TAG_TEMPLATES.pageById, TAG_TEMPLATES.siteRoutableContent, TAG_TEMPLATES.pageById, TAG_TEMPLATES.siteRoutableContent, TAG_TEMPLATES.pageById, TAG_TEMPLATES.siteRoutableContent, TAG_TEMPLATES.pageById, TAG_TEMPLATES.siteRoutableContent, TAG_TEMPLATES.siteRoutableContent, TAG_TEMPLATES.siteRedirects, TAG_TEMPLATES.siteRouteOutcomes, TAG_TEMPLATES.siteRoutableContent, TAG_TEMPLATES.siteRedirects, TAG_TEMPLATES.siteRouteOutcomes, TAG_TEMPLATES.siteRoutableContent, TAG_TEMPLATES.siteForms, TAG_TEMPLATES.siteForms, TAG_TEMPLATES.siteForms, TAG_TEMPLATES.siteForms, TAG_TEMPLATES.siteForms, TAG_TEMPLATES.siteForms, TAG_TEMPLATES.formSubmissionsBySlug, TAG_TEMPLATES.siteForms, TAG_TEMPLATES.formSubmissionsBySlug, TAG_TEMPLATES.siteForms, TAG_TEMPLATES.formSubmissionsBySlug, TAG_TEMPLATES.formById, TAG_TEMPLATES.siteForms, TAG_TEMPLATES.publicEvents, TAG_TEMPLATES.publicEventFilters, { ...PUBLIC_WHOLE_SERIES_ENDPOINT_DEFINITIONS }, TAG_TEMPLATES.publicTeamMembers, TAG_TEMPLATES.publicWholeSeriesOfferings, TAG_TEMPLATES.publicEvents, TAG_TEMPLATES.publicPasses, TAG_TEMPLATES.publicMemberships, TAG_TEMPLATES.publicProducts, TAG_TEMPLATES.publicShop, TAG_TEMPLATES.publicCheckout, TAG_TEMPLATES.siteRoutableContent, TAG_TEMPLATES.publicEvents, TAG_TEMPLATES.publicEvents, TAG_TEMPLATES.publicEvents, TAG_TEMPLATES.publicEvents, TAG_TEMPLATES.publicEvents, { ...EVENT_MANAGEMENT_ENDPOINT_DEFINITIONS }, { ...SDK_ENDPOINT_DEFINITIONS }, { ...SDK_ENTRY_READ_ENDPOINTS };
74574
+ }, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.siteById, TAG_TEMPLATES.siteBySlug, TAG_TEMPLATES.siteByDomain, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.siteById, TAG_TEMPLATES.siteBySlug, TAG_TEMPLATES.siteByDomain, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.pageSurfaceById, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.pageSurfaceById, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.siteRoutableContent, TAG_TEMPLATES.pageSurfaceById, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.siteRoutableContent, TAG_TEMPLATES.pageSurfaceById, TAG_TEMPLATES.workflowRunById, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.siteRoutableContent, TAG_TEMPLATES.routeById, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.site, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.site, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.siteNavigation, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.siteNavigation, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.siteNavigation, TAG_TEMPLATES.navigationMenuById, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.siteNavigation, TAG_TEMPLATES.navigationMenuById, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.siteNavigation, TAG_TEMPLATES.navigationMenuById, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.siteNavigation, TAG_TEMPLATES.navigationMenuById, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.siteNavigation, TAG_TEMPLATES.navigationMenuById, TAG_TEMPLATES.navigationItemById, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.siteNavigation, TAG_TEMPLATES.navigationMenuById, TAG_TEMPLATES.navigationItemById, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.siteNavigation, TAG_TEMPLATES.navigationMenuById, TAG_TEMPLATES.siteRoutableContent, TAG_TEMPLATES.siteRoutableContent, TAG_TEMPLATES.siteRoutableContent, TAG_TEMPLATES.siteSettings, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.siteTheme, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.siteTheme, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.siteTheme, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.pageSurfaceById, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.pageSurfaceById, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.pageSurfaceById, TAG_TEMPLATES.siteCollection, TAG_TEMPLATES.pageSurfaceById, TAG_TEMPLATES.siteRoutableContent, TAG_TEMPLATES.pageById, TAG_TEMPLATES.siteRoutableContent, TAG_TEMPLATES.pageById, TAG_TEMPLATES.siteRoutableContent, TAG_TEMPLATES.pageById, TAG_TEMPLATES.siteRoutableContent, TAG_TEMPLATES.pageById, TAG_TEMPLATES.siteRoutableContent, TAG_TEMPLATES.pageById, TAG_TEMPLATES.siteRoutableContent, TAG_TEMPLATES.pageById, TAG_TEMPLATES.siteRoutableContent, TAG_TEMPLATES.pageById, TAG_TEMPLATES.siteRoutableContent, TAG_TEMPLATES.pageById, TAG_TEMPLATES.siteRoutableContent, TAG_TEMPLATES.pageById, TAG_TEMPLATES.siteRoutableContent, TAG_TEMPLATES.siteRoutableContent, TAG_TEMPLATES.siteRedirects, TAG_TEMPLATES.siteRouteOutcomes, TAG_TEMPLATES.siteRoutableContent, TAG_TEMPLATES.siteRedirects, TAG_TEMPLATES.siteRouteOutcomes, TAG_TEMPLATES.siteRoutableContent, TAG_TEMPLATES.siteForms, TAG_TEMPLATES.siteForms, TAG_TEMPLATES.siteForms, TAG_TEMPLATES.siteForms, TAG_TEMPLATES.siteForms, TAG_TEMPLATES.siteForms, TAG_TEMPLATES.formSubmissionsBySlug, TAG_TEMPLATES.siteForms, TAG_TEMPLATES.formSubmissionsBySlug, TAG_TEMPLATES.siteForms, TAG_TEMPLATES.formSubmissionsBySlug, TAG_TEMPLATES.formById, TAG_TEMPLATES.siteForms, TAG_TEMPLATES.publicEvents, TAG_TEMPLATES.publicEventFilters, { ...PUBLIC_WHOLE_SERIES_ENDPOINT_DEFINITIONS }, TAG_TEMPLATES.publicTeamMembers, TAG_TEMPLATES.publicWholeSeriesOfferings, TAG_TEMPLATES.publicEvents, TAG_TEMPLATES.publicPasses, TAG_TEMPLATES.publicMemberships, TAG_TEMPLATES.publicProducts, TAG_TEMPLATES.publicShop, TAG_TEMPLATES.publicCheckout, TAG_TEMPLATES.siteRoutableContent, TAG_TEMPLATES.publicEvents, TAG_TEMPLATES.publicEvents, TAG_TEMPLATES.publicEvents, TAG_TEMPLATES.publicEvents, TAG_TEMPLATES.publicEvents, { ...EVENT_MANAGEMENT_ENDPOINT_DEFINITIONS }, { ...SDK_MEDIA_ENDPOINT_DEFINITIONS }, { ...SDK_ENDPOINT_DEFINITIONS }, { ...SDK_ENTRY_READ_ENDPOINTS };
73850
74575
  z$1.object({ refreshToken: z$1.string().min(1).max(4096) }).strict();
73851
74576
  z$1.object({
73852
74577
  accessToken: z$1.string().min(1).max(4096),
@@ -74499,6 +75224,50 @@ z$1.object({
74499
75224
  title: z$1.string(),
74500
75225
  slug: z$1.string().min(1)
74501
75226
  });
75227
+ z$1.object({
75228
+ signedUrl: z$1.string().url(),
75229
+ token: z$1.string().min(1),
75230
+ storagePath: z$1.string().min(1)
75231
+ }).strict();
75232
+ const backupManifestSchema = z$1.object({
75233
+ siteId: z$1.string().min(1),
75234
+ siteSlug: z$1.string().min(1),
75235
+ siteTitle: z$1.string(),
75236
+ exportedAt: z$1.string().min(1),
75237
+ tableCount: z$1.number().int().nonnegative(),
75238
+ totalRecords: z$1.number().int().nonnegative(),
75239
+ pageCount: z$1.number().int().nonnegative(),
75240
+ blockCount: z$1.number().int().nonnegative(),
75241
+ contentEntryCount: z$1.number().int().nonnegative(),
75242
+ formCount: z$1.number().int().nonnegative(),
75243
+ navigationMenuCount: z$1.number().int().nonnegative(),
75244
+ mediaCount: z$1.number().int().nonnegative(),
75245
+ mediaBytes: z$1.number().int().nonnegative()
75246
+ }).strict();
75247
+ z$1.object({
75248
+ ok: z$1.literal(true),
75249
+ manifest: backupManifestSchema,
75250
+ storagePath: z$1.string().min(1)
75251
+ }).strict();
75252
+ z$1.object({
75253
+ ok: z$1.literal(true),
75254
+ siteId: brandedIdSchema(parseSiteId, "site id"),
75255
+ slug: z$1.string().min(1),
75256
+ title: z$1.string(),
75257
+ counts: z$1.record(z$1.string(), z$1.object({
75258
+ created: z$1.number().int().nonnegative(),
75259
+ updated: z$1.number().int().nonnegative(),
75260
+ skipped: z$1.number().int().nonnegative(),
75261
+ failed: z$1.number().int().nonnegative()
75262
+ }).strict()),
75263
+ warnings: z$1.array(z$1.string()),
75264
+ errors: z$1.array(z$1.object({
75265
+ table: z$1.string(),
75266
+ id: z$1.string(),
75267
+ error: z$1.string()
75268
+ }).strict())
75269
+ }).strict();
75270
+ z$1.object({ deleted: z$1.boolean() }).strict();
74502
75271
  const apiKeyIdSchema = brandedIdSchema(parseApiKeyId, "API key id");
74503
75272
  const standardKeyIdsSchema = z$1.object({
74504
75273
  live: apiKeyIdSchema.optional(),
@@ -78860,9 +79629,7 @@ function admitMaterializedSdkTemplateBlockContent(input) {
78860
79629
  const baseContent = sanitizeSdkRuntimeBlockContent(input.block.blockKind, input.block.content ?? {});
78861
79630
  const bindings = input.block.bindings ?? {};
78862
79631
  const materialized = input.block.scope === "entry" && Object.keys(bindings).length > 0 ? applyBindingsToContent(baseContent, bindings, input.bindingContext) : baseContent;
78863
- const contract = compileManifestContentContract(input.manifestOrFields);
78864
- const stored = admitStoredManifestContent(contract, materialized);
78865
- return stored.ok ? deriveRenderableManifestContent(contract, stored.value, (content) => content) : stored;
79632
+ return admitTransportedRenderableManifestContent(compileManifestContentContract(input.manifestOrFields), materialized);
78866
79633
  }
78867
79634
  function createTemplateRenderTarget(source, templateId) {
78868
79635
  switch (source.kind) {
@@ -78994,13 +79761,7 @@ function admitEntrySlotBlock(block, customBlocks) {
78994
79761
  if (transportedAdmission) return invalidRenderableBlock(base, transportedAdmission.error);
78995
79762
  const manifest = resolveSdkBlockManifest(block.kind, customBlocks);
78996
79763
  if (manifest === null) return invalidRenderableBlock(base, { kind: "manifest_unavailable" });
78997
- const contract = compileManifestContentContract(manifest);
78998
- const stored = admitStoredManifestContent(contract, block.content ?? {});
78999
- if (!stored.ok) return invalidRenderableBlock(base, {
79000
- kind: "invalid_manifest_content",
79001
- error: stored.error
79002
- });
79003
- const renderable = deriveRenderableManifestContent(contract, stored.value, (content) => content);
79764
+ const renderable = admitTransportedRenderableManifestContent(compileManifestContentContract(manifest), block.content ?? {});
79004
79765
  return renderable.ok ? {
79005
79766
  ...base,
79006
79767
  admission: {