@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
@@ -7665,6 +7665,11 @@ const gradientConfigSchema = z.object({
7665
7665
  position: z.string().optional(),
7666
7666
  stops: z.array(gradientStopSchema).min(2)
7667
7667
  });
7668
+ const gradientsSchema = z.object({
7669
+ button: z.string().min(1).nullable().optional(),
7670
+ hero: z.string().min(1).nullable().optional(),
7671
+ background: z.string().min(1).nullable().optional()
7672
+ });
7668
7673
  //#endregion
7669
7674
  //#region ../theme-core/src/interactive/baseSchema.ts
7670
7675
  /**
@@ -8666,11 +8671,30 @@ const headerContainerSchema = z.object({
8666
8671
  tint: z.string().nullable().optional(),
8667
8672
  opacity: z.number().min(0).max(1).default(.12)
8668
8673
  }).optional();
8669
- const headerInteractionPaletteSchema = z.enum([
8674
+ //#endregion
8675
+ //#region ../theme-core/src/header/interactionPalette.ts
8676
+ /**
8677
+ * The palettes that fill the nav link with a colour on hover and focus. Each
8678
+ * one names a background/foreground token pair.
8679
+ */
8680
+ const headerInteractionPillPaletteValues = [
8670
8681
  "primary",
8671
8682
  "secondary",
8672
8683
  "accent"
8673
- ]);
8684
+ ];
8685
+ /**
8686
+ * "No pill": the nav link keeps whatever hover treatment its recipe compiled,
8687
+ * and the palette step writes nothing at all (#1318 Phase 4c decision 2).
8688
+ *
8689
+ * It exists because "a colour change on hover, no filled background" was not
8690
+ * expressible in the vocabulary, and it is what
8691
+ * `anna-raven-therapy-and-yoga` and `kirtan-lucy` have rendered since before
8692
+ * the palette existed. Without it those two rows can only be classified as a
8693
+ * conscious visual change.
8694
+ */
8695
+ const headerInteractionQuietPalette = "quiet";
8696
+ const headerInteractionPaletteValues = [...headerInteractionPillPaletteValues, headerInteractionQuietPalette];
8697
+ const headerInteractionPaletteSchema = z.enum(headerInteractionPaletteValues);
8674
8698
  //#endregion
8675
8699
  //#region ../theme-core/src/boxBackgroundSchema.ts
8676
8700
  const boxBackgroundOverlaySchema = z.preprocess(normalizeAuthoringOpacity, z.discriminatedUnion("type", [
@@ -9052,11 +9076,12 @@ const inputStyle = z.object({
9052
9076
  ]),
9053
9077
  label: z.enum(["inside", "above"])
9054
9078
  });
9055
- const headerPositioning = z.enum([
9079
+ const headerPositioningValues = [
9056
9080
  "static",
9057
9081
  "sticky",
9058
9082
  "fixed"
9059
- ]);
9083
+ ];
9084
+ const headerPositioning = z.enum(headerPositioningValues);
9060
9085
  const headerNavStyle = z.enum([
9061
9086
  "minimal",
9062
9087
  "underline",
@@ -9191,7 +9216,7 @@ const headerSchema = z.object({
9191
9216
  variant: headerVariantWithLegacyAliasSchema,
9192
9217
  positioning: headerPositioning,
9193
9218
  shrinkOnScroll: z.boolean(),
9194
- maxWidth: headerMaxWidth,
9219
+ maxWidth: headerMaxWidth.default("container"),
9195
9220
  scrollTreatment: headerScrollTreatmentSchema,
9196
9221
  background: headerBackgroundSchema.default({
9197
9222
  type: "color",
@@ -9390,11 +9415,6 @@ const bodyTextSchema = z.object({ intro: z.object({
9390
9415
  paragraphSize: bodyTextIntroTypographySizeSchema.optional(),
9391
9416
  headingSize: bodyTextIntroTypographySizeSchema.optional()
9392
9417
  }).optional() }).optional();
9393
- const gradientsSchema = z.object({
9394
- button: z.string().min(1).nullable().optional(),
9395
- hero: z.string().min(1).nullable().optional(),
9396
- background: z.string().min(1).nullable().optional()
9397
- });
9398
9418
  /**
9399
9419
  * Testimonial quote-mark treatment, owned by the site style (analogous to the
9400
9420
  * accordion icon). `style` is the coarse selector; the optional fields fine-tune
@@ -14831,6 +14851,12 @@ function cardRoleFromLegacyVocabulary(value) {
14831
14851
  */
14832
14852
  const cardRoleWithLegacyAliasSchema = z.preprocess((value) => cardRoleFromLegacyVocabulary(value) ?? value, cardRoleSchema);
14833
14853
  //#endregion
14854
+ //#region ../theme-core/src/site-styles/compilationPlan.ts
14855
+ const defaultHeaderBrandPreferences = {
14856
+ brandDisplay: "auto",
14857
+ logoSize: "auto"
14858
+ };
14859
+ //#endregion
14834
14860
  //#region ../theme-core/src/site-styles/sectionTransitions.ts
14835
14861
  function boundaryRuntimeMetrics(family, size, heightExpression) {
14836
14862
  return {
@@ -17220,6 +17246,12 @@ const overlayLogoAccommodations = [
17220
17246
  inlineMarkWithTitleLogoAccommodation,
17221
17247
  unbackedOverlayLogoAccommodation
17222
17248
  ];
17249
+ const allCuratedLogoAccommodations = [
17250
+ ...inlineClassicLogoAccommodations,
17251
+ ...centeredLogoAccommodations,
17252
+ ...overlayLogoAccommodations
17253
+ ];
17254
+ new Set(allCuratedLogoAccommodations.filter((option) => option.providesBacking === true).map((option) => option.pattern));
17223
17255
  const headerLayoutCatalog = defineExactCatalog()([
17224
17256
  {
17225
17257
  id: asHeaderLayoutId("classic"),
@@ -17369,6 +17401,10 @@ defineLookOptionGroup({
17369
17401
  {
17370
17402
  id: "secondary",
17371
17403
  label: "Secondary"
17404
+ },
17405
+ {
17406
+ id: headerInteractionQuietPalette,
17407
+ label: "Text only"
17372
17408
  }
17373
17409
  ]
17374
17410
  });
@@ -18084,6 +18120,37 @@ function uniformSectionForeground(token) {
18084
18120
  supportingColor: token
18085
18121
  };
18086
18122
  }
18123
+ function compileTransitionRuntime(input) {
18124
+ return {
18125
+ treatment: input.treatment,
18126
+ from: input.from,
18127
+ to: input.to,
18128
+ cssVars: {
18129
+ from: transitionEndpointCssValue(input.from),
18130
+ to: transitionEndpointCssValue(input.to)
18131
+ }
18132
+ };
18133
+ }
18134
+ function transitionEndpointCssValue(endpoint) {
18135
+ switch (endpoint.kind) {
18136
+ case "color-token": return themeColorTokenCssValue(endpoint.token);
18137
+ case "surface": return themeColorTokenCssValue(tokenForTransitionSurface(endpoint.surface));
18138
+ default: return assertNever$4(endpoint);
18139
+ }
18140
+ }
18141
+ function tokenForTransitionSurface(surface) {
18142
+ switch (surface) {
18143
+ case "base":
18144
+ case "hero-image": return "background";
18145
+ case "muted-band": return "muted";
18146
+ case "accent-band": return "accent";
18147
+ case "brand-depth-band": return "primary-800";
18148
+ default: return assertNever$4(surface);
18149
+ }
18150
+ }
18151
+ function themeColorTokenCssValue(token) {
18152
+ return `rgb(var(--tb-${token}))`;
18153
+ }
18087
18154
  //#endregion
18088
18155
  //#region ../theme-core/src/site-styles/curatedSiteStyles.ts
18089
18156
  const SOFT_EARTH_SITE_STYLE_ID_VALUE = "site-style:soft-earth";
@@ -19793,6 +19860,123 @@ const blockDesignFallback = {
19793
19860
  };
19794
19861
  blockDesignFallback.sectionSurface, blockDesignFallback.contentFrame, blockDesignFallback.transitionAfter, blockDesignFallback.sectionHeight, blockDesignFallback.heroLegibility, blockDesignFallback.imageSurfaceTreatment, blockDesignFallback.imageSurfaceMotion, blockDesignFallback.tileTreatment, blockDesignFallback.eventListTreatment, blockDesignFallback.eyebrowTreatment, blockDesignFallback.accordionIconTreatment, blockDesignFallback.composition;
19795
19862
  //#endregion
19863
+ //#region ../theme-core/src/site-styles/siteChromeBackgrounds.ts
19864
+ const siteChromeBackgroundColorTokens = toSiteChromeBackgroundColorTokens(backgroundAppearanceColorTokenNames);
19865
+ const quickSiteChromeBackgroundColorTokens = toSiteChromeBackgroundColorTokens(backgroundAppearanceQuickColorTokenNames);
19866
+ siteChromeBackgroundColorTokens.filter((token) => !quickSiteChromeBackgroundColorTokens.includes(token));
19867
+ function parseSiteChromeBackgroundColorToken(value) {
19868
+ const token = parseBackgroundAppearanceColorToken(value);
19869
+ return token ? token : null;
19870
+ }
19871
+ function toSiteChromeBackgroundColorTokens(tokenNames) {
19872
+ return tokenNames.flatMap((name) => {
19873
+ const token = parseSiteChromeBackgroundColorToken(name);
19874
+ return token ? [token] : [];
19875
+ });
19876
+ }
19877
+ //#endregion
19878
+ //#region ../theme-core/src/site-styles/siteChromeNavPreferences.ts
19879
+ const headerNavTypographyPreferenceValues = [
19880
+ "inherit",
19881
+ "body",
19882
+ "heading"
19883
+ ];
19884
+ const headerNavFontSizePreferenceValues = [
19885
+ "inherit",
19886
+ "sm",
19887
+ "base",
19888
+ "lg",
19889
+ "xl"
19890
+ ];
19891
+ const defaultHeaderNavPreferences = {
19892
+ typography: "inherit",
19893
+ fontSize: "inherit",
19894
+ interactionPalette: "recommended"
19895
+ };
19896
+ /**
19897
+ * Applies the user's nav font-role/size overrides on top of a style-resolved
19898
+ * nav link style. This is where the precedence contract lands:
19899
+ *
19900
+ * - The nav typography TREATMENT (case/tracking identity: plain / heading /
19901
+ * quiet-caps / brand-caps) is ALWAYS owned by the active header style and is
19902
+ * baked into `style` before this call (see `applyNavTypography` in
19903
+ * headerLayoutStyles.ts).
19904
+ * - Each treatment resolves through a DEFAULT font role: `heading` resolves
19905
+ * through the heading role; `plain` / `quiet-caps` / `brand-caps` resolve
19906
+ * through the body role.
19907
+ * - The user's `navFontRole` (surfaced here as `preferences.typography`) only
19908
+ * overrides which font ROLE the treatment resolves through. It never changes
19909
+ * the treatment. `'inherit'` keeps the treatment's default role.
19910
+ */
19911
+ function mergeHeaderNavTypographyPreference(style, preferences) {
19912
+ return preferences.typography === "inherit" && preferences.fontSize === "inherit" ? style : {
19913
+ ...style,
19914
+ ...preferences.typography === "inherit" ? {} : { typography: preferences.typography },
19915
+ ...preferences.fontSize === "inherit" ? {} : { textSize: preferences.fontSize }
19916
+ };
19917
+ }
19918
+ function readHeaderNavFontSizePreference(value) {
19919
+ return value === "sm" || value === "base" || value === "lg" || value === "xl" ? value : "inherit";
19920
+ }
19921
+ //#endregion
19922
+ //#region ../theme-core/src/site-styles/headerBrandPresentation.ts
19923
+ /**
19924
+ * Whether the header wears a brand presentation at all (#1318 Phase 4c).
19925
+ *
19926
+ * `applyHeaderBrandPresentation` is unconditional today: given a logo context
19927
+ * it always writes a `logo` object carrying a resolved display, size and
19928
+ * accommodation. That makes "this header has no brand presentation block" —
19929
+ * which is what several live sites persist, including one whose site *does*
19930
+ * have a logo asset — a state the preference record could not express, so a
19931
+ * recompile would silently give the site a logo treatment it has never
19932
+ * rendered.
19933
+ *
19934
+ * This is the third field in this phase with that shape, after the interaction
19935
+ * palette and the nav text size: the persisted bytes carry intent the reader
19936
+ * cannot say, so the recompile "corrects" it. The fix is the same one — widen
19937
+ * the closed vocabulary with a named member and derive it by baseline
19938
+ * comparison — and it is a *widening*, not a restyle: `'recommended'` is the
19939
+ * default and reproduces today's behaviour exactly, so no row whose bytes carry
19940
+ * a presentation compiles differently for `'plain'` existing.
19941
+ *
19942
+ * `'plain'` is deliberately not "preserve whatever bytes are there". It is a
19943
+ * constructive instruction — *write no presentation block* — so Phase 4e's
19944
+ * compiler, which builds the header from nothing, can produce it.
19945
+ */
19946
+ const headerBrandPresentationValues = ["recommended", "plain"];
19947
+ defaultHeaderBrandPreferences.brandDisplay, defaultHeaderBrandPreferences.logoSize, defaultHeaderNavPreferences.typography, defaultHeaderNavPreferences.fontSize, defaultHeaderNavPreferences.interactionPalette;
19948
+ //#endregion
19949
+ //#region ../theme-core/src/site-styles/previewCompiler.ts
19950
+ function retargetCompiledBoundaryRuntimeForContentAdjacency(boundary, adjacency) {
19951
+ const fromSurface = contentOwnedBackgroundEndpoint(adjacency.currentContent) ?? boundary.from;
19952
+ const toSurface = contentOwnedBackgroundEndpoint(adjacency.nextContent) ?? boundary.to;
19953
+ if (fromSurface === boundary.from && toSurface === boundary.to) return boundary;
19954
+ const transition = compileTransitionRuntime({
19955
+ treatment: boundary.treatment,
19956
+ from: fromSurface,
19957
+ to: toSurface
19958
+ });
19959
+ return {
19960
+ ...boundary,
19961
+ from: transition.from,
19962
+ to: transition.to,
19963
+ cssVars: transition.cssVars
19964
+ };
19965
+ }
19966
+ function contentOwnedBackgroundEndpoint(content) {
19967
+ const background = readRecord(readRecord(content?._sectionStyles)?.background);
19968
+ if (background?.contentOwner !== "page-design-editor" || background.type !== "color") return null;
19969
+ const token = parseThemeColorToken(background.color);
19970
+ if (!token) return null;
19971
+ return {
19972
+ kind: "color-token",
19973
+ token
19974
+ };
19975
+ }
19976
+ function readRecord(value) {
19977
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
19978
+ }
19979
+ //#endregion
19796
19980
  //#region ../theme-core/src/site-styles/generatedSchemas.ts
19797
19981
  function uniqueNonEmpty(values) {
19798
19982
  const uniqueValues = [...new Set(values)];
@@ -19861,6 +20045,56 @@ z.object({
19861
20045
  blocks: z.array(generatedPageDesignBlockIntentSchema)
19862
20046
  }).strict();
19863
20047
  //#endregion
20048
+ //#region ../theme-core/src/site-styles/headerPreferencesPersistence.ts
20049
+ const backgroundColorTokenSchema = z.unknown().transform((value, ctx) => {
20050
+ const token = parseSiteChromeBackgroundColorToken(value);
20051
+ if (token === null) {
20052
+ ctx.addIssue({
20053
+ code: "custom",
20054
+ message: "Unknown site chrome background colour token"
20055
+ });
20056
+ return z.NEVER;
20057
+ }
20058
+ return token;
20059
+ });
20060
+ const backgroundOverrideSchema = z.discriminatedUnion("kind", [z.object({ kind: z.literal("default") }).strict(), z.object({
20061
+ kind: z.literal("color"),
20062
+ token: backgroundColorTokenSchema
20063
+ }).strict()]);
20064
+ const logoTextIntentSchema = z.object({
20065
+ mobileMaxWidth: z.enum([
20066
+ "xs",
20067
+ "sm",
20068
+ "md"
20069
+ ]).optional(),
20070
+ mobileWrapLines: z.union([z.literal(1), z.literal(2)]).optional(),
20071
+ hideOnShrink: z.boolean().optional()
20072
+ }).strict();
20073
+ const structureSchema = z.object({
20074
+ positioning: z.union([z.literal("inherit"), z.enum(headerPositioningValues)]),
20075
+ shrinkOnScroll: z.union([z.literal("inherit"), z.boolean()]),
20076
+ logoText: z.union([
20077
+ z.literal("inherit"),
20078
+ z.literal("none"),
20079
+ logoTextIntentSchema
20080
+ ])
20081
+ }).strict();
20082
+ const headerPreferencesShape = {
20083
+ brandPresentation: z.enum(headerBrandPresentationValues),
20084
+ brandDisplay: z.enum(brandDisplayModeValues),
20085
+ logoSize: z.enum(logoSizePolicyValues),
20086
+ navFontRole: z.enum(headerNavTypographyPreferenceValues),
20087
+ navTextSize: z.enum(headerNavFontSizePreferenceValues),
20088
+ interactionPalette: z.union([z.literal("recommended"), z.enum(headerInteractionPaletteValues)]),
20089
+ background: backgroundOverrideSchema,
20090
+ structure: structureSchema
20091
+ };
20092
+ z.object(headerPreferencesShape).strict();
20093
+ z.object(headerPreferencesShape).omit({
20094
+ brandPresentation: true,
20095
+ structure: true
20096
+ }).strict();
20097
+ //#endregion
19864
20098
  //#region ../theme-core/src/site-styles/pageDesignAuthority.ts
19865
20099
  const nonEmptyStringSchema = z.string().min(1);
19866
20100
  const blockContentVersionIdSchema = nonEmptyStringSchema.transform(asBlockContentVersionId);
@@ -19962,48 +20196,6 @@ function collectBlockSequenceIssues(snapshots) {
19962
20196
  return issues;
19963
20197
  }
19964
20198
  //#endregion
19965
- //#region ../theme-core/src/site-styles/siteChromeBackgrounds.ts
19966
- const siteChromeBackgroundColorTokens = toSiteChromeBackgroundColorTokens(backgroundAppearanceColorTokenNames);
19967
- const quickSiteChromeBackgroundColorTokens = toSiteChromeBackgroundColorTokens(backgroundAppearanceQuickColorTokenNames);
19968
- siteChromeBackgroundColorTokens.filter((token) => !quickSiteChromeBackgroundColorTokens.includes(token));
19969
- function parseSiteChromeBackgroundColorToken(value) {
19970
- const token = parseBackgroundAppearanceColorToken(value);
19971
- return token ? token : null;
19972
- }
19973
- function toSiteChromeBackgroundColorTokens(tokenNames) {
19974
- return tokenNames.flatMap((name) => {
19975
- const token = parseSiteChromeBackgroundColorToken(name);
19976
- return token ? [token] : [];
19977
- });
19978
- }
19979
- //#endregion
19980
- //#region ../theme-core/src/site-styles/siteChromeNavPreferences.ts
19981
- /**
19982
- * Applies the user's nav font-role/size overrides on top of a style-resolved
19983
- * nav link style. This is where the precedence contract lands:
19984
- *
19985
- * - The nav typography TREATMENT (case/tracking identity: plain / heading /
19986
- * quiet-caps / brand-caps) is ALWAYS owned by the active header style and is
19987
- * baked into `style` before this call (see `applyNavTypography` in
19988
- * headerLayoutStyles.ts).
19989
- * - Each treatment resolves through a DEFAULT font role: `heading` resolves
19990
- * through the heading role; `plain` / `quiet-caps` / `brand-caps` resolve
19991
- * through the body role.
19992
- * - The user's `navFontRole` (surfaced here as `preferences.typography`) only
19993
- * overrides which font ROLE the treatment resolves through. It never changes
19994
- * the treatment. `'inherit'` keeps the treatment's default role.
19995
- */
19996
- function mergeHeaderNavTypographyPreference(style, preferences) {
19997
- return preferences.typography === "inherit" && preferences.fontSize === "inherit" ? style : {
19998
- ...style,
19999
- ...preferences.typography === "inherit" ? {} : { typography: preferences.typography },
20000
- ...preferences.fontSize === "inherit" ? {} : { textSize: preferences.fontSize }
20001
- };
20002
- }
20003
- function readHeaderNavFontSizePreference(value) {
20004
- return value === "sm" || value === "base" || value === "lg" || value === "xl" ? value : "inherit";
20005
- }
20006
- //#endregion
20007
20199
  //#region ../theme-core/src/site-styles/tokenRecipes.ts
20008
20200
  const themeTokenRecipeOptions = {
20009
20201
  palette: [
@@ -31558,7 +31750,7 @@ const eventDisplayFields = [
31558
31750
  {
31559
31751
  id: "showCapacity",
31560
31752
  type: "boolean",
31561
- label: "Show available spots",
31753
+ label: "Show available places",
31562
31754
  description: "Display remaining capacity",
31563
31755
  required: false,
31564
31756
  defaultValue: true,
@@ -31883,330 +32075,1300 @@ function planEventRegistrationScope(input) {
31883
32075
  };
31884
32076
  }
31885
32077
  //#endregion
31886
- //#region ../blocks/src/system/runtime/nodes/events/shared/destination.ts
31887
- function isRecord$14(value) {
31888
- return typeof value === "object" && value !== null && !Array.isArray(value);
31889
- }
31890
- function isRuntimeEventDestination(value) {
31891
- if (!isRecord$14(value) || typeof value.kind !== "string") return false;
31892
- if (value.kind === "native") return (value.mode === "cms-managed" || value.mode === "system-managed") && typeof value.path === "string";
31893
- return value.kind === "external" && typeof value.url === "string" && typeof value.label === "string";
31894
- }
31895
- function resolveRuntimeEventDestination(event) {
31896
- if (isRuntimeEventDestination(event.destination)) return event.destination;
31897
- return {
31898
- kind: "native",
31899
- mode: event.eventDestinationMode === "system-managed" ? "system-managed" : "cms-managed",
31900
- path: typeof event.path === "string" ? event.path : ""
31901
- };
31902
- }
31903
- //#endregion
31904
- //#region ../blocks/src/system/runtime/nodes/events/shared/pricing.ts
31905
- const DEFAULT_EVENT_CURRENCY = "USD";
31906
- function resolveRuntimeEventPricing(event) {
31907
- if (isPublicEventPricing(event.pricing)) return event.pricing;
31908
- if (resolveRuntimeEventDestination(event).kind === "external") return {
31909
- kind: "external",
31910
- display: {
31911
- kind: "external",
31912
- label: "External registration"
31913
- }
31914
- };
31915
- const legacy = event;
31916
- const currency = readCurrency(legacy.currency);
31917
- if (event.requiresRegistration === false) return {
31918
- kind: "drop_in",
31919
- currency,
31920
- display: {
31921
- kind: "free",
31922
- currency
31923
- }
31924
- };
31925
- if (legacy.pricingMode === "ticket_types") return {
31926
- kind: "ticket_types",
31927
- currency,
31928
- ticketTypes: Array.isArray(legacy.ticketTypes) ? legacy.ticketTypes.filter(isPublicEventTicketType) : [],
31929
- display: readTicketTypePriceDisplay(legacy.priceDisplay, currency)
32078
+ //#region ../blocks/src/system/runtime/nodes/events/domain/capacity.ts
32079
+ /**
32080
+ * Compute remaining spots for an event from `availableSpots` with a fallback
32081
+ * to `capacity - registeredCount`. Returns `{ available: null, isFull: false }`
32082
+ * for uncapped events so call sites can render no capacity indicator at all.
32083
+ *
32084
+ * The single source of truth for capacity math across event cards, agenda
32085
+ * rows, and timetable cells — keep wording variations in the call sites.
32086
+ */
32087
+ function getEventAvailability(event) {
32088
+ if (event.capacity == null) return {
32089
+ available: null,
32090
+ isFull: false
31930
32091
  };
31931
- const amount = typeof legacy.price === "number" ? legacy.price : 0;
31932
- if (legacy.pricingType === "paid" && amount > 0) return {
31933
- kind: "single_paid",
31934
- amount,
31935
- currency,
31936
- display: {
31937
- kind: "exact",
31938
- amount,
31939
- currency
31940
- }
32092
+ const available = event.availableSpots ?? event.capacity - event.registeredCount;
32093
+ if (available <= 0) return {
32094
+ available: 0,
32095
+ isFull: true
31941
32096
  };
31942
32097
  return {
31943
- kind: "free_registration",
31944
- currency,
31945
- display: {
31946
- kind: "free",
31947
- currency
31948
- }
32098
+ available,
32099
+ isFull: false
31949
32100
  };
31950
32101
  }
31951
- function readCurrency(value) {
31952
- return typeof value === "string" && value.trim().length > 0 ? value : DEFAULT_EVENT_CURRENCY;
31953
- }
31954
- function isRecord$13(value) {
31955
- return typeof value === "object" && value !== null && !Array.isArray(value);
31956
- }
31957
- function isPublicEventPricing(value) {
31958
- if (!isRecord$13(value) || typeof value.kind !== "string") return false;
31959
- 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";
32102
+ /**
32103
+ * Pick the capacity row a booking decision should read: the selected
32104
+ * attendance mode's availability when the visitor has chosen one and the
32105
+ * event publishes it, otherwise the event-level aggregate.
32106
+ *
32107
+ * The mode row is used wholesale. A mode with `capacity: null` on a capped
32108
+ * event is uncapped *by design*: the registration write path applies the
32109
+ * same rule via `getEffectiveOccurrenceCapacityForAttendanceMode`, so falling
32110
+ * back to the event's `availableSpots` here would block bookings the server
32111
+ * accepts. (Waitlist claims are still occurrence-scoped; see issue 1377.)
32112
+ */
32113
+ function selectEventCapacitySource(event, attendanceMode) {
32114
+ if (!attendanceMode) return event;
32115
+ return (event.attendanceModes ?? []).find((mode) => mode.mode === attendanceMode) ?? event;
31960
32116
  }
31961
- function isPublicEventTicketType(value) {
31962
- return isRecord$13(value) && typeof value.id === "string";
32117
+ /**
32118
+ * Whether the visitor's next step is the waitlist: the source is full *and*
32119
+ * a waitlist is open. The one place this is decided — cards, rows, the
32120
+ * wizard's steps and its action bar all route through here so they agree,
32121
+ * including for producers that omit `availableSpots` and rely on the
32122
+ * `capacity - registeredCount` fallback.
32123
+ */
32124
+ function canJoinEventWaitlist(event, options) {
32125
+ const source = selectEventCapacitySource(event, options?.attendanceMode);
32126
+ return getEventAvailability(source).isFull && source.waitlistEnabled === true;
31963
32127
  }
31964
- function readTicketTypePriceDisplay(value, currency) {
31965
- const display = readLegacyPriceDisplay(value, currency);
31966
- return display.kind === "whole_series" || display.kind === "external" ? {
31967
- kind: "unavailable",
31968
- currency
31969
- } : display;
32128
+ /**
32129
+ * Remaining places at or below which an event reads as scarce and the exact
32130
+ * count is worth showing. Above it, no number is shown at all: "147 places
32131
+ * left" is not something a visitor acts on, and it is the figure most likely
32132
+ * to be stale on a cached page.
32133
+ *
32134
+ * max( min(3, ceil(capacity * 0.25)), ceil(capacity * 0.05) )
32135
+ *
32136
+ * The 25% arm stops small groups shouting early (3 of 4 is not urgency); the
32137
+ * 5% floor stops large events staying silent until three remain.
32138
+ * capacity 4 -> 1, 12 -> 3, 40 -> 3, 100 -> 5, 200 -> 10.
32139
+ */
32140
+ function lowCapacityThreshold(capacity) {
32141
+ return Math.max(Math.min(3, Math.ceil(capacity * .25)), Math.ceil(capacity * .05));
31970
32142
  }
31971
- function readLegacyPriceDisplay(value, currency) {
31972
- if (!isRecord$13(value) || typeof value.kind !== "string") return {
31973
- kind: "unavailable",
31974
- currency
31975
- };
31976
- const displayCurrency = readCurrency(value.currency);
31977
- switch (value.kind) {
31978
- case "free": return {
31979
- kind: "free",
31980
- currency: displayCurrency
31981
- };
31982
- case "exact": return readAmount(value.amount) !== null ? {
31983
- kind: "exact",
31984
- amount: readAmount(value.amount) ?? 0,
31985
- currency: displayCurrency
31986
- } : {
31987
- kind: "unavailable",
31988
- currency
31989
- };
31990
- case "from": return readAmount(value.amount) !== null ? {
31991
- kind: "from",
31992
- amount: readAmount(value.amount) ?? 0,
31993
- currency: displayCurrency
31994
- } : {
31995
- kind: "unavailable",
31996
- currency
31997
- };
31998
- case "range": {
31999
- const minAmount = readAmount(value.minAmount);
32000
- const maxAmount = readAmount(value.maxAmount);
32001
- return minAmount !== null && maxAmount !== null ? {
32002
- kind: "range",
32003
- minAmount,
32004
- maxAmount,
32005
- currency: displayCurrency
32006
- } : {
32007
- kind: "unavailable",
32008
- currency
32009
- };
32010
- }
32011
- case "mixed": return readAmount(value.paidFromAmount) !== null ? {
32012
- kind: "mixed",
32013
- paidFromAmount: readAmount(value.paidFromAmount) ?? 0,
32014
- paidToAmount: readAmount(value.paidToAmount) ?? readAmount(value.paidFromAmount) ?? 0,
32015
- currency: displayCurrency
32016
- } : {
32017
- kind: "unavailable",
32018
- currency
32143
+ function getRemainingPlaces(source) {
32144
+ const { available, isFull } = getEventAvailability(source);
32145
+ if (available === null || source.capacity === null) return { kind: "unlimited" };
32146
+ if (isFull) return {
32147
+ kind: "full",
32148
+ waitlist: canJoinEventWaitlist(source)
32149
+ };
32150
+ if (available <= lowCapacityThreshold(source.capacity)) return {
32151
+ kind: "low",
32152
+ remaining: available
32153
+ };
32154
+ return { kind: "open" };
32155
+ }
32156
+ /**
32157
+ * The one place a remaining-places count becomes text. Returns `null` when
32158
+ * nothing should be shown: uncapped events, and capped events above the
32159
+ * scarcity threshold. Surfaces that need their own full-state wording pass
32160
+ * it in rather than re-deriving the count.
32161
+ */
32162
+ function formatRemainingPlaces(source, wording = {}) {
32163
+ const remaining = getRemainingPlaces(source);
32164
+ switch (remaining.kind) {
32165
+ case "unlimited":
32166
+ case "open": return null;
32167
+ case "low": return formatPlacesLeft(remaining.remaining, wording.compact === true);
32168
+ case "full": return remaining.waitlist ? wording.waitlist ?? "Waitlist" : wording.full ?? "Full";
32169
+ }
32170
+ }
32171
+ function formatPlacesLeft(count, compact) {
32172
+ if (compact) return `${count} left`;
32173
+ return count === 1 ? "1 place left" : `${count} places left`;
32174
+ }
32175
+ /** Total capacity as copy ("12 places"), for detail surfaces that state the size of an event. */
32176
+ function formatTotalCapacity(capacity) {
32177
+ if (capacity === null) return null;
32178
+ return capacity === 1 ? "1 place" : `${capacity} places`;
32179
+ }
32180
+ function getEventAvailabilityStatus(event) {
32181
+ const source = selectMostOpenCapacitySource(event);
32182
+ const remaining = getRemainingPlaces(source);
32183
+ const label = formatEventRemainingPlaces(event, { compact: true });
32184
+ switch (remaining.kind) {
32185
+ case "unlimited": return {
32186
+ status: "available",
32187
+ available: null,
32188
+ isFull: false,
32189
+ label
32019
32190
  };
32020
- case "unavailable": return {
32021
- kind: "unavailable",
32022
- currency: displayCurrency
32191
+ case "open": return {
32192
+ status: "available",
32193
+ available: getEventAvailability(source).available,
32194
+ isFull: false,
32195
+ label
32023
32196
  };
32024
- case "whole_series": return isRecord$13(value.display) ? {
32025
- kind: "whole_series",
32026
- display: readCoursePriceDisplay(value.display, displayCurrency)
32027
- } : {
32028
- kind: "whole_series",
32029
- display: {
32030
- kind: "unavailable",
32031
- currency: displayCurrency
32032
- }
32197
+ case "low": return {
32198
+ status: "filling",
32199
+ available: remaining.remaining,
32200
+ isFull: false,
32201
+ label
32033
32202
  };
32034
- default: return {
32035
- kind: "unavailable",
32036
- currency
32203
+ case "full": return {
32204
+ status: "waitlist",
32205
+ available: 0,
32206
+ isFull: true,
32207
+ label
32037
32208
  };
32038
32209
  }
32039
32210
  }
32040
- function readCoursePriceDisplay(value, currency) {
32041
- switch (value.kind) {
32042
- case "free": return {
32043
- kind: "free",
32044
- currency: readCurrency(value.currency)
32045
- };
32046
- case "exact": return readAmount(value.amount) !== null ? {
32047
- kind: "exact",
32048
- amount: readAmount(value.amount) ?? 0,
32049
- currency: readCurrency(value.currency)
32050
- } : {
32051
- kind: "unavailable",
32052
- currency
32053
- };
32054
- case "from": return readAmount(value.amount) !== null ? {
32055
- kind: "from",
32056
- amount: readAmount(value.amount) ?? 0,
32057
- currency: readCurrency(value.currency)
32058
- } : {
32059
- kind: "unavailable",
32060
- currency
32061
- };
32062
- default: return {
32063
- kind: "unavailable",
32064
- currency
32065
- };
32211
+ /**
32212
+ * Which capacity figure wins when they disagree.
32213
+ *
32214
+ * For a hybrid event that publishes both attendance-mode rows, **the mode
32215
+ * rows are authoritative and the event-level aggregate is only a summary**.
32216
+ * The registration write path admits or refuses per mode
32217
+ * (`getEffectiveOccurrenceCapacityForAttendanceMode` on the server), so the
32218
+ * aggregate can read full while a mode still has room, and can read open
32219
+ * while the mode the visitor wants is full. Believing the aggregate on a
32220
+ * card is how a "places available" event turns into a dead end at step two.
32221
+ *
32222
+ * Everything else — single-mode events, events that publish no rows, and the
32223
+ * aggregate-only helpers {@link getEventAvailability} /
32224
+ * {@link getRemainingPlaces} that existing callers rely on — keeps reading
32225
+ * the aggregate, unchanged.
32226
+ */
32227
+ function findHybridModeRows(event) {
32228
+ if (event.deliveryMode !== "hybrid") return null;
32229
+ const rows = event.attendanceModes ?? [];
32230
+ const inPerson = rows.find((row) => row.mode === "in_person");
32231
+ const online = rows.find((row) => row.mode === "online");
32232
+ return inPerson && online ? {
32233
+ inPerson,
32234
+ online
32235
+ } : null;
32236
+ }
32237
+ /** Openness rank; higher means more room. A full mode with a waitlist beats one without. */
32238
+ function remainingPlacesRank(remaining) {
32239
+ switch (remaining.kind) {
32240
+ case "unlimited": return 4;
32241
+ case "open": return 3;
32242
+ case "low": return 2;
32243
+ case "full": return remaining.waitlist ? 1 : 0;
32066
32244
  }
32067
32245
  }
32068
- function readAmount(value) {
32069
- return typeof value === "number" && Number.isFinite(value) ? value : null;
32246
+ function isMoreOpen(candidate, than) {
32247
+ const rank = remainingPlacesRank(candidate);
32248
+ const otherRank = remainingPlacesRank(than);
32249
+ if (rank !== otherRank) return rank > otherRank;
32250
+ return candidate.kind === "low" && than.kind === "low" ? candidate.remaining > than.remaining : false;
32070
32251
  }
32071
- //#endregion
32072
- //#region ../blocks/src/system/constants/events.ts
32073
- const eventCalendarDisplayModeValues = [
32074
- "combined",
32075
- "calendar",
32076
- "list",
32077
- "timetable",
32078
- "week-board"
32079
- ];
32080
- const eventCalendarDefaultViewValues = ["month", "week"];
32081
- const eventCalendarListLayoutValues = [
32082
- "stack",
32083
- "grid",
32084
- "compact"
32085
- ];
32086
- const eventCalendarListLayoutOptions = [
32087
- {
32088
- id: "stack",
32089
- label: "Stack"
32090
- },
32091
- {
32092
- id: "grid",
32093
- label: "Grid"
32094
- },
32095
- {
32096
- id: "compact",
32097
- label: "Compact"
32098
- }
32099
- ];
32100
- function isEventCalendarDisplayMode(value) {
32101
- return typeof value === "string" && eventCalendarDisplayModeValues.includes(value);
32252
+ /** True when `candidate` has less room left than `than`. */
32253
+ function isScarcer(candidate, than) {
32254
+ return isMoreOpen(than, candidate);
32102
32255
  }
32103
- function isEventCalendarDefaultView(value) {
32104
- return typeof value === "string" && eventCalendarDefaultViewValues.includes(value);
32256
+ /**
32257
+ * The capacity row a whole-event decision should read: the most open
32258
+ * attendance mode of a hybrid event, otherwise the aggregate. A hybrid event
32259
+ * is sold out only when both of its modes are, and its waitlist is reachable
32260
+ * when either full mode has one open.
32261
+ */
32262
+ function selectMostOpenCapacitySource(event) {
32263
+ const rows = findHybridModeRows(event);
32264
+ if (!rows) return event;
32265
+ return isMoreOpen(getRemainingPlaces(rows.online), getRemainingPlaces(rows.inPerson)) ? rows.online : rows.inPerson;
32105
32266
  }
32106
- function isEventCalendarListLayout(value) {
32107
- return typeof value === "string" && eventCalendarListLayoutValues.includes(value);
32267
+ /** True when no attendance mode has room. Hybrid-aware; use this for sold-out decisions. */
32268
+ function isEventFull(event) {
32269
+ return getEventAvailability(selectMostOpenCapacitySource(event)).isFull;
32108
32270
  }
32109
- //#endregion
32110
- //#region ../blocks/src/system/runtime/nodes/shared/contract-schemas.ts
32111
- const previewStageSchema = z.enum(["preview", "published"]);
32112
- const registrationButtonVariantSchema = blockButtonRoleSchema;
32113
- const eventButtonVariantSchema = blockButtonRoleSchema;
32114
- const eventLayoutSchema = z.enum(eventCalendarListLayoutValues);
32115
- const eventCalendarDisplayModeSchema = z.enum(eventCalendarDisplayModeValues);
32116
- const eventCalendarDefaultViewSchema = z.enum(eventCalendarDefaultViewValues);
32117
- const columnOptionSchema = z.enum([
32118
- "2",
32119
- "3",
32120
- "4"
32121
- ]);
32122
- const weekStartSchema = z.enum(["sunday", "monday"]);
32123
- const filterAutoShowSchema = z.enum([
32124
- "auto",
32125
- "always",
32126
- "never"
32127
- ]);
32128
- const calendarTabViewsSchema = z.enum([
32129
- "both",
32130
- "month",
32131
- "week"
32132
- ]);
32133
- //#endregion
32134
- //#region ../blocks/src/system/runtime/nodes/event-registration/wizardSteps.ts
32135
32271
  /**
32136
- * Determine which wizard steps to show based on context and payment requirements.
32272
+ * True when the event is full on every mode and at least one of those full
32273
+ * modes has a waitlist open. An event with a mode still bookable is not a
32274
+ * waitlist prospect: it should send the visitor to that mode instead.
32275
+ */
32276
+ function canJoinEventWaitlistOnAnyMode(event) {
32277
+ return canJoinEventWaitlist(selectMostOpenCapacitySource(event));
32278
+ }
32279
+ const ATTENDANCE_MODE_LABEL = {
32280
+ in_person: "In person",
32281
+ online: "Online"
32282
+ };
32283
+ /** Wording for a mode with room to spare, where the count is deliberately withheld. */
32284
+ const PLACES_AVAILABLE = "Places available";
32285
+ /**
32286
+ * The browse-surface label for an event's remaining places. Single-mode
32287
+ * events get exactly the aggregate label {@link formatRemainingPlaces}
32288
+ * produces, and so does a hybrid event whose two modes tell the same story.
32289
+ * When the modes differ, both sides are named: "available" on a card is a
32290
+ * promise the registration wizard would otherwise have to break.
32137
32291
  *
32138
- * @param hasPreSelectedContext - Whether event is pre-selected (skips event selection)
32139
- * @param needsPaymentStep - Whether user must complete payment method selection
32140
- * @returns Array of wizard steps to display
32292
+ * Hybrid examples, in person full with 3 online places left:
32293
+ * default -> "Online: 3 places left · In person: Full"
32294
+ * { full: 'Sold out' } -> "Online: 3 places left · In person: Sold out"
32295
+ * { compact: true } -> "Online: 3 left"
32296
+ */
32297
+ function formatEventRemainingPlaces(event, wording = {}) {
32298
+ const rows = findHybridModeRows(event);
32299
+ if (!rows) return formatRemainingPlaces(event, wording);
32300
+ const inPerson = formatRemainingPlaces(rows.inPerson, wording);
32301
+ const online = formatRemainingPlaces(rows.online, wording);
32302
+ if (inPerson === online) return inPerson;
32303
+ if (wording.compact === true) return formatCompactHybridPlaces(rows, wording);
32304
+ return formatBothHybridSides(rows, {
32305
+ inPerson,
32306
+ online
32307
+ });
32308
+ }
32309
+ /**
32310
+ * Both sides, the one with room first: that is the half a visitor can act on,
32311
+ * and it should not arrive after a full stop's worth of bad news.
32141
32312
  */
32142
- function getWizardStepsForContext(hasPreSelectedContext, needsPaymentStep) {
32143
- const baseSteps = hasPreSelectedContext ? ["select-tickets", "enter-details"] : [
32144
- "select-event",
32145
- "select-tickets",
32146
- "enter-details"
32147
- ];
32148
- if (!needsPaymentStep) return baseSteps;
32149
- return [...baseSteps, "select-payment"];
32313
+ function formatBothHybridSides(rows, labels) {
32314
+ 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(" · ");
32150
32315
  }
32151
- //#endregion
32152
- //#region ../blocks/src/system/runtime/nodes/event-registration/registrationDerivations.ts
32153
- function eventMatchesOccurrenceId(event, occurrenceId) {
32154
- return event.registrationSubject.kind === "occurrence" && (event.registrationSubject.occurrenceId === occurrenceId || event.selectionId === occurrenceId);
32316
+ function namedModePlaces(mode, label) {
32317
+ return `${ATTENDANCE_MODE_LABEL[mode]}: ${label ?? PLACES_AVAILABLE}`;
32155
32318
  }
32156
- function findPreSelectedEvent(input) {
32157
- const occurrenceContext = input.occurrenceContext;
32158
- if (!occurrenceContext) return input.scopedEvents.length === 1 ? input.scopedEvents[0] ?? null : null;
32159
- const contextDate = getBookingLocalDateKey(toScheduledBookingDateTime({
32160
- startsAt: occurrenceContext.startsAt,
32161
- timeZone: occurrenceContext.timeZone
32162
- }));
32163
- return input.scopedEvents.find((event) => eventMatchesOccurrenceId(event, occurrenceContext.id) || getBookingLocalDateKey(toScheduledBookingDateTime({
32164
- startsAt: event.startsAt,
32165
- timeZone: event.timeZone
32166
- })) === contextDate) ?? null;
32319
+ /**
32320
+ * Pills and timetable cells have room for one short phrase, so they name the
32321
+ * single mode that carries news rather than listing both: the scarce side
32322
+ * when there is a count to show, and otherwise the side that has closed —
32323
+ * "In person: Full", never "Online only", which reads as a statement about
32324
+ * how the event is delivered rather than about its remaining places.
32325
+ */
32326
+ function formatCompactHybridPlaces(rows, wording) {
32327
+ const inPerson = getRemainingPlaces(rows.inPerson);
32328
+ const online = getRemainingPlaces(rows.online);
32329
+ if (inPerson.kind === "full" && online.kind === "full") return formatRemainingPlaces(inPerson.waitlist ? rows.inPerson : rows.online, wording);
32330
+ if (inPerson.kind === "full" || online.kind === "full") {
32331
+ const fullSide = inPerson.kind === "full" ? "in_person" : "online";
32332
+ const openSide = inPerson.kind === "full" ? "online" : "in_person";
32333
+ const openLabel = formatRemainingPlaces(rowFor(rows, openSide), wording);
32334
+ return openLabel !== null ? namedModePlaces(openSide, openLabel) : namedModePlaces(fullSide, formatRemainingPlaces(rowFor(rows, fullSide), wording));
32335
+ }
32336
+ const scarcerSide = isScarcer(online, inPerson) ? "online" : "in_person";
32337
+ return namedModePlaces(scarcerSide, formatRemainingPlaces(rowFor(rows, scarcerSide), wording));
32167
32338
  }
32168
- function resolveEventRequiresPayment(input) {
32169
- if (input.eligibility) return input.eligibility.requiresPayment;
32170
- if (input.selectedTicketType) return input.selectedTicketType.priceAmount > 0;
32171
- if (!input.event) return false;
32172
- return publicEventPricingType(resolveRuntimeEventPricing(input.event)) === "paid";
32339
+ function rowFor(rows, mode) {
32340
+ return mode === "online" ? rows.online : rows.inPerson;
32173
32341
  }
32174
32342
  //#endregion
32175
- //#region ../blocks/src/system/runtime/nodes/event-registration/registrationEventAdapter.ts
32176
- function toRegistrationEvent(event) {
32177
- return {
32178
- ...event,
32179
- pricing: resolveRuntimeEventPricing(event),
32180
- selectionId: event.id,
32181
- registrationSubject: {
32182
- kind: "occurrence",
32183
- occurrenceId: event.occurrenceId
32184
- }
32185
- };
32343
+ //#region ../blocks/src/system/runtime/nodes/events/domain/destination.ts
32344
+ function isRecord$14(value) {
32345
+ return typeof value === "object" && value !== null && !Array.isArray(value);
32186
32346
  }
32187
- function toRegistrationEvents(events) {
32188
- return events.filter((event) => event.bookingScope === "single_occurrence").map(toRegistrationEvent);
32347
+ function isRuntimeEventDestination(value) {
32348
+ if (!isRecord$14(value) || typeof value.kind !== "string") return false;
32349
+ if (value.kind === "native") return (value.mode === "cms-managed" || value.mode === "system-managed") && typeof value.path === "string";
32350
+ return value.kind === "external" && typeof value.url === "string" && typeof value.label === "string";
32189
32351
  }
32190
- function toWholeSeriesTicketType(option) {
32352
+ function resolveRuntimeEventDestination(event) {
32353
+ if (isRuntimeEventDestination(event.destination)) return event.destination;
32191
32354
  return {
32192
- ...option,
32193
- capacityMode: option.capacityLimit === null ? "unlimited" : "limited",
32194
- attendanceScope: option.attendanceScope ?? void 0,
32195
- availability: {
32196
- kind: "available",
32197
- remainingCapacity: option.capacityLimit
32198
- }
32355
+ kind: "native",
32356
+ mode: event.eventDestinationMode === "system-managed" ? "system-managed" : "cms-managed",
32357
+ path: typeof event.path === "string" ? event.path : ""
32199
32358
  };
32200
32359
  }
32201
- function toWholeSeriesPricing(offering) {
32202
- const resolved = resolveWholeSeriesPricingState(offering);
32203
- switch (offering.pricing.pricingStrategy) {
32204
- case "free": return {
32205
- kind: "free_registration",
32206
- currency: offering.currency,
32207
- display: {
32208
- kind: "free",
32209
- currency: offering.currency
32360
+ //#endregion
32361
+ //#region ../blocks/src/system/runtime/nodes/events/domain/cta.ts
32362
+ /**
32363
+ * Booking call-to-action resolution for events: where an event links to and
32364
+ * what the button says. Sold-out and waitlist decisions come from capacity.ts
32365
+ * so cards, rows and the wizard never disagree.
32366
+ */
32367
+ function closedEventBookingLabel(reason) {
32368
+ switch (reason) {
32369
+ case "occurrence_cancelled":
32370
+ case "series_cancelled": return "Cancelled";
32371
+ case "occurrence_completed":
32372
+ case "series_completed": return "Completed";
32373
+ case "series_draft":
32374
+ case "series_archived":
32375
+ case "parent_archived":
32376
+ case "parent_inconsistent":
32377
+ case "not_public":
32378
+ case "already_started":
32379
+ case "enrollment_closed": return "Registration closed";
32380
+ default: return assertNever$3(reason);
32381
+ }
32382
+ }
32383
+ function resolveEventHref(event) {
32384
+ const destination = resolveRuntimeEventDestination(event);
32385
+ return destination.kind === "external" ? destination.url : destination.path;
32386
+ }
32387
+ function resolveEventLinkProps(event) {
32388
+ const href = resolveEventHref(event);
32389
+ return resolveRuntimeEventDestination(event).kind === "external" ? {
32390
+ href,
32391
+ target: "_blank",
32392
+ rel: "noopener noreferrer"
32393
+ } : { href };
32394
+ }
32395
+ /**
32396
+ * Resolve the CTA state for an event.
32397
+ * Shared by EventCard and EventCompactRow to keep sold-out, registration-closed,
32398
+ * and course-specific defaults consistent.
32399
+ */
32400
+ function resolveEventCta(event, buttonText) {
32401
+ const isSoldOut = isEventFull(event);
32402
+ const destination = resolveRuntimeEventDestination(event);
32403
+ const isExternal = destination.kind === "external";
32404
+ const defaultButtonText = isExternal ? destination.label : event.bookingScope === "whole_series" ? "Book course" : "Book";
32405
+ const effectiveButtonText = buttonText ?? defaultButtonText;
32406
+ const href = resolveEventHref(event);
32407
+ if (event.bookingAvailability.kind === "closed") {
32408
+ const linksToClosedWholeSeriesDetails = event.bookingAvailability.reason === "enrollment_closed" && !isExternal;
32409
+ return {
32410
+ label: closedEventBookingLabel(event.bookingAvailability.reason),
32411
+ href,
32412
+ external: isExternal,
32413
+ hidden: !linksToClosedWholeSeriesDetails
32414
+ };
32415
+ }
32416
+ if (!isExternal && isSoldOut) {
32417
+ if (canJoinEventWaitlistOnAnyMode(event)) return {
32418
+ label: "Join waitlist",
32419
+ href,
32420
+ external: false,
32421
+ hidden: false
32422
+ };
32423
+ return {
32424
+ label: "Sold out",
32425
+ href,
32426
+ external: false,
32427
+ hidden: true
32428
+ };
32429
+ }
32430
+ return {
32431
+ label: effectiveButtonText,
32432
+ href,
32433
+ external: isExternal,
32434
+ hidden: false
32435
+ };
32436
+ }
32437
+ //#endregion
32438
+ //#region ../calendar/src/grid.ts
32439
+ const DAY_NAMES = [
32440
+ "Sun",
32441
+ "Mon",
32442
+ "Tue",
32443
+ "Wed",
32444
+ "Thu",
32445
+ "Fri",
32446
+ "Sat"
32447
+ ];
32448
+ const MONTH_NAMES = [
32449
+ "January",
32450
+ "February",
32451
+ "March",
32452
+ "April",
32453
+ "May",
32454
+ "June",
32455
+ "July",
32456
+ "August",
32457
+ "September",
32458
+ "October",
32459
+ "November",
32460
+ "December"
32461
+ ];
32462
+ function formatDateKey(date) {
32463
+ return `${String(date.year).padStart(4, "0")}-${String(date.month).padStart(2, "0")}-${String(date.day).padStart(2, "0")}`;
32464
+ }
32465
+ function isToday(date, today) {
32466
+ return isSameCalendarDay(date, today);
32467
+ }
32468
+ function isSameCalendarDay(left, right) {
32469
+ return left.year === right.year && left.month === right.month && left.day === right.day;
32470
+ }
32471
+ function addDays(date, days) {
32472
+ const result = new Date(Date.UTC(date.year, date.month - 1, date.day + days, 12));
32473
+ return {
32474
+ year: result.getUTCFullYear(),
32475
+ month: result.getUTCMonth() + 1,
32476
+ day: result.getUTCDate()
32477
+ };
32478
+ }
32479
+ function getMonthCalendarDays(year, month, startOfWeek) {
32480
+ const firstDay = {
32481
+ year,
32482
+ month,
32483
+ day: 1
32484
+ };
32485
+ const dayOfWeek = getDayOfWeek(firstDay);
32486
+ const gridStart = addDays(firstDay, -(startOfWeek === "monday" ? dayOfWeek === 0 ? 6 : dayOfWeek - 1 : dayOfWeek));
32487
+ return Array.from({ length: 42 }, (_, index) => addDays(gridStart, index));
32488
+ }
32489
+ function getWeekCalendarDays(baseDate, startOfWeek) {
32490
+ const dayOfWeek = getDayOfWeek(baseDate);
32491
+ const weekStart = addDays(baseDate, -(startOfWeek === "monday" ? dayOfWeek === 0 ? 6 : dayOfWeek - 1 : dayOfWeek));
32492
+ return Array.from({ length: 7 }, (_, index) => addDays(weekStart, index));
32493
+ }
32494
+ function getDayOfWeek(date) {
32495
+ return new Date(Date.UTC(date.year, date.month - 1, date.day, 12)).getUTCDay();
32496
+ }
32497
+ function getWeekCalendarDayTuple(baseDate, startOfWeek) {
32498
+ const days = getWeekCalendarDays(baseDate, startOfWeek);
32499
+ assertWeekCalendarDays(days);
32500
+ return days;
32501
+ }
32502
+ function assertWeekCalendarDays(days) {
32503
+ if (days.length !== 7) throw new Error("Expected exactly 7 calendar days");
32504
+ }
32505
+ function getOrderedDayNames(startOfWeek, dayNames = DAY_NAMES) {
32506
+ if (startOfWeek === "monday") {
32507
+ const sunday = dayNames[0] ?? "Sun";
32508
+ return [...dayNames.slice(1), sunday];
32509
+ }
32510
+ return [...dayNames];
32511
+ }
32512
+ function formatCalendarTitle(viewDate, view, days, monthNames = MONTH_NAMES) {
32513
+ if (view === "week") {
32514
+ const weekStart = days[0];
32515
+ const weekEnd = days[6];
32516
+ if (weekStart === void 0 || weekEnd === void 0) return `${monthNames[viewDate.month - 1] ?? ""} ${viewDate.year}`;
32517
+ const startMonth = monthNames[weekStart.month - 1] ?? "";
32518
+ const endMonth = monthNames[weekEnd.month - 1] ?? "";
32519
+ if (weekStart.month === weekEnd.month) return `${startMonth} ${weekStart.day} - ${weekEnd.day}, ${weekEnd.year}`;
32520
+ return `${startMonth} ${weekStart.day} - ${endMonth} ${weekEnd.day}, ${weekEnd.year}`;
32521
+ }
32522
+ return `${monthNames[viewDate.month - 1] ?? ""} ${viewDate.year}`;
32523
+ }
32524
+ function buildCalendarViewport(input) {
32525
+ const monthNames = input.monthNames ?? MONTH_NAMES;
32526
+ if (input.view === "week") {
32527
+ const days = getWeekCalendarDayTuple(input.anchorDate, input.startOfWeek);
32528
+ return {
32529
+ kind: "week",
32530
+ anchorDate: input.anchorDate,
32531
+ days,
32532
+ title: formatCalendarTitle(input.anchorDate, "week", days, monthNames)
32533
+ };
32534
+ }
32535
+ const days = getMonthCalendarDays(input.anchorDate.year, input.anchorDate.month, input.startOfWeek);
32536
+ return {
32537
+ kind: "month",
32538
+ anchorDate: input.anchorDate,
32539
+ days,
32540
+ title: formatCalendarTitle(input.anchorDate, "month", days, monthNames)
32541
+ };
32542
+ }
32543
+ //#endregion
32544
+ //#region ../calendar/src/lane-packing.ts
32545
+ /**
32546
+ * Assign lanes to a list of intervals.
32547
+ *
32548
+ * Returns one result per input in the same order as the input. Internally
32549
+ * sorts a copy by `(start, end, orderKey)` to ensure determinism, then
32550
+ * permutes results back to input order.
32551
+ */
32552
+ function assignLanes(intervals) {
32553
+ if (intervals.length === 0) return [];
32554
+ const indexed = intervals.map((interval, index) => ({
32555
+ interval,
32556
+ index
32557
+ }));
32558
+ indexed.sort((left, right) => left.interval.start - right.interval.start || left.interval.end - right.interval.end || left.interval.orderKey.localeCompare(right.interval.orderKey));
32559
+ const results = new Array(intervals.length);
32560
+ let cluster = [];
32561
+ let activeLaneEnds = [];
32562
+ let clusterMaxEnd = -Infinity;
32563
+ const closeCluster = () => {
32564
+ if (cluster.length === 0) return;
32565
+ const laneCount = Math.max(1, cluster.reduce((max, entry) => Math.max(max, entry.laneIndex + 1), 0));
32566
+ for (const entry of cluster) results[entry.index] = {
32567
+ laneIndex: entry.laneIndex,
32568
+ laneCount
32569
+ };
32570
+ cluster = [];
32571
+ activeLaneEnds = [];
32572
+ clusterMaxEnd = -Infinity;
32573
+ };
32574
+ for (const { interval, index } of indexed) {
32575
+ if (interval.start >= clusterMaxEnd) closeCluster();
32576
+ const laneIndex = firstAvailableLane(activeLaneEnds, interval.start);
32577
+ activeLaneEnds[laneIndex] = interval.end;
32578
+ clusterMaxEnd = Math.max(clusterMaxEnd, interval.end);
32579
+ cluster.push({
32580
+ index,
32581
+ laneIndex
32582
+ });
32583
+ }
32584
+ closeCluster();
32585
+ return results;
32586
+ }
32587
+ function firstAvailableLane(activeLaneEnds, start) {
32588
+ const laneIndex = activeLaneEnds.findIndex((end) => end <= start);
32589
+ return laneIndex >= 0 ? laneIndex : activeLaneEnds.length;
32590
+ }
32591
+ //#endregion
32592
+ //#region ../calendar/src/timezone.ts
32593
+ /**
32594
+ * Timezone boundary helpers — internal.
32595
+ *
32596
+ * Both functions read an ISO instant in a given IANA timezone and project it
32597
+ * onto the wall clock of that zone. They use Intl.DateTimeFormat with
32598
+ * hourCycle: 'h23' to avoid 12/24-hour ambiguity. Used internally by the
32599
+ * timetable model and `groupItemsByDate`; not exported from the package's
32600
+ * public surface.
32601
+ */
32602
+ const MINUTES_PER_HOUR = 60;
32603
+ /**
32604
+ * Local minute-of-day [0, 24*60) for an ISO instant in the given zone.
32605
+ * Returns NaN if the inputs cannot be parsed (caller should drop the item).
32606
+ */
32607
+ function getLocalMinuteOfDay(isoTimestamp, timeZone) {
32608
+ const date = new Date(isoTimestamp);
32609
+ if (Number.isNaN(date.getTime())) return NaN;
32610
+ if (!isValidTimeZone(timeZone)) return NaN;
32611
+ const parts = new Intl.DateTimeFormat("en-GB", {
32612
+ timeZone,
32613
+ hour: "2-digit",
32614
+ minute: "2-digit",
32615
+ hourCycle: "h23"
32616
+ }).formatToParts(date);
32617
+ const hourPart = Number(parts.find((part) => part.type === "hour")?.value);
32618
+ const minute = Number(parts.find((part) => part.type === "minute")?.value);
32619
+ const hour = hourPart === 24 ? 0 : hourPart;
32620
+ if (Number.isNaN(hour) || Number.isNaN(minute)) return NaN;
32621
+ return hour * MINUTES_PER_HOUR + minute;
32622
+ }
32623
+ /**
32624
+ * Local YYYY-MM-DD for an ISO instant in the given zone.
32625
+ * Returns null if the inputs cannot be parsed.
32626
+ */
32627
+ function getLocalDateKey(isoTimestamp, timeZone) {
32628
+ const date = new Date(isoTimestamp);
32629
+ if (Number.isNaN(date.getTime())) return null;
32630
+ if (!isValidTimeZone(timeZone)) return null;
32631
+ const parts = new Intl.DateTimeFormat("en-CA", {
32632
+ timeZone,
32633
+ year: "numeric",
32634
+ month: "2-digit",
32635
+ day: "2-digit"
32636
+ }).formatToParts(date);
32637
+ const year = parts.find((part) => part.type === "year")?.value;
32638
+ const month = parts.find((part) => part.type === "month")?.value;
32639
+ const day = parts.find((part) => part.type === "day")?.value;
32640
+ if (year === void 0 || year.length === 0) return null;
32641
+ if (month === void 0 || month.length === 0) return null;
32642
+ if (day === void 0 || day.length === 0) return null;
32643
+ return `${year}-${month}-${day}`;
32644
+ }
32645
+ const validTimeZoneCache = /* @__PURE__ */ new Map();
32646
+ function isValidTimeZone(timeZone) {
32647
+ if (typeof timeZone !== "string" || timeZone.length === 0) return false;
32648
+ const cached = validTimeZoneCache.get(timeZone);
32649
+ if (cached !== void 0) return cached;
32650
+ try {
32651
+ new Intl.DateTimeFormat("en-US", { timeZone });
32652
+ validTimeZoneCache.set(timeZone, true);
32653
+ return true;
32654
+ } catch {
32655
+ validTimeZoneCache.set(timeZone, false);
32656
+ return false;
32657
+ }
32658
+ }
32659
+ //#endregion
32660
+ //#region ../calendar/src/timetable.ts
32661
+ /**
32662
+ * Timetable model — the package's core surface.
32663
+ *
32664
+ * Generic over `T`. Takes a list of timed items + a viewport; returns a
32665
+ * day-by-day layout with overlap-packed lanes and percent-based positions
32666
+ * plus a diagnostics list for items dropped during processing.
32667
+ *
32668
+ * The function is pure: given identical input (including `now`), it returns
32669
+ * the same output. The model has no opinion on what an item is — pass
32670
+ * accessors.
32671
+ */
32672
+ const DEFAULT_BOUNDS = {
32673
+ minStartHour: 5,
32674
+ maxEndHour: 23,
32675
+ defaultStartHour: 6,
32676
+ defaultEndHour: 21,
32677
+ minHeightPercent: 2.5
32678
+ };
32679
+ function buildTimetableModel$1(input) {
32680
+ const bounds = resolveBounds(input.bounds);
32681
+ const viewportDays = getTimetableDays(input.viewDate, input.view ?? "week", input.startOfWeek);
32682
+ const viewportKeys = new Set(viewportDays.map(formatDateKey));
32683
+ const parsed = parseAllItems(input.items, input);
32684
+ const inViewport = deriveVisibleSegments(parsed.drafts, viewportKeys);
32685
+ const hourRange = resolveHourRange(inViewport, bounds);
32686
+ const laid = layoutCellsByDate(inViewport, hourRange, bounds);
32687
+ return {
32688
+ days: assembleDays(viewportDays, laid.cellsByDate, input.today),
32689
+ hourTicks: buildHourTicks(hourRange.startHour, hourRange.endHour),
32690
+ startHour: hourRange.startHour,
32691
+ endHour: hourRange.endHour,
32692
+ rangeMinutes: (hourRange.endHour - hourRange.startHour) * 60,
32693
+ hasItems: inViewport.length > 0,
32694
+ diagnostics: [...parsed.diagnostics, ...laid.diagnostics]
32695
+ };
32696
+ }
32697
+ function parseAllItems(items, accessors) {
32698
+ const drafts = [];
32699
+ const diagnostics = [];
32700
+ for (const item of items) {
32701
+ const result = parseDraft(item, accessors);
32702
+ if (result.ok) drafts.push(result.draft);
32703
+ else diagnostics.push({
32704
+ item,
32705
+ reason: result.reason
32706
+ });
32707
+ }
32708
+ return {
32709
+ drafts,
32710
+ diagnostics
32711
+ };
32712
+ }
32713
+ function layoutCellsByDate(segments, hourRange, bounds) {
32714
+ const rangeMinutes = (hourRange.endHour - hourRange.startHour) * 60;
32715
+ const rangeStartMinutes = hourRange.startHour * 60;
32716
+ const rangeEndMinutes = hourRange.endHour * 60;
32717
+ const segmentsByDate = groupSegmentsByDate(segments);
32718
+ const cellsByDate = /* @__PURE__ */ new Map();
32719
+ const diagnostics = [];
32720
+ for (const [dateKey, daySegments] of segmentsByDate) {
32721
+ const clipped = [];
32722
+ for (const segment of daySegments) {
32723
+ const result = clipDraft(segment, rangeStartMinutes, rangeEndMinutes);
32724
+ if (result.ok) clipped.push(result.clipped);
32725
+ else diagnostics.push({
32726
+ item: segment.draft.item,
32727
+ reason: result.reason
32728
+ });
32729
+ }
32730
+ const laneResults = assignLanes(clipped.map((entry) => ({
32731
+ start: entry.visibleStart,
32732
+ end: entry.visibleEnd,
32733
+ orderKey: entry.segment.draft.orderKey
32734
+ })));
32735
+ cellsByDate.set(dateKey, clipped.map((entry, idx) => buildCell(entry, laneResults[idx] ?? {
32736
+ laneIndex: 0,
32737
+ laneCount: 1
32738
+ }, rangeStartMinutes, rangeMinutes, bounds.minHeightPercent)));
32739
+ }
32740
+ return {
32741
+ cellsByDate,
32742
+ diagnostics
32743
+ };
32744
+ }
32745
+ function assembleDays(viewportDays, cellsByDate, today) {
32746
+ return viewportDays.map((date) => {
32747
+ const dateKey = formatDateKey(date);
32748
+ return {
32749
+ date,
32750
+ dateKey,
32751
+ isToday: isSameCalendarDay(date, today),
32752
+ cells: cellsByDate.get(dateKey) ?? []
32753
+ };
32754
+ });
32755
+ }
32756
+ function parseDraft(item, accessors) {
32757
+ const startsAtIso = accessors.getStartsAt(item);
32758
+ const endsAtIso = accessors.getEndsAt(item);
32759
+ const timeZone = accessors.getTimeZone(item);
32760
+ const startsAtMinutes = getLocalMinuteOfDay(startsAtIso, timeZone);
32761
+ if (Number.isNaN(startsAtMinutes)) return {
32762
+ ok: false,
32763
+ reason: "invalid-start"
32764
+ };
32765
+ const startDateKey = getLocalDateKey(startsAtIso, timeZone);
32766
+ if (startDateKey == null) return {
32767
+ ok: false,
32768
+ reason: "invalid-timezone"
32769
+ };
32770
+ const endDateKey = getLocalDateKey(endsAtIso, timeZone);
32771
+ if (endDateKey == null) return {
32772
+ ok: false,
32773
+ reason: "invalid-end"
32774
+ };
32775
+ const endsAtMinutesRaw = getLocalMinuteOfDay(endsAtIso, timeZone);
32776
+ if (Number.isNaN(endsAtMinutesRaw)) return {
32777
+ ok: false,
32778
+ reason: "invalid-end"
32779
+ };
32780
+ const spansMultipleDays = startDateKey < endDateKey;
32781
+ const endsAtMinutes = spansMultipleDays || endsAtMinutesRaw > startsAtMinutes ? endsAtMinutesRaw : Math.min(1440, startsAtMinutes + 60);
32782
+ if (!spansMultipleDays && endsAtMinutes <= startsAtMinutes) return {
32783
+ ok: false,
32784
+ reason: "non-positive-duration"
32785
+ };
32786
+ return {
32787
+ ok: true,
32788
+ draft: {
32789
+ item,
32790
+ startDateKey,
32791
+ endDateKey,
32792
+ startsAtMinutes,
32793
+ endsAtMinutes,
32794
+ color: accessors.getColor?.(item) ?? null,
32795
+ orderKey: accessors.getOrderKey?.(item) ?? accessors.getId(item)
32796
+ }
32797
+ };
32798
+ }
32799
+ function deriveVisibleSegments(drafts, viewportKeys) {
32800
+ const segments = [];
32801
+ for (const draft of drafts) for (const dateKey of enumerateDateKeys(draft.startDateKey, draft.endDateKey)) {
32802
+ if (!viewportKeys.has(dateKey)) continue;
32803
+ const isStartDay = dateKey === draft.startDateKey;
32804
+ const isEndDay = dateKey === draft.endDateKey;
32805
+ const startsAtMinutes = isStartDay ? draft.startsAtMinutes : 0;
32806
+ const endsAtMinutes = isEndDay ? draft.endsAtMinutes : 1440;
32807
+ if (endsAtMinutes <= startsAtMinutes) continue;
32808
+ segments.push({
32809
+ draft,
32810
+ dateKey,
32811
+ startsAtMinutes,
32812
+ endsAtMinutes,
32813
+ continuesBefore: !isStartDay,
32814
+ continuesAfter: !isEndDay
32815
+ });
32816
+ }
32817
+ return segments;
32818
+ }
32819
+ function clipDraft(segment, rangeStartMinutes, rangeEndMinutes) {
32820
+ const visibleStart = Math.max(rangeStartMinutes, segment.startsAtMinutes);
32821
+ const visibleEnd = Math.min(rangeEndMinutes, segment.endsAtMinutes);
32822
+ if (visibleEnd <= visibleStart) return {
32823
+ ok: false,
32824
+ reason: "out-of-visible-hours"
32825
+ };
32826
+ return {
32827
+ ok: true,
32828
+ clipped: {
32829
+ segment,
32830
+ visibleStart,
32831
+ visibleEnd
32832
+ }
32833
+ };
32834
+ }
32835
+ function buildCell(entry, lane, rangeStartMinutes, rangeMinutes, minHeightPercent) {
32836
+ const heightPercent = Math.max(minHeightPercent, toRangePercent(entry.visibleEnd - entry.visibleStart, rangeMinutes));
32837
+ const rawTopPercent = toRangePercent(entry.visibleStart - rangeStartMinutes, rangeMinutes);
32838
+ const topPercent = Math.min(rawTopPercent, 100 - heightPercent);
32839
+ return {
32840
+ item: entry.segment.draft.item,
32841
+ dateKey: entry.segment.dateKey,
32842
+ startsAtMinutes: entry.segment.startsAtMinutes,
32843
+ endsAtMinutes: entry.segment.endsAtMinutes,
32844
+ visibleStartsAtMinutes: entry.visibleStart,
32845
+ visibleEndsAtMinutes: entry.visibleEnd,
32846
+ topPercent,
32847
+ heightPercent,
32848
+ isClippedStart: entry.segment.continuesBefore || entry.segment.startsAtMinutes < entry.visibleStart,
32849
+ isClippedEnd: entry.segment.continuesAfter || entry.segment.endsAtMinutes > entry.visibleEnd,
32850
+ laneIndex: lane.laneIndex,
32851
+ laneCount: lane.laneCount,
32852
+ color: entry.segment.draft.color
32853
+ };
32854
+ }
32855
+ function resolveBounds(bounds) {
32856
+ return {
32857
+ minStartHour: bounds?.minStartHour ?? DEFAULT_BOUNDS.minStartHour,
32858
+ maxEndHour: bounds?.maxEndHour ?? DEFAULT_BOUNDS.maxEndHour,
32859
+ defaultStartHour: bounds?.defaultStartHour ?? DEFAULT_BOUNDS.defaultStartHour,
32860
+ defaultEndHour: bounds?.defaultEndHour ?? DEFAULT_BOUNDS.defaultEndHour,
32861
+ minHeightPercent: bounds?.minHeightPercent ?? DEFAULT_BOUNDS.minHeightPercent
32862
+ };
32863
+ }
32864
+ function resolveHourRange(segments, bounds) {
32865
+ if (segments.length === 0) return {
32866
+ startHour: bounds.defaultStartHour,
32867
+ endHour: bounds.defaultEndHour
32868
+ };
32869
+ const earliestStart = Math.min(...segments.map((segment) => segment.startsAtMinutes));
32870
+ const latestEnd = Math.max(...segments.map((segment) => segment.endsAtMinutes));
32871
+ const startHour = Math.max(bounds.minStartHour, Math.floor(earliestStart / 60));
32872
+ const endHour = Math.min(bounds.maxEndHour, Math.ceil(latestEnd / 60));
32873
+ return {
32874
+ startHour,
32875
+ endHour: Math.max(startHour + 1, endHour)
32876
+ };
32877
+ }
32878
+ function buildHourTicks(startHour, endHour) {
32879
+ const ticks = [];
32880
+ for (let hour = startHour; hour <= endHour; hour += 1) ticks.push(hour);
32881
+ return ticks;
32882
+ }
32883
+ function groupSegmentsByDate(segments) {
32884
+ const grouped = /* @__PURE__ */ new Map();
32885
+ for (const segment of segments) {
32886
+ const existing = grouped.get(segment.dateKey) ?? [];
32887
+ existing.push(segment);
32888
+ grouped.set(segment.dateKey, existing);
32889
+ }
32890
+ return grouped;
32891
+ }
32892
+ function toRangePercent(minutes, rangeMinutes) {
32893
+ if (rangeMinutes <= 0) return 0;
32894
+ return minutes / rangeMinutes * 100;
32895
+ }
32896
+ function getTimetableDays(viewDate, view, startOfWeek) {
32897
+ if (view === "day") return [viewDate];
32898
+ return getWeekCalendarDays(viewDate, startOfWeek);
32899
+ }
32900
+ function enumerateDateKeys(startDateKey, endDateKey) {
32901
+ if (startDateKey > endDateKey) throw new Error(`Invalid timetable date range: ${startDateKey} is after ${endDateKey}`);
32902
+ const start = parseDateKey(startDateKey);
32903
+ parseDateKey(endDateKey);
32904
+ const keys = [];
32905
+ let cursor = start;
32906
+ while (formatDateKey(cursor) <= endDateKey) {
32907
+ keys.push(formatDateKey(cursor));
32908
+ cursor = addDays(cursor, 1);
32909
+ }
32910
+ return keys;
32911
+ }
32912
+ function parseDateKey(dateKey) {
32913
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(dateKey);
32914
+ if (match === null) throw new Error(`Invalid timetable date key: ${dateKey}`);
32915
+ const [, year, month, day] = match;
32916
+ return {
32917
+ year: Number(year),
32918
+ month: Number(month),
32919
+ day: Number(day)
32920
+ };
32921
+ }
32922
+ //#endregion
32923
+ //#region ../calendar/src/group.ts
32924
+ /**
32925
+ * Group items by their local date (YYYY-MM-DD in the item's own timezone).
32926
+ *
32927
+ * Generic over `T`. Items with un-parseable startsAt or invalid timezone are
32928
+ * silently dropped — call sites that need diagnostics should validate upstream.
32929
+ *
32930
+ * The output map preserves insertion order of dates (ES2015 spec) and the
32931
+ * relative order of items within each date matches input order.
32932
+ */
32933
+ function groupItemsByDate(items, accessors) {
32934
+ const grouped = /* @__PURE__ */ new Map();
32935
+ for (const item of items) {
32936
+ const dateKey = getLocalDateKey(accessors.getStartsAt(item), accessors.getTimeZone(item));
32937
+ if (dateKey == null) continue;
32938
+ const existing = grouped.get(dateKey);
32939
+ if (existing !== void 0) existing.push(item);
32940
+ else grouped.set(dateKey, [item]);
32941
+ }
32942
+ return grouped;
32943
+ }
32944
+ //#endregion
32945
+ //#region ../blocks/src/system/runtime/nodes/events/domain/dates.ts
32946
+ /**
32947
+ * Date and time presentation for events: the single place an event's
32948
+ * timestamps become strings or date buckets. Calendar-grid primitives are
32949
+ * re-exported from @riverbankcms/calendar so block code has one import.
32950
+ */
32951
+ /**
32952
+ * Format date for display.
32953
+ * Returns: "Monday, January 15, 2025"
32954
+ */
32955
+ function formatDate(isoTimestamp, options) {
32956
+ return formatBookingDate(toScheduledBookingDateTime({
32957
+ startsAt: isoTimestamp,
32958
+ timeZone: toBookingTimeZone(options?.timeZone ?? "UTC"),
32959
+ locale: options?.locale ?? "en-US"
32960
+ }), {
32961
+ weekday: "long",
32962
+ year: "numeric",
32963
+ month: "long",
32964
+ day: "numeric"
32965
+ });
32966
+ }
32967
+ /**
32968
+ * Format time for display.
32969
+ * Returns: "10:00 AM"
32970
+ */
32971
+ function formatTime(isoTimestamp, options) {
32972
+ return formatBookingTime(toScheduledBookingDateTime({
32973
+ startsAt: isoTimestamp,
32974
+ timeZone: toBookingTimeZone(options?.timeZone ?? "UTC"),
32975
+ locale: options?.locale ?? "en-US"
32976
+ }), {
32977
+ hour: "numeric",
32978
+ minute: "2-digit",
32979
+ hour12: true
32980
+ });
32981
+ }
32982
+ function formatEventDateParts(args) {
32983
+ const scheduledRange = toScheduledBookingDateRange({
32984
+ startsAt: args.startsAt,
32985
+ endsAt: args.endsAt,
32986
+ timeZone: toBookingTimeZone(args.timeZone ?? "UTC"),
32987
+ locale: args.locale ?? "en-US"
32988
+ });
32989
+ const { date, time: start } = formatBookingDateParts(scheduledRange, { dateFormat: {
32990
+ weekday: "long",
32991
+ year: "numeric",
32992
+ month: "long",
32993
+ day: "numeric"
32994
+ } });
32995
+ return {
32996
+ date,
32997
+ timeRange: `${start} – ${formatBookingTime(toScheduledBookingDateTime({
32998
+ startsAt: args.endsAt,
32999
+ timeZone: scheduledRange.timeZone,
33000
+ locale: args.locale ?? "en-US"
33001
+ }))}`
33002
+ };
33003
+ }
33004
+ function formatEventCompactDateRail(isoTimestamp, options) {
33005
+ const dateTime = toScheduledBookingDateTime({
33006
+ startsAt: isoTimestamp,
33007
+ timeZone: toBookingTimeZone(options?.timeZone ?? "UTC"),
33008
+ locale: options?.locale ?? "en-US"
33009
+ });
33010
+ const weekday = formatBookingDate(dateTime, { weekday: "short" }).toLocaleUpperCase(options?.locale ?? "en-US");
33011
+ const month = formatBookingDate(dateTime, { month: "short" }).toLocaleUpperCase(options?.locale ?? "en-US");
33012
+ return {
33013
+ weekday,
33014
+ day: formatBookingDate(dateTime, { day: "numeric" }),
33015
+ month,
33016
+ label: formatBookingDate(dateTime, {
33017
+ weekday: "long",
33018
+ year: "numeric",
33019
+ month: "long",
33020
+ day: "numeric"
33021
+ })
33022
+ };
33023
+ }
33024
+ /**
33025
+ * Group events by their local date (YYYY-MM-DD key in each event's own
33026
+ * timezone). Thin domain wrapper over `groupItemsByDate` from the calendar
33027
+ * package; preserves the historical `Map<string, PublicEvent[]>` shape.
33028
+ */
33029
+ function groupEventsByDate(events, calendarTimeZone) {
33030
+ const grouped = groupItemsByDate(events, {
33031
+ getStartsAt: (event) => event.startsAt,
33032
+ getTimeZone: () => calendarTimeZone
33033
+ });
33034
+ const map = /* @__PURE__ */ new Map();
33035
+ for (const [dateKey, bucket] of grouped) map.set(dateKey, [...bucket]);
33036
+ return map;
33037
+ }
33038
+ //#endregion
33039
+ //#region ../blocks/src/system/runtime/nodes/events/domain/people.ts
33040
+ function formatTeamMemberNames(names) {
33041
+ const normalized = names.map((name) => name.trim()).filter(Boolean);
33042
+ if (normalized.length === 0) return null;
33043
+ const [firstName, secondName] = normalized;
33044
+ if (!firstName) return null;
33045
+ if (normalized.length === 1) return firstName;
33046
+ if (normalized.length === 2 && secondName) return `${firstName} and ${secondName}`;
33047
+ const finalName = normalized[normalized.length - 1] ?? "";
33048
+ return `${normalized.slice(0, -1).join(", ")}, and ${finalName}`;
33049
+ }
33050
+ function formatEventTeacherLine(event) {
33051
+ const names = formatTeamMemberNames((event.teamMembers ?? []).map((teamMember) => teamMember.name));
33052
+ return names ? `with ${names}` : null;
33053
+ }
33054
+ //#endregion
33055
+ //#region ../blocks/src/system/runtime/nodes/events/domain/pricing.ts
33056
+ const DEFAULT_EVENT_CURRENCY = "USD";
33057
+ function resolveRuntimeEventPricing(event) {
33058
+ if (isPublicEventPricing(event.pricing)) return event.pricing;
33059
+ if (resolveRuntimeEventDestination(event).kind === "external") return {
33060
+ kind: "external",
33061
+ display: {
33062
+ kind: "external",
33063
+ label: "External registration"
33064
+ }
33065
+ };
33066
+ const legacy = event;
33067
+ const currency = readCurrency(legacy.currency);
33068
+ if (event.requiresRegistration === false) return {
33069
+ kind: "drop_in",
33070
+ currency,
33071
+ display: {
33072
+ kind: "free",
33073
+ currency
33074
+ }
33075
+ };
33076
+ if (legacy.pricingMode === "ticket_types") return {
33077
+ kind: "ticket_types",
33078
+ currency,
33079
+ ticketTypes: Array.isArray(legacy.ticketTypes) ? legacy.ticketTypes.filter(isPublicEventTicketType) : [],
33080
+ display: readTicketTypePriceDisplay(legacy.priceDisplay, currency)
33081
+ };
33082
+ const amount = typeof legacy.price === "number" ? legacy.price : 0;
33083
+ if (legacy.pricingType === "paid" && amount > 0) return {
33084
+ kind: "single_paid",
33085
+ amount,
33086
+ currency,
33087
+ display: {
33088
+ kind: "exact",
33089
+ amount,
33090
+ currency
33091
+ }
33092
+ };
33093
+ return {
33094
+ kind: "free_registration",
33095
+ currency,
33096
+ display: {
33097
+ kind: "free",
33098
+ currency
33099
+ }
33100
+ };
33101
+ }
33102
+ function readCurrency(value) {
33103
+ return typeof value === "string" && value.trim().length > 0 ? value : DEFAULT_EVENT_CURRENCY;
33104
+ }
33105
+ function isRecord$13(value) {
33106
+ return typeof value === "object" && value !== null && !Array.isArray(value);
33107
+ }
33108
+ function isPublicEventPricing(value) {
33109
+ if (!isRecord$13(value) || typeof value.kind !== "string") return false;
33110
+ 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";
33111
+ }
33112
+ function isPublicEventTicketType(value) {
33113
+ return isRecord$13(value) && typeof value.id === "string";
33114
+ }
33115
+ function readTicketTypePriceDisplay(value, currency) {
33116
+ const display = readLegacyPriceDisplay(value, currency);
33117
+ return display.kind === "whole_series" || display.kind === "external" ? {
33118
+ kind: "unavailable",
33119
+ currency
33120
+ } : display;
33121
+ }
33122
+ function readLegacyPriceDisplay(value, currency) {
33123
+ if (!isRecord$13(value) || typeof value.kind !== "string") return {
33124
+ kind: "unavailable",
33125
+ currency
33126
+ };
33127
+ const displayCurrency = readCurrency(value.currency);
33128
+ switch (value.kind) {
33129
+ case "free": return {
33130
+ kind: "free",
33131
+ currency: displayCurrency
33132
+ };
33133
+ case "exact": return readAmount(value.amount) !== null ? {
33134
+ kind: "exact",
33135
+ amount: readAmount(value.amount) ?? 0,
33136
+ currency: displayCurrency
33137
+ } : {
33138
+ kind: "unavailable",
33139
+ currency
33140
+ };
33141
+ case "from": return readAmount(value.amount) !== null ? {
33142
+ kind: "from",
33143
+ amount: readAmount(value.amount) ?? 0,
33144
+ currency: displayCurrency
33145
+ } : {
33146
+ kind: "unavailable",
33147
+ currency
33148
+ };
33149
+ case "range": {
33150
+ const minAmount = readAmount(value.minAmount);
33151
+ const maxAmount = readAmount(value.maxAmount);
33152
+ return minAmount !== null && maxAmount !== null ? {
33153
+ kind: "range",
33154
+ minAmount,
33155
+ maxAmount,
33156
+ currency: displayCurrency
33157
+ } : {
33158
+ kind: "unavailable",
33159
+ currency
33160
+ };
33161
+ }
33162
+ case "mixed": return readAmount(value.paidFromAmount) !== null ? {
33163
+ kind: "mixed",
33164
+ paidFromAmount: readAmount(value.paidFromAmount) ?? 0,
33165
+ paidToAmount: readAmount(value.paidToAmount) ?? readAmount(value.paidFromAmount) ?? 0,
33166
+ currency: displayCurrency
33167
+ } : {
33168
+ kind: "unavailable",
33169
+ currency
33170
+ };
33171
+ case "unavailable": return {
33172
+ kind: "unavailable",
33173
+ currency: displayCurrency
33174
+ };
33175
+ case "whole_series": return isRecord$13(value.display) ? {
33176
+ kind: "whole_series",
33177
+ display: readCoursePriceDisplay(value.display, displayCurrency)
33178
+ } : {
33179
+ kind: "whole_series",
33180
+ display: {
33181
+ kind: "unavailable",
33182
+ currency: displayCurrency
33183
+ }
33184
+ };
33185
+ default: return {
33186
+ kind: "unavailable",
33187
+ currency
33188
+ };
33189
+ }
33190
+ }
33191
+ function readCoursePriceDisplay(value, currency) {
33192
+ switch (value.kind) {
33193
+ case "free": return {
33194
+ kind: "free",
33195
+ currency: readCurrency(value.currency)
33196
+ };
33197
+ case "exact": return readAmount(value.amount) !== null ? {
33198
+ kind: "exact",
33199
+ amount: readAmount(value.amount) ?? 0,
33200
+ currency: readCurrency(value.currency)
33201
+ } : {
33202
+ kind: "unavailable",
33203
+ currency
33204
+ };
33205
+ case "from": return readAmount(value.amount) !== null ? {
33206
+ kind: "from",
33207
+ amount: readAmount(value.amount) ?? 0,
33208
+ currency: readCurrency(value.currency)
33209
+ } : {
33210
+ kind: "unavailable",
33211
+ currency
33212
+ };
33213
+ default: return {
33214
+ kind: "unavailable",
33215
+ currency
33216
+ };
33217
+ }
33218
+ }
33219
+ function readAmount(value) {
33220
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
33221
+ }
33222
+ /**
33223
+ * Format event price for display.
33224
+ * @returns "Free", "£25", "£12 - £25", "Free / £12 - £25", "Course (From £40)", etc.
33225
+ *
33226
+ * Events render compact amounts (no trailing ".00"); the actual ADT→string
33227
+ * mapping lives in `formatBookingPriceDisplay` in @riverbankcms/core so all
33228
+ * surfaces stay consistent.
33229
+ */
33230
+ function formatEventPrice(event) {
33231
+ return formatEventPricing(resolveRuntimeEventPricing(event), { decimals: "auto" });
33232
+ }
33233
+ //#endregion
33234
+ //#region ../blocks/src/system/constants/events.ts
33235
+ const eventCalendarDisplayModeValues = [
33236
+ "combined",
33237
+ "calendar",
33238
+ "list",
33239
+ "timetable",
33240
+ "week-board"
33241
+ ];
33242
+ const eventCalendarDefaultViewValues = ["month", "week"];
33243
+ const eventCalendarListLayoutValues = [
33244
+ "stack",
33245
+ "grid",
33246
+ "compact"
33247
+ ];
33248
+ const eventCalendarListLayoutOptions = [
33249
+ {
33250
+ id: "stack",
33251
+ label: "Stack"
33252
+ },
33253
+ {
33254
+ id: "grid",
33255
+ label: "Grid"
33256
+ },
33257
+ {
33258
+ id: "compact",
33259
+ label: "Compact"
33260
+ }
33261
+ ];
33262
+ function isEventCalendarDisplayMode(value) {
33263
+ return typeof value === "string" && eventCalendarDisplayModeValues.includes(value);
33264
+ }
33265
+ function isEventCalendarDefaultView(value) {
33266
+ return typeof value === "string" && eventCalendarDefaultViewValues.includes(value);
33267
+ }
33268
+ function isEventCalendarListLayout(value) {
33269
+ return typeof value === "string" && eventCalendarListLayoutValues.includes(value);
33270
+ }
33271
+ //#endregion
33272
+ //#region ../blocks/src/system/runtime/nodes/shared/contract-schemas.ts
33273
+ const previewStageSchema = z.enum(["preview", "published"]);
33274
+ const registrationButtonVariantSchema = blockButtonRoleSchema;
33275
+ const eventButtonVariantSchema = blockButtonRoleSchema;
33276
+ const eventLayoutSchema = z.enum(eventCalendarListLayoutValues);
33277
+ const eventCalendarDisplayModeSchema = z.enum(eventCalendarDisplayModeValues);
33278
+ const eventCalendarDefaultViewSchema = z.enum(eventCalendarDefaultViewValues);
33279
+ const columnOptionSchema = z.enum([
33280
+ "2",
33281
+ "3",
33282
+ "4"
33283
+ ]);
33284
+ const weekStartSchema = z.enum(["sunday", "monday"]);
33285
+ const filterAutoShowSchema = z.enum([
33286
+ "auto",
33287
+ "always",
33288
+ "never"
33289
+ ]);
33290
+ const calendarTabViewsSchema = z.enum([
33291
+ "both",
33292
+ "month",
33293
+ "week"
33294
+ ]);
33295
+ //#endregion
33296
+ //#region ../blocks/src/system/runtime/nodes/event-registration/wizardSteps.ts
33297
+ /**
33298
+ * Determine which wizard steps to show based on context and payment requirements.
33299
+ *
33300
+ * @param hasPreSelectedContext - Whether event is pre-selected (skips event selection)
33301
+ * @param needsPaymentStep - Whether user must complete payment method selection
33302
+ * @returns Array of wizard steps to display
33303
+ */
33304
+ function getWizardStepsForContext(hasPreSelectedContext, needsPaymentStep) {
33305
+ const baseSteps = hasPreSelectedContext ? ["select-tickets", "enter-details"] : [
33306
+ "select-event",
33307
+ "select-tickets",
33308
+ "enter-details"
33309
+ ];
33310
+ if (!needsPaymentStep) return baseSteps;
33311
+ return [...baseSteps, "select-payment"];
33312
+ }
33313
+ //#endregion
33314
+ //#region ../blocks/src/system/runtime/nodes/event-registration/registrationDerivations.ts
33315
+ function eventMatchesOccurrenceId(event, occurrenceId) {
33316
+ return event.registrationSubject.kind === "occurrence" && (event.registrationSubject.occurrenceId === occurrenceId || event.selectionId === occurrenceId);
33317
+ }
33318
+ function findPreSelectedEvent(input) {
33319
+ const occurrenceContext = input.occurrenceContext;
33320
+ if (!occurrenceContext) return input.scopedEvents.length === 1 ? input.scopedEvents[0] ?? null : null;
33321
+ const contextDate = getBookingLocalDateKey(toScheduledBookingDateTime({
33322
+ startsAt: occurrenceContext.startsAt,
33323
+ timeZone: occurrenceContext.timeZone
33324
+ }));
33325
+ return input.scopedEvents.find((event) => eventMatchesOccurrenceId(event, occurrenceContext.id) || getBookingLocalDateKey(toScheduledBookingDateTime({
33326
+ startsAt: event.startsAt,
33327
+ timeZone: event.timeZone
33328
+ })) === contextDate) ?? null;
33329
+ }
33330
+ function resolveEventRequiresPayment(input) {
33331
+ if (input.eligibility) return input.eligibility.requiresPayment;
33332
+ if (input.selectedTicketType) return input.selectedTicketType.priceAmount > 0;
33333
+ if (!input.event) return false;
33334
+ return publicEventPricingType(resolveRuntimeEventPricing(input.event)) === "paid";
33335
+ }
33336
+ //#endregion
33337
+ //#region ../blocks/src/system/runtime/nodes/event-registration/registrationEventAdapter.ts
33338
+ function toRegistrationEvent(event) {
33339
+ return {
33340
+ ...event,
33341
+ pricing: resolveRuntimeEventPricing(event),
33342
+ selectionId: event.id,
33343
+ registrationSubject: {
33344
+ kind: "occurrence",
33345
+ occurrenceId: event.occurrenceId
33346
+ }
33347
+ };
33348
+ }
33349
+ function toRegistrationEvents(events) {
33350
+ return events.filter((event) => event.bookingScope === "single_occurrence").map(toRegistrationEvent);
33351
+ }
33352
+ function toWholeSeriesTicketType(option) {
33353
+ return {
33354
+ ...option,
33355
+ capacityMode: option.capacityLimit === null ? "unlimited" : "limited",
33356
+ attendanceScope: option.attendanceScope ?? void 0,
33357
+ availability: {
33358
+ kind: "available",
33359
+ remainingCapacity: option.capacityLimit
33360
+ }
33361
+ };
33362
+ }
33363
+ function toWholeSeriesPricing(offering) {
33364
+ const resolved = resolveWholeSeriesPricingState(offering);
33365
+ switch (offering.pricing.pricingStrategy) {
33366
+ case "free": return {
33367
+ kind: "free_registration",
33368
+ currency: offering.currency,
33369
+ display: {
33370
+ kind: "free",
33371
+ currency: offering.currency
32210
33372
  }
32211
33373
  };
32212
33374
  case "single": {
@@ -32595,7 +33757,7 @@ const eventRegistrationBlockDefinition = {
32595
33757
  {
32596
33758
  id: "showCapacity",
32597
33759
  type: "boolean",
32598
- label: "Show available spots",
33760
+ label: "Show available places",
32599
33761
  description: "Display remaining capacity",
32600
33762
  required: false,
32601
33763
  defaultValue: true,
@@ -33140,591 +34302,26 @@ const eventSpotlightBlockDefinition = {
33140
34302
  dataLoaders: { events: {
33141
34303
  endpoint: "listPublicEvents",
33142
34304
  params: {
33143
- siteId: { $bind: { from: "$root.siteId" } },
33144
- limit: { $bind: {
33145
- from: "content.maxEvents",
33146
- fallback: "3"
33147
- } },
33148
- stage: { $bind: {
33149
- from: "$root.previewStage",
33150
- fallback: "published"
33151
- } },
33152
- detailLevel: "listing",
33153
- readProfile: "spotlight"
33154
- },
33155
- mode: "server"
33156
- } }
33157
- };
33158
- //#endregion
33159
- //#region ../calendar/src/grid.ts
33160
- const DAY_NAMES = [
33161
- "Sun",
33162
- "Mon",
33163
- "Tue",
33164
- "Wed",
33165
- "Thu",
33166
- "Fri",
33167
- "Sat"
33168
- ];
33169
- const MONTH_NAMES = [
33170
- "January",
33171
- "February",
33172
- "March",
33173
- "April",
33174
- "May",
33175
- "June",
33176
- "July",
33177
- "August",
33178
- "September",
33179
- "October",
33180
- "November",
33181
- "December"
33182
- ];
33183
- function formatDateKey(date) {
33184
- return `${String(date.year).padStart(4, "0")}-${String(date.month).padStart(2, "0")}-${String(date.day).padStart(2, "0")}`;
33185
- }
33186
- function isToday(date, today) {
33187
- return isSameCalendarDay(date, today);
33188
- }
33189
- function isSameCalendarDay(left, right) {
33190
- return left.year === right.year && left.month === right.month && left.day === right.day;
33191
- }
33192
- function addDays(date, days) {
33193
- const result = new Date(Date.UTC(date.year, date.month - 1, date.day + days, 12));
33194
- return {
33195
- year: result.getUTCFullYear(),
33196
- month: result.getUTCMonth() + 1,
33197
- day: result.getUTCDate()
33198
- };
33199
- }
33200
- function getMonthCalendarDays(year, month, startOfWeek) {
33201
- const firstDay = {
33202
- year,
33203
- month,
33204
- day: 1
33205
- };
33206
- const dayOfWeek = getDayOfWeek(firstDay);
33207
- const gridStart = addDays(firstDay, -(startOfWeek === "monday" ? dayOfWeek === 0 ? 6 : dayOfWeek - 1 : dayOfWeek));
33208
- return Array.from({ length: 42 }, (_, index) => addDays(gridStart, index));
33209
- }
33210
- function getWeekCalendarDays(baseDate, startOfWeek) {
33211
- const dayOfWeek = getDayOfWeek(baseDate);
33212
- const weekStart = addDays(baseDate, -(startOfWeek === "monday" ? dayOfWeek === 0 ? 6 : dayOfWeek - 1 : dayOfWeek));
33213
- return Array.from({ length: 7 }, (_, index) => addDays(weekStart, index));
33214
- }
33215
- function getDayOfWeek(date) {
33216
- return new Date(Date.UTC(date.year, date.month - 1, date.day, 12)).getUTCDay();
33217
- }
33218
- function getWeekCalendarDayTuple(baseDate, startOfWeek) {
33219
- const days = getWeekCalendarDays(baseDate, startOfWeek);
33220
- assertWeekCalendarDays(days);
33221
- return days;
33222
- }
33223
- function assertWeekCalendarDays(days) {
33224
- if (days.length !== 7) throw new Error("Expected exactly 7 calendar days");
33225
- }
33226
- function getOrderedDayNames(startOfWeek, dayNames = DAY_NAMES) {
33227
- if (startOfWeek === "monday") {
33228
- const sunday = dayNames[0] ?? "Sun";
33229
- return [...dayNames.slice(1), sunday];
33230
- }
33231
- return [...dayNames];
33232
- }
33233
- function formatCalendarTitle(viewDate, view, days, monthNames = MONTH_NAMES) {
33234
- if (view === "week") {
33235
- const weekStart = days[0];
33236
- const weekEnd = days[6];
33237
- if (weekStart === void 0 || weekEnd === void 0) return `${monthNames[viewDate.month - 1] ?? ""} ${viewDate.year}`;
33238
- const startMonth = monthNames[weekStart.month - 1] ?? "";
33239
- const endMonth = monthNames[weekEnd.month - 1] ?? "";
33240
- if (weekStart.month === weekEnd.month) return `${startMonth} ${weekStart.day} - ${weekEnd.day}, ${weekEnd.year}`;
33241
- return `${startMonth} ${weekStart.day} - ${endMonth} ${weekEnd.day}, ${weekEnd.year}`;
33242
- }
33243
- return `${monthNames[viewDate.month - 1] ?? ""} ${viewDate.year}`;
33244
- }
33245
- function buildCalendarViewport(input) {
33246
- const monthNames = input.monthNames ?? MONTH_NAMES;
33247
- if (input.view === "week") {
33248
- const days = getWeekCalendarDayTuple(input.anchorDate, input.startOfWeek);
33249
- return {
33250
- kind: "week",
33251
- anchorDate: input.anchorDate,
33252
- days,
33253
- title: formatCalendarTitle(input.anchorDate, "week", days, monthNames)
33254
- };
33255
- }
33256
- const days = getMonthCalendarDays(input.anchorDate.year, input.anchorDate.month, input.startOfWeek);
33257
- return {
33258
- kind: "month",
33259
- anchorDate: input.anchorDate,
33260
- days,
33261
- title: formatCalendarTitle(input.anchorDate, "month", days, monthNames)
33262
- };
33263
- }
33264
- //#endregion
33265
- //#region ../calendar/src/lane-packing.ts
33266
- /**
33267
- * Assign lanes to a list of intervals.
33268
- *
33269
- * Returns one result per input in the same order as the input. Internally
33270
- * sorts a copy by `(start, end, orderKey)` to ensure determinism, then
33271
- * permutes results back to input order.
33272
- */
33273
- function assignLanes(intervals) {
33274
- if (intervals.length === 0) return [];
33275
- const indexed = intervals.map((interval, index) => ({
33276
- interval,
33277
- index
33278
- }));
33279
- indexed.sort((left, right) => left.interval.start - right.interval.start || left.interval.end - right.interval.end || left.interval.orderKey.localeCompare(right.interval.orderKey));
33280
- const results = new Array(intervals.length);
33281
- let cluster = [];
33282
- let activeLaneEnds = [];
33283
- let clusterMaxEnd = -Infinity;
33284
- const closeCluster = () => {
33285
- if (cluster.length === 0) return;
33286
- const laneCount = Math.max(1, cluster.reduce((max, entry) => Math.max(max, entry.laneIndex + 1), 0));
33287
- for (const entry of cluster) results[entry.index] = {
33288
- laneIndex: entry.laneIndex,
33289
- laneCount
33290
- };
33291
- cluster = [];
33292
- activeLaneEnds = [];
33293
- clusterMaxEnd = -Infinity;
33294
- };
33295
- for (const { interval, index } of indexed) {
33296
- if (interval.start >= clusterMaxEnd) closeCluster();
33297
- const laneIndex = firstAvailableLane(activeLaneEnds, interval.start);
33298
- activeLaneEnds[laneIndex] = interval.end;
33299
- clusterMaxEnd = Math.max(clusterMaxEnd, interval.end);
33300
- cluster.push({
33301
- index,
33302
- laneIndex
33303
- });
33304
- }
33305
- closeCluster();
33306
- return results;
33307
- }
33308
- function firstAvailableLane(activeLaneEnds, start) {
33309
- const laneIndex = activeLaneEnds.findIndex((end) => end <= start);
33310
- return laneIndex >= 0 ? laneIndex : activeLaneEnds.length;
33311
- }
33312
- //#endregion
33313
- //#region ../calendar/src/timezone.ts
33314
- /**
33315
- * Timezone boundary helpers — internal.
33316
- *
33317
- * Both functions read an ISO instant in a given IANA timezone and project it
33318
- * onto the wall clock of that zone. They use Intl.DateTimeFormat with
33319
- * hourCycle: 'h23' to avoid 12/24-hour ambiguity. Used internally by the
33320
- * timetable model and `groupItemsByDate`; not exported from the package's
33321
- * public surface.
33322
- */
33323
- const MINUTES_PER_HOUR = 60;
33324
- /**
33325
- * Local minute-of-day [0, 24*60) for an ISO instant in the given zone.
33326
- * Returns NaN if the inputs cannot be parsed (caller should drop the item).
33327
- */
33328
- function getLocalMinuteOfDay(isoTimestamp, timeZone) {
33329
- const date = new Date(isoTimestamp);
33330
- if (Number.isNaN(date.getTime())) return NaN;
33331
- if (!isValidTimeZone(timeZone)) return NaN;
33332
- const parts = new Intl.DateTimeFormat("en-GB", {
33333
- timeZone,
33334
- hour: "2-digit",
33335
- minute: "2-digit",
33336
- hourCycle: "h23"
33337
- }).formatToParts(date);
33338
- const hourPart = Number(parts.find((part) => part.type === "hour")?.value);
33339
- const minute = Number(parts.find((part) => part.type === "minute")?.value);
33340
- const hour = hourPart === 24 ? 0 : hourPart;
33341
- if (Number.isNaN(hour) || Number.isNaN(minute)) return NaN;
33342
- return hour * MINUTES_PER_HOUR + minute;
33343
- }
33344
- /**
33345
- * Local YYYY-MM-DD for an ISO instant in the given zone.
33346
- * Returns null if the inputs cannot be parsed.
33347
- */
33348
- function getLocalDateKey(isoTimestamp, timeZone) {
33349
- const date = new Date(isoTimestamp);
33350
- if (Number.isNaN(date.getTime())) return null;
33351
- if (!isValidTimeZone(timeZone)) return null;
33352
- const parts = new Intl.DateTimeFormat("en-CA", {
33353
- timeZone,
33354
- year: "numeric",
33355
- month: "2-digit",
33356
- day: "2-digit"
33357
- }).formatToParts(date);
33358
- const year = parts.find((part) => part.type === "year")?.value;
33359
- const month = parts.find((part) => part.type === "month")?.value;
33360
- const day = parts.find((part) => part.type === "day")?.value;
33361
- if (year === void 0 || year.length === 0) return null;
33362
- if (month === void 0 || month.length === 0) return null;
33363
- if (day === void 0 || day.length === 0) return null;
33364
- return `${year}-${month}-${day}`;
33365
- }
33366
- const validTimeZoneCache = /* @__PURE__ */ new Map();
33367
- function isValidTimeZone(timeZone) {
33368
- if (typeof timeZone !== "string" || timeZone.length === 0) return false;
33369
- const cached = validTimeZoneCache.get(timeZone);
33370
- if (cached !== void 0) return cached;
33371
- try {
33372
- new Intl.DateTimeFormat("en-US", { timeZone });
33373
- validTimeZoneCache.set(timeZone, true);
33374
- return true;
33375
- } catch {
33376
- validTimeZoneCache.set(timeZone, false);
33377
- return false;
33378
- }
33379
- }
33380
- //#endregion
33381
- //#region ../calendar/src/timetable.ts
33382
- /**
33383
- * Timetable model — the package's core surface.
33384
- *
33385
- * Generic over `T`. Takes a list of timed items + a viewport; returns a
33386
- * day-by-day layout with overlap-packed lanes and percent-based positions
33387
- * plus a diagnostics list for items dropped during processing.
33388
- *
33389
- * The function is pure: given identical input (including `now`), it returns
33390
- * the same output. The model has no opinion on what an item is — pass
33391
- * accessors.
33392
- */
33393
- const DEFAULT_BOUNDS = {
33394
- minStartHour: 5,
33395
- maxEndHour: 23,
33396
- defaultStartHour: 6,
33397
- defaultEndHour: 21,
33398
- minHeightPercent: 2.5
34305
+ siteId: { $bind: { from: "$root.siteId" } },
34306
+ limit: { $bind: {
34307
+ from: "content.maxEvents",
34308
+ fallback: "3"
34309
+ } },
34310
+ stage: { $bind: {
34311
+ from: "$root.previewStage",
34312
+ fallback: "published"
34313
+ } },
34314
+ detailLevel: "listing",
34315
+ readProfile: "spotlight"
34316
+ },
34317
+ mode: "server"
34318
+ } }
33399
34319
  };
33400
- function buildTimetableModel$1(input) {
33401
- const bounds = resolveBounds(input.bounds);
33402
- const viewportDays = getTimetableDays(input.viewDate, input.view ?? "week", input.startOfWeek);
33403
- const viewportKeys = new Set(viewportDays.map(formatDateKey));
33404
- const parsed = parseAllItems(input.items, input);
33405
- const inViewport = deriveVisibleSegments(parsed.drafts, viewportKeys);
33406
- const hourRange = resolveHourRange(inViewport, bounds);
33407
- const laid = layoutCellsByDate(inViewport, hourRange, bounds);
33408
- return {
33409
- days: assembleDays(viewportDays, laid.cellsByDate, input.today),
33410
- hourTicks: buildHourTicks(hourRange.startHour, hourRange.endHour),
33411
- startHour: hourRange.startHour,
33412
- endHour: hourRange.endHour,
33413
- rangeMinutes: (hourRange.endHour - hourRange.startHour) * 60,
33414
- hasItems: inViewport.length > 0,
33415
- diagnostics: [...parsed.diagnostics, ...laid.diagnostics]
33416
- };
33417
- }
33418
- function parseAllItems(items, accessors) {
33419
- const drafts = [];
33420
- const diagnostics = [];
33421
- for (const item of items) {
33422
- const result = parseDraft(item, accessors);
33423
- if (result.ok) drafts.push(result.draft);
33424
- else diagnostics.push({
33425
- item,
33426
- reason: result.reason
33427
- });
33428
- }
33429
- return {
33430
- drafts,
33431
- diagnostics
33432
- };
33433
- }
33434
- function layoutCellsByDate(segments, hourRange, bounds) {
33435
- const rangeMinutes = (hourRange.endHour - hourRange.startHour) * 60;
33436
- const rangeStartMinutes = hourRange.startHour * 60;
33437
- const rangeEndMinutes = hourRange.endHour * 60;
33438
- const segmentsByDate = groupSegmentsByDate(segments);
33439
- const cellsByDate = /* @__PURE__ */ new Map();
33440
- const diagnostics = [];
33441
- for (const [dateKey, daySegments] of segmentsByDate) {
33442
- const clipped = [];
33443
- for (const segment of daySegments) {
33444
- const result = clipDraft(segment, rangeStartMinutes, rangeEndMinutes);
33445
- if (result.ok) clipped.push(result.clipped);
33446
- else diagnostics.push({
33447
- item: segment.draft.item,
33448
- reason: result.reason
33449
- });
33450
- }
33451
- const laneResults = assignLanes(clipped.map((entry) => ({
33452
- start: entry.visibleStart,
33453
- end: entry.visibleEnd,
33454
- orderKey: entry.segment.draft.orderKey
33455
- })));
33456
- cellsByDate.set(dateKey, clipped.map((entry, idx) => buildCell(entry, laneResults[idx] ?? {
33457
- laneIndex: 0,
33458
- laneCount: 1
33459
- }, rangeStartMinutes, rangeMinutes, bounds.minHeightPercent)));
33460
- }
33461
- return {
33462
- cellsByDate,
33463
- diagnostics
33464
- };
33465
- }
33466
- function assembleDays(viewportDays, cellsByDate, today) {
33467
- return viewportDays.map((date) => {
33468
- const dateKey = formatDateKey(date);
33469
- return {
33470
- date,
33471
- dateKey,
33472
- isToday: isSameCalendarDay(date, today),
33473
- cells: cellsByDate.get(dateKey) ?? []
33474
- };
33475
- });
33476
- }
33477
- function parseDraft(item, accessors) {
33478
- const startsAtIso = accessors.getStartsAt(item);
33479
- const endsAtIso = accessors.getEndsAt(item);
33480
- const timeZone = accessors.getTimeZone(item);
33481
- const startsAtMinutes = getLocalMinuteOfDay(startsAtIso, timeZone);
33482
- if (Number.isNaN(startsAtMinutes)) return {
33483
- ok: false,
33484
- reason: "invalid-start"
33485
- };
33486
- const startDateKey = getLocalDateKey(startsAtIso, timeZone);
33487
- if (startDateKey == null) return {
33488
- ok: false,
33489
- reason: "invalid-timezone"
33490
- };
33491
- const endDateKey = getLocalDateKey(endsAtIso, timeZone);
33492
- if (endDateKey == null) return {
33493
- ok: false,
33494
- reason: "invalid-end"
33495
- };
33496
- const endsAtMinutesRaw = getLocalMinuteOfDay(endsAtIso, timeZone);
33497
- if (Number.isNaN(endsAtMinutesRaw)) return {
33498
- ok: false,
33499
- reason: "invalid-end"
33500
- };
33501
- const spansMultipleDays = startDateKey < endDateKey;
33502
- const endsAtMinutes = spansMultipleDays || endsAtMinutesRaw > startsAtMinutes ? endsAtMinutesRaw : Math.min(1440, startsAtMinutes + 60);
33503
- if (!spansMultipleDays && endsAtMinutes <= startsAtMinutes) return {
33504
- ok: false,
33505
- reason: "non-positive-duration"
33506
- };
33507
- return {
33508
- ok: true,
33509
- draft: {
33510
- item,
33511
- startDateKey,
33512
- endDateKey,
33513
- startsAtMinutes,
33514
- endsAtMinutes,
33515
- color: accessors.getColor?.(item) ?? null,
33516
- orderKey: accessors.getOrderKey?.(item) ?? accessors.getId(item)
33517
- }
33518
- };
33519
- }
33520
- function deriveVisibleSegments(drafts, viewportKeys) {
33521
- const segments = [];
33522
- for (const draft of drafts) for (const dateKey of enumerateDateKeys(draft.startDateKey, draft.endDateKey)) {
33523
- if (!viewportKeys.has(dateKey)) continue;
33524
- const isStartDay = dateKey === draft.startDateKey;
33525
- const isEndDay = dateKey === draft.endDateKey;
33526
- const startsAtMinutes = isStartDay ? draft.startsAtMinutes : 0;
33527
- const endsAtMinutes = isEndDay ? draft.endsAtMinutes : 1440;
33528
- if (endsAtMinutes <= startsAtMinutes) continue;
33529
- segments.push({
33530
- draft,
33531
- dateKey,
33532
- startsAtMinutes,
33533
- endsAtMinutes,
33534
- continuesBefore: !isStartDay,
33535
- continuesAfter: !isEndDay
33536
- });
33537
- }
33538
- return segments;
33539
- }
33540
- function clipDraft(segment, rangeStartMinutes, rangeEndMinutes) {
33541
- const visibleStart = Math.max(rangeStartMinutes, segment.startsAtMinutes);
33542
- const visibleEnd = Math.min(rangeEndMinutes, segment.endsAtMinutes);
33543
- if (visibleEnd <= visibleStart) return {
33544
- ok: false,
33545
- reason: "out-of-visible-hours"
33546
- };
33547
- return {
33548
- ok: true,
33549
- clipped: {
33550
- segment,
33551
- visibleStart,
33552
- visibleEnd
33553
- }
33554
- };
33555
- }
33556
- function buildCell(entry, lane, rangeStartMinutes, rangeMinutes, minHeightPercent) {
33557
- const heightPercent = Math.max(minHeightPercent, toRangePercent(entry.visibleEnd - entry.visibleStart, rangeMinutes));
33558
- const rawTopPercent = toRangePercent(entry.visibleStart - rangeStartMinutes, rangeMinutes);
33559
- const topPercent = Math.min(rawTopPercent, 100 - heightPercent);
33560
- return {
33561
- item: entry.segment.draft.item,
33562
- dateKey: entry.segment.dateKey,
33563
- startsAtMinutes: entry.segment.startsAtMinutes,
33564
- endsAtMinutes: entry.segment.endsAtMinutes,
33565
- visibleStartsAtMinutes: entry.visibleStart,
33566
- visibleEndsAtMinutes: entry.visibleEnd,
33567
- topPercent,
33568
- heightPercent,
33569
- isClippedStart: entry.segment.continuesBefore || entry.segment.startsAtMinutes < entry.visibleStart,
33570
- isClippedEnd: entry.segment.continuesAfter || entry.segment.endsAtMinutes > entry.visibleEnd,
33571
- laneIndex: lane.laneIndex,
33572
- laneCount: lane.laneCount,
33573
- color: entry.segment.draft.color
33574
- };
33575
- }
33576
- function resolveBounds(bounds) {
33577
- return {
33578
- minStartHour: bounds?.minStartHour ?? DEFAULT_BOUNDS.minStartHour,
33579
- maxEndHour: bounds?.maxEndHour ?? DEFAULT_BOUNDS.maxEndHour,
33580
- defaultStartHour: bounds?.defaultStartHour ?? DEFAULT_BOUNDS.defaultStartHour,
33581
- defaultEndHour: bounds?.defaultEndHour ?? DEFAULT_BOUNDS.defaultEndHour,
33582
- minHeightPercent: bounds?.minHeightPercent ?? DEFAULT_BOUNDS.minHeightPercent
33583
- };
33584
- }
33585
- function resolveHourRange(segments, bounds) {
33586
- if (segments.length === 0) return {
33587
- startHour: bounds.defaultStartHour,
33588
- endHour: bounds.defaultEndHour
33589
- };
33590
- const earliestStart = Math.min(...segments.map((segment) => segment.startsAtMinutes));
33591
- const latestEnd = Math.max(...segments.map((segment) => segment.endsAtMinutes));
33592
- const startHour = Math.max(bounds.minStartHour, Math.floor(earliestStart / 60));
33593
- const endHour = Math.min(bounds.maxEndHour, Math.ceil(latestEnd / 60));
33594
- return {
33595
- startHour,
33596
- endHour: Math.max(startHour + 1, endHour)
33597
- };
33598
- }
33599
- function buildHourTicks(startHour, endHour) {
33600
- const ticks = [];
33601
- for (let hour = startHour; hour <= endHour; hour += 1) ticks.push(hour);
33602
- return ticks;
33603
- }
33604
- function groupSegmentsByDate(segments) {
33605
- const grouped = /* @__PURE__ */ new Map();
33606
- for (const segment of segments) {
33607
- const existing = grouped.get(segment.dateKey) ?? [];
33608
- existing.push(segment);
33609
- grouped.set(segment.dateKey, existing);
33610
- }
33611
- return grouped;
33612
- }
33613
- function toRangePercent(minutes, rangeMinutes) {
33614
- if (rangeMinutes <= 0) return 0;
33615
- return minutes / rangeMinutes * 100;
33616
- }
33617
- function getTimetableDays(viewDate, view, startOfWeek) {
33618
- if (view === "day") return [viewDate];
33619
- return getWeekCalendarDays(viewDate, startOfWeek);
33620
- }
33621
- function enumerateDateKeys(startDateKey, endDateKey) {
33622
- if (startDateKey > endDateKey) throw new Error(`Invalid timetable date range: ${startDateKey} is after ${endDateKey}`);
33623
- const start = parseDateKey(startDateKey);
33624
- parseDateKey(endDateKey);
33625
- const keys = [];
33626
- let cursor = start;
33627
- while (formatDateKey(cursor) <= endDateKey) {
33628
- keys.push(formatDateKey(cursor));
33629
- cursor = addDays(cursor, 1);
33630
- }
33631
- return keys;
33632
- }
33633
- function parseDateKey(dateKey) {
33634
- const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(dateKey);
33635
- if (match === null) throw new Error(`Invalid timetable date key: ${dateKey}`);
33636
- const [, year, month, day] = match;
33637
- return {
33638
- year: Number(year),
33639
- month: Number(month),
33640
- day: Number(day)
33641
- };
33642
- }
33643
- //#endregion
33644
- //#region ../calendar/src/group.ts
33645
- /**
33646
- * Group items by their local date (YYYY-MM-DD in the item's own timezone).
33647
- *
33648
- * Generic over `T`. Items with un-parseable startsAt or invalid timezone are
33649
- * silently dropped — call sites that need diagnostics should validate upstream.
33650
- *
33651
- * The output map preserves insertion order of dates (ES2015 spec) and the
33652
- * relative order of items within each date matches input order.
33653
- */
33654
- function groupItemsByDate(items, accessors) {
33655
- const grouped = /* @__PURE__ */ new Map();
33656
- for (const item of items) {
33657
- const dateKey = getLocalDateKey(accessors.getStartsAt(item), accessors.getTimeZone(item));
33658
- if (dateKey == null) continue;
33659
- const existing = grouped.get(dateKey);
33660
- if (existing !== void 0) existing.push(item);
33661
- else grouped.set(dateKey, [item]);
33662
- }
33663
- return grouped;
33664
- }
33665
- //#endregion
33666
- //#region ../blocks/src/system/runtime/nodes/events/shared/eventCapacity.ts
33667
- /**
33668
- * Compute remaining spots for an event from `availableSpots` with a fallback
33669
- * to `capacity - registeredCount`. Returns `{ available: null, isFull: false }`
33670
- * for uncapped events so call sites can render no capacity indicator at all.
33671
- *
33672
- * The single source of truth for capacity math across event cards, agenda
33673
- * rows, and timetable cells — keep wording variations in the call sites.
33674
- */
33675
- function getEventAvailability(event) {
33676
- if (event.capacity == null) return {
33677
- available: null,
33678
- isFull: false
33679
- };
33680
- const available = event.availableSpots ?? event.capacity - event.registeredCount;
33681
- if (available <= 0) return {
33682
- available: 0,
33683
- isFull: true
33684
- };
33685
- return {
33686
- available,
33687
- isFull: false
33688
- };
33689
- }
33690
- /**
33691
- * Derive the three-state availability status (available / filling / waitlist)
33692
- * plus a short pill label. Builds on {@link getEventAvailability} so the
33693
- * capacity math (availableSpots with a `capacity - registeredCount` fallback)
33694
- * stays consistent with the binary helper used elsewhere.
33695
- */
33696
- function getEventAvailabilityStatus(event, options) {
33697
- const threshold = options?.fillingThreshold ?? 2;
33698
- const { available, isFull } = getEventAvailability(event);
33699
- if (available == null) return {
33700
- status: "available",
33701
- available: null,
33702
- isFull: false,
33703
- label: null
33704
- };
33705
- if (isFull) return {
33706
- status: "waitlist",
33707
- available: 0,
33708
- isFull: true,
33709
- label: event.waitlistEnabled ? "Waitlist" : "Full"
33710
- };
33711
- if (available <= threshold) return {
33712
- status: "filling",
33713
- available,
33714
- isFull: false,
33715
- label: `${available} left`
33716
- };
33717
- return {
33718
- status: "available",
33719
- available,
33720
- isFull: false,
33721
- label: `${available} places`
33722
- };
33723
- }
33724
34320
  //#endregion
33725
- //#region ../blocks/src/system/runtime/nodes/events/shared/utils.ts
34321
+ //#region ../blocks/src/system/runtime/nodes/events/shared/listingLayout.ts
33726
34322
  /**
33727
- * Shared utility functions for event blocks
34323
+ * Layout classes for event listing containers and cards. Presentation
34324
+ * plumbing rather than event domain logic, so it lives beside the components.
33728
34325
  */
33729
34326
  /**
33730
34327
  * Column class mappings for grid layouts, derived from the canonical
@@ -33759,180 +34356,6 @@ function getCardOrientation(layout) {
33759
34356
  case "compact": return "vertical";
33760
34357
  }
33761
34358
  }
33762
- /**
33763
- * Format date for display.
33764
- * Returns: "Monday, January 15, 2025"
33765
- */
33766
- function formatDate(isoTimestamp, options) {
33767
- return formatBookingDate(toScheduledBookingDateTime({
33768
- startsAt: isoTimestamp,
33769
- timeZone: toBookingTimeZone(options?.timeZone ?? "UTC"),
33770
- locale: options?.locale ?? "en-US"
33771
- }), {
33772
- weekday: "long",
33773
- year: "numeric",
33774
- month: "long",
33775
- day: "numeric"
33776
- });
33777
- }
33778
- /**
33779
- * Format time for display.
33780
- * Returns: "10:00 AM"
33781
- */
33782
- function formatTime(isoTimestamp, options) {
33783
- return formatBookingTime(toScheduledBookingDateTime({
33784
- startsAt: isoTimestamp,
33785
- timeZone: toBookingTimeZone(options?.timeZone ?? "UTC"),
33786
- locale: options?.locale ?? "en-US"
33787
- }), {
33788
- hour: "numeric",
33789
- minute: "2-digit",
33790
- hour12: true
33791
- });
33792
- }
33793
- function formatEventDateParts(args) {
33794
- const scheduledRange = toScheduledBookingDateRange({
33795
- startsAt: args.startsAt,
33796
- endsAt: args.endsAt,
33797
- timeZone: toBookingTimeZone(args.timeZone ?? "UTC"),
33798
- locale: args.locale ?? "en-US"
33799
- });
33800
- const { date, time: start } = formatBookingDateParts(scheduledRange, { dateFormat: {
33801
- weekday: "long",
33802
- year: "numeric",
33803
- month: "long",
33804
- day: "numeric"
33805
- } });
33806
- return {
33807
- date,
33808
- timeRange: `${start} – ${formatBookingTime(toScheduledBookingDateTime({
33809
- startsAt: args.endsAt,
33810
- timeZone: scheduledRange.timeZone,
33811
- locale: args.locale ?? "en-US"
33812
- }))}`
33813
- };
33814
- }
33815
- function formatEventCompactDateRail(isoTimestamp, options) {
33816
- const dateTime = toScheduledBookingDateTime({
33817
- startsAt: isoTimestamp,
33818
- timeZone: toBookingTimeZone(options?.timeZone ?? "UTC"),
33819
- locale: options?.locale ?? "en-US"
33820
- });
33821
- const weekday = formatBookingDate(dateTime, { weekday: "short" }).toLocaleUpperCase(options?.locale ?? "en-US");
33822
- const month = formatBookingDate(dateTime, { month: "short" }).toLocaleUpperCase(options?.locale ?? "en-US");
33823
- return {
33824
- weekday,
33825
- day: formatBookingDate(dateTime, { day: "numeric" }),
33826
- month,
33827
- label: formatBookingDate(dateTime, {
33828
- weekday: "long",
33829
- year: "numeric",
33830
- month: "long",
33831
- day: "numeric"
33832
- })
33833
- };
33834
- }
33835
- function formatTeamMemberNames(names) {
33836
- const normalized = names.map((name) => name.trim()).filter(Boolean);
33837
- if (normalized.length === 0) return null;
33838
- const [firstName, secondName] = normalized;
33839
- if (!firstName) return null;
33840
- if (normalized.length === 1) return firstName;
33841
- if (normalized.length === 2 && secondName) return `${firstName} and ${secondName}`;
33842
- const finalName = normalized[normalized.length - 1] ?? "";
33843
- return `${normalized.slice(0, -1).join(", ")}, and ${finalName}`;
33844
- }
33845
- function formatEventTeacherLine(event) {
33846
- const names = formatTeamMemberNames((event.teamMembers ?? []).map((teamMember) => teamMember.name));
33847
- return names ? `with ${names}` : null;
33848
- }
33849
- /**
33850
- * Group events by their local date (YYYY-MM-DD key in each event's own
33851
- * timezone). Thin domain wrapper over `groupItemsByDate` from the calendar
33852
- * package; preserves the historical `Map<string, PublicEvent[]>` shape.
33853
- */
33854
- function groupEventsByDate(events, calendarTimeZone) {
33855
- const grouped = groupItemsByDate(events, {
33856
- getStartsAt: (event) => event.startsAt,
33857
- getTimeZone: () => calendarTimeZone
33858
- });
33859
- const map = /* @__PURE__ */ new Map();
33860
- for (const [dateKey, bucket] of grouped) map.set(dateKey, [...bucket]);
33861
- return map;
33862
- }
33863
- /**
33864
- * Format event price for display.
33865
- * @returns "Free", "£25", "£12 - £25", "Free / £12 - £25", "Course (From £40)", etc.
33866
- *
33867
- * Events render compact amounts (no trailing ".00"); the actual ADT→string
33868
- * mapping lives in `formatBookingPriceDisplay` in @riverbankcms/core so all
33869
- * surfaces stay consistent.
33870
- */
33871
- function formatEventPrice(event) {
33872
- return formatEventPricing(resolveRuntimeEventPricing(event), { decimals: "auto" });
33873
- }
33874
- function closedEventBookingLabel(reason) {
33875
- switch (reason) {
33876
- case "occurrence_cancelled":
33877
- case "series_cancelled": return "Cancelled";
33878
- case "occurrence_completed":
33879
- case "series_completed": return "Completed";
33880
- case "series_draft":
33881
- case "series_archived":
33882
- case "parent_archived":
33883
- case "parent_inconsistent":
33884
- case "not_public":
33885
- case "already_started":
33886
- case "enrollment_closed": return "Registration closed";
33887
- default: return assertNever$3(reason);
33888
- }
33889
- }
33890
- function resolveEventHref(event) {
33891
- const destination = resolveRuntimeEventDestination(event);
33892
- return destination.kind === "external" ? destination.url : destination.path;
33893
- }
33894
- function resolveEventLinkProps(event) {
33895
- const href = resolveEventHref(event);
33896
- return resolveRuntimeEventDestination(event).kind === "external" ? {
33897
- href,
33898
- target: "_blank",
33899
- rel: "noopener noreferrer"
33900
- } : { href };
33901
- }
33902
- /**
33903
- * Resolve the CTA state for an event.
33904
- * Shared by EventCard and EventCompactRow to keep sold-out, registration-closed,
33905
- * and course-specific defaults consistent.
33906
- */
33907
- function resolveEventCta(event, buttonText) {
33908
- const { isFull: isSoldOut } = getEventAvailability(event);
33909
- const destination = resolveRuntimeEventDestination(event);
33910
- const isExternal = destination.kind === "external";
33911
- const defaultButtonText = isExternal ? destination.label : event.bookingScope === "whole_series" ? "Book course" : "Book";
33912
- const effectiveButtonText = buttonText ?? defaultButtonText;
33913
- const href = resolveEventHref(event);
33914
- if (event.bookingAvailability.kind === "closed") {
33915
- const linksToClosedWholeSeriesDetails = event.bookingAvailability.reason === "enrollment_closed" && !isExternal;
33916
- return {
33917
- label: closedEventBookingLabel(event.bookingAvailability.reason),
33918
- href,
33919
- external: isExternal,
33920
- hidden: !linksToClosedWholeSeriesDetails
33921
- };
33922
- }
33923
- if (!isExternal && isSoldOut) return {
33924
- label: "Sold out",
33925
- href,
33926
- external: false,
33927
- hidden: true
33928
- };
33929
- return {
33930
- label: effectiveButtonText,
33931
- href,
33932
- external: isExternal,
33933
- hidden: false
33934
- };
33935
- }
33936
34359
  //#endregion
33937
34360
  //#region ../blocks/src/system/media/slotContracts.ts
33938
34361
  const DEFAULT_MIN_SOURCE_WIDTH = 1200;
@@ -35196,7 +35619,6 @@ const EventCard = ({ event, cardRole = "default", buttonVariant = "primary", but
35196
35619
  const formattedDate = formatDate(event.startsAt, { timeZone });
35197
35620
  const formattedTime = `${formatTime(event.startsAt, { timeZone })} - ${formatTime(event.endsAt, { timeZone })}`;
35198
35621
  const cta = resolveEventCta(event, buttonText);
35199
- const { available: spotsLeft, isFull: isSoldOut } = getEventAvailability(event);
35200
35622
  const cardClass = boxStyleClassNames({ role: cardRole }, { baseClass: "event-card" });
35201
35623
  const buttonClass = themeButtonClassName({
35202
35624
  variant: buttonVariant,
@@ -35212,10 +35634,7 @@ const EventCard = ({ event, cardRole = "default", buttonVariant = "primary", but
35212
35634
  } : null;
35213
35635
  const hasImage = Boolean(showImage && image);
35214
35636
  const teacherLine = formatEventTeacherLine(event);
35215
- const capacityText = showCapacity ? formatCapacityText(event, {
35216
- isSoldOut,
35217
- spotsLeft
35218
- }) : null;
35637
+ const capacityText = showCapacity ? formatEventRemainingPlaces(event, { full: "Sold out" }) : null;
35219
35638
  return /* @__PURE__ */ jsxs("article", {
35220
35639
  className: `${cardClass} group rb-flex rb-flex-col rb-h-full ${eventCardOrientationClass(orientation)}`,
35221
35640
  "data-orientation": orientation,
@@ -35281,7 +35700,11 @@ function EventCardContent(props) {
35281
35700
  }),
35282
35701
  /* @__PURE__ */ jsx("h3", {
35283
35702
  className: "card-title rb-mb-3",
35284
- children: props.title
35703
+ children: /* @__PURE__ */ jsx("a", {
35704
+ className: "event-title-link",
35705
+ ...resolveEventLinkProps(event),
35706
+ children: props.title
35707
+ })
35285
35708
  }),
35286
35709
  /* @__PURE__ */ jsx("div", {
35287
35710
  className: "rb-mb-2 rb-caption",
@@ -35358,13 +35781,6 @@ function eventCardOrientationClass(orientation) {
35358
35781
  case "vertical": return "event-card--vertical";
35359
35782
  }
35360
35783
  }
35361
- function formatCapacityText(event, options) {
35362
- if (event.capacity == null) return null;
35363
- if (options.isSoldOut) return "Sold out";
35364
- if (options.spotsLeft == null) return null;
35365
- const count = options.spotsLeft;
35366
- return `${count} ${count === 1 ? "spot" : "spots"} available`;
35367
- }
35368
35784
  function getCategoryBadgeStyle(background) {
35369
35785
  const normalized = background?.trim() ?? "";
35370
35786
  if (!normalized) return {
@@ -35433,7 +35849,11 @@ const EventCompactRow = ({ event, buttonVariant = "primary", buttonText }) => {
35433
35849
  className: "event-compact-row-content",
35434
35850
  children: [/* @__PURE__ */ jsx("h3", {
35435
35851
  className: "event-compact-row-title",
35436
- children: event.title
35852
+ children: /* @__PURE__ */ jsx("a", {
35853
+ className: "event-title-link",
35854
+ ...resolveEventLinkProps(event),
35855
+ children: event.title
35856
+ })
35437
35857
  }), /* @__PURE__ */ jsxs("div", {
35438
35858
  className: "event-compact-row-meta",
35439
35859
  children: [
@@ -35499,6 +35919,23 @@ const EmptyState = ({ message = "No upcoming events scheduled.", className, hasA
35499
35919
  });
35500
35920
  };
35501
35921
  //#endregion
35922
+ //#region ../blocks/src/system/runtime/nodes/events/shared/OfferingTitle.tsx
35923
+ /**
35924
+ * Heading for an offering card or row. Links to the offering's page when one
35925
+ * exists so a sold-out or closed offering stays reachable (issue 1372); the
35926
+ * `.event-title-link` rule lives in theme-core's event-list.css.
35927
+ */
35928
+ function OfferingTitle({ className, path, title }) {
35929
+ return /* @__PURE__ */ jsx("h3", {
35930
+ className,
35931
+ children: path ? /* @__PURE__ */ jsx("a", {
35932
+ className: "event-title-link",
35933
+ href: path,
35934
+ children: title
35935
+ }) : title
35936
+ });
35937
+ }
35938
+ //#endregion
35502
35939
  //#region ../blocks/src/system/runtime/nodes/events/shared/listOptions.ts
35503
35940
  const eventPageSizeValues = [
35504
35941
  3,
@@ -36843,7 +37280,7 @@ const eventCalendarAdditionalFields = [
36843
37280
  {
36844
37281
  id: "showCapacity",
36845
37282
  type: "boolean",
36846
- label: "Show available spots",
37283
+ label: "Show available places",
36847
37284
  description: "Display remaining capacity on event cards and timetable cells",
36848
37285
  required: false,
36849
37286
  defaultValue: true,
@@ -46604,7 +47041,7 @@ function EventFacts({ selection, selected, venue, teamMembers, pricing, showCapa
46604
47041
  children: [/* @__PURE__ */ jsx("div", {
46605
47042
  className: "rb-label rb-mb-2",
46606
47043
  children: "Places"
46607
- }), /* @__PURE__ */ jsxs("p", { children: [selected.capacity, " places"] })]
47044
+ }), /* @__PURE__ */ jsx("p", { children: formatTotalCapacity(selected.capacity) })]
46608
47045
  }) : null,
46609
47046
  pricing,
46610
47047
  selected.bookingScope === "whole_series" ? /* @__PURE__ */ jsxs("div", {
@@ -47448,9 +47885,10 @@ function CourseCompactRow({ course, path }) {
47448
47885
  }),
47449
47886
  /* @__PURE__ */ jsxs("div", {
47450
47887
  className: "event-compact-row-content",
47451
- children: [/* @__PURE__ */ jsx("h3", {
47888
+ children: [/* @__PURE__ */ jsx(OfferingTitle, {
47452
47889
  className: "event-compact-row-title",
47453
- children: course.title
47890
+ path,
47891
+ title: course.title
47454
47892
  }), /* @__PURE__ */ jsxs("div", {
47455
47893
  className: "event-compact-row-meta",
47456
47894
  children: [
@@ -47507,9 +47945,10 @@ function CourseFeatureCard({ course, path, buttonVariant, buttonText }) {
47507
47945
  children: /* @__PURE__ */ jsxs("div", {
47508
47946
  className: "card-content rb-flex rb-flex-1 rb-flex-col",
47509
47947
  children: [
47510
- /* @__PURE__ */ jsx("h3", {
47948
+ /* @__PURE__ */ jsx(OfferingTitle, {
47511
47949
  className: "card-title rb-mb-3",
47512
- children: course.title
47950
+ path,
47951
+ title: course.title
47513
47952
  }),
47514
47953
  /* @__PURE__ */ jsxs("div", {
47515
47954
  className: "rb-mb-2 rb-caption",
@@ -48365,8 +48804,8 @@ const AgendaEventCard = ({ event, showCapacity, onClick }) => {
48365
48804
  const eventLinkProps = resolveEventLinkProps(event);
48366
48805
  const categoryColor = event.eventCategory?.color || "rgb(var(--tb-accent))";
48367
48806
  const price = formatEventPrice(event);
48368
- const { available, isFull } = getEventAvailability(event);
48369
- const capacity = showCapacity && available != null ? isFull ? "Full" : `${available} spots left` : null;
48807
+ const capacity = showCapacity ? formatEventRemainingPlaces(event) : null;
48808
+ const isFull = isEventFull(event);
48370
48809
  const title = event.title;
48371
48810
  const timeZone = event.timeZone ?? null;
48372
48811
  const teacherLine = formatEventTeacherLine(event);
@@ -48808,7 +49247,7 @@ function buildTimetableModel(input) {
48808
49247
  getEndsAt: (event) => event.endsAt,
48809
49248
  getTimeZone: (event) => event.timeZone ?? "UTC",
48810
49249
  getColor: (event) => event.eventCategory?.color ?? null,
48811
- getOrderKey: (event) => `${event.title}${event.id}`
49250
+ getOrderKey: (event) => `${event.title}\u0000${event.id}`
48812
49251
  });
48813
49252
  return {
48814
49253
  hourTicks: [...generic.hourTicks],
@@ -48845,18 +49284,9 @@ function toEventCell(cell) {
48845
49284
  };
48846
49285
  }
48847
49286
  function formatTimetableCapacity(event) {
48848
- const { available, isFull } = getEventAvailability(event);
48849
- if (available == null) return {
48850
- label: null,
48851
- isFull: false
48852
- };
48853
- if (isFull) return {
48854
- label: "Full",
48855
- isFull: true
48856
- };
48857
49287
  return {
48858
- label: `${available} spots left`,
48859
- isFull: false
49288
+ label: formatEventRemainingPlaces(event, { compact: true }),
49289
+ isFull: isEventFull(event)
48860
49290
  };
48861
49291
  }
48862
49292
  function formatEventTimeRange(event) {
@@ -49332,7 +49762,7 @@ function EventRegistrationSSR({ blockId, subject, children, ...sourceProps }) {
49332
49762
  className: `event-registration ${className ?? ""}`.trim(),
49333
49763
  "data-block": "event-registration",
49334
49764
  "data-rb-island-required": "true",
49335
- "data-rb-island-error-message": "Booking options failed to load. Please refresh the page.",
49765
+ "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.",
49336
49766
  "data-rb-island-error-class": "event-registration__hydration-error",
49337
49767
  ...getIslandRootProps("event-registration", blockId),
49338
49768
  children: [
@@ -55157,6 +55587,32 @@ function isSiteStyleTransitionStyleGroup(value) {
55157
55587
  function boundaryMapForAdjacentBlockIds(boundaries) {
55158
55588
  return new Map(boundaries.map((boundary) => [boundaryKey(boundary.previousBlockId, boundary.nextBlockId), boundary]));
55159
55589
  }
55590
+ /** Keep a surviving block's outgoing treatment when omitted blocks change its neighbor. */
55591
+ function boundariesForSurvivingBlocks(boundaries, blocks) {
55592
+ const outgoing = new Map(boundaries.map((boundary) => [boundary.previousBlockId, boundary]));
55593
+ const incoming = new Map(boundaries.map((boundary) => [boundary.nextBlockId, boundary]));
55594
+ return blocks.flatMap((block, index) => {
55595
+ const next = blocks[index + 1];
55596
+ if (!block.id || !next?.id) return [];
55597
+ const boundary = outgoing.get(block.id);
55598
+ if (!boundary) return [];
55599
+ if (boundary.nextBlockId === next.id) return [boundary];
55600
+ const destination = incoming.get(next.id);
55601
+ return [retargetCompiledBoundaryRuntimeForContentAdjacency({
55602
+ ...boundary,
55603
+ boundaryId: `boundary:${block.id}:${next.id}`,
55604
+ nextBlockId: next.id,
55605
+ to: destination?.to ?? boundary.to,
55606
+ cssVars: {
55607
+ ...boundary.cssVars,
55608
+ to: destination?.cssVars.to ?? boundary.cssVars.to
55609
+ }
55610
+ }, {
55611
+ currentContent: block.content,
55612
+ nextContent: next.content
55613
+ })];
55614
+ });
55615
+ }
55160
55616
  function boundaryBetweenBlocks(previousBlock, nextBlock, boundaries) {
55161
55617
  const previousBlockId = previousBlock?.id ?? null;
55162
55618
  const nextBlockId = nextBlock?.id ?? null;
@@ -55248,41 +55704,173 @@ function toPresentationPage(page) {
55248
55704
  };
55249
55705
  }
55250
55706
  //#endregion
55707
+ //#region ../blocks/src/pageRendererBlockSupport.tsx
55708
+ function fallback(message) {
55709
+ return /* @__PURE__ */ jsx("div", {
55710
+ className: "rb-p-6 rb-caption",
55711
+ style: textColorStyle("mutedText"),
55712
+ children: message
55713
+ });
55714
+ }
55715
+ /**
55716
+ * A page with nothing to show. Authoring surfaces get the diagnostic; a public
55717
+ * page renders nothing at all rather than telling a visitor about the outline.
55718
+ */
55719
+ function emptyPageNotice(audience) {
55720
+ if (audience === "public") return null;
55721
+ return /* @__PURE__ */ jsx("div", {
55722
+ className: "rb-p-6 rb-caption",
55723
+ style: textColorStyle("mutedText"),
55724
+ children: "No blocks found."
55725
+ });
55726
+ }
55727
+ /**
55728
+ * A public page dropped a block, so nothing in the rendered HTML says the
55729
+ * content is missing. Say it in the logs instead.
55730
+ */
55731
+ function warnOmittedFromPublicPage(input) {
55732
+ try {
55733
+ console.warn("[blocks:PageRenderer] omitted a non-renderable block from a public page", {
55734
+ blockId: input.block.id ?? null,
55735
+ blockKind: input.block.kind,
55736
+ outcome: input.outcome,
55737
+ ...input.admissionError ? { error: input.admissionError } : {}
55738
+ });
55739
+ } catch {}
55740
+ }
55741
+ function shouldDebugCustomBlocks() {
55742
+ try {
55743
+ return globalThis.process?.env?.DEBUG_CUSTOM_BLOCKS === "1";
55744
+ } catch {
55745
+ return false;
55746
+ }
55747
+ }
55748
+ /**
55749
+ * Look up a block override component by block kind.
55750
+ * Supports both full ("block.hero") and short ("hero") keys.
55751
+ */
55752
+ function getBlockOverride(blockKind, overrides) {
55753
+ if (!overrides) return void 0;
55754
+ if (overrides[blockKind]) return overrides[blockKind];
55755
+ const shortKind = blockKind.replace(/^block\./, "");
55756
+ if (overrides[shortKind]) return overrides[shortKind];
55757
+ }
55758
+ /**
55759
+ * Resolves block content based on stage priority.
55760
+ * For draft/preview: draftContent takes priority over content.
55761
+ * For published: content is the source of truth.
55762
+ * Returns empty object if no content exists (validator will handle placeholders if needed).
55763
+ */
55764
+ function getRenderableContent(block, previewStage = "published") {
55765
+ const source = previewStage === "preview" && block.draftContent ? block.draftContent : block.content ?? {};
55766
+ if (!isObjectRecord$1(source)) return {};
55767
+ return source;
55768
+ }
55769
+ function resolvePageRendererImageResolveContext(dataContext) {
55770
+ const input = dataContext?.imageResolveContext;
55771
+ return buildResolveImageUrlContext({
55772
+ siteId: input?.siteId ?? dataContext?.siteId,
55773
+ supabaseUrl: input?.supabaseUrl ?? dataContext?.supabaseUrl,
55774
+ mediaAssetIndex: input?.mediaAssetIndex,
55775
+ mediaAssets: input?.mediaAssets ?? dataContext?.mediaAssets
55776
+ });
55777
+ }
55778
+ /**
55779
+ * The RSC-safe props every override component receives, whether the block
55780
+ * resolved a manifest or is being rendered from an override alone.
55781
+ */
55782
+ function buildBlockOverrideProps(input) {
55783
+ return {
55784
+ content: input.content,
55785
+ ...overrideThemeProps(input.themeResources),
55786
+ ...blockOverrideContextProps(input.dataContext),
55787
+ data: input.data,
55788
+ imageResolveContext: input.imageResolveContext,
55789
+ blockId: input.block.id ?? null,
55790
+ blockKind: input.block.kind
55791
+ };
55792
+ }
55793
+ /** The render-context half of the override props, absent context included. */
55794
+ function blockOverrideContextProps(dataContext) {
55795
+ const context = dataContext ?? {};
55796
+ return {
55797
+ siteId: context.siteId,
55798
+ apiBaseUrl: context.apiBaseUrl,
55799
+ bookingTimeZone: context.bookingTimeZone,
55800
+ portalToken: context.portalToken ?? null,
55801
+ previewToken: context.previewToken ?? null,
55802
+ stage: context.previewStage,
55803
+ occurrenceContext: context.occurrenceContext ?? null,
55804
+ entrySubrouteContext: context.entrySubrouteContext ?? null,
55805
+ contentEntry: context.contentEntry ?? null,
55806
+ pageSurface: context.pageSurface ?? null,
55807
+ subject: context.subject ?? null,
55808
+ supabaseUrl: context.supabaseUrl
55809
+ };
55810
+ }
55811
+ const blockComponentCache = /* @__PURE__ */ new WeakMap();
55812
+ const DEFAULT_PERF_SLOW_BLOCK_RENDER_THRESHOLD_MS = 25;
55813
+ let cachedPerfSlowBlockRenderThresholdMs = null;
55814
+ function getPerfSlowBlockRenderThresholdMs() {
55815
+ if (cachedPerfSlowBlockRenderThresholdMs !== null) return cachedPerfSlowBlockRenderThresholdMs;
55816
+ const raw = globalThis.process?.env?.DEBUG_PERF_BLOCK_MS;
55817
+ const parsed = raw ? Number.parseInt(raw, 10) : NaN;
55818
+ cachedPerfSlowBlockRenderThresholdMs = Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_PERF_SLOW_BLOCK_RENDER_THRESHOLD_MS;
55819
+ return cachedPerfSlowBlockRenderThresholdMs;
55820
+ }
55821
+ function getCachedBlockComponent(manifest) {
55822
+ const cached = blockComponentCache.get(manifest);
55823
+ if (cached) return cached;
55824
+ const created = makeDefaultBlockComponent({ manifest });
55825
+ blockComponentCache.set(manifest, created);
55826
+ return created;
55827
+ }
55828
+ //#endregion
55251
55829
  //#region ../blocks/src/PageRenderer.tsx
55252
55830
  function PageRenderer$1(props) {
55253
55831
  const page = React$1.useMemo(() => toPresentationPage(props.page), [props.page]);
55254
55832
  return /* @__PURE__ */ jsx(PageRendererCore, {
55255
55833
  ...props,
55256
- page
55834
+ page,
55835
+ audience: "public"
55257
55836
  });
55258
55837
  }
55259
55838
  function PageRendererCore(props) {
55260
- const { page, runtimeBoundaries, dataContext, wrapBlock, wrapBoundary, registry } = props;
55839
+ const { page, audience, runtimeBoundaries, dataContext, wrapBlock, wrapBoundary, registry } = props;
55261
55840
  const imageResolveContext = React$1.useMemo(() => resolvePageRendererImageResolveContext(dataContext), [
55262
55841
  dataContext?.imageResolveContext,
55263
55842
  dataContext?.mediaAssets,
55264
55843
  dataContext?.siteId,
55265
55844
  dataContext?.supabaseUrl
55266
55845
  ]);
55267
- const boundaryByAdjacentBlockIds = React$1.useMemo(() => boundaryMapForAdjacentBlockIds(runtimeBoundaries ?? []), [runtimeBoundaries]);
55268
55846
  const usesRuntimeBoundaries = runtimeBoundaries !== void 0;
55269
55847
  const themeResources = React$1.useMemo(() => resolvePageThemeResources(props), [
55270
55848
  props.rendering,
55271
55849
  props.theme,
55272
55850
  props.themeTokens
55273
55851
  ]);
55274
- if (!page || page.blocks.length === 0) return /* @__PURE__ */ jsx("div", {
55275
- className: "rb-p-6 rb-caption",
55276
- style: textColorStyle("mutedText"),
55277
- children: "No blocks found."
55278
- });
55279
55852
  const resolvedRegistry = registry ?? getDefaultComponentRegistry();
55280
55853
  setContextResolveImageUrlContext(imageResolveContext);
55281
- return /* @__PURE__ */ jsx("div", { children: page.blocks.map((block, index) => {
55282
- const blockPosition = pageBlockPosition(index, page.blocks.length);
55283
- const blockKey = block.id ?? `${block.kind}-${index}`;
55284
- const previousBlock = page.blocks[index - 1] ?? null;
55285
- const nextBlock = page.blocks[index + 1] ?? null;
55854
+ const sections = usePageSections(props, {
55855
+ page,
55856
+ themeResources,
55857
+ imageResolveContext,
55858
+ registry: resolvedRegistry
55859
+ });
55860
+ const boundaryByAdjacentBlockIds = React$1.useMemo(() => boundaryMapForAdjacentBlockIds(boundariesForSurvivingBlocks(runtimeBoundaries ?? [], sections.map(({ block }) => ({
55861
+ ...block,
55862
+ content: getRenderableContent(block, dataContext?.previewStage)
55863
+ })))), [
55864
+ runtimeBoundaries,
55865
+ sections,
55866
+ dataContext?.previewStage
55867
+ ]);
55868
+ if (sections.length === 0) return emptyPageNotice(audience);
55869
+ return /* @__PURE__ */ jsx("div", { children: sections.map((section, index) => {
55870
+ const { block, blockKey, node } = section;
55871
+ const blockPosition = pageBlockPosition(index, sections.length);
55872
+ const previousBlock = sections[index - 1]?.block ?? null;
55873
+ const nextBlock = sections[index + 1]?.block ?? null;
55286
55874
  const boundaryBefore = boundaryBetweenBlocks(previousBlock, block, boundaryByAdjacentBlockIds);
55287
55875
  const boundaryAfter = boundaryBetweenBlocks(block, nextBlock, boundaryByAdjacentBlockIds);
55288
55876
  const boundaryElement = renderBoundaryAfterBlock({
@@ -55297,19 +55885,7 @@ function PageRendererCore(props) {
55297
55885
  const managesOwnWrapper = definition?.manifest.behaviours?.managesOwnWrapper;
55298
55886
  const renderableContent = getRenderableContent(block, dataContext?.previewStage);
55299
55887
  const variantKey = definition?.manifest ? resolveVariantKey(definition.manifest, renderableContent) : null;
55300
- const shouldUseWrapper = !managesOwnWrapper || Boolean(wrapBlock);
55301
- const blockSectionProps = buildBlockSectionProps({
55302
- props,
55303
- themeResources,
55304
- block,
55305
- blockKey,
55306
- imageResolveContext,
55307
- registry: resolvedRegistry
55308
- });
55309
- if (!shouldUseWrapper) {
55310
- const rendered = /* @__PURE__ */ jsx(MemoBlockSection, { ...blockSectionProps }, `${blockKey}:block`);
55311
- return boundaryElement ? /* @__PURE__ */ jsxs(React$1.Fragment, { children: [rendered, boundaryElement] }, blockKey) : rendered;
55312
- }
55888
+ if (!(!managesOwnWrapper || Boolean(wrapBlock))) return /* @__PURE__ */ jsxs(React$1.Fragment, { children: [node, boundaryElement] }, blockKey);
55313
55889
  const wrapperClassName = [`block-${cleanKind}`, wrapBlock ? "group relative" : null].filter(Boolean).join(" ");
55314
55890
  const styleGroup = typeof renderableContent.styleGroup === "string" ? renderableContent.styleGroup : void 0;
55315
55891
  const blockLook = typeof renderableContent.blockLook === "string" ? renderableContent.blockLook : void 0;
@@ -55334,7 +55910,7 @@ function PageRendererCore(props) {
55334
55910
  "data-site-style-boundary-after": boundaryAfter?.metrics.size,
55335
55911
  "data-style-group": styleGroup,
55336
55912
  style: blockStyle,
55337
- children: /* @__PURE__ */ jsx(MemoBlockSection, { ...blockSectionProps })
55913
+ children: node
55338
55914
  }, blockKey);
55339
55915
  if (boundaryElement) return /* @__PURE__ */ jsxs(React$1.Fragment, { children: [blockElement, boundaryElement] }, blockKey);
55340
55916
  if (usesRuntimeBoundaries || !isSiteStyleTransitionStyleGroup(styleGroup)) return blockElement;
@@ -55347,18 +55923,69 @@ function PageRendererCore(props) {
55347
55923
  })] }, blockKey);
55348
55924
  }) });
55349
55925
  }
55350
- function resolvePageRendererImageResolveContext(dataContext) {
55351
- const input = dataContext?.imageResolveContext;
55352
- return buildResolveImageUrlContext({
55353
- siteId: input?.siteId ?? dataContext?.siteId,
55354
- supabaseUrl: input?.supabaseUrl ?? dataContext?.supabaseUrl,
55355
- mediaAssetIndex: input?.mediaAssetIndex,
55356
- mediaAssets: input?.mediaAssets ?? dataContext?.mediaAssets
55926
+ /**
55927
+ * Memoised so that a re-render with unchanged inputs reuses the same section
55928
+ * elements and React can skip the block subtrees, exactly as the per-block memo
55929
+ * did before public pages started resolving their bodies up front.
55930
+ */
55931
+ function usePageSections(props, input) {
55932
+ const { page, themeResources, imageResolveContext, registry } = input;
55933
+ return React$1.useMemo(() => admitPageSections({
55934
+ props,
55935
+ blocks: page?.blocks ?? [],
55936
+ audience: props.audience,
55937
+ themeResources,
55938
+ imageResolveContext,
55939
+ registry
55940
+ }), [
55941
+ page,
55942
+ props.audience,
55943
+ themeResources,
55944
+ imageResolveContext,
55945
+ registry,
55946
+ props.dataContext,
55947
+ props.routeMap,
55948
+ props.wrapBlock,
55949
+ props.blockOverrides,
55950
+ props.sdkConfig,
55951
+ props.customBlocks,
55952
+ props.previewAuthoring
55953
+ ]);
55954
+ }
55955
+ /**
55956
+ * Resolves each block's rendered body before the page decides what to wrap.
55957
+ *
55958
+ * On a public page the body is resolved eagerly so a block that cannot render
55959
+ * is dropped along with its wrapper, its data attributes, its boundary reserve
55960
+ * space and its style-group sibling — the section body is the single source of
55961
+ * truth for whether the block is on the page (issue 1378). Authoring surfaces
55962
+ * keep the memoised component so the editor's render-loop guards still hold.
55963
+ */
55964
+ function admitPageSections(input) {
55965
+ const sections = [];
55966
+ input.blocks.forEach((block, index) => {
55967
+ const blockKey = block.id ?? `${block.kind}-${index}`;
55968
+ const blockSectionProps = buildBlockSectionProps({
55969
+ props: input.props,
55970
+ themeResources: input.themeResources,
55971
+ block,
55972
+ blockKey,
55973
+ imageResolveContext: input.imageResolveContext,
55974
+ registry: input.registry
55975
+ });
55976
+ const node = input.audience === "public" ? renderBlockSection(blockSectionProps) : /* @__PURE__ */ jsx(MemoBlockSection, { ...blockSectionProps }, `${blockKey}:block`);
55977
+ if (node === null || node === void 0) return;
55978
+ sections.push({
55979
+ block,
55980
+ blockKey,
55981
+ node
55982
+ });
55357
55983
  });
55984
+ return sections;
55358
55985
  }
55359
- const resolveBookingTimeZone = (dataContext) => dataContext?.bookingTimeZone;
55360
55986
  function buildBlockSectionProps(input) {
55361
55987
  return {
55988
+ audience: input.props.audience,
55362
55989
  themeResources: input.themeResources,
55363
55990
  block: input.block,
55364
55991
  dataContext: input.props.dataContext,
@@ -55374,40 +56001,24 @@ function buildBlockSectionProps(input) {
55374
56001
  };
55375
56002
  }
55376
56003
  /**
55377
- * Look up a block override component by block kind.
55378
- * Supports both full ("block.hero") and short ("hero") keys.
55379
- */
55380
- function getBlockOverride(blockKind, overrides) {
55381
- if (!overrides) return void 0;
55382
- if (overrides[blockKind]) return overrides[blockKind];
55383
- const shortKind = blockKind.replace(/^block\./, "");
55384
- if (overrides[shortKind]) return overrides[shortKind];
55385
- }
55386
- const blockComponentCache = /* @__PURE__ */ new WeakMap();
55387
- const DEFAULT_PERF_SLOW_BLOCK_RENDER_THRESHOLD_MS = 25;
55388
- let cachedPerfSlowBlockRenderThresholdMs = null;
55389
- function getPerfSlowBlockRenderThresholdMs() {
55390
- if (cachedPerfSlowBlockRenderThresholdMs !== null) return cachedPerfSlowBlockRenderThresholdMs;
55391
- const raw = globalThis.process?.env?.DEBUG_PERF_BLOCK_MS;
55392
- const parsed = raw ? Number.parseInt(raw, 10) : NaN;
55393
- cachedPerfSlowBlockRenderThresholdMs = Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_PERF_SLOW_BLOCK_RENDER_THRESHOLD_MS;
55394
- return cachedPerfSlowBlockRenderThresholdMs;
55395
- }
55396
- function getCachedBlockComponent(manifest) {
55397
- const cached = blockComponentCache.get(manifest);
55398
- if (cached) return cached;
55399
- const created = makeDefaultBlockComponent({ manifest });
55400
- blockComponentCache.set(manifest, created);
55401
- return created;
55402
- }
55403
- /**
55404
56004
  * Memoized block section component.
55405
56005
  *
55406
56006
  * Custom memoization prevents render loops during editing by explicitly checking
55407
56007
  * referential equality of all props that affect rendering. Standard React.memo
55408
56008
  * wouldn't catch all the render triggers from the editor store.
55409
56009
  */
55410
- const MemoBlockSection = React$1.memo(function MemoBlockSection({ themeResources, block, dataContext, registry, routeMap, wrapBlock, blockOverrides, sdkConfig, customBlocks, imageResolveContext, previewAuthoring }) {
56010
+ const MemoBlockSection = React$1.memo(function MemoBlockSection(props) {
56011
+ return renderBlockSection(props);
56012
+ }, (prev, next) => {
56013
+ 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;
56014
+ });
56015
+ /**
56016
+ * Renders a single block body.
56017
+ *
56018
+ * Returns `null` when the block cannot render and the audience is `public`;
56019
+ * the caller treats that as "this block is not on the page".
56020
+ */
56021
+ function renderBlockSection({ audience, themeResources, block, dataContext, registry, routeMap, wrapBlock, blockOverrides, sdkConfig, customBlocks, imageResolveContext, previewAuthoring }) {
55411
56022
  const perfStart = globalThis.process?.env?.DEBUG_PERF === "1" && typeof window === "undefined" ? Date.now() : 0;
55412
56023
  const fallbackImageResolveContext = getCurrentResolveImageUrlContext();
55413
56024
  const resolvedImageResolveContext = imageResolveContext ?? fallbackImageResolveContext;
@@ -55427,19 +56038,30 @@ const MemoBlockSection = React$1.memo(function MemoBlockSection({ themeResources
55427
56038
  } catch {}
55428
56039
  return node;
55429
56040
  }
55430
- if (block.presentationAdmission.kind === "invalid") {
55431
- try {
55432
- if (globalThis.process?.env?.NODE_ENV !== "production") console.warn("[blocks:PageRenderer] block admission failed", {
55433
- blockId: block.id ?? null,
55434
- blockKind: block.kind,
55435
- error: block.presentationAdmission.error
55436
- });
55437
- } catch {}
55438
- return finish(fallback(`Invalid content for block kind "${block.kind}".`), {
55439
- outcome: "invalidContent",
55440
- override: false
56041
+ /**
56042
+ * The one place that decides what a block that cannot render leaves behind.
56043
+ * A public page gets nothing (and a log line, since dropping content on a
56044
+ * 200 is otherwise invisible); authoring surfaces get the diagnostic.
56045
+ */
56046
+ function diagnostic(message, detail) {
56047
+ if (audience !== "public") return fallback(message);
56048
+ warnOmittedFromPublicPage({
56049
+ block,
56050
+ ...detail
55441
56051
  });
56052
+ return null;
55442
56053
  }
56054
+ function withBlockWrapper(node) {
56055
+ if (node === null) return null;
56056
+ return wrapBlock && block.id ? /* @__PURE__ */ jsx(Fragment, { children: wrapBlock(block.id, node) }) : /* @__PURE__ */ jsx(Fragment, { children: node });
56057
+ }
56058
+ if (block.presentationAdmission.kind === "invalid") return finish(diagnostic(`Invalid content for block kind "${block.kind}".`, {
56059
+ outcome: "invalidContent",
56060
+ admissionError: block.presentationAdmission.error
56061
+ }), {
56062
+ outcome: "invalidContent",
56063
+ override: false
56064
+ });
55443
56065
  const OverrideComponent = getBlockOverride(block.kind, blockOverrides);
55444
56066
  requireBlockOverride(themeResources, OverrideComponent, block.kind);
55445
56067
  let definition = getBlockDefinition(block.kind);
@@ -55452,25 +56074,14 @@ const MemoBlockSection = React$1.memo(function MemoBlockSection({ themeResources
55452
56074
  }).findManifest(block.kind)?.descriptor.definition;
55453
56075
  if (!definition) {
55454
56076
  if (OverrideComponent) {
55455
- const rendered = /* @__PURE__ */ jsx(OverrideComponent, {
56077
+ const rendered = /* @__PURE__ */ jsx(OverrideComponent, { ...buildBlockOverrideProps({
56078
+ block,
55456
56079
  content: getRenderableContent(block, dataContext?.previewStage),
55457
- ...overrideThemeProps(themeResources),
55458
56080
  data: void 0,
55459
- siteId: dataContext?.siteId,
55460
- apiBaseUrl: dataContext?.apiBaseUrl,
55461
- portalToken: dataContext?.portalToken ?? null,
55462
- previewToken: dataContext?.previewToken ?? null,
55463
- stage: dataContext?.previewStage,
55464
- occurrenceContext: dataContext?.occurrenceContext ?? null,
55465
- entrySubrouteContext: dataContext?.entrySubrouteContext ?? null,
55466
- contentEntry: dataContext?.contentEntry ?? null,
55467
- pageSurface: dataContext?.pageSurface ?? null,
55468
- subject: dataContext?.subject ?? null,
55469
- supabaseUrl: dataContext?.supabaseUrl,
55470
- imageResolveContext: resolvedImageResolveContext,
55471
- blockId: block.id ?? null,
55472
- blockKind: block.kind
55473
- });
56081
+ themeResources,
56082
+ dataContext,
56083
+ imageResolveContext: resolvedImageResolveContext
56084
+ }) });
55474
56085
  return finish(wrapBlock && block.id ? /* @__PURE__ */ jsx(Fragment, { children: wrapBlock(block.id, rendered) }) : /* @__PURE__ */ jsx(Fragment, { children: rendered }), {
55475
56086
  outcome: "overrideWithoutDefinition",
55476
56087
  override: true,
@@ -55486,8 +56097,7 @@ const MemoBlockSection = React$1.memo(function MemoBlockSection({ themeResources
55486
56097
  dbCustomBlocksCount: customBlocks?.length ?? 0
55487
56098
  });
55488
56099
  } catch {}
55489
- 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}".`);
55490
- return finish(wrapBlock && block.id ? /* @__PURE__ */ jsx(Fragment, { children: wrapBlock(block.id, rendered) }) : /* @__PURE__ */ jsx(Fragment, { children: rendered }), {
56100
+ 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" })), {
55491
56101
  outcome: "missingDefinition",
55492
56102
  override: false
55493
56103
  });
@@ -55502,8 +56112,7 @@ const MemoBlockSection = React$1.memo(function MemoBlockSection({ themeResources
55502
56112
  manifestKeys: Object.keys(definition.manifest ?? {}).sort()
55503
56113
  });
55504
56114
  } catch {}
55505
- 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}".`);
55506
- return finish(wrapBlock && block.id ? /* @__PURE__ */ jsx(Fragment, { children: wrapBlock(block.id, rendered) }) : /* @__PURE__ */ jsx(Fragment, { children: rendered }), {
56115
+ 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" })), {
55507
56116
  outcome: "missingLayout",
55508
56117
  override: false
55509
56118
  });
@@ -55513,39 +56122,24 @@ const MemoBlockSection = React$1.memo(function MemoBlockSection({ themeResources
55513
56122
  if (block.presentationAdmission.kind === "editor") {
55514
56123
  const draft = validateManifestDraft(compileManifestContentContract(definition.manifest), content);
55515
56124
  if (draft.kind === "previewable") data = draft.data;
55516
- else {
55517
- const rendered = fallback(`Unable to render block kind "${block.kind}".`);
55518
- return finish(wrapBlock && block.id ? /* @__PURE__ */ jsx(Fragment, { children: wrapBlock(block.id, rendered) }) : /* @__PURE__ */ jsx(Fragment, { children: rendered }), {
55519
- outcome: "invalidPlaceholder",
55520
- override: false
55521
- });
55522
- }
56125
+ else return finish(withBlockWrapper(diagnostic(`Unable to render block kind "${block.kind}".`, { outcome: "invalidPlaceholder" })), {
56126
+ outcome: "invalidPlaceholder",
56127
+ override: false
56128
+ });
55523
56129
  }
55524
56130
  try {
55525
56131
  const routes = routeMap ?? dataContext?.routes;
55526
56132
  const hydratedContent = routes ? hydrateManifestLinks(definition.manifest, data, routes, dataContext?.pagesByIdentifier, dataContext?.entriesByIdentifier) : data;
55527
56133
  const resolved = resolveBlockData(block.id, definition.dataLoaders, dataContext, routes);
55528
56134
  const hasResolvedData = !!resolved && typeof resolved === "object" && Object.keys(resolved).length > 0;
55529
- const baseProps = {
56135
+ const baseProps = buildBlockOverrideProps({
56136
+ block,
55530
56137
  content: hydratedContent,
55531
- ...overrideThemeProps(themeResources),
55532
56138
  data: resolved,
55533
- siteId: dataContext?.siteId,
55534
- apiBaseUrl: dataContext?.apiBaseUrl,
55535
- portalToken: dataContext?.portalToken ?? null,
55536
- previewToken: dataContext?.previewToken ?? null,
55537
- stage: dataContext?.previewStage,
55538
- occurrenceContext: dataContext?.occurrenceContext ?? null,
55539
- entrySubrouteContext: dataContext?.entrySubrouteContext ?? null,
55540
- contentEntry: dataContext?.contentEntry ?? null,
55541
- pageSurface: dataContext?.pageSurface ?? null,
55542
- subject: dataContext?.subject ?? null,
55543
- supabaseUrl: dataContext?.supabaseUrl,
55544
- imageResolveContext: resolvedImageResolveContext,
55545
- blockId: block.id ?? null,
55546
- blockKind: block.kind
55547
- };
55548
- baseProps.bookingTimeZone = resolveBookingTimeZone(dataContext);
56139
+ themeResources,
56140
+ dataContext,
56141
+ imageResolveContext: resolvedImageResolveContext
56142
+ });
55549
56143
  let rendered;
55550
56144
  if (OverrideComponent) rendered = /* @__PURE__ */ jsx(OverrideComponent, { ...baseProps });
55551
56145
  else {
@@ -55592,39 +56186,11 @@ const MemoBlockSection = React$1.memo(function MemoBlockSection({ themeResources
55592
56186
  hasResolvedData
55593
56187
  });
55594
56188
  } catch {
55595
- const rendered = block.presentationAdmission.kind === "editor" ? fallback(`Unable to render block kind "${block.kind}".`) : fallback(`Invalid content for block kind "${block.kind}".`);
55596
- return finish(wrapBlock && block.id ? /* @__PURE__ */ jsx(Fragment, { children: wrapBlock(block.id, rendered) }) : /* @__PURE__ */ jsx(Fragment, { children: rendered }), {
56189
+ return finish(withBlockWrapper(diagnostic(block.presentationAdmission.kind === "editor" ? `Unable to render block kind "${block.kind}".` : `Invalid content for block kind "${block.kind}".`, { outcome: "exception" })), {
55597
56190
  outcome: "exception",
55598
56191
  override: false
55599
56192
  });
55600
56193
  }
55601
- }, (prev, next) => {
55602
- 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;
55603
- });
55604
- function fallback(message) {
55605
- return /* @__PURE__ */ jsx("div", {
55606
- className: "rb-p-6 rb-caption",
55607
- style: textColorStyle("mutedText"),
55608
- children: message
55609
- });
55610
- }
55611
- function shouldDebugCustomBlocks() {
55612
- try {
55613
- return globalThis.process?.env?.DEBUG_CUSTOM_BLOCKS === "1";
55614
- } catch {
55615
- return false;
55616
- }
55617
- }
55618
- /**
55619
- * Resolves block content based on stage priority.
55620
- * For draft/preview: draftContent takes priority over content.
55621
- * For published: content is the source of truth.
55622
- * Returns empty object if no content exists (validator will handle placeholders if needed).
55623
- */
55624
- function getRenderableContent(block, previewStage = "published") {
55625
- const source = previewStage === "preview" && block.draftContent ? block.draftContent : block.content ?? {};
55626
- if (!isObjectRecord$1(source)) return {};
55627
- return source;
55628
56194
  }
55629
56195
  //#endregion
55630
56196
  //#region ../theme-core/src/footer/mode.ts
@@ -64587,6 +65153,21 @@ const containerResponsiveThemeCss = `/*
64587
65153
  outline-offset: 2px;
64588
65154
  }
64589
65155
 
65156
+ /* Event titles always link to the event page, so a card or row whose CTA is
65157
+ hidden (sold out, closed) is never a dead end (issue 1372). The link inherits
65158
+ the heading's colour and only reveals itself on hover or keyboard focus.
65159
+ Shared by EventCard, EventCompactRow and the offerings-preview course cards via \`.event-title-link\`. */
65160
+ :where(.theme-scope) .event-title-link {
65161
+ color: inherit;
65162
+ text-decoration: none;
65163
+ }
65164
+
65165
+ :where(.theme-scope) .event-title-link:hover,
65166
+ :where(.theme-scope) .event-title-link:focus-visible {
65167
+ text-decoration: underline;
65168
+ text-underline-offset: 0.12em;
65169
+ }
65170
+
64590
65171
  /* =============================================================================
64591
65172
  * Block: Carousel Navigation (Phase 3 client component migration)
64592
65173
  * =============================================================================
@@ -71675,6 +72256,83 @@ const SDK_ENDPOINT_DEFINITIONS = defineEndpointDefinitions({
71675
72256
  errors: ["validation:invalid_input", "server:internal_error"]
71676
72257
  }
71677
72258
  });
72259
+ //#endregion
72260
+ //#region ../api/src/sdkMediaEndpointDefinitions.ts
72261
+ const SDK_MEDIA_ENDPOINT_DEFINITIONS = {
72262
+ sdkMediaExists: {
72263
+ path: "/sdk/{siteId}/media/exists",
72264
+ method: "POST",
72265
+ errors: ["validation:invalid_input"],
72266
+ auth: "service",
72267
+ responseKind: "json"
72268
+ },
72269
+ sdkMediaFindByChecksum: {
72270
+ path: "/sdk/{siteId}/media/find-by-checksum",
72271
+ method: "POST",
72272
+ errors: ["server:internal_error"],
72273
+ auth: "service",
72274
+ responseKind: "json"
72275
+ },
72276
+ sdkMediaResolve: {
72277
+ path: "/sdk/{siteId}/media/resolve",
72278
+ method: "POST",
72279
+ errors: ["server:internal_error"],
72280
+ auth: "service",
72281
+ responseKind: "json"
72282
+ },
72283
+ sdkMediaList: {
72284
+ path: "/sdk/{siteId}/media",
72285
+ method: "GET",
72286
+ auth: "service",
72287
+ responseKind: "json"
72288
+ },
72289
+ sdkMediaDownload: {
72290
+ path: "/sdk/{siteId}/media/{identifier}/download",
72291
+ method: "GET",
72292
+ auth: "service",
72293
+ responseKind: "json"
72294
+ },
72295
+ sdkMediaDeletePreview: {
72296
+ path: "/sdk/{siteId}/media/{identifier}/delete-preview",
72297
+ method: "POST",
72298
+ auth: "service",
72299
+ responseKind: "json"
72300
+ },
72301
+ sdkMediaDeleteApply: {
72302
+ path: "/sdk/{siteId}/media/{identifier}",
72303
+ method: "DELETE",
72304
+ auth: "service",
72305
+ responseKind: "json"
72306
+ },
72307
+ sdkMediaUploadUrl: {
72308
+ path: "/sdk/{siteId}/media/upload-url",
72309
+ method: "POST",
72310
+ errors: [
72311
+ "resource:not_found",
72312
+ "validation:invalid_input",
72313
+ "resource:conflict",
72314
+ "validation:payload_too_large",
72315
+ "billing:plan_limit_exceeded",
72316
+ "server:internal_error"
72317
+ ],
72318
+ auth: "service",
72319
+ responseKind: "json"
72320
+ },
72321
+ sdkMediaUploadFinalize: {
72322
+ path: "/sdk/{siteId}/media/upload",
72323
+ method: "POST",
72324
+ errors: [
72325
+ "resource:not_found",
72326
+ "validation:invalid_input",
72327
+ "resource:conflict",
72328
+ "validation:payload_too_large",
72329
+ "billing:plan_limit_exceeded",
72330
+ "server:internal_error"
72331
+ ],
72332
+ auth: "service",
72333
+ responseKind: "json"
72334
+ }
72335
+ };
71678
72336
  ({
71679
72337
  ...ACCOUNT_ENDPOINT_DEFINITIONS,
71680
72338
  ...PUBLIC_EVENT_ENDPOINT_DEFINITIONS,
@@ -71683,7 +72341,7 @@ const SDK_ENDPOINT_DEFINITIONS = defineEndpointDefinitions({
71683
72341
  ...NEWSLETTER_ENDPOINT_DEFINITIONS,
71684
72342
  ...APPOINTMENT_ENDPOINT_DEFINITIONS,
71685
72343
  ...EVENT_SERIES_REGISTRATION_ENDPOINT_DEFINITIONS
71686
- }, 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 };
72344
+ }, 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 };
71687
72345
  z.object({ refreshToken: z.string().min(1).max(4096) }).strict();
71688
72346
  z.object({
71689
72347
  accessToken: z.string().min(1).max(4096),
@@ -72207,6 +72865,50 @@ z.object({
72207
72865
  title: z.string(),
72208
72866
  slug: z.string().min(1)
72209
72867
  });
72868
+ z.object({
72869
+ signedUrl: z.string().url(),
72870
+ token: z.string().min(1),
72871
+ storagePath: z.string().min(1)
72872
+ }).strict();
72873
+ const backupManifestSchema = z.object({
72874
+ siteId: z.string().min(1),
72875
+ siteSlug: z.string().min(1),
72876
+ siteTitle: z.string(),
72877
+ exportedAt: z.string().min(1),
72878
+ tableCount: z.number().int().nonnegative(),
72879
+ totalRecords: z.number().int().nonnegative(),
72880
+ pageCount: z.number().int().nonnegative(),
72881
+ blockCount: z.number().int().nonnegative(),
72882
+ contentEntryCount: z.number().int().nonnegative(),
72883
+ formCount: z.number().int().nonnegative(),
72884
+ navigationMenuCount: z.number().int().nonnegative(),
72885
+ mediaCount: z.number().int().nonnegative(),
72886
+ mediaBytes: z.number().int().nonnegative()
72887
+ }).strict();
72888
+ z.object({
72889
+ ok: z.literal(true),
72890
+ manifest: backupManifestSchema,
72891
+ storagePath: z.string().min(1)
72892
+ }).strict();
72893
+ z.object({
72894
+ ok: z.literal(true),
72895
+ siteId: brandedIdSchema(parseSiteId, "site id"),
72896
+ slug: z.string().min(1),
72897
+ title: z.string(),
72898
+ counts: z.record(z.string(), z.object({
72899
+ created: z.number().int().nonnegative(),
72900
+ updated: z.number().int().nonnegative(),
72901
+ skipped: z.number().int().nonnegative(),
72902
+ failed: z.number().int().nonnegative()
72903
+ }).strict()),
72904
+ warnings: z.array(z.string()),
72905
+ errors: z.array(z.object({
72906
+ table: z.string(),
72907
+ id: z.string(),
72908
+ error: z.string()
72909
+ }).strict())
72910
+ }).strict();
72911
+ z.object({ deleted: z.boolean() }).strict();
72210
72912
  const apiKeyIdSchema = brandedIdSchema(parseApiKeyId, "API key id");
72211
72913
  const standardKeyIdsSchema = z.object({
72212
72914
  live: apiKeyIdSchema.optional(),