@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;
@@ -14472,6 +14492,23 @@ function defineLookOptionGroup(group) {
14472
14492
  defaultOption(group);
14473
14493
  return group;
14474
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
+ }
14475
14512
  function defaultOption(group) {
14476
14513
  const option = group.options.find((candidate) => candidate.id === group.defaultOptionId);
14477
14514
  if (option) return option;
@@ -15362,6 +15399,12 @@ function cardRoleFromLegacyVocabulary(value) {
15362
15399
  */
15363
15400
  const cardRoleWithLegacyAliasSchema = z$1.preprocess((value) => cardRoleFromLegacyVocabulary(value) ?? value, cardRoleSchema);
15364
15401
  //#endregion
15402
+ //#region ../theme-core/src/site-styles/compilationPlan.ts
15403
+ const defaultHeaderBrandPreferences = {
15404
+ brandDisplay: "auto",
15405
+ logoSize: "auto"
15406
+ };
15407
+ //#endregion
15365
15408
  //#region ../theme-core/src/site-styles/sectionTransitions.ts
15366
15409
  function boundaryRuntimeMetrics(family, size, heightExpression) {
15367
15410
  return {
@@ -17743,6 +17786,12 @@ const overlayLogoAccommodations = [
17743
17786
  inlineMarkWithTitleLogoAccommodation,
17744
17787
  unbackedOverlayLogoAccommodation
17745
17788
  ];
17789
+ const allCuratedLogoAccommodations = [
17790
+ ...inlineClassicLogoAccommodations,
17791
+ ...centeredLogoAccommodations,
17792
+ ...overlayLogoAccommodations
17793
+ ];
17794
+ new Set(allCuratedLogoAccommodations.filter((option) => option.providesBacking === true).map((option) => option.pattern));
17746
17795
  const headerLayoutCatalog = defineExactCatalog()([
17747
17796
  {
17748
17797
  id: asHeaderLayoutId("classic"),
@@ -17892,9 +17941,50 @@ defineLookOptionGroup({
17892
17941
  {
17893
17942
  id: "secondary",
17894
17943
  label: "Secondary"
17944
+ },
17945
+ {
17946
+ id: headerInteractionQuietPalette,
17947
+ label: "Text only"
17895
17948
  }
17896
17949
  ]
17897
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
+ }
17898
17988
  function asFooterLookId(value) {
17899
17989
  return value;
17900
17990
  }
@@ -18603,6 +18693,37 @@ function uniformSectionForeground(token) {
18603
18693
  supportingColor: token
18604
18694
  };
18605
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
+ }
18606
18727
  //#endregion
18607
18728
  //#region ../theme-core/src/site-styles/curatedSiteStyles.ts
18608
18729
  const SOFT_EARTH_SITE_STYLE_ID_VALUE = "site-style:soft-earth";
@@ -20310,6 +20431,142 @@ const blockDesignFallback = {
20310
20431
  };
20311
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;
20312
20433
  //#endregion
20434
+ //#region ../theme-core/src/site-styles/siteChromeBackgrounds.ts
20435
+ const siteChromeBackgroundColorTokens = toSiteChromeBackgroundColorTokens(backgroundAppearanceColorTokenNames);
20436
+ const quickSiteChromeBackgroundColorTokens = toSiteChromeBackgroundColorTokens(backgroundAppearanceQuickColorTokenNames);
20437
+ siteChromeBackgroundColorTokens.filter((token) => !quickSiteChromeBackgroundColorTokens.includes(token));
20438
+ function parseSiteChromeBackgroundColorToken(value) {
20439
+ const token = parseBackgroundAppearanceColorToken(value);
20440
+ return token ? token : null;
20441
+ }
20442
+ function toSiteChromeBackgroundColorTokens(tokenNames) {
20443
+ return tokenNames.flatMap((name) => {
20444
+ const token = parseSiteChromeBackgroundColorToken(name);
20445
+ return token ? [token] : [];
20446
+ });
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
+ };
20467
+ /**
20468
+ * Applies the user's nav font-role/size overrides on top of a style-resolved
20469
+ * nav link style. This is where the precedence contract lands:
20470
+ *
20471
+ * - The nav typography TREATMENT (case/tracking identity: plain / heading /
20472
+ * quiet-caps / brand-caps) is ALWAYS owned by the active header style and is
20473
+ * baked into `style` before this call (see `applyNavTypography` in
20474
+ * headerLayoutStyles.ts).
20475
+ * - Each treatment resolves through a DEFAULT font role: `heading` resolves
20476
+ * through the heading role; `plain` / `quiet-caps` / `brand-caps` resolve
20477
+ * through the body role.
20478
+ * - The user's `navFontRole` (surfaced here as `preferences.typography`) only
20479
+ * overrides which font ROLE the treatment resolves through. It never changes
20480
+ * the treatment. `'inherit'` keeps the treatment's default role.
20481
+ */
20482
+ function mergeHeaderNavTypographyPreference(style, preferences) {
20483
+ return preferences.typography === "inherit" && preferences.fontSize === "inherit" ? style : {
20484
+ ...style,
20485
+ ...preferences.typography === "inherit" ? {} : { typography: preferences.typography },
20486
+ ...preferences.fontSize === "inherit" ? {} : { textSize: preferences.fontSize }
20487
+ };
20488
+ }
20489
+ function readHeaderNavFontSizePreference(value) {
20490
+ return value === "sm" || value === "base" || value === "lg" || value === "xl" ? value : "inherit";
20491
+ }
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
20313
20570
  //#region ../theme-core/src/site-styles/generatedSchemas.ts
20314
20571
  function uniqueNonEmpty(values) {
20315
20572
  const uniqueValues = [...new Set(values)];
@@ -20378,6 +20635,76 @@ z$1.object({
20378
20635
  blocks: z$1.array(generatedPageDesignBlockIntentSchema)
20379
20636
  }).strict();
20380
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
20381
20708
  //#region ../theme-core/src/site-styles/pageDesignAuthority.ts
20382
20709
  const nonEmptyStringSchema = z$1.string().min(1);
20383
20710
  const blockContentVersionIdSchema = nonEmptyStringSchema.transform(asBlockContentVersionId);
@@ -20479,46 +20806,6 @@ function collectBlockSequenceIssues(snapshots) {
20479
20806
  return issues;
20480
20807
  }
20481
20808
  //#endregion
20482
- //#region ../theme-core/src/site-styles/siteChromeBackgrounds.ts
20483
- const siteChromeBackgroundColorTokens = toSiteChromeBackgroundColorTokens(backgroundAppearanceColorTokenNames);
20484
- const quickSiteChromeBackgroundColorTokens = toSiteChromeBackgroundColorTokens(backgroundAppearanceQuickColorTokenNames);
20485
- siteChromeBackgroundColorTokens.filter((token) => !quickSiteChromeBackgroundColorTokens.includes(token));
20486
- function parseSiteChromeBackgroundColorToken(value) {
20487
- const token = parseBackgroundAppearanceColorToken(value);
20488
- return token ? token : null;
20489
- }
20490
- function toSiteChromeBackgroundColorTokens(tokenNames) {
20491
- return tokenNames.flatMap((name) => {
20492
- const token = parseSiteChromeBackgroundColorToken(name);
20493
- return token ? [token] : [];
20494
- });
20495
- }
20496
- /**
20497
- * Applies the user's nav font-role/size overrides on top of a style-resolved
20498
- * nav link style. This is where the precedence contract lands:
20499
- *
20500
- * - The nav typography TREATMENT (case/tracking identity: plain / heading /
20501
- * quiet-caps / brand-caps) is ALWAYS owned by the active header style and is
20502
- * baked into `style` before this call (see `applyNavTypography` in
20503
- * headerLayoutStyles.ts).
20504
- * - Each treatment resolves through a DEFAULT font role: `heading` resolves
20505
- * through the heading role; `plain` / `quiet-caps` / `brand-caps` resolve
20506
- * through the body role.
20507
- * - The user's `navFontRole` (surfaced here as `preferences.typography`) only
20508
- * overrides which font ROLE the treatment resolves through. It never changes
20509
- * the treatment. `'inherit'` keeps the treatment's default role.
20510
- */
20511
- function mergeHeaderNavTypographyPreference(style, preferences) {
20512
- return preferences.typography === "inherit" && preferences.fontSize === "inherit" ? style : {
20513
- ...style,
20514
- ...preferences.typography === "inherit" ? {} : { typography: preferences.typography },
20515
- ...preferences.fontSize === "inherit" ? {} : { textSize: preferences.fontSize }
20516
- };
20517
- }
20518
- function readHeaderNavFontSizePreference(value) {
20519
- return value === "sm" || value === "base" || value === "lg" || value === "xl" ? value : "inherit";
20520
- }
20521
- //#endregion
20522
20809
  //#region ../theme-core/src/site-styles/tokenRecipes.ts
20523
20810
  const themeTokenRecipeOptions = {
20524
20811
  palette: [
@@ -32342,7 +32629,7 @@ const eventDisplayFields = [
32342
32629
  {
32343
32630
  id: "showCapacity",
32344
32631
  type: "boolean",
32345
- label: "Show available spots",
32632
+ label: "Show available places",
32346
32633
  description: "Display remaining capacity",
32347
32634
  required: false,
32348
32635
  defaultValue: true,
@@ -32667,303 +32954,1273 @@ function planEventRegistrationScope(input) {
32667
32954
  };
32668
32955
  }
32669
32956
  //#endregion
32670
- //#region ../blocks/src/system/runtime/nodes/events/shared/destination.ts
32671
- function isRecord$24(value) {
32672
- return typeof value === "object" && value !== null && !Array.isArray(value);
32673
- }
32674
- function isRuntimeEventDestination(value) {
32675
- if (!isRecord$24(value) || typeof value.kind !== "string") return false;
32676
- if (value.kind === "native") return (value.mode === "cms-managed" || value.mode === "system-managed") && typeof value.path === "string";
32677
- return value.kind === "external" && typeof value.url === "string" && typeof value.label === "string";
32678
- }
32679
- function resolveRuntimeEventDestination(event) {
32680
- if (isRuntimeEventDestination(event.destination)) return event.destination;
32681
- return {
32682
- kind: "native",
32683
- mode: event.eventDestinationMode === "system-managed" ? "system-managed" : "cms-managed",
32684
- path: typeof event.path === "string" ? event.path : ""
32685
- };
32686
- }
32687
- //#endregion
32688
- //#region ../blocks/src/system/runtime/nodes/events/shared/pricing.ts
32689
- const DEFAULT_EVENT_CURRENCY = "USD";
32690
- function resolveRuntimeEventPricing(event) {
32691
- if (isPublicEventPricing(event.pricing)) return event.pricing;
32692
- if (resolveRuntimeEventDestination(event).kind === "external") return {
32693
- kind: "external",
32694
- display: {
32695
- kind: "external",
32696
- label: "External registration"
32697
- }
32698
- };
32699
- const legacy = event;
32700
- const currency = readCurrency(legacy.currency);
32701
- if (event.requiresRegistration === false) return {
32702
- kind: "drop_in",
32703
- currency,
32704
- display: {
32705
- kind: "free",
32706
- currency
32707
- }
32708
- };
32709
- if (legacy.pricingMode === "ticket_types") return {
32710
- kind: "ticket_types",
32711
- currency,
32712
- ticketTypes: Array.isArray(legacy.ticketTypes) ? legacy.ticketTypes.filter(isPublicEventTicketType) : [],
32713
- display: readTicketTypePriceDisplay(legacy.priceDisplay, currency)
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
32714
32970
  };
32715
- const amount = typeof legacy.price === "number" ? legacy.price : 0;
32716
- if (legacy.pricingType === "paid" && amount > 0) return {
32717
- kind: "single_paid",
32718
- amount,
32719
- currency,
32720
- display: {
32721
- kind: "exact",
32722
- amount,
32723
- currency
32724
- }
32971
+ const available = event.availableSpots ?? event.capacity - event.registeredCount;
32972
+ if (available <= 0) return {
32973
+ available: 0,
32974
+ isFull: true
32725
32975
  };
32726
32976
  return {
32727
- kind: "free_registration",
32728
- currency,
32729
- display: {
32730
- kind: "free",
32731
- currency
32732
- }
32977
+ available,
32978
+ isFull: false
32733
32979
  };
32734
32980
  }
32735
- function readCurrency(value) {
32736
- return typeof value === "string" && value.trim().length > 0 ? value : DEFAULT_EVENT_CURRENCY;
32737
- }
32738
- function isRecord$23(value) {
32739
- return typeof value === "object" && value !== null && !Array.isArray(value);
32740
- }
32741
- function isPublicEventPricing(value) {
32742
- if (!isRecord$23(value) || typeof value.kind !== "string") return false;
32743
- return value.kind === "drop_in" || value.kind === "free_registration" || value.kind === "single_paid" || value.kind === "sliding_scale" || value.kind === "ticket_types" || value.kind === "whole_series" || value.kind === "external";
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;
32744
32995
  }
32745
- function isPublicEventTicketType(value) {
32746
- return isRecord$23(value) && typeof value.id === "string";
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;
32747
33006
  }
32748
- function readTicketTypePriceDisplay(value, currency) {
32749
- const display = readLegacyPriceDisplay(value, currency);
32750
- return display.kind === "whole_series" || display.kind === "external" ? {
32751
- kind: "unavailable",
32752
- currency
32753
- } : display;
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));
32754
33021
  }
32755
- function readLegacyPriceDisplay(value, currency) {
32756
- if (!isRecord$23(value) || typeof value.kind !== "string") return {
32757
- kind: "unavailable",
32758
- currency
32759
- };
32760
- const displayCurrency = readCurrency(value.currency);
32761
- switch (value.kind) {
32762
- case "free": return {
32763
- kind: "free",
32764
- currency: displayCurrency
32765
- };
32766
- case "exact": return readAmount(value.amount) !== null ? {
32767
- kind: "exact",
32768
- amount: readAmount(value.amount) ?? 0,
32769
- currency: displayCurrency
32770
- } : {
32771
- kind: "unavailable",
32772
- currency
32773
- };
32774
- case "from": return readAmount(value.amount) !== null ? {
32775
- kind: "from",
32776
- amount: readAmount(value.amount) ?? 0,
32777
- currency: displayCurrency
32778
- } : {
32779
- kind: "unavailable",
32780
- currency
32781
- };
32782
- case "range": {
32783
- const minAmount = readAmount(value.minAmount);
32784
- const maxAmount = readAmount(value.maxAmount);
32785
- return minAmount !== null && maxAmount !== null ? {
32786
- kind: "range",
32787
- minAmount,
32788
- maxAmount,
32789
- currency: displayCurrency
32790
- } : {
32791
- kind: "unavailable",
32792
- currency
32793
- };
32794
- }
32795
- case "mixed": return readAmount(value.paidFromAmount) !== null ? {
32796
- kind: "mixed",
32797
- paidFromAmount: readAmount(value.paidFromAmount) ?? 0,
32798
- paidToAmount: readAmount(value.paidToAmount) ?? readAmount(value.paidFromAmount) ?? 0,
32799
- currency: displayCurrency
32800
- } : {
32801
- kind: "unavailable",
32802
- currency
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
32803
33069
  };
32804
- case "unavailable": return {
32805
- kind: "unavailable",
32806
- currency: displayCurrency
33070
+ case "open": return {
33071
+ status: "available",
33072
+ available: getEventAvailability(source).available,
33073
+ isFull: false,
33074
+ label
32807
33075
  };
32808
- case "whole_series": return isRecord$23(value.display) ? {
32809
- kind: "whole_series",
32810
- display: readCoursePriceDisplay(value.display, displayCurrency)
32811
- } : {
32812
- kind: "whole_series",
32813
- display: {
32814
- kind: "unavailable",
32815
- currency: displayCurrency
32816
- }
33076
+ case "low": return {
33077
+ status: "filling",
33078
+ available: remaining.remaining,
33079
+ isFull: false,
33080
+ label
32817
33081
  };
32818
- default: return {
32819
- kind: "unavailable",
32820
- currency
33082
+ case "full": return {
33083
+ status: "waitlist",
33084
+ available: 0,
33085
+ isFull: true,
33086
+ label
32821
33087
  };
32822
33088
  }
32823
33089
  }
32824
- function readCoursePriceDisplay(value, currency) {
32825
- switch (value.kind) {
32826
- case "free": return {
32827
- kind: "free",
32828
- currency: readCurrency(value.currency)
32829
- };
32830
- case "exact": return readAmount(value.amount) !== null ? {
32831
- kind: "exact",
32832
- amount: readAmount(value.amount) ?? 0,
32833
- currency: readCurrency(value.currency)
32834
- } : {
32835
- kind: "unavailable",
32836
- currency
32837
- };
32838
- case "from": return readAmount(value.amount) !== null ? {
32839
- kind: "from",
32840
- amount: readAmount(value.amount) ?? 0,
32841
- currency: readCurrency(value.currency)
32842
- } : {
32843
- kind: "unavailable",
32844
- currency
32845
- };
32846
- default: return {
32847
- kind: "unavailable",
32848
- currency
32849
- };
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;
32850
33123
  }
32851
33124
  }
32852
- function readAmount(value) {
32853
- return typeof value === "number" && Number.isFinite(value) ? value : null;
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;
32854
33130
  }
32855
- const eventCalendarDisplayModeValues = [
32856
- "combined",
32857
- "calendar",
32858
- "list",
32859
- "timetable",
32860
- "week-board"
32861
- ];
32862
- const eventCalendarDefaultViewValues = ["month", "week"];
32863
- const eventCalendarListLayoutValues = [
32864
- "stack",
32865
- "grid",
32866
- "compact"
32867
- ];
32868
- const eventCalendarListLayoutOptions = [
32869
- {
32870
- id: "stack",
32871
- label: "Stack"
32872
- },
32873
- {
32874
- id: "grid",
32875
- label: "Grid"
32876
- },
32877
- {
32878
- id: "compact",
32879
- label: "Compact"
32880
- }
32881
- ];
32882
- const eventCalendarListLayoutCompatibleDisplayModeValues = ["combined", "list"];
32883
- function isEventCalendarDisplayMode(value) {
32884
- return typeof value === "string" && eventCalendarDisplayModeValues.includes(value);
33131
+ /** True when `candidate` has less room left than `than`. */
33132
+ function isScarcer(candidate, than) {
33133
+ return isMoreOpen(than, candidate);
32885
33134
  }
32886
- function isEventCalendarDefaultView(value) {
32887
- return typeof value === "string" && eventCalendarDefaultViewValues.includes(value);
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;
32888
33145
  }
32889
- function isEventCalendarListLayout(value) {
32890
- return typeof value === "string" && eventCalendarListLayoutValues.includes(value);
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;
32891
33149
  }
32892
- //#endregion
32893
- //#region ../blocks/src/system/runtime/nodes/shared/contract-schemas.ts
32894
- const previewStageSchema = z$1.enum(["preview", "published"]);
32895
- const registrationButtonVariantSchema = blockButtonRoleSchema;
32896
- const eventButtonVariantSchema = blockButtonRoleSchema;
32897
- const eventLayoutSchema = z$1.enum(eventCalendarListLayoutValues);
32898
- const eventCalendarDisplayModeSchema = z$1.enum(eventCalendarDisplayModeValues);
32899
- const eventCalendarDefaultViewSchema = z$1.enum(eventCalendarDefaultViewValues);
32900
- const columnOptionSchema = z$1.enum([
32901
- "2",
32902
- "3",
32903
- "4"
32904
- ]);
32905
- const weekStartSchema = z$1.enum(["sunday", "monday"]);
32906
- const filterAutoShowSchema = z$1.enum([
32907
- "auto",
32908
- "always",
32909
- "never"
32910
- ]);
32911
- const calendarTabViewsSchema = z$1.enum([
32912
- "both",
32913
- "month",
32914
- "week"
32915
- ]);
32916
33150
  /**
32917
- * Determine which wizard steps to show based on context and payment requirements.
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.
32918
33170
  *
32919
- * @param hasPreSelectedContext - Whether event is pre-selected (skips event selection)
32920
- * @param needsPaymentStep - Whether user must complete payment method selection
32921
- * @returns Array of wizard steps to display
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.
32922
33191
  */
32923
- function getWizardStepsForContext(hasPreSelectedContext, needsPaymentStep) {
32924
- const baseSteps = hasPreSelectedContext ? ["select-tickets", "enter-details"] : [
32925
- "select-event",
32926
- "select-tickets",
32927
- "enter-details"
32928
- ];
32929
- if (!needsPaymentStep) return baseSteps;
32930
- return [...baseSteps, "select-payment"];
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(" · ");
32931
33194
  }
32932
- //#endregion
32933
- //#region ../blocks/src/system/runtime/nodes/event-registration/registrationDerivations.ts
32934
- function eventMatchesOccurrenceId(event, occurrenceId) {
32935
- return event.registrationSubject.kind === "occurrence" && (event.registrationSubject.occurrenceId === occurrenceId || event.selectionId === occurrenceId);
33195
+ function namedModePlaces(mode, label) {
33196
+ return `${ATTENDANCE_MODE_LABEL[mode]}: ${label ?? PLACES_AVAILABLE}`;
32936
33197
  }
32937
- function findPreSelectedEvent(input) {
32938
- const occurrenceContext = input.occurrenceContext;
32939
- if (!occurrenceContext) return input.scopedEvents.length === 1 ? input.scopedEvents[0] ?? null : null;
32940
- const contextDate = getBookingLocalDateKey(toScheduledBookingDateTime({
32941
- startsAt: occurrenceContext.startsAt,
32942
- timeZone: occurrenceContext.timeZone
32943
- }));
32944
- return input.scopedEvents.find((event) => eventMatchesOccurrenceId(event, occurrenceContext.id) || getBookingLocalDateKey(toScheduledBookingDateTime({
32945
- startsAt: event.startsAt,
32946
- timeZone: event.timeZone
32947
- })) === contextDate) ?? null;
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));
32948
33217
  }
32949
- function resolveEventRequiresPayment(input) {
32950
- if (input.eligibility) return input.eligibility.requiresPayment;
32951
- if (input.selectedTicketType) return input.selectedTicketType.priceAmount > 0;
32952
- if (!input.event) return false;
32953
- return publicEventPricingType(resolveRuntimeEventPricing(input.event)) === "paid";
33218
+ function rowFor(rows, mode) {
33219
+ return mode === "online" ? rows.online : rows.inPerson;
32954
33220
  }
32955
33221
  //#endregion
32956
- //#region ../blocks/src/system/runtime/nodes/event-registration/registrationEventAdapter.ts
32957
- function toRegistrationEvent(event) {
32958
- return {
32959
- ...event,
32960
- pricing: resolveRuntimeEventPricing(event),
32961
- selectionId: event.id,
32962
- registrationSubject: {
32963
- kind: "occurrence",
32964
- occurrenceId: event.occurrenceId
32965
- }
32966
- };
33222
+ //#region ../blocks/src/system/runtime/nodes/events/domain/destination.ts
33223
+ function isRecord$24(value) {
33224
+ return typeof value === "object" && value !== null && !Array.isArray(value);
33225
+ }
33226
+ function isRuntimeEventDestination(value) {
33227
+ if (!isRecord$24(value) || typeof value.kind !== "string") return false;
33228
+ if (value.kind === "native") return (value.mode === "cms-managed" || value.mode === "system-managed") && typeof value.path === "string";
33229
+ return value.kind === "external" && typeof value.url === "string" && typeof value.label === "string";
33230
+ }
33231
+ function resolveRuntimeEventDestination(event) {
33232
+ if (isRuntimeEventDestination(event.destination)) return event.destination;
33233
+ return {
33234
+ kind: "native",
33235
+ mode: event.eventDestinationMode === "system-managed" ? "system-managed" : "cms-managed",
33236
+ path: typeof event.path === "string" ? event.path : ""
33237
+ };
33238
+ }
33239
+ //#endregion
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
33935
+ const DEFAULT_EVENT_CURRENCY = "USD";
33936
+ function resolveRuntimeEventPricing(event) {
33937
+ if (isPublicEventPricing(event.pricing)) return event.pricing;
33938
+ if (resolveRuntimeEventDestination(event).kind === "external") return {
33939
+ kind: "external",
33940
+ display: {
33941
+ kind: "external",
33942
+ label: "External registration"
33943
+ }
33944
+ };
33945
+ const legacy = event;
33946
+ const currency = readCurrency(legacy.currency);
33947
+ if (event.requiresRegistration === false) return {
33948
+ kind: "drop_in",
33949
+ currency,
33950
+ display: {
33951
+ kind: "free",
33952
+ currency
33953
+ }
33954
+ };
33955
+ if (legacy.pricingMode === "ticket_types") return {
33956
+ kind: "ticket_types",
33957
+ currency,
33958
+ ticketTypes: Array.isArray(legacy.ticketTypes) ? legacy.ticketTypes.filter(isPublicEventTicketType) : [],
33959
+ display: readTicketTypePriceDisplay(legacy.priceDisplay, currency)
33960
+ };
33961
+ const amount = typeof legacy.price === "number" ? legacy.price : 0;
33962
+ if (legacy.pricingType === "paid" && amount > 0) return {
33963
+ kind: "single_paid",
33964
+ amount,
33965
+ currency,
33966
+ display: {
33967
+ kind: "exact",
33968
+ amount,
33969
+ currency
33970
+ }
33971
+ };
33972
+ return {
33973
+ kind: "free_registration",
33974
+ currency,
33975
+ display: {
33976
+ kind: "free",
33977
+ currency
33978
+ }
33979
+ };
33980
+ }
33981
+ function readCurrency(value) {
33982
+ return typeof value === "string" && value.trim().length > 0 ? value : DEFAULT_EVENT_CURRENCY;
33983
+ }
33984
+ function isRecord$23(value) {
33985
+ return typeof value === "object" && value !== null && !Array.isArray(value);
33986
+ }
33987
+ function isPublicEventPricing(value) {
33988
+ if (!isRecord$23(value) || typeof value.kind !== "string") return false;
33989
+ return value.kind === "drop_in" || value.kind === "free_registration" || value.kind === "single_paid" || value.kind === "sliding_scale" || value.kind === "ticket_types" || value.kind === "whole_series" || value.kind === "external";
33990
+ }
33991
+ function isPublicEventTicketType(value) {
33992
+ return isRecord$23(value) && typeof value.id === "string";
33993
+ }
33994
+ function readTicketTypePriceDisplay(value, currency) {
33995
+ const display = readLegacyPriceDisplay(value, currency);
33996
+ return display.kind === "whole_series" || display.kind === "external" ? {
33997
+ kind: "unavailable",
33998
+ currency
33999
+ } : display;
34000
+ }
34001
+ function readLegacyPriceDisplay(value, currency) {
34002
+ if (!isRecord$23(value) || typeof value.kind !== "string") return {
34003
+ kind: "unavailable",
34004
+ currency
34005
+ };
34006
+ const displayCurrency = readCurrency(value.currency);
34007
+ switch (value.kind) {
34008
+ case "free": return {
34009
+ kind: "free",
34010
+ currency: displayCurrency
34011
+ };
34012
+ case "exact": return readAmount(value.amount) !== null ? {
34013
+ kind: "exact",
34014
+ amount: readAmount(value.amount) ?? 0,
34015
+ currency: displayCurrency
34016
+ } : {
34017
+ kind: "unavailable",
34018
+ currency
34019
+ };
34020
+ case "from": return readAmount(value.amount) !== null ? {
34021
+ kind: "from",
34022
+ amount: readAmount(value.amount) ?? 0,
34023
+ currency: displayCurrency
34024
+ } : {
34025
+ kind: "unavailable",
34026
+ currency
34027
+ };
34028
+ case "range": {
34029
+ const minAmount = readAmount(value.minAmount);
34030
+ const maxAmount = readAmount(value.maxAmount);
34031
+ return minAmount !== null && maxAmount !== null ? {
34032
+ kind: "range",
34033
+ minAmount,
34034
+ maxAmount,
34035
+ currency: displayCurrency
34036
+ } : {
34037
+ kind: "unavailable",
34038
+ currency
34039
+ };
34040
+ }
34041
+ case "mixed": return readAmount(value.paidFromAmount) !== null ? {
34042
+ kind: "mixed",
34043
+ paidFromAmount: readAmount(value.paidFromAmount) ?? 0,
34044
+ paidToAmount: readAmount(value.paidToAmount) ?? readAmount(value.paidFromAmount) ?? 0,
34045
+ currency: displayCurrency
34046
+ } : {
34047
+ kind: "unavailable",
34048
+ currency
34049
+ };
34050
+ case "unavailable": return {
34051
+ kind: "unavailable",
34052
+ currency: displayCurrency
34053
+ };
34054
+ case "whole_series": return isRecord$23(value.display) ? {
34055
+ kind: "whole_series",
34056
+ display: readCoursePriceDisplay(value.display, displayCurrency)
34057
+ } : {
34058
+ kind: "whole_series",
34059
+ display: {
34060
+ kind: "unavailable",
34061
+ currency: displayCurrency
34062
+ }
34063
+ };
34064
+ default: return {
34065
+ kind: "unavailable",
34066
+ currency
34067
+ };
34068
+ }
34069
+ }
34070
+ function readCoursePriceDisplay(value, currency) {
34071
+ switch (value.kind) {
34072
+ case "free": return {
34073
+ kind: "free",
34074
+ currency: readCurrency(value.currency)
34075
+ };
34076
+ case "exact": return readAmount(value.amount) !== null ? {
34077
+ kind: "exact",
34078
+ amount: readAmount(value.amount) ?? 0,
34079
+ currency: readCurrency(value.currency)
34080
+ } : {
34081
+ kind: "unavailable",
34082
+ currency
34083
+ };
34084
+ case "from": return readAmount(value.amount) !== null ? {
34085
+ kind: "from",
34086
+ amount: readAmount(value.amount) ?? 0,
34087
+ currency: readCurrency(value.currency)
34088
+ } : {
34089
+ kind: "unavailable",
34090
+ currency
34091
+ };
34092
+ default: return {
34093
+ kind: "unavailable",
34094
+ currency
34095
+ };
34096
+ }
34097
+ }
34098
+ function readAmount(value) {
34099
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
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
+ }
34112
+ const eventCalendarDisplayModeValues = [
34113
+ "combined",
34114
+ "calendar",
34115
+ "list",
34116
+ "timetable",
34117
+ "week-board"
34118
+ ];
34119
+ const eventCalendarDefaultViewValues = ["month", "week"];
34120
+ const eventCalendarListLayoutValues = [
34121
+ "stack",
34122
+ "grid",
34123
+ "compact"
34124
+ ];
34125
+ const eventCalendarListLayoutOptions = [
34126
+ {
34127
+ id: "stack",
34128
+ label: "Stack"
34129
+ },
34130
+ {
34131
+ id: "grid",
34132
+ label: "Grid"
34133
+ },
34134
+ {
34135
+ id: "compact",
34136
+ label: "Compact"
34137
+ }
34138
+ ];
34139
+ const eventCalendarListLayoutCompatibleDisplayModeValues = ["combined", "list"];
34140
+ function isEventCalendarDisplayMode(value) {
34141
+ return typeof value === "string" && eventCalendarDisplayModeValues.includes(value);
34142
+ }
34143
+ function isEventCalendarDefaultView(value) {
34144
+ return typeof value === "string" && eventCalendarDefaultViewValues.includes(value);
34145
+ }
34146
+ function isEventCalendarListLayout(value) {
34147
+ return typeof value === "string" && eventCalendarListLayoutValues.includes(value);
34148
+ }
34149
+ //#endregion
34150
+ //#region ../blocks/src/system/runtime/nodes/shared/contract-schemas.ts
34151
+ const previewStageSchema = z$1.enum(["preview", "published"]);
34152
+ const registrationButtonVariantSchema = blockButtonRoleSchema;
34153
+ const eventButtonVariantSchema = blockButtonRoleSchema;
34154
+ const eventLayoutSchema = z$1.enum(eventCalendarListLayoutValues);
34155
+ const eventCalendarDisplayModeSchema = z$1.enum(eventCalendarDisplayModeValues);
34156
+ const eventCalendarDefaultViewSchema = z$1.enum(eventCalendarDefaultViewValues);
34157
+ const columnOptionSchema = z$1.enum([
34158
+ "2",
34159
+ "3",
34160
+ "4"
34161
+ ]);
34162
+ const weekStartSchema = z$1.enum(["sunday", "monday"]);
34163
+ const filterAutoShowSchema = z$1.enum([
34164
+ "auto",
34165
+ "always",
34166
+ "never"
34167
+ ]);
34168
+ const calendarTabViewsSchema = z$1.enum([
34169
+ "both",
34170
+ "month",
34171
+ "week"
34172
+ ]);
34173
+ /**
34174
+ * Determine which wizard steps to show based on context and payment requirements.
34175
+ *
34176
+ * @param hasPreSelectedContext - Whether event is pre-selected (skips event selection)
34177
+ * @param needsPaymentStep - Whether user must complete payment method selection
34178
+ * @returns Array of wizard steps to display
34179
+ */
34180
+ function getWizardStepsForContext(hasPreSelectedContext, needsPaymentStep) {
34181
+ const baseSteps = hasPreSelectedContext ? ["select-tickets", "enter-details"] : [
34182
+ "select-event",
34183
+ "select-tickets",
34184
+ "enter-details"
34185
+ ];
34186
+ if (!needsPaymentStep) return baseSteps;
34187
+ return [...baseSteps, "select-payment"];
34188
+ }
34189
+ //#endregion
34190
+ //#region ../blocks/src/system/runtime/nodes/event-registration/registrationDerivations.ts
34191
+ function eventMatchesOccurrenceId(event, occurrenceId) {
34192
+ return event.registrationSubject.kind === "occurrence" && (event.registrationSubject.occurrenceId === occurrenceId || event.selectionId === occurrenceId);
34193
+ }
34194
+ function findPreSelectedEvent(input) {
34195
+ const occurrenceContext = input.occurrenceContext;
34196
+ if (!occurrenceContext) return input.scopedEvents.length === 1 ? input.scopedEvents[0] ?? null : null;
34197
+ const contextDate = getBookingLocalDateKey(toScheduledBookingDateTime({
34198
+ startsAt: occurrenceContext.startsAt,
34199
+ timeZone: occurrenceContext.timeZone
34200
+ }));
34201
+ return input.scopedEvents.find((event) => eventMatchesOccurrenceId(event, occurrenceContext.id) || getBookingLocalDateKey(toScheduledBookingDateTime({
34202
+ startsAt: event.startsAt,
34203
+ timeZone: event.timeZone
34204
+ })) === contextDate) ?? null;
34205
+ }
34206
+ function resolveEventRequiresPayment(input) {
34207
+ if (input.eligibility) return input.eligibility.requiresPayment;
34208
+ if (input.selectedTicketType) return input.selectedTicketType.priceAmount > 0;
34209
+ if (!input.event) return false;
34210
+ return publicEventPricingType(resolveRuntimeEventPricing(input.event)) === "paid";
34211
+ }
34212
+ //#endregion
34213
+ //#region ../blocks/src/system/runtime/nodes/event-registration/registrationEventAdapter.ts
34214
+ function toRegistrationEvent(event) {
34215
+ return {
34216
+ ...event,
34217
+ pricing: resolveRuntimeEventPricing(event),
34218
+ selectionId: event.id,
34219
+ registrationSubject: {
34220
+ kind: "occurrence",
34221
+ occurrenceId: event.occurrenceId
34222
+ }
34223
+ };
32967
34224
  }
32968
34225
  function toRegistrationEvents(events) {
32969
34226
  return events.filter((event) => event.bookingScope === "single_occurrence").map(toRegistrationEvent);
@@ -33376,7 +34633,7 @@ const eventRegistrationBlockDefinition = {
33376
34633
  {
33377
34634
  id: "showCapacity",
33378
34635
  type: "boolean",
33379
- label: "Show available spots",
34636
+ label: "Show available places",
33380
34637
  description: "Display remaining capacity",
33381
34638
  required: false,
33382
34639
  defaultValue: true,
@@ -33921,591 +35178,26 @@ const eventSpotlightBlockDefinition = {
33921
35178
  dataLoaders: { events: {
33922
35179
  endpoint: "listPublicEvents",
33923
35180
  params: {
33924
- siteId: { $bind: { from: "$root.siteId" } },
33925
- limit: { $bind: {
33926
- from: "content.maxEvents",
33927
- fallback: "3"
33928
- } },
33929
- stage: { $bind: {
33930
- from: "$root.previewStage",
33931
- fallback: "published"
33932
- } },
33933
- detailLevel: "listing",
33934
- readProfile: "spotlight"
33935
- },
33936
- mode: "server"
33937
- } }
33938
- };
33939
- //#endregion
33940
- //#region ../calendar/src/grid.ts
33941
- const DAY_NAMES = [
33942
- "Sun",
33943
- "Mon",
33944
- "Tue",
33945
- "Wed",
33946
- "Thu",
33947
- "Fri",
33948
- "Sat"
33949
- ];
33950
- const MONTH_NAMES = [
33951
- "January",
33952
- "February",
33953
- "March",
33954
- "April",
33955
- "May",
33956
- "June",
33957
- "July",
33958
- "August",
33959
- "September",
33960
- "October",
33961
- "November",
33962
- "December"
33963
- ];
33964
- function formatDateKey(date) {
33965
- return `${String(date.year).padStart(4, "0")}-${String(date.month).padStart(2, "0")}-${String(date.day).padStart(2, "0")}`;
33966
- }
33967
- function isToday(date, today) {
33968
- return isSameCalendarDay(date, today);
33969
- }
33970
- function isSameCalendarDay(left, right) {
33971
- return left.year === right.year && left.month === right.month && left.day === right.day;
33972
- }
33973
- function addDays(date, days) {
33974
- const result = new Date(Date.UTC(date.year, date.month - 1, date.day + days, 12));
33975
- return {
33976
- year: result.getUTCFullYear(),
33977
- month: result.getUTCMonth() + 1,
33978
- day: result.getUTCDate()
33979
- };
33980
- }
33981
- function getMonthCalendarDays(year, month, startOfWeek) {
33982
- const firstDay = {
33983
- year,
33984
- month,
33985
- day: 1
33986
- };
33987
- const dayOfWeek = getDayOfWeek(firstDay);
33988
- const gridStart = addDays(firstDay, -(startOfWeek === "monday" ? dayOfWeek === 0 ? 6 : dayOfWeek - 1 : dayOfWeek));
33989
- return Array.from({ length: 42 }, (_, index) => addDays(gridStart, index));
33990
- }
33991
- function getWeekCalendarDays(baseDate, startOfWeek) {
33992
- const dayOfWeek = getDayOfWeek(baseDate);
33993
- const weekStart = addDays(baseDate, -(startOfWeek === "monday" ? dayOfWeek === 0 ? 6 : dayOfWeek - 1 : dayOfWeek));
33994
- return Array.from({ length: 7 }, (_, index) => addDays(weekStart, index));
33995
- }
33996
- function getDayOfWeek(date) {
33997
- return new Date(Date.UTC(date.year, date.month - 1, date.day, 12)).getUTCDay();
33998
- }
33999
- function getWeekCalendarDayTuple(baseDate, startOfWeek) {
34000
- const days = getWeekCalendarDays(baseDate, startOfWeek);
34001
- assertWeekCalendarDays(days);
34002
- return days;
34003
- }
34004
- function assertWeekCalendarDays(days) {
34005
- if (days.length !== 7) throw new Error("Expected exactly 7 calendar days");
34006
- }
34007
- function getOrderedDayNames(startOfWeek, dayNames = DAY_NAMES) {
34008
- if (startOfWeek === "monday") {
34009
- const sunday = dayNames[0] ?? "Sun";
34010
- return [...dayNames.slice(1), sunday];
34011
- }
34012
- return [...dayNames];
34013
- }
34014
- function formatCalendarTitle(viewDate, view, days, monthNames = MONTH_NAMES) {
34015
- if (view === "week") {
34016
- const weekStart = days[0];
34017
- const weekEnd = days[6];
34018
- if (weekStart === void 0 || weekEnd === void 0) return `${monthNames[viewDate.month - 1] ?? ""} ${viewDate.year}`;
34019
- const startMonth = monthNames[weekStart.month - 1] ?? "";
34020
- const endMonth = monthNames[weekEnd.month - 1] ?? "";
34021
- if (weekStart.month === weekEnd.month) return `${startMonth} ${weekStart.day} - ${weekEnd.day}, ${weekEnd.year}`;
34022
- return `${startMonth} ${weekStart.day} - ${endMonth} ${weekEnd.day}, ${weekEnd.year}`;
34023
- }
34024
- return `${monthNames[viewDate.month - 1] ?? ""} ${viewDate.year}`;
34025
- }
34026
- function buildCalendarViewport(input) {
34027
- const monthNames = input.monthNames ?? MONTH_NAMES;
34028
- if (input.view === "week") {
34029
- const days = getWeekCalendarDayTuple(input.anchorDate, input.startOfWeek);
34030
- return {
34031
- kind: "week",
34032
- anchorDate: input.anchorDate,
34033
- days,
34034
- title: formatCalendarTitle(input.anchorDate, "week", days, monthNames)
34035
- };
34036
- }
34037
- const days = getMonthCalendarDays(input.anchorDate.year, input.anchorDate.month, input.startOfWeek);
34038
- return {
34039
- kind: "month",
34040
- anchorDate: input.anchorDate,
34041
- days,
34042
- title: formatCalendarTitle(input.anchorDate, "month", days, monthNames)
34043
- };
34044
- }
34045
- //#endregion
34046
- //#region ../calendar/src/lane-packing.ts
34047
- /**
34048
- * Assign lanes to a list of intervals.
34049
- *
34050
- * Returns one result per input in the same order as the input. Internally
34051
- * sorts a copy by `(start, end, orderKey)` to ensure determinism, then
34052
- * permutes results back to input order.
34053
- */
34054
- function assignLanes(intervals) {
34055
- if (intervals.length === 0) return [];
34056
- const indexed = intervals.map((interval, index) => ({
34057
- interval,
34058
- index
34059
- }));
34060
- indexed.sort((left, right) => left.interval.start - right.interval.start || left.interval.end - right.interval.end || left.interval.orderKey.localeCompare(right.interval.orderKey));
34061
- const results = new Array(intervals.length);
34062
- let cluster = [];
34063
- let activeLaneEnds = [];
34064
- let clusterMaxEnd = -Infinity;
34065
- const closeCluster = () => {
34066
- if (cluster.length === 0) return;
34067
- const laneCount = Math.max(1, cluster.reduce((max, entry) => Math.max(max, entry.laneIndex + 1), 0));
34068
- for (const entry of cluster) results[entry.index] = {
34069
- laneIndex: entry.laneIndex,
34070
- laneCount
34071
- };
34072
- cluster = [];
34073
- activeLaneEnds = [];
34074
- clusterMaxEnd = -Infinity;
34075
- };
34076
- for (const { interval, index } of indexed) {
34077
- if (interval.start >= clusterMaxEnd) closeCluster();
34078
- const laneIndex = firstAvailableLane(activeLaneEnds, interval.start);
34079
- activeLaneEnds[laneIndex] = interval.end;
34080
- clusterMaxEnd = Math.max(clusterMaxEnd, interval.end);
34081
- cluster.push({
34082
- index,
34083
- laneIndex
34084
- });
34085
- }
34086
- closeCluster();
34087
- return results;
34088
- }
34089
- function firstAvailableLane(activeLaneEnds, start) {
34090
- const laneIndex = activeLaneEnds.findIndex((end) => end <= start);
34091
- return laneIndex >= 0 ? laneIndex : activeLaneEnds.length;
34092
- }
34093
- //#endregion
34094
- //#region ../calendar/src/timezone.ts
34095
- /**
34096
- * Timezone boundary helpers — internal.
34097
- *
34098
- * Both functions read an ISO instant in a given IANA timezone and project it
34099
- * onto the wall clock of that zone. They use Intl.DateTimeFormat with
34100
- * hourCycle: 'h23' to avoid 12/24-hour ambiguity. Used internally by the
34101
- * timetable model and `groupItemsByDate`; not exported from the package's
34102
- * public surface.
34103
- */
34104
- const MINUTES_PER_HOUR = 60;
34105
- /**
34106
- * Local minute-of-day [0, 24*60) for an ISO instant in the given zone.
34107
- * Returns NaN if the inputs cannot be parsed (caller should drop the item).
34108
- */
34109
- function getLocalMinuteOfDay(isoTimestamp, timeZone) {
34110
- const date = new Date(isoTimestamp);
34111
- if (Number.isNaN(date.getTime())) return NaN;
34112
- if (!isValidTimeZone(timeZone)) return NaN;
34113
- const parts = new Intl.DateTimeFormat("en-GB", {
34114
- timeZone,
34115
- hour: "2-digit",
34116
- minute: "2-digit",
34117
- hourCycle: "h23"
34118
- }).formatToParts(date);
34119
- const hourPart = Number(parts.find((part) => part.type === "hour")?.value);
34120
- const minute = Number(parts.find((part) => part.type === "minute")?.value);
34121
- const hour = hourPart === 24 ? 0 : hourPart;
34122
- if (Number.isNaN(hour) || Number.isNaN(minute)) return NaN;
34123
- return hour * MINUTES_PER_HOUR + minute;
34124
- }
34125
- /**
34126
- * Local YYYY-MM-DD for an ISO instant in the given zone.
34127
- * Returns null if the inputs cannot be parsed.
34128
- */
34129
- function getLocalDateKey(isoTimestamp, timeZone) {
34130
- const date = new Date(isoTimestamp);
34131
- if (Number.isNaN(date.getTime())) return null;
34132
- if (!isValidTimeZone(timeZone)) return null;
34133
- const parts = new Intl.DateTimeFormat("en-CA", {
34134
- timeZone,
34135
- year: "numeric",
34136
- month: "2-digit",
34137
- day: "2-digit"
34138
- }).formatToParts(date);
34139
- const year = parts.find((part) => part.type === "year")?.value;
34140
- const month = parts.find((part) => part.type === "month")?.value;
34141
- const day = parts.find((part) => part.type === "day")?.value;
34142
- if (year === void 0 || year.length === 0) return null;
34143
- if (month === void 0 || month.length === 0) return null;
34144
- if (day === void 0 || day.length === 0) return null;
34145
- return `${year}-${month}-${day}`;
34146
- }
34147
- const validTimeZoneCache = /* @__PURE__ */ new Map();
34148
- function isValidTimeZone(timeZone) {
34149
- if (typeof timeZone !== "string" || timeZone.length === 0) return false;
34150
- const cached = validTimeZoneCache.get(timeZone);
34151
- if (cached !== void 0) return cached;
34152
- try {
34153
- new Intl.DateTimeFormat("en-US", { timeZone });
34154
- validTimeZoneCache.set(timeZone, true);
34155
- return true;
34156
- } catch {
34157
- validTimeZoneCache.set(timeZone, false);
34158
- return false;
34159
- }
34160
- }
34161
- //#endregion
34162
- //#region ../calendar/src/timetable.ts
34163
- /**
34164
- * Timetable model — the package's core surface.
34165
- *
34166
- * Generic over `T`. Takes a list of timed items + a viewport; returns a
34167
- * day-by-day layout with overlap-packed lanes and percent-based positions
34168
- * plus a diagnostics list for items dropped during processing.
34169
- *
34170
- * The function is pure: given identical input (including `now`), it returns
34171
- * the same output. The model has no opinion on what an item is — pass
34172
- * accessors.
34173
- */
34174
- const DEFAULT_BOUNDS = {
34175
- minStartHour: 5,
34176
- maxEndHour: 23,
34177
- defaultStartHour: 6,
34178
- defaultEndHour: 21,
34179
- 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
+ } }
34180
35195
  };
34181
- function buildTimetableModel$1(input) {
34182
- const bounds = resolveBounds(input.bounds);
34183
- const viewportDays = getTimetableDays(input.viewDate, input.view ?? "week", input.startOfWeek);
34184
- const viewportKeys = new Set(viewportDays.map(formatDateKey));
34185
- const parsed = parseAllItems(input.items, input);
34186
- const inViewport = deriveVisibleSegments(parsed.drafts, viewportKeys);
34187
- const hourRange = resolveHourRange(inViewport, bounds);
34188
- const laid = layoutCellsByDate(inViewport, hourRange, bounds);
34189
- return {
34190
- days: assembleDays(viewportDays, laid.cellsByDate, input.today),
34191
- hourTicks: buildHourTicks(hourRange.startHour, hourRange.endHour),
34192
- startHour: hourRange.startHour,
34193
- endHour: hourRange.endHour,
34194
- rangeMinutes: (hourRange.endHour - hourRange.startHour) * 60,
34195
- hasItems: inViewport.length > 0,
34196
- diagnostics: [...parsed.diagnostics, ...laid.diagnostics]
34197
- };
34198
- }
34199
- function parseAllItems(items, accessors) {
34200
- const drafts = [];
34201
- const diagnostics = [];
34202
- for (const item of items) {
34203
- const result = parseDraft(item, accessors);
34204
- if (result.ok) drafts.push(result.draft);
34205
- else diagnostics.push({
34206
- item,
34207
- reason: result.reason
34208
- });
34209
- }
34210
- return {
34211
- drafts,
34212
- diagnostics
34213
- };
34214
- }
34215
- function layoutCellsByDate(segments, hourRange, bounds) {
34216
- const rangeMinutes = (hourRange.endHour - hourRange.startHour) * 60;
34217
- const rangeStartMinutes = hourRange.startHour * 60;
34218
- const rangeEndMinutes = hourRange.endHour * 60;
34219
- const segmentsByDate = groupSegmentsByDate(segments);
34220
- const cellsByDate = /* @__PURE__ */ new Map();
34221
- const diagnostics = [];
34222
- for (const [dateKey, daySegments] of segmentsByDate) {
34223
- const clipped = [];
34224
- for (const segment of daySegments) {
34225
- const result = clipDraft(segment, rangeStartMinutes, rangeEndMinutes);
34226
- if (result.ok) clipped.push(result.clipped);
34227
- else diagnostics.push({
34228
- item: segment.draft.item,
34229
- reason: result.reason
34230
- });
34231
- }
34232
- const laneResults = assignLanes(clipped.map((entry) => ({
34233
- start: entry.visibleStart,
34234
- end: entry.visibleEnd,
34235
- orderKey: entry.segment.draft.orderKey
34236
- })));
34237
- cellsByDate.set(dateKey, clipped.map((entry, idx) => buildCell(entry, laneResults[idx] ?? {
34238
- laneIndex: 0,
34239
- laneCount: 1
34240
- }, rangeStartMinutes, rangeMinutes, bounds.minHeightPercent)));
34241
- }
34242
- return {
34243
- cellsByDate,
34244
- diagnostics
34245
- };
34246
- }
34247
- function assembleDays(viewportDays, cellsByDate, today) {
34248
- return viewportDays.map((date) => {
34249
- const dateKey = formatDateKey(date);
34250
- return {
34251
- date,
34252
- dateKey,
34253
- isToday: isSameCalendarDay(date, today),
34254
- cells: cellsByDate.get(dateKey) ?? []
34255
- };
34256
- });
34257
- }
34258
- function parseDraft(item, accessors) {
34259
- const startsAtIso = accessors.getStartsAt(item);
34260
- const endsAtIso = accessors.getEndsAt(item);
34261
- const timeZone = accessors.getTimeZone(item);
34262
- const startsAtMinutes = getLocalMinuteOfDay(startsAtIso, timeZone);
34263
- if (Number.isNaN(startsAtMinutes)) return {
34264
- ok: false,
34265
- reason: "invalid-start"
34266
- };
34267
- const startDateKey = getLocalDateKey(startsAtIso, timeZone);
34268
- if (startDateKey == null) return {
34269
- ok: false,
34270
- reason: "invalid-timezone"
34271
- };
34272
- const endDateKey = getLocalDateKey(endsAtIso, timeZone);
34273
- if (endDateKey == null) return {
34274
- ok: false,
34275
- reason: "invalid-end"
34276
- };
34277
- const endsAtMinutesRaw = getLocalMinuteOfDay(endsAtIso, timeZone);
34278
- if (Number.isNaN(endsAtMinutesRaw)) return {
34279
- ok: false,
34280
- reason: "invalid-end"
34281
- };
34282
- const spansMultipleDays = startDateKey < endDateKey;
34283
- const endsAtMinutes = spansMultipleDays || endsAtMinutesRaw > startsAtMinutes ? endsAtMinutesRaw : Math.min(1440, startsAtMinutes + 60);
34284
- if (!spansMultipleDays && endsAtMinutes <= startsAtMinutes) return {
34285
- ok: false,
34286
- reason: "non-positive-duration"
34287
- };
34288
- return {
34289
- ok: true,
34290
- draft: {
34291
- item,
34292
- startDateKey,
34293
- endDateKey,
34294
- startsAtMinutes,
34295
- endsAtMinutes,
34296
- color: accessors.getColor?.(item) ?? null,
34297
- orderKey: accessors.getOrderKey?.(item) ?? accessors.getId(item)
34298
- }
34299
- };
34300
- }
34301
- function deriveVisibleSegments(drafts, viewportKeys) {
34302
- const segments = [];
34303
- for (const draft of drafts) for (const dateKey of enumerateDateKeys(draft.startDateKey, draft.endDateKey)) {
34304
- if (!viewportKeys.has(dateKey)) continue;
34305
- const isStartDay = dateKey === draft.startDateKey;
34306
- const isEndDay = dateKey === draft.endDateKey;
34307
- const startsAtMinutes = isStartDay ? draft.startsAtMinutes : 0;
34308
- const endsAtMinutes = isEndDay ? draft.endsAtMinutes : 1440;
34309
- if (endsAtMinutes <= startsAtMinutes) continue;
34310
- segments.push({
34311
- draft,
34312
- dateKey,
34313
- startsAtMinutes,
34314
- endsAtMinutes,
34315
- continuesBefore: !isStartDay,
34316
- continuesAfter: !isEndDay
34317
- });
34318
- }
34319
- return segments;
34320
- }
34321
- function clipDraft(segment, rangeStartMinutes, rangeEndMinutes) {
34322
- const visibleStart = Math.max(rangeStartMinutes, segment.startsAtMinutes);
34323
- const visibleEnd = Math.min(rangeEndMinutes, segment.endsAtMinutes);
34324
- if (visibleEnd <= visibleStart) return {
34325
- ok: false,
34326
- reason: "out-of-visible-hours"
34327
- };
34328
- return {
34329
- ok: true,
34330
- clipped: {
34331
- segment,
34332
- visibleStart,
34333
- visibleEnd
34334
- }
34335
- };
34336
- }
34337
- function buildCell(entry, lane, rangeStartMinutes, rangeMinutes, minHeightPercent) {
34338
- const heightPercent = Math.max(minHeightPercent, toRangePercent(entry.visibleEnd - entry.visibleStart, rangeMinutes));
34339
- const rawTopPercent = toRangePercent(entry.visibleStart - rangeStartMinutes, rangeMinutes);
34340
- const topPercent = Math.min(rawTopPercent, 100 - heightPercent);
34341
- return {
34342
- item: entry.segment.draft.item,
34343
- dateKey: entry.segment.dateKey,
34344
- startsAtMinutes: entry.segment.startsAtMinutes,
34345
- endsAtMinutes: entry.segment.endsAtMinutes,
34346
- visibleStartsAtMinutes: entry.visibleStart,
34347
- visibleEndsAtMinutes: entry.visibleEnd,
34348
- topPercent,
34349
- heightPercent,
34350
- isClippedStart: entry.segment.continuesBefore || entry.segment.startsAtMinutes < entry.visibleStart,
34351
- isClippedEnd: entry.segment.continuesAfter || entry.segment.endsAtMinutes > entry.visibleEnd,
34352
- laneIndex: lane.laneIndex,
34353
- laneCount: lane.laneCount,
34354
- color: entry.segment.draft.color
34355
- };
34356
- }
34357
- function resolveBounds(bounds) {
34358
- return {
34359
- minStartHour: bounds?.minStartHour ?? DEFAULT_BOUNDS.minStartHour,
34360
- maxEndHour: bounds?.maxEndHour ?? DEFAULT_BOUNDS.maxEndHour,
34361
- defaultStartHour: bounds?.defaultStartHour ?? DEFAULT_BOUNDS.defaultStartHour,
34362
- defaultEndHour: bounds?.defaultEndHour ?? DEFAULT_BOUNDS.defaultEndHour,
34363
- minHeightPercent: bounds?.minHeightPercent ?? DEFAULT_BOUNDS.minHeightPercent
34364
- };
34365
- }
34366
- function resolveHourRange(segments, bounds) {
34367
- if (segments.length === 0) return {
34368
- startHour: bounds.defaultStartHour,
34369
- endHour: bounds.defaultEndHour
34370
- };
34371
- const earliestStart = Math.min(...segments.map((segment) => segment.startsAtMinutes));
34372
- const latestEnd = Math.max(...segments.map((segment) => segment.endsAtMinutes));
34373
- const startHour = Math.max(bounds.minStartHour, Math.floor(earliestStart / 60));
34374
- const endHour = Math.min(bounds.maxEndHour, Math.ceil(latestEnd / 60));
34375
- return {
34376
- startHour,
34377
- endHour: Math.max(startHour + 1, endHour)
34378
- };
34379
- }
34380
- function buildHourTicks(startHour, endHour) {
34381
- const ticks = [];
34382
- for (let hour = startHour; hour <= endHour; hour += 1) ticks.push(hour);
34383
- return ticks;
34384
- }
34385
- function groupSegmentsByDate(segments) {
34386
- const grouped = /* @__PURE__ */ new Map();
34387
- for (const segment of segments) {
34388
- const existing = grouped.get(segment.dateKey) ?? [];
34389
- existing.push(segment);
34390
- grouped.set(segment.dateKey, existing);
34391
- }
34392
- return grouped;
34393
- }
34394
- function toRangePercent(minutes, rangeMinutes) {
34395
- if (rangeMinutes <= 0) return 0;
34396
- return minutes / rangeMinutes * 100;
34397
- }
34398
- function getTimetableDays(viewDate, view, startOfWeek) {
34399
- if (view === "day") return [viewDate];
34400
- return getWeekCalendarDays(viewDate, startOfWeek);
34401
- }
34402
- function enumerateDateKeys(startDateKey, endDateKey) {
34403
- if (startDateKey > endDateKey) throw new Error(`Invalid timetable date range: ${startDateKey} is after ${endDateKey}`);
34404
- const start = parseDateKey(startDateKey);
34405
- parseDateKey(endDateKey);
34406
- const keys = [];
34407
- let cursor = start;
34408
- while (formatDateKey(cursor) <= endDateKey) {
34409
- keys.push(formatDateKey(cursor));
34410
- cursor = addDays(cursor, 1);
34411
- }
34412
- return keys;
34413
- }
34414
- function parseDateKey(dateKey) {
34415
- const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(dateKey);
34416
- if (match === null) throw new Error(`Invalid timetable date key: ${dateKey}`);
34417
- const [, year, month, day] = match;
34418
- return {
34419
- year: Number(year),
34420
- month: Number(month),
34421
- day: Number(day)
34422
- };
34423
- }
34424
- //#endregion
34425
- //#region ../calendar/src/group.ts
34426
- /**
34427
- * Group items by their local date (YYYY-MM-DD in the item's own timezone).
34428
- *
34429
- * Generic over `T`. Items with un-parseable startsAt or invalid timezone are
34430
- * silently dropped — call sites that need diagnostics should validate upstream.
34431
- *
34432
- * The output map preserves insertion order of dates (ES2015 spec) and the
34433
- * relative order of items within each date matches input order.
34434
- */
34435
- function groupItemsByDate(items, accessors) {
34436
- const grouped = /* @__PURE__ */ new Map();
34437
- for (const item of items) {
34438
- const dateKey = getLocalDateKey(accessors.getStartsAt(item), accessors.getTimeZone(item));
34439
- if (dateKey == null) continue;
34440
- const existing = grouped.get(dateKey);
34441
- if (existing !== void 0) existing.push(item);
34442
- else grouped.set(dateKey, [item]);
34443
- }
34444
- return grouped;
34445
- }
34446
- //#endregion
34447
- //#region ../blocks/src/system/runtime/nodes/events/shared/eventCapacity.ts
34448
- /**
34449
- * Compute remaining spots for an event from `availableSpots` with a fallback
34450
- * to `capacity - registeredCount`. Returns `{ available: null, isFull: false }`
34451
- * for uncapped events so call sites can render no capacity indicator at all.
34452
- *
34453
- * The single source of truth for capacity math across event cards, agenda
34454
- * rows, and timetable cells — keep wording variations in the call sites.
34455
- */
34456
- function getEventAvailability(event) {
34457
- if (event.capacity == null) return {
34458
- available: null,
34459
- isFull: false
34460
- };
34461
- const available = event.availableSpots ?? event.capacity - event.registeredCount;
34462
- if (available <= 0) return {
34463
- available: 0,
34464
- isFull: true
34465
- };
34466
- return {
34467
- available,
34468
- isFull: false
34469
- };
34470
- }
34471
- /**
34472
- * Derive the three-state availability status (available / filling / waitlist)
34473
- * plus a short pill label. Builds on {@link getEventAvailability} so the
34474
- * capacity math (availableSpots with a `capacity - registeredCount` fallback)
34475
- * stays consistent with the binary helper used elsewhere.
34476
- */
34477
- function getEventAvailabilityStatus(event, options) {
34478
- const threshold = options?.fillingThreshold ?? 2;
34479
- const { available, isFull } = getEventAvailability(event);
34480
- if (available == null) return {
34481
- status: "available",
34482
- available: null,
34483
- isFull: false,
34484
- label: null
34485
- };
34486
- if (isFull) return {
34487
- status: "waitlist",
34488
- available: 0,
34489
- isFull: true,
34490
- label: event.waitlistEnabled ? "Waitlist" : "Full"
34491
- };
34492
- if (available <= threshold) return {
34493
- status: "filling",
34494
- available,
34495
- isFull: false,
34496
- label: `${available} left`
34497
- };
34498
- return {
34499
- status: "available",
34500
- available,
34501
- isFull: false,
34502
- label: `${available} places`
34503
- };
34504
- }
34505
35196
  //#endregion
34506
- //#region ../blocks/src/system/runtime/nodes/events/shared/utils.ts
35197
+ //#region ../blocks/src/system/runtime/nodes/events/shared/listingLayout.ts
34507
35198
  /**
34508
- * 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.
34509
35201
  */
34510
35202
  /**
34511
35203
  * Column class mappings for grid layouts, derived from the canonical
@@ -34540,180 +35232,6 @@ function getCardOrientation(layout) {
34540
35232
  case "compact": return "vertical";
34541
35233
  }
34542
35234
  }
34543
- /**
34544
- * Format date for display.
34545
- * Returns: "Monday, January 15, 2025"
34546
- */
34547
- function formatDate$2(isoTimestamp, options) {
34548
- return formatBookingDate(toScheduledBookingDateTime({
34549
- startsAt: isoTimestamp,
34550
- timeZone: toBookingTimeZone(options?.timeZone ?? "UTC"),
34551
- locale: options?.locale ?? "en-US"
34552
- }), {
34553
- weekday: "long",
34554
- year: "numeric",
34555
- month: "long",
34556
- day: "numeric"
34557
- });
34558
- }
34559
- /**
34560
- * Format time for display.
34561
- * Returns: "10:00 AM"
34562
- */
34563
- function formatTime(isoTimestamp, options) {
34564
- return formatBookingTime(toScheduledBookingDateTime({
34565
- startsAt: isoTimestamp,
34566
- timeZone: toBookingTimeZone(options?.timeZone ?? "UTC"),
34567
- locale: options?.locale ?? "en-US"
34568
- }), {
34569
- hour: "numeric",
34570
- minute: "2-digit",
34571
- hour12: true
34572
- });
34573
- }
34574
- function formatEventDateParts(args) {
34575
- const scheduledRange = toScheduledBookingDateRange({
34576
- startsAt: args.startsAt,
34577
- endsAt: args.endsAt,
34578
- timeZone: toBookingTimeZone(args.timeZone ?? "UTC"),
34579
- locale: args.locale ?? "en-US"
34580
- });
34581
- const { date, time: start } = formatBookingDateParts(scheduledRange, { dateFormat: {
34582
- weekday: "long",
34583
- year: "numeric",
34584
- month: "long",
34585
- day: "numeric"
34586
- } });
34587
- return {
34588
- date,
34589
- timeRange: `${start} – ${formatBookingTime(toScheduledBookingDateTime({
34590
- startsAt: args.endsAt,
34591
- timeZone: scheduledRange.timeZone,
34592
- locale: args.locale ?? "en-US"
34593
- }))}`
34594
- };
34595
- }
34596
- function formatEventCompactDateRail(isoTimestamp, options) {
34597
- const dateTime = toScheduledBookingDateTime({
34598
- startsAt: isoTimestamp,
34599
- timeZone: toBookingTimeZone(options?.timeZone ?? "UTC"),
34600
- locale: options?.locale ?? "en-US"
34601
- });
34602
- const weekday = formatBookingDate(dateTime, { weekday: "short" }).toLocaleUpperCase(options?.locale ?? "en-US");
34603
- const month = formatBookingDate(dateTime, { month: "short" }).toLocaleUpperCase(options?.locale ?? "en-US");
34604
- return {
34605
- weekday,
34606
- day: formatBookingDate(dateTime, { day: "numeric" }),
34607
- month,
34608
- label: formatBookingDate(dateTime, {
34609
- weekday: "long",
34610
- year: "numeric",
34611
- month: "long",
34612
- day: "numeric"
34613
- })
34614
- };
34615
- }
34616
- function formatTeamMemberNames(names) {
34617
- const normalized = names.map((name) => name.trim()).filter(Boolean);
34618
- if (normalized.length === 0) return null;
34619
- const [firstName, secondName] = normalized;
34620
- if (!firstName) return null;
34621
- if (normalized.length === 1) return firstName;
34622
- if (normalized.length === 2 && secondName) return `${firstName} and ${secondName}`;
34623
- const finalName = normalized[normalized.length - 1] ?? "";
34624
- return `${normalized.slice(0, -1).join(", ")}, and ${finalName}`;
34625
- }
34626
- function formatEventTeacherLine(event) {
34627
- const names = formatTeamMemberNames((event.teamMembers ?? []).map((teamMember) => teamMember.name));
34628
- return names ? `with ${names}` : null;
34629
- }
34630
- /**
34631
- * Group events by their local date (YYYY-MM-DD key in each event's own
34632
- * timezone). Thin domain wrapper over `groupItemsByDate` from the calendar
34633
- * package; preserves the historical `Map<string, PublicEvent[]>` shape.
34634
- */
34635
- function groupEventsByDate(events, calendarTimeZone) {
34636
- const grouped = groupItemsByDate(events, {
34637
- getStartsAt: (event) => event.startsAt,
34638
- getTimeZone: () => calendarTimeZone
34639
- });
34640
- const map = /* @__PURE__ */ new Map();
34641
- for (const [dateKey, bucket] of grouped) map.set(dateKey, [...bucket]);
34642
- return map;
34643
- }
34644
- /**
34645
- * Format event price for display.
34646
- * @returns "Free", "£25", "£12 - £25", "Free / £12 - £25", "Course (From £40)", etc.
34647
- *
34648
- * Events render compact amounts (no trailing ".00"); the actual ADT→string
34649
- * mapping lives in `formatBookingPriceDisplay` in @riverbankcms/core so all
34650
- * surfaces stay consistent.
34651
- */
34652
- function formatEventPrice(event) {
34653
- return formatEventPricing(resolveRuntimeEventPricing(event), { decimals: "auto" });
34654
- }
34655
- function closedEventBookingLabel(reason) {
34656
- switch (reason) {
34657
- case "occurrence_cancelled":
34658
- case "series_cancelled": return "Cancelled";
34659
- case "occurrence_completed":
34660
- case "series_completed": return "Completed";
34661
- case "series_draft":
34662
- case "series_archived":
34663
- case "parent_archived":
34664
- case "parent_inconsistent":
34665
- case "not_public":
34666
- case "already_started":
34667
- case "enrollment_closed": return "Registration closed";
34668
- default: return assertNever$23(reason);
34669
- }
34670
- }
34671
- function resolveEventHref(event) {
34672
- const destination = resolveRuntimeEventDestination(event);
34673
- return destination.kind === "external" ? destination.url : destination.path;
34674
- }
34675
- function resolveEventLinkProps(event) {
34676
- const href = resolveEventHref(event);
34677
- return resolveRuntimeEventDestination(event).kind === "external" ? {
34678
- href,
34679
- target: "_blank",
34680
- rel: "noopener noreferrer"
34681
- } : { href };
34682
- }
34683
- /**
34684
- * Resolve the CTA state for an event.
34685
- * Shared by EventCard and EventCompactRow to keep sold-out, registration-closed,
34686
- * and course-specific defaults consistent.
34687
- */
34688
- function resolveEventCta(event, buttonText) {
34689
- const { isFull: isSoldOut } = getEventAvailability(event);
34690
- const destination = resolveRuntimeEventDestination(event);
34691
- const isExternal = destination.kind === "external";
34692
- const defaultButtonText = isExternal ? destination.label : event.bookingScope === "whole_series" ? "Book course" : "Book";
34693
- const effectiveButtonText = buttonText ?? defaultButtonText;
34694
- const href = resolveEventHref(event);
34695
- if (event.bookingAvailability.kind === "closed") {
34696
- const linksToClosedWholeSeriesDetails = event.bookingAvailability.reason === "enrollment_closed" && !isExternal;
34697
- return {
34698
- label: closedEventBookingLabel(event.bookingAvailability.reason),
34699
- href,
34700
- external: isExternal,
34701
- hidden: !linksToClosedWholeSeriesDetails
34702
- };
34703
- }
34704
- if (!isExternal && isSoldOut) return {
34705
- label: "Sold out",
34706
- href,
34707
- external: false,
34708
- hidden: true
34709
- };
34710
- return {
34711
- label: effectiveButtonText,
34712
- href,
34713
- external: isExternal,
34714
- hidden: false
34715
- };
34716
- }
34717
35235
  //#endregion
34718
35236
  //#region ../blocks/src/system/media/slotContracts.ts
34719
35237
  const DEFAULT_MIN_SOURCE_WIDTH = 1200;
@@ -36009,7 +36527,6 @@ const EventCard = ({ event, cardRole = "default", buttonVariant = "primary", but
36009
36527
  const formattedDate = formatDate$2(event.startsAt, { timeZone });
36010
36528
  const formattedTime = `${formatTime(event.startsAt, { timeZone })} - ${formatTime(event.endsAt, { timeZone })}`;
36011
36529
  const cta = resolveEventCta(event, buttonText);
36012
- const { available: spotsLeft, isFull: isSoldOut } = getEventAvailability(event);
36013
36530
  const cardClass = boxStyleClassNames({ role: cardRole }, { baseClass: "event-card" });
36014
36531
  const buttonClass = themeButtonClassName({
36015
36532
  variant: buttonVariant,
@@ -36025,10 +36542,7 @@ const EventCard = ({ event, cardRole = "default", buttonVariant = "primary", but
36025
36542
  } : null;
36026
36543
  const hasImage = Boolean(showImage && image);
36027
36544
  const teacherLine = formatEventTeacherLine(event);
36028
- const capacityText = showCapacity ? formatCapacityText(event, {
36029
- isSoldOut,
36030
- spotsLeft
36031
- }) : null;
36545
+ const capacityText = showCapacity ? formatEventRemainingPlaces(event, { full: "Sold out" }) : null;
36032
36546
  return /* @__PURE__ */ jsxs("article", {
36033
36547
  className: `${cardClass} group rb-flex rb-flex-col rb-h-full ${eventCardOrientationClass(orientation)}`,
36034
36548
  "data-orientation": orientation,
@@ -36094,7 +36608,11 @@ function EventCardContent(props) {
36094
36608
  }),
36095
36609
  /* @__PURE__ */ jsx("h3", {
36096
36610
  className: "card-title rb-mb-3",
36097
- children: props.title
36611
+ children: /* @__PURE__ */ jsx("a", {
36612
+ className: "event-title-link",
36613
+ ...resolveEventLinkProps(event),
36614
+ children: props.title
36615
+ })
36098
36616
  }),
36099
36617
  /* @__PURE__ */ jsx("div", {
36100
36618
  className: "rb-mb-2 rb-caption",
@@ -36171,13 +36689,6 @@ function eventCardOrientationClass(orientation) {
36171
36689
  case "vertical": return "event-card--vertical";
36172
36690
  }
36173
36691
  }
36174
- function formatCapacityText(event, options) {
36175
- if (event.capacity == null) return null;
36176
- if (options.isSoldOut) return "Sold out";
36177
- if (options.spotsLeft == null) return null;
36178
- const count = options.spotsLeft;
36179
- return `${count} ${count === 1 ? "spot" : "spots"} available`;
36180
- }
36181
36692
  function getCategoryBadgeStyle(background) {
36182
36693
  const normalized = background?.trim() ?? "";
36183
36694
  if (!normalized) return {
@@ -36246,7 +36757,11 @@ const EventCompactRow = ({ event, buttonVariant = "primary", buttonText }) => {
36246
36757
  className: "event-compact-row-content",
36247
36758
  children: [/* @__PURE__ */ jsx("h3", {
36248
36759
  className: "event-compact-row-title",
36249
- children: event.title
36760
+ children: /* @__PURE__ */ jsx("a", {
36761
+ className: "event-title-link",
36762
+ ...resolveEventLinkProps(event),
36763
+ children: event.title
36764
+ })
36250
36765
  }), /* @__PURE__ */ jsxs("div", {
36251
36766
  className: "event-compact-row-meta",
36252
36767
  children: [
@@ -36335,6 +36850,23 @@ function DateTimeTile({ startsAt, endsAt, timeZone }) {
36335
36850
  });
36336
36851
  }
36337
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
36338
36870
  //#region ../blocks/src/system/runtime/nodes/events/shared/listOptions.ts
36339
36871
  const eventPageSizeValues = [
36340
36872
  3,
@@ -37679,7 +38211,7 @@ const eventCalendarAdditionalFields = [
37679
38211
  {
37680
38212
  id: "showCapacity",
37681
38213
  type: "boolean",
37682
- label: "Show available spots",
38214
+ label: "Show available places",
37683
38215
  description: "Display remaining capacity on event cards and timetable cells",
37684
38216
  required: false,
37685
38217
  defaultValue: true,
@@ -47069,7 +47601,7 @@ function EventFacts({ selection, selected, venue, teamMembers, pricing, showCapa
47069
47601
  children: [/* @__PURE__ */ jsx("div", {
47070
47602
  className: "rb-label rb-mb-2",
47071
47603
  children: "Places"
47072
- }), /* @__PURE__ */ jsxs("p", { children: [selected.capacity, " places"] })]
47604
+ }), /* @__PURE__ */ jsx("p", { children: formatTotalCapacity(selected.capacity) })]
47073
47605
  }) : null,
47074
47606
  pricing,
47075
47607
  selected.bookingScope === "whole_series" ? /* @__PURE__ */ jsxs("div", {
@@ -47913,9 +48445,10 @@ function CourseCompactRow({ course, path }) {
47913
48445
  }),
47914
48446
  /* @__PURE__ */ jsxs("div", {
47915
48447
  className: "event-compact-row-content",
47916
- children: [/* @__PURE__ */ jsx("h3", {
48448
+ children: [/* @__PURE__ */ jsx(OfferingTitle, {
47917
48449
  className: "event-compact-row-title",
47918
- children: course.title
48450
+ path,
48451
+ title: course.title
47919
48452
  }), /* @__PURE__ */ jsxs("div", {
47920
48453
  className: "event-compact-row-meta",
47921
48454
  children: [
@@ -47972,9 +48505,10 @@ function CourseFeatureCard({ course, path, buttonVariant, buttonText }) {
47972
48505
  children: /* @__PURE__ */ jsxs("div", {
47973
48506
  className: "card-content rb-flex rb-flex-1 rb-flex-col",
47974
48507
  children: [
47975
- /* @__PURE__ */ jsx("h3", {
48508
+ /* @__PURE__ */ jsx(OfferingTitle, {
47976
48509
  className: "card-title rb-mb-3",
47977
- children: course.title
48510
+ path,
48511
+ title: course.title
47978
48512
  }),
47979
48513
  /* @__PURE__ */ jsxs("div", {
47980
48514
  className: "rb-mb-2 rb-caption",
@@ -48830,8 +49364,8 @@ const AgendaEventCard = ({ event, showCapacity, onClick }) => {
48830
49364
  const eventLinkProps = resolveEventLinkProps(event);
48831
49365
  const categoryColor = event.eventCategory?.color || "rgb(var(--tb-accent))";
48832
49366
  const price = formatEventPrice(event);
48833
- const { available, isFull } = getEventAvailability(event);
48834
- const capacity = showCapacity && available != null ? isFull ? "Full" : `${available} spots left` : null;
49367
+ const capacity = showCapacity ? formatEventRemainingPlaces(event) : null;
49368
+ const isFull = isEventFull(event);
48835
49369
  const title = event.title;
48836
49370
  const timeZone = event.timeZone ?? null;
48837
49371
  const teacherLine = formatEventTeacherLine(event);
@@ -49273,7 +49807,7 @@ function buildTimetableModel(input) {
49273
49807
  getEndsAt: (event) => event.endsAt,
49274
49808
  getTimeZone: (event) => event.timeZone ?? "UTC",
49275
49809
  getColor: (event) => event.eventCategory?.color ?? null,
49276
- getOrderKey: (event) => `${event.title}${event.id}`
49810
+ getOrderKey: (event) => `${event.title}\u0000${event.id}`
49277
49811
  });
49278
49812
  return {
49279
49813
  hourTicks: [...generic.hourTicks],
@@ -49310,18 +49844,9 @@ function toEventCell(cell) {
49310
49844
  };
49311
49845
  }
49312
49846
  function formatTimetableCapacity(event) {
49313
- const { available, isFull } = getEventAvailability(event);
49314
- if (available == null) return {
49315
- label: null,
49316
- isFull: false
49317
- };
49318
- if (isFull) return {
49319
- label: "Full",
49320
- isFull: true
49321
- };
49322
49847
  return {
49323
- label: `${available} spots left`,
49324
- isFull: false
49848
+ label: formatEventRemainingPlaces(event, { compact: true }),
49849
+ isFull: isEventFull(event)
49325
49850
  };
49326
49851
  }
49327
49852
  function formatEventTimeRange(event) {
@@ -49797,7 +50322,7 @@ function EventRegistrationSSR({ blockId, subject, children, ...sourceProps }) {
49797
50322
  className: `event-registration ${className ?? ""}`.trim(),
49798
50323
  "data-block": "event-registration",
49799
50324
  "data-rb-island-required": "true",
49800
- "data-rb-island-error-message": "Booking options failed to load. Please refresh the page.",
50325
+ "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.",
49801
50326
  "data-rb-island-error-class": "event-registration__hydration-error",
49802
50327
  ...getIslandRootProps("event-registration", blockId),
49803
50328
  children: [
@@ -55707,6 +56232,32 @@ function isSiteStyleTransitionStyleGroup(value) {
55707
56232
  function boundaryMapForAdjacentBlockIds(boundaries) {
55708
56233
  return new Map(boundaries.map((boundary) => [boundaryKey(boundary.previousBlockId, boundary.nextBlockId), boundary]));
55709
56234
  }
56235
+ /** Keep a surviving block's outgoing treatment when omitted blocks change its neighbor. */
56236
+ function boundariesForSurvivingBlocks(boundaries, blocks) {
56237
+ const outgoing = new Map(boundaries.map((boundary) => [boundary.previousBlockId, boundary]));
56238
+ const incoming = new Map(boundaries.map((boundary) => [boundary.nextBlockId, boundary]));
56239
+ return blocks.flatMap((block, index) => {
56240
+ const next = blocks[index + 1];
56241
+ if (!block.id || !next?.id) return [];
56242
+ const boundary = outgoing.get(block.id);
56243
+ if (!boundary) return [];
56244
+ if (boundary.nextBlockId === next.id) return [boundary];
56245
+ const destination = incoming.get(next.id);
56246
+ return [retargetCompiledBoundaryRuntimeForContentAdjacency({
56247
+ ...boundary,
56248
+ boundaryId: `boundary:${block.id}:${next.id}`,
56249
+ nextBlockId: next.id,
56250
+ to: destination?.to ?? boundary.to,
56251
+ cssVars: {
56252
+ ...boundary.cssVars,
56253
+ to: destination?.cssVars.to ?? boundary.cssVars.to
56254
+ }
56255
+ }, {
56256
+ currentContent: block.content,
56257
+ nextContent: next.content
56258
+ })];
56259
+ });
56260
+ }
55710
56261
  function boundaryBetweenBlocks(previousBlock, nextBlock, boundaries) {
55711
56262
  const previousBlockId = previousBlock?.id ?? null;
55712
56263
  const nextBlockId = nextBlock?.id ?? null;
@@ -55798,41 +56349,179 @@ function toPresentationPage(page) {
55798
56349
  };
55799
56350
  }
55800
56351
  //#endregion
56352
+ //#region ../blocks/src/pageRendererBlockSupport.tsx
56353
+ /**
56354
+ * Support for rendering one block section: what a block that cannot render
56355
+ * leaves behind, how an override is found, and the props both override paths
56356
+ * hand to a component. `PageRenderer` owns the page-level composition; this
56357
+ * module owns the pieces it reaches for per block.
56358
+ */
56359
+ function fallback(message) {
56360
+ return /* @__PURE__ */ jsx("div", {
56361
+ className: "rb-p-6 rb-caption",
56362
+ style: textColorStyle("mutedText"),
56363
+ children: message
56364
+ });
56365
+ }
56366
+ /**
56367
+ * A page with nothing to show. Authoring surfaces get the diagnostic; a public
56368
+ * page renders nothing at all rather than telling a visitor about the outline.
56369
+ */
56370
+ function emptyPageNotice(audience) {
56371
+ if (audience === "public") return null;
56372
+ return /* @__PURE__ */ jsx("div", {
56373
+ className: "rb-p-6 rb-caption",
56374
+ style: textColorStyle("mutedText"),
56375
+ children: "No blocks found."
56376
+ });
56377
+ }
56378
+ /**
56379
+ * A public page dropped a block, so nothing in the rendered HTML says the
56380
+ * content is missing. Say it in the logs instead.
56381
+ */
56382
+ function warnOmittedFromPublicPage(input) {
56383
+ try {
56384
+ console.warn("[blocks:PageRenderer] omitted a non-renderable block from a public page", {
56385
+ blockId: input.block.id ?? null,
56386
+ blockKind: input.block.kind,
56387
+ outcome: input.outcome,
56388
+ ...input.admissionError ? { error: input.admissionError } : {}
56389
+ });
56390
+ } catch {}
56391
+ }
56392
+ function shouldDebugCustomBlocks() {
56393
+ try {
56394
+ return globalThis.process?.env?.DEBUG_CUSTOM_BLOCKS === "1";
56395
+ } catch {
56396
+ return false;
56397
+ }
56398
+ }
56399
+ /**
56400
+ * Look up a block override component by block kind.
56401
+ * Supports both full ("block.hero") and short ("hero") keys.
56402
+ */
56403
+ function getBlockOverride(blockKind, overrides) {
56404
+ if (!overrides) return void 0;
56405
+ if (overrides[blockKind]) return overrides[blockKind];
56406
+ const shortKind = blockKind.replace(/^block\./, "");
56407
+ if (overrides[shortKind]) return overrides[shortKind];
56408
+ }
56409
+ /**
56410
+ * Resolves block content based on stage priority.
56411
+ * For draft/preview: draftContent takes priority over content.
56412
+ * For published: content is the source of truth.
56413
+ * Returns empty object if no content exists (validator will handle placeholders if needed).
56414
+ */
56415
+ function getRenderableContent(block, previewStage = "published") {
56416
+ const source = previewStage === "preview" && block.draftContent ? block.draftContent : block.content ?? {};
56417
+ if (!isObjectRecord$1(source)) return {};
56418
+ return source;
56419
+ }
56420
+ function resolvePageRendererImageResolveContext(dataContext) {
56421
+ const input = dataContext?.imageResolveContext;
56422
+ return buildResolveImageUrlContext({
56423
+ siteId: input?.siteId ?? dataContext?.siteId,
56424
+ supabaseUrl: input?.supabaseUrl ?? dataContext?.supabaseUrl,
56425
+ mediaAssetIndex: input?.mediaAssetIndex,
56426
+ mediaAssets: input?.mediaAssets ?? dataContext?.mediaAssets
56427
+ });
56428
+ }
56429
+ /**
56430
+ * The RSC-safe props every override component receives, whether the block
56431
+ * resolved a manifest or is being rendered from an override alone.
56432
+ */
56433
+ function buildBlockOverrideProps(input) {
56434
+ return {
56435
+ content: input.content,
56436
+ ...overrideThemeProps(input.themeResources),
56437
+ ...blockOverrideContextProps(input.dataContext),
56438
+ data: input.data,
56439
+ imageResolveContext: input.imageResolveContext,
56440
+ blockId: input.block.id ?? null,
56441
+ blockKind: input.block.kind
56442
+ };
56443
+ }
56444
+ /** The render-context half of the override props, absent context included. */
56445
+ function blockOverrideContextProps(dataContext) {
56446
+ const context = dataContext ?? {};
56447
+ return {
56448
+ siteId: context.siteId,
56449
+ apiBaseUrl: context.apiBaseUrl,
56450
+ bookingTimeZone: context.bookingTimeZone,
56451
+ portalToken: context.portalToken ?? null,
56452
+ previewToken: context.previewToken ?? null,
56453
+ stage: context.previewStage,
56454
+ occurrenceContext: context.occurrenceContext ?? null,
56455
+ entrySubrouteContext: context.entrySubrouteContext ?? null,
56456
+ contentEntry: context.contentEntry ?? null,
56457
+ pageSurface: context.pageSurface ?? null,
56458
+ subject: context.subject ?? null,
56459
+ supabaseUrl: context.supabaseUrl
56460
+ };
56461
+ }
56462
+ const blockComponentCache = /* @__PURE__ */ new WeakMap();
56463
+ const DEFAULT_PERF_SLOW_BLOCK_RENDER_THRESHOLD_MS = 25;
56464
+ let cachedPerfSlowBlockRenderThresholdMs = null;
56465
+ function getPerfSlowBlockRenderThresholdMs() {
56466
+ if (cachedPerfSlowBlockRenderThresholdMs !== null) return cachedPerfSlowBlockRenderThresholdMs;
56467
+ const raw = globalThis.process?.env?.DEBUG_PERF_BLOCK_MS;
56468
+ const parsed = raw ? Number.parseInt(raw, 10) : NaN;
56469
+ cachedPerfSlowBlockRenderThresholdMs = Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_PERF_SLOW_BLOCK_RENDER_THRESHOLD_MS;
56470
+ return cachedPerfSlowBlockRenderThresholdMs;
56471
+ }
56472
+ function getCachedBlockComponent(manifest) {
56473
+ const cached = blockComponentCache.get(manifest);
56474
+ if (cached) return cached;
56475
+ const created = makeDefaultBlockComponent({ manifest });
56476
+ blockComponentCache.set(manifest, created);
56477
+ return created;
56478
+ }
56479
+ //#endregion
55801
56480
  //#region ../blocks/src/PageRenderer.tsx
55802
56481
  function PageRenderer$1(props) {
55803
56482
  const page = React$1.useMemo(() => toPresentationPage(props.page), [props.page]);
55804
56483
  return /* @__PURE__ */ jsx(PageRendererCore, {
55805
56484
  ...props,
55806
- page
56485
+ page,
56486
+ audience: "public"
55807
56487
  });
55808
56488
  }
55809
56489
  function PageRendererCore(props) {
55810
- const { page, runtimeBoundaries, dataContext, wrapBlock, wrapBoundary, registry } = props;
56490
+ const { page, audience, runtimeBoundaries, dataContext, wrapBlock, wrapBoundary, registry } = props;
55811
56491
  const imageResolveContext = React$1.useMemo(() => resolvePageRendererImageResolveContext(dataContext), [
55812
56492
  dataContext?.imageResolveContext,
55813
56493
  dataContext?.mediaAssets,
55814
56494
  dataContext?.siteId,
55815
56495
  dataContext?.supabaseUrl
55816
56496
  ]);
55817
- const boundaryByAdjacentBlockIds = React$1.useMemo(() => boundaryMapForAdjacentBlockIds(runtimeBoundaries ?? []), [runtimeBoundaries]);
55818
56497
  const usesRuntimeBoundaries = runtimeBoundaries !== void 0;
55819
56498
  const themeResources = React$1.useMemo(() => resolvePageThemeResources(props), [
55820
56499
  props.rendering,
55821
56500
  props.theme,
55822
56501
  props.themeTokens
55823
56502
  ]);
55824
- if (!page || page.blocks.length === 0) return /* @__PURE__ */ jsx("div", {
55825
- className: "rb-p-6 rb-caption",
55826
- style: textColorStyle("mutedText"),
55827
- children: "No blocks found."
55828
- });
55829
56503
  const resolvedRegistry = registry ?? getDefaultComponentRegistry();
55830
56504
  setContextResolveImageUrlContext(imageResolveContext);
55831
- return /* @__PURE__ */ jsx("div", { children: page.blocks.map((block, index) => {
55832
- const blockPosition = pageBlockPosition(index, page.blocks.length);
55833
- const blockKey = block.id ?? `${block.kind}-${index}`;
55834
- const previousBlock = page.blocks[index - 1] ?? null;
55835
- const nextBlock = page.blocks[index + 1] ?? null;
56505
+ const sections = usePageSections(props, {
56506
+ page,
56507
+ themeResources,
56508
+ imageResolveContext,
56509
+ registry: resolvedRegistry
56510
+ });
56511
+ const boundaryByAdjacentBlockIds = React$1.useMemo(() => boundaryMapForAdjacentBlockIds(boundariesForSurvivingBlocks(runtimeBoundaries ?? [], sections.map(({ block }) => ({
56512
+ ...block,
56513
+ content: getRenderableContent(block, dataContext?.previewStage)
56514
+ })))), [
56515
+ runtimeBoundaries,
56516
+ sections,
56517
+ dataContext?.previewStage
56518
+ ]);
56519
+ if (sections.length === 0) return emptyPageNotice(audience);
56520
+ return /* @__PURE__ */ jsx("div", { children: sections.map((section, index) => {
56521
+ const { block, blockKey, node } = section;
56522
+ const blockPosition = pageBlockPosition(index, sections.length);
56523
+ const previousBlock = sections[index - 1]?.block ?? null;
56524
+ const nextBlock = sections[index + 1]?.block ?? null;
55836
56525
  const boundaryBefore = boundaryBetweenBlocks(previousBlock, block, boundaryByAdjacentBlockIds);
55837
56526
  const boundaryAfter = boundaryBetweenBlocks(block, nextBlock, boundaryByAdjacentBlockIds);
55838
56527
  const boundaryElement = renderBoundaryAfterBlock({
@@ -55847,19 +56536,7 @@ function PageRendererCore(props) {
55847
56536
  const managesOwnWrapper = definition?.manifest.behaviours?.managesOwnWrapper;
55848
56537
  const renderableContent = getRenderableContent(block, dataContext?.previewStage);
55849
56538
  const variantKey = definition?.manifest ? resolveVariantKey(definition.manifest, renderableContent) : null;
55850
- const shouldUseWrapper = !managesOwnWrapper || Boolean(wrapBlock);
55851
- const blockSectionProps = buildBlockSectionProps({
55852
- props,
55853
- themeResources,
55854
- block,
55855
- blockKey,
55856
- imageResolveContext,
55857
- registry: resolvedRegistry
55858
- });
55859
- if (!shouldUseWrapper) {
55860
- const rendered = /* @__PURE__ */ jsx(MemoBlockSection, { ...blockSectionProps }, `${blockKey}:block`);
55861
- return boundaryElement ? /* @__PURE__ */ jsxs(React$1.Fragment, { children: [rendered, boundaryElement] }, blockKey) : rendered;
55862
- }
56539
+ if (!(!managesOwnWrapper || Boolean(wrapBlock))) return /* @__PURE__ */ jsxs(React$1.Fragment, { children: [node, boundaryElement] }, blockKey);
55863
56540
  const wrapperClassName = [`block-${cleanKind}`, wrapBlock ? "group relative" : null].filter(Boolean).join(" ");
55864
56541
  const styleGroup = typeof renderableContent.styleGroup === "string" ? renderableContent.styleGroup : void 0;
55865
56542
  const blockLook = typeof renderableContent.blockLook === "string" ? renderableContent.blockLook : void 0;
@@ -55884,7 +56561,7 @@ function PageRendererCore(props) {
55884
56561
  "data-site-style-boundary-after": boundaryAfter?.metrics.size,
55885
56562
  "data-style-group": styleGroup,
55886
56563
  style: blockStyle,
55887
- children: /* @__PURE__ */ jsx(MemoBlockSection, { ...blockSectionProps })
56564
+ children: node
55888
56565
  }, blockKey);
55889
56566
  if (boundaryElement) return /* @__PURE__ */ jsxs(React$1.Fragment, { children: [blockElement, boundaryElement] }, blockKey);
55890
56567
  if (usesRuntimeBoundaries || !isSiteStyleTransitionStyleGroup(styleGroup)) return blockElement;
@@ -55897,18 +56574,69 @@ function PageRendererCore(props) {
55897
56574
  })] }, blockKey);
55898
56575
  }) });
55899
56576
  }
55900
- function resolvePageRendererImageResolveContext(dataContext) {
55901
- const input = dataContext?.imageResolveContext;
55902
- return buildResolveImageUrlContext({
55903
- siteId: input?.siteId ?? dataContext?.siteId,
55904
- supabaseUrl: input?.supabaseUrl ?? dataContext?.supabaseUrl,
55905
- mediaAssetIndex: input?.mediaAssetIndex,
55906
- mediaAssets: input?.mediaAssets ?? dataContext?.mediaAssets
56577
+ /**
56578
+ * Memoised so that a re-render with unchanged inputs reuses the same section
56579
+ * elements and React can skip the block subtrees, exactly as the per-block memo
56580
+ * did before public pages started resolving their bodies up front.
56581
+ */
56582
+ function usePageSections(props, input) {
56583
+ const { page, themeResources, imageResolveContext, registry } = input;
56584
+ return React$1.useMemo(() => admitPageSections({
56585
+ props,
56586
+ blocks: page?.blocks ?? [],
56587
+ audience: props.audience,
56588
+ themeResources,
56589
+ imageResolveContext,
56590
+ registry
56591
+ }), [
56592
+ page,
56593
+ props.audience,
56594
+ themeResources,
56595
+ imageResolveContext,
56596
+ registry,
56597
+ props.dataContext,
56598
+ props.routeMap,
56599
+ props.wrapBlock,
56600
+ props.blockOverrides,
56601
+ props.sdkConfig,
56602
+ props.customBlocks,
56603
+ props.previewAuthoring
56604
+ ]);
56605
+ }
56606
+ /**
56607
+ * Resolves each block's rendered body before the page decides what to wrap.
56608
+ *
56609
+ * On a public page the body is resolved eagerly so a block that cannot render
56610
+ * is dropped along with its wrapper, its data attributes, its boundary reserve
56611
+ * space and its style-group sibling — the section body is the single source of
56612
+ * truth for whether the block is on the page (issue 1378). Authoring surfaces
56613
+ * keep the memoised component so the editor's render-loop guards still hold.
56614
+ */
56615
+ function admitPageSections(input) {
56616
+ const sections = [];
56617
+ input.blocks.forEach((block, index) => {
56618
+ const blockKey = block.id ?? `${block.kind}-${index}`;
56619
+ const blockSectionProps = buildBlockSectionProps({
56620
+ props: input.props,
56621
+ themeResources: input.themeResources,
56622
+ block,
56623
+ blockKey,
56624
+ imageResolveContext: input.imageResolveContext,
56625
+ registry: input.registry
56626
+ });
56627
+ const node = input.audience === "public" ? renderBlockSection(blockSectionProps) : /* @__PURE__ */ jsx(MemoBlockSection, { ...blockSectionProps }, `${blockKey}:block`);
56628
+ if (node === null || node === void 0) return;
56629
+ sections.push({
56630
+ block,
56631
+ blockKey,
56632
+ node
56633
+ });
55907
56634
  });
56635
+ return sections;
55908
56636
  }
55909
- const resolveBookingTimeZone = (dataContext) => dataContext?.bookingTimeZone;
55910
56637
  function buildBlockSectionProps(input) {
55911
56638
  return {
56639
+ audience: input.props.audience,
55912
56640
  themeResources: input.themeResources,
55913
56641
  block: input.block,
55914
56642
  dataContext: input.props.dataContext,
@@ -55924,40 +56652,24 @@ function buildBlockSectionProps(input) {
55924
56652
  };
55925
56653
  }
55926
56654
  /**
55927
- * Look up a block override component by block kind.
55928
- * Supports both full ("block.hero") and short ("hero") keys.
55929
- */
55930
- function getBlockOverride(blockKind, overrides) {
55931
- if (!overrides) return void 0;
55932
- if (overrides[blockKind]) return overrides[blockKind];
55933
- const shortKind = blockKind.replace(/^block\./, "");
55934
- if (overrides[shortKind]) return overrides[shortKind];
55935
- }
55936
- const blockComponentCache = /* @__PURE__ */ new WeakMap();
55937
- const DEFAULT_PERF_SLOW_BLOCK_RENDER_THRESHOLD_MS = 25;
55938
- let cachedPerfSlowBlockRenderThresholdMs = null;
55939
- function getPerfSlowBlockRenderThresholdMs() {
55940
- if (cachedPerfSlowBlockRenderThresholdMs !== null) return cachedPerfSlowBlockRenderThresholdMs;
55941
- const raw = globalThis.process?.env?.DEBUG_PERF_BLOCK_MS;
55942
- const parsed = raw ? Number.parseInt(raw, 10) : NaN;
55943
- cachedPerfSlowBlockRenderThresholdMs = Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_PERF_SLOW_BLOCK_RENDER_THRESHOLD_MS;
55944
- return cachedPerfSlowBlockRenderThresholdMs;
55945
- }
55946
- function getCachedBlockComponent(manifest) {
55947
- const cached = blockComponentCache.get(manifest);
55948
- if (cached) return cached;
55949
- const created = makeDefaultBlockComponent({ manifest });
55950
- blockComponentCache.set(manifest, created);
55951
- return created;
55952
- }
55953
- /**
55954
56655
  * Memoized block section component.
55955
56656
  *
55956
56657
  * Custom memoization prevents render loops during editing by explicitly checking
55957
56658
  * referential equality of all props that affect rendering. Standard React.memo
55958
56659
  * wouldn't catch all the render triggers from the editor store.
55959
56660
  */
55960
- const MemoBlockSection = React$1.memo(function MemoBlockSection({ themeResources, block, dataContext, registry, routeMap, wrapBlock, blockOverrides, sdkConfig, customBlocks, imageResolveContext, previewAuthoring }) {
56661
+ const MemoBlockSection = React$1.memo(function MemoBlockSection(props) {
56662
+ return renderBlockSection(props);
56663
+ }, (prev, next) => {
56664
+ 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;
56665
+ });
56666
+ /**
56667
+ * Renders a single block body.
56668
+ *
56669
+ * Returns `null` when the block cannot render and the audience is `public`;
56670
+ * the caller treats that as "this block is not on the page".
56671
+ */
56672
+ function renderBlockSection({ audience, themeResources, block, dataContext, registry, routeMap, wrapBlock, blockOverrides, sdkConfig, customBlocks, imageResolveContext, previewAuthoring }) {
55961
56673
  const perfStart = globalThis.process?.env?.DEBUG_PERF === "1" && typeof window === "undefined" ? Date.now() : 0;
55962
56674
  const fallbackImageResolveContext = getCurrentResolveImageUrlContext();
55963
56675
  const resolvedImageResolveContext = imageResolveContext ?? fallbackImageResolveContext;
@@ -55977,19 +56689,30 @@ const MemoBlockSection = React$1.memo(function MemoBlockSection({ themeResources
55977
56689
  } catch {}
55978
56690
  return node;
55979
56691
  }
55980
- if (block.presentationAdmission.kind === "invalid") {
55981
- try {
55982
- if (globalThis.process?.env?.NODE_ENV !== "production") console.warn("[blocks:PageRenderer] block admission failed", {
55983
- blockId: block.id ?? null,
55984
- blockKind: block.kind,
55985
- error: block.presentationAdmission.error
55986
- });
55987
- } catch {}
55988
- return finish(fallback(`Invalid content for block kind "${block.kind}".`), {
55989
- outcome: "invalidContent",
55990
- override: false
56692
+ /**
56693
+ * The one place that decides what a block that cannot render leaves behind.
56694
+ * A public page gets nothing (and a log line, since dropping content on a
56695
+ * 200 is otherwise invisible); authoring surfaces get the diagnostic.
56696
+ */
56697
+ function diagnostic(message, detail) {
56698
+ if (audience !== "public") return fallback(message);
56699
+ warnOmittedFromPublicPage({
56700
+ block,
56701
+ ...detail
55991
56702
  });
56703
+ return null;
56704
+ }
56705
+ function withBlockWrapper(node) {
56706
+ if (node === null) return null;
56707
+ return wrapBlock && block.id ? /* @__PURE__ */ jsx(Fragment, { children: wrapBlock(block.id, node) }) : /* @__PURE__ */ jsx(Fragment, { children: node });
55992
56708
  }
56709
+ if (block.presentationAdmission.kind === "invalid") return finish(diagnostic(`Invalid content for block kind "${block.kind}".`, {
56710
+ outcome: "invalidContent",
56711
+ admissionError: block.presentationAdmission.error
56712
+ }), {
56713
+ outcome: "invalidContent",
56714
+ override: false
56715
+ });
55993
56716
  const OverrideComponent = getBlockOverride(block.kind, blockOverrides);
55994
56717
  requireBlockOverride(themeResources, OverrideComponent, block.kind);
55995
56718
  let definition = getBlockDefinition(block.kind);
@@ -56002,25 +56725,14 @@ const MemoBlockSection = React$1.memo(function MemoBlockSection({ themeResources
56002
56725
  }).findManifest(block.kind)?.descriptor.definition;
56003
56726
  if (!definition) {
56004
56727
  if (OverrideComponent) {
56005
- const rendered = /* @__PURE__ */ jsx(OverrideComponent, {
56728
+ const rendered = /* @__PURE__ */ jsx(OverrideComponent, { ...buildBlockOverrideProps({
56729
+ block,
56006
56730
  content: getRenderableContent(block, dataContext?.previewStage),
56007
- ...overrideThemeProps(themeResources),
56008
56731
  data: void 0,
56009
- siteId: dataContext?.siteId,
56010
- apiBaseUrl: dataContext?.apiBaseUrl,
56011
- portalToken: dataContext?.portalToken ?? null,
56012
- previewToken: dataContext?.previewToken ?? null,
56013
- stage: dataContext?.previewStage,
56014
- occurrenceContext: dataContext?.occurrenceContext ?? null,
56015
- entrySubrouteContext: dataContext?.entrySubrouteContext ?? null,
56016
- contentEntry: dataContext?.contentEntry ?? null,
56017
- pageSurface: dataContext?.pageSurface ?? null,
56018
- subject: dataContext?.subject ?? null,
56019
- supabaseUrl: dataContext?.supabaseUrl,
56020
- imageResolveContext: resolvedImageResolveContext,
56021
- blockId: block.id ?? null,
56022
- blockKind: block.kind
56023
- });
56732
+ themeResources,
56733
+ dataContext,
56734
+ imageResolveContext: resolvedImageResolveContext
56735
+ }) });
56024
56736
  return finish(wrapBlock && block.id ? /* @__PURE__ */ jsx(Fragment, { children: wrapBlock(block.id, rendered) }) : /* @__PURE__ */ jsx(Fragment, { children: rendered }), {
56025
56737
  outcome: "overrideWithoutDefinition",
56026
56738
  override: true,
@@ -56036,8 +56748,7 @@ const MemoBlockSection = React$1.memo(function MemoBlockSection({ themeResources
56036
56748
  dbCustomBlocksCount: customBlocks?.length ?? 0
56037
56749
  });
56038
56750
  } catch {}
56039
- 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}".`);
56040
- return finish(wrapBlock && block.id ? /* @__PURE__ */ jsx(Fragment, { children: wrapBlock(block.id, rendered) }) : /* @__PURE__ */ jsx(Fragment, { children: rendered }), {
56751
+ 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" })), {
56041
56752
  outcome: "missingDefinition",
56042
56753
  override: false
56043
56754
  });
@@ -56052,8 +56763,7 @@ const MemoBlockSection = React$1.memo(function MemoBlockSection({ themeResources
56052
56763
  manifestKeys: Object.keys(definition.manifest ?? {}).sort()
56053
56764
  });
56054
56765
  } catch {}
56055
- 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}".`);
56056
- return finish(wrapBlock && block.id ? /* @__PURE__ */ jsx(Fragment, { children: wrapBlock(block.id, rendered) }) : /* @__PURE__ */ jsx(Fragment, { children: rendered }), {
56766
+ 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" })), {
56057
56767
  outcome: "missingLayout",
56058
56768
  override: false
56059
56769
  });
@@ -56063,39 +56773,24 @@ const MemoBlockSection = React$1.memo(function MemoBlockSection({ themeResources
56063
56773
  if (block.presentationAdmission.kind === "editor") {
56064
56774
  const draft = validateManifestDraft(compileManifestContentContract(definition.manifest), content);
56065
56775
  if (draft.kind === "previewable") data = draft.data;
56066
- else {
56067
- const rendered = fallback(`Unable to render block kind "${block.kind}".`);
56068
- return finish(wrapBlock && block.id ? /* @__PURE__ */ jsx(Fragment, { children: wrapBlock(block.id, rendered) }) : /* @__PURE__ */ jsx(Fragment, { children: rendered }), {
56069
- outcome: "invalidPlaceholder",
56070
- override: false
56071
- });
56072
- }
56776
+ else return finish(withBlockWrapper(diagnostic(`Unable to render block kind "${block.kind}".`, { outcome: "invalidPlaceholder" })), {
56777
+ outcome: "invalidPlaceholder",
56778
+ override: false
56779
+ });
56073
56780
  }
56074
56781
  try {
56075
56782
  const routes = routeMap ?? dataContext?.routes;
56076
56783
  const hydratedContent = routes ? hydrateManifestLinks(definition.manifest, data, routes, dataContext?.pagesByIdentifier, dataContext?.entriesByIdentifier) : data;
56077
56784
  const resolved = resolveBlockData$1(block.id, definition.dataLoaders, dataContext, routes);
56078
56785
  const hasResolvedData = !!resolved && typeof resolved === "object" && Object.keys(resolved).length > 0;
56079
- const baseProps = {
56786
+ const baseProps = buildBlockOverrideProps({
56787
+ block,
56080
56788
  content: hydratedContent,
56081
- ...overrideThemeProps(themeResources),
56082
56789
  data: resolved,
56083
- siteId: dataContext?.siteId,
56084
- apiBaseUrl: dataContext?.apiBaseUrl,
56085
- portalToken: dataContext?.portalToken ?? null,
56086
- previewToken: dataContext?.previewToken ?? null,
56087
- stage: dataContext?.previewStage,
56088
- occurrenceContext: dataContext?.occurrenceContext ?? null,
56089
- entrySubrouteContext: dataContext?.entrySubrouteContext ?? null,
56090
- contentEntry: dataContext?.contentEntry ?? null,
56091
- pageSurface: dataContext?.pageSurface ?? null,
56092
- subject: dataContext?.subject ?? null,
56093
- supabaseUrl: dataContext?.supabaseUrl,
56094
- imageResolveContext: resolvedImageResolveContext,
56095
- blockId: block.id ?? null,
56096
- blockKind: block.kind
56097
- };
56098
- baseProps.bookingTimeZone = resolveBookingTimeZone(dataContext);
56790
+ themeResources,
56791
+ dataContext,
56792
+ imageResolveContext: resolvedImageResolveContext
56793
+ });
56099
56794
  let rendered;
56100
56795
  if (OverrideComponent) rendered = /* @__PURE__ */ jsx(OverrideComponent, { ...baseProps });
56101
56796
  else {
@@ -56142,39 +56837,11 @@ const MemoBlockSection = React$1.memo(function MemoBlockSection({ themeResources
56142
56837
  hasResolvedData
56143
56838
  });
56144
56839
  } catch {
56145
- const rendered = block.presentationAdmission.kind === "editor" ? fallback(`Unable to render block kind "${block.kind}".`) : fallback(`Invalid content for block kind "${block.kind}".`);
56146
- return finish(wrapBlock && block.id ? /* @__PURE__ */ jsx(Fragment, { children: wrapBlock(block.id, rendered) }) : /* @__PURE__ */ jsx(Fragment, { children: rendered }), {
56840
+ return finish(withBlockWrapper(diagnostic(block.presentationAdmission.kind === "editor" ? `Unable to render block kind "${block.kind}".` : `Invalid content for block kind "${block.kind}".`, { outcome: "exception" })), {
56147
56841
  outcome: "exception",
56148
56842
  override: false
56149
56843
  });
56150
56844
  }
56151
- }, (prev, next) => {
56152
- 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;
56153
- });
56154
- function fallback(message) {
56155
- return /* @__PURE__ */ jsx("div", {
56156
- className: "rb-p-6 rb-caption",
56157
- style: textColorStyle("mutedText"),
56158
- children: message
56159
- });
56160
- }
56161
- function shouldDebugCustomBlocks() {
56162
- try {
56163
- return globalThis.process?.env?.DEBUG_CUSTOM_BLOCKS === "1";
56164
- } catch {
56165
- return false;
56166
- }
56167
- }
56168
- /**
56169
- * Resolves block content based on stage priority.
56170
- * For draft/preview: draftContent takes priority over content.
56171
- * For published: content is the source of truth.
56172
- * Returns empty object if no content exists (validator will handle placeholders if needed).
56173
- */
56174
- function getRenderableContent(block, previewStage = "published") {
56175
- const source = previewStage === "preview" && block.draftContent ? block.draftContent : block.content ?? {};
56176
- if (!isObjectRecord$1(source)) return {};
56177
- return source;
56178
56845
  }
56179
56846
  //#endregion
56180
56847
  //#region ../theme-core/src/generated/containerResponsiveThemeCss.ts
@@ -65100,6 +65767,21 @@ const containerResponsiveThemeCss = `/*
65100
65767
  outline-offset: 2px;
65101
65768
  }
65102
65769
 
65770
+ /* Event titles always link to the event page, so a card or row whose CTA is
65771
+ hidden (sold out, closed) is never a dead end (issue 1372). The link inherits
65772
+ the heading's colour and only reveals itself on hover or keyboard focus.
65773
+ Shared by EventCard, EventCompactRow and the offerings-preview course cards via \`.event-title-link\`. */
65774
+ :where(.theme-scope) .event-title-link {
65775
+ color: inherit;
65776
+ text-decoration: none;
65777
+ }
65778
+
65779
+ :where(.theme-scope) .event-title-link:hover,
65780
+ :where(.theme-scope) .event-title-link:focus-visible {
65781
+ text-decoration: underline;
65782
+ text-underline-offset: 0.12em;
65783
+ }
65784
+
65103
65785
  /* =============================================================================
65104
65786
  * Block: Carousel Navigation (Phase 3 client component migration)
65105
65787
  * =============================================================================
@@ -73155,6 +73837,83 @@ const SDK_ENDPOINT_DEFINITIONS = defineEndpointDefinitions({
73155
73837
  errors: ["validation:invalid_input", "server:internal_error"]
73156
73838
  }
73157
73839
  });
73840
+ //#endregion
73841
+ //#region ../api/src/sdkMediaEndpointDefinitions.ts
73842
+ const SDK_MEDIA_ENDPOINT_DEFINITIONS = {
73843
+ sdkMediaExists: {
73844
+ path: "/sdk/{siteId}/media/exists",
73845
+ method: "POST",
73846
+ errors: ["validation:invalid_input"],
73847
+ auth: "service",
73848
+ responseKind: "json"
73849
+ },
73850
+ sdkMediaFindByChecksum: {
73851
+ path: "/sdk/{siteId}/media/find-by-checksum",
73852
+ method: "POST",
73853
+ errors: ["server:internal_error"],
73854
+ auth: "service",
73855
+ responseKind: "json"
73856
+ },
73857
+ sdkMediaResolve: {
73858
+ path: "/sdk/{siteId}/media/resolve",
73859
+ method: "POST",
73860
+ errors: ["server:internal_error"],
73861
+ auth: "service",
73862
+ responseKind: "json"
73863
+ },
73864
+ sdkMediaList: {
73865
+ path: "/sdk/{siteId}/media",
73866
+ method: "GET",
73867
+ auth: "service",
73868
+ responseKind: "json"
73869
+ },
73870
+ sdkMediaDownload: {
73871
+ path: "/sdk/{siteId}/media/{identifier}/download",
73872
+ method: "GET",
73873
+ auth: "service",
73874
+ responseKind: "json"
73875
+ },
73876
+ sdkMediaDeletePreview: {
73877
+ path: "/sdk/{siteId}/media/{identifier}/delete-preview",
73878
+ method: "POST",
73879
+ auth: "service",
73880
+ responseKind: "json"
73881
+ },
73882
+ sdkMediaDeleteApply: {
73883
+ path: "/sdk/{siteId}/media/{identifier}",
73884
+ method: "DELETE",
73885
+ auth: "service",
73886
+ responseKind: "json"
73887
+ },
73888
+ sdkMediaUploadUrl: {
73889
+ path: "/sdk/{siteId}/media/upload-url",
73890
+ method: "POST",
73891
+ errors: [
73892
+ "resource:not_found",
73893
+ "validation:invalid_input",
73894
+ "resource:conflict",
73895
+ "validation:payload_too_large",
73896
+ "billing:plan_limit_exceeded",
73897
+ "server:internal_error"
73898
+ ],
73899
+ auth: "service",
73900
+ responseKind: "json"
73901
+ },
73902
+ sdkMediaUploadFinalize: {
73903
+ path: "/sdk/{siteId}/media/upload",
73904
+ method: "POST",
73905
+ errors: [
73906
+ "resource:not_found",
73907
+ "validation:invalid_input",
73908
+ "resource:conflict",
73909
+ "validation:payload_too_large",
73910
+ "billing:plan_limit_exceeded",
73911
+ "server:internal_error"
73912
+ ],
73913
+ auth: "service",
73914
+ responseKind: "json"
73915
+ }
73916
+ };
73158
73917
  ({
73159
73918
  ...ACCOUNT_ENDPOINT_DEFINITIONS,
73160
73919
  ...PUBLIC_EVENT_ENDPOINT_DEFINITIONS,
@@ -73163,7 +73922,7 @@ const SDK_ENDPOINT_DEFINITIONS = defineEndpointDefinitions({
73163
73922
  ...NEWSLETTER_ENDPOINT_DEFINITIONS,
73164
73923
  ...APPOINTMENT_ENDPOINT_DEFINITIONS,
73165
73924
  ...EVENT_SERIES_REGISTRATION_ENDPOINT_DEFINITIONS
73166
- }, 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 };
73925
+ }, 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 };
73167
73926
  z$1.object({ refreshToken: z$1.string().min(1).max(4096) }).strict();
73168
73927
  z$1.object({
73169
73928
  accessToken: z$1.string().min(1).max(4096),
@@ -73816,6 +74575,50 @@ z$1.object({
73816
74575
  title: z$1.string(),
73817
74576
  slug: z$1.string().min(1)
73818
74577
  });
74578
+ z$1.object({
74579
+ signedUrl: z$1.string().url(),
74580
+ token: z$1.string().min(1),
74581
+ storagePath: z$1.string().min(1)
74582
+ }).strict();
74583
+ const backupManifestSchema = z$1.object({
74584
+ siteId: z$1.string().min(1),
74585
+ siteSlug: z$1.string().min(1),
74586
+ siteTitle: z$1.string(),
74587
+ exportedAt: z$1.string().min(1),
74588
+ tableCount: z$1.number().int().nonnegative(),
74589
+ totalRecords: z$1.number().int().nonnegative(),
74590
+ pageCount: z$1.number().int().nonnegative(),
74591
+ blockCount: z$1.number().int().nonnegative(),
74592
+ contentEntryCount: z$1.number().int().nonnegative(),
74593
+ formCount: z$1.number().int().nonnegative(),
74594
+ navigationMenuCount: z$1.number().int().nonnegative(),
74595
+ mediaCount: z$1.number().int().nonnegative(),
74596
+ mediaBytes: z$1.number().int().nonnegative()
74597
+ }).strict();
74598
+ z$1.object({
74599
+ ok: z$1.literal(true),
74600
+ manifest: backupManifestSchema,
74601
+ storagePath: z$1.string().min(1)
74602
+ }).strict();
74603
+ z$1.object({
74604
+ ok: z$1.literal(true),
74605
+ siteId: brandedIdSchema(parseSiteId, "site id"),
74606
+ slug: z$1.string().min(1),
74607
+ title: z$1.string(),
74608
+ counts: z$1.record(z$1.string(), z$1.object({
74609
+ created: z$1.number().int().nonnegative(),
74610
+ updated: z$1.number().int().nonnegative(),
74611
+ skipped: z$1.number().int().nonnegative(),
74612
+ failed: z$1.number().int().nonnegative()
74613
+ }).strict()),
74614
+ warnings: z$1.array(z$1.string()),
74615
+ errors: z$1.array(z$1.object({
74616
+ table: z$1.string(),
74617
+ id: z$1.string(),
74618
+ error: z$1.string()
74619
+ }).strict())
74620
+ }).strict();
74621
+ z$1.object({ deleted: z$1.boolean() }).strict();
73819
74622
  const apiKeyIdSchema = brandedIdSchema(parseApiKeyId, "API key id");
73820
74623
  const standardKeyIdsSchema = z$1.object({
73821
74624
  live: apiKeyIdSchema.optional(),