@riverbankcms/sdk 0.109.0 → 0.111.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 (186) hide show
  1. package/dist/{PreviewEditorSidebar-DWqwfy3_.mjs → PreviewEditorSidebar-CTR6LUhN.mjs} +178 -117
  2. package/dist/{PreviewEditorUI-B7OL4sfR.mjs → PreviewEditorUI-DZg15Zip.mjs} +3 -4
  3. package/dist/{PreviewQueryProvider-D0y2l8JN.mjs → PreviewQueryProvider-kkmjklLG.mjs} +1 -1
  4. package/dist/{SdkPreviewModeRuntime-D8Gt9Gbe.mjs → SdkPreviewModeRuntime-DeOTAFvR.mjs} +229 -219
  5. package/dist/{SiteChromeCustomizeContext-DUpalEdL.mjs → SiteChromeCustomizeContext-DOSDLCiJ.mjs} +1695 -2169
  6. package/dist/_dts/ai/src/contracts/proposals.d.ts +1716 -76
  7. package/dist/_dts/api/src/commercePortalEndpoints.d.ts +5 -5
  8. package/dist/_dts/api/src/contentRuntime.d.ts +4 -1
  9. package/dist/_dts/api/src/courses.d.ts +30 -15
  10. package/dist/_dts/api/src/endpoints.d.ts +14 -4
  11. package/dist/_dts/api/src/index.d.ts +2 -2
  12. package/dist/_dts/api/src/sdkContracts.d.ts +23 -11
  13. package/dist/_dts/api/src/siteManagementEndpoints.d.ts +5 -1
  14. package/dist/_dts/api/src/siteOperations.d.ts +11 -19
  15. package/dist/_dts/api/src/siteRuntimeEndpoints.d.ts +6 -6
  16. package/dist/_dts/api/src/webhooks.d.ts +98 -1
  17. package/dist/_dts/billing/src/site-arrangements.d.ts +3 -0
  18. package/dist/_dts/blocks/src/client/course-registration.d.ts +4 -1
  19. package/dist/_dts/blocks/src/client.d.ts +4 -1
  20. package/dist/_dts/blocks/src/contracts/proof-block-islands.d.ts +4 -4
  21. package/dist/_dts/blocks/src/index.d.ts +3 -1
  22. package/dist/_dts/blocks/src/system/blocks/course-registration.d.ts +2 -1
  23. package/dist/_dts/blocks/src/system/blocks/courses/shared/types.d.ts +2 -1
  24. package/dist/_dts/blocks/src/system/blocks/cta-full.d.ts +2 -4
  25. package/dist/_dts/blocks/src/system/blocks/event-registration.d.ts +2 -1
  26. package/dist/_dts/blocks/src/system/blocks/events/event-listing.d.ts +2 -1
  27. package/dist/_dts/blocks/src/system/blocks/events/event-spotlight.d.ts +2 -1
  28. package/dist/_dts/blocks/src/system/blocks/events/recurring-schedule.d.ts +3 -2
  29. package/dist/_dts/blocks/src/system/blocks/events/shared/types.d.ts +2 -1
  30. package/dist/_dts/blocks/src/system/blocks/file-download.d.ts +2 -1
  31. package/dist/_dts/blocks/src/system/blocks/newsletter-signup.d.ts +2 -1
  32. package/dist/_dts/blocks/src/system/blocks/shop.d.ts +2 -1
  33. package/dist/_dts/blocks/src/system/blocks/single-button.d.ts +2 -1
  34. package/dist/_dts/blocks/src/system/constants/buttonRoles.d.ts +35 -0
  35. package/dist/_dts/blocks/src/system/fields/button.d.ts +10 -21
  36. package/dist/_dts/blocks/src/system/fields/ctas.d.ts +2 -1
  37. package/dist/_dts/blocks/src/system/fragments/library/heroCtaRow.d.ts +9 -5
  38. package/dist/_dts/blocks/src/system/runtime/components/registry.client.d.ts +18 -18
  39. package/dist/_dts/blocks/src/system/runtime/components/runtime-backed-blocks.d.ts +9 -9
  40. package/dist/_dts/blocks/src/system/runtime/hooks/useHeadlessWholeSeriesRegistrationFlow.d.ts +29 -0
  41. package/dist/_dts/blocks/src/system/runtime/nodes/buttonAffordance.d.ts +1 -1
  42. package/dist/_dts/blocks/src/system/runtime/nodes/course-registration/ErrorStep.d.ts +2 -1
  43. package/dist/_dts/blocks/src/system/runtime/nodes/course-registration/PaymentFailedStep.d.ts +2 -1
  44. package/dist/_dts/blocks/src/system/runtime/nodes/course-registration/VerifyingTimeoutStep.d.ts +2 -1
  45. package/dist/_dts/blocks/src/system/runtime/nodes/course-registration/types.d.ts +21 -0
  46. package/dist/_dts/blocks/src/system/runtime/nodes/course-registration/useCourseRegistrationWizard.d.ts +4 -4
  47. package/dist/_dts/blocks/src/system/runtime/nodes/course-registration/whole-series-registration.headless.contract.d.ts +326 -0
  48. package/dist/_dts/blocks/src/system/runtime/nodes/course-registration/whole-series-registration.headless.planning.d.ts +27 -0
  49. package/dist/_dts/blocks/src/system/runtime/nodes/course-registration.interactive.d.ts +5 -4
  50. package/dist/_dts/blocks/src/system/runtime/nodes/event-registration/event-registration.headless.contract.d.ts +3 -2
  51. package/dist/_dts/blocks/src/system/runtime/nodes/event-registration/types.d.ts +2 -1
  52. package/dist/_dts/blocks/src/system/runtime/nodes/event-registration.interactive.d.ts +5 -4
  53. package/dist/_dts/blocks/src/system/runtime/nodes/file-download.d.ts +2 -3
  54. package/dist/_dts/blocks/src/system/runtime/nodes/newsletter-form.types.d.ts +2 -1
  55. package/dist/_dts/blocks/src/system/runtime/nodes/shared/contract-schemas.d.ts +6 -3
  56. package/dist/_dts/blocks/src/system/runtime/nodes/shop.interactive.d.ts +7 -6
  57. package/dist/_dts/blocks/src/system/runtime/nodes/shop.types.d.ts +2 -1
  58. package/dist/_dts/blocks/src/system/runtime/shared/types.d.ts +2 -1
  59. package/dist/_dts/content-editor/src/FooterComposerPanel.d.ts +4 -2
  60. package/dist/_dts/content-editor/src/index.d.ts +12 -12
  61. package/dist/_dts/content-editor/src/ui/editorUiStore.d.ts +17 -2
  62. package/dist/_dts/db/src/generated/supabase/database.types.d.ts +221 -53
  63. package/dist/_dts/editor-blocks/src/widgets/button/context.d.ts +3 -2
  64. package/dist/_dts/preview-next/src/client/index.d.ts +3 -2
  65. package/dist/_dts/preview-next/src/client/preview/ButtonStylePicker.d.ts +16 -0
  66. package/dist/_dts/preview-next/src/client/preview/PageBlockEditorPanel.d.ts +2 -3
  67. package/dist/_dts/preview-next/src/client/preview/PreviewDesignOpenRequestContext.d.ts +7 -0
  68. package/dist/_dts/preview-next/src/client/preview/PreviewEditorSidebar.d.ts +5 -11
  69. package/dist/_dts/preview-next/src/client/preview/PreviewShell.d.ts +5 -6
  70. package/dist/_dts/preview-next/src/client/preview/PreviewShellLayout.d.ts +6 -7
  71. package/dist/_dts/preview-next/src/client/preview/SiteChromeCustomizeContext.d.ts +7 -9
  72. package/dist/_dts/preview-next/src/client/preview/StyleConfigurator.d.ts +4 -11
  73. package/dist/_dts/preview-next/src/client/preview/StyleConfigurator.state.d.ts +7 -7
  74. package/dist/_dts/preview-next/src/client/preview/sidebarTabPreference.d.ts +0 -8
  75. package/dist/_dts/preview-next/src/client/preview/siteChromeLookRows.d.ts +1 -11
  76. package/dist/_dts/preview-next/src/client/preview/siteChromeQuickControlHistoryState.d.ts +1 -5
  77. package/dist/_dts/preview-next/src/client/preview/siteChromeSavePlan.d.ts +2 -3
  78. package/dist/_dts/preview-next/src/client/preview/siteStylePreviewPlanner.d.ts +3 -4
  79. package/dist/_dts/preview-next/src/client/preview/styleConfiguratorSnapshot.d.ts +3 -5
  80. package/dist/_dts/sdk/src/bookings/index.d.ts +5 -4
  81. package/dist/_dts/sdk/src/bookings/types.d.ts +3 -3
  82. package/dist/_dts/sdk/src/cli/commands/style.d.ts +3 -2
  83. package/dist/_dts/sdk/src/client/management/types.d.ts +2 -1
  84. package/dist/_dts/sdk/src/contracts/system-block-content.d.ts +12 -15
  85. package/dist/_dts/sdk/src/public-api/contracts.d.ts +1 -1
  86. package/dist/_dts/sdk/src/version.d.ts +1 -1
  87. package/dist/_dts/sdk/src/webhooks/index.d.ts +1 -1
  88. package/dist/_dts/sdk/src/webhooks/verify.d.ts +3 -3
  89. package/dist/_dts/site-commands/src/commands.d.ts +129 -6
  90. package/dist/_dts/theme-core/src/buttons/index.d.ts +1 -1
  91. package/dist/_dts/theme-core/src/buttons/styles/domain.d.ts +80 -0
  92. package/dist/_dts/theme-core/src/buttons/styles/index.d.ts +3 -0
  93. package/dist/_dts/theme-core/src/buttons/styles/recipes/defineStaticButtonStyleRecipe.d.ts +49 -0
  94. package/dist/_dts/theme-core/src/buttons/styles/recipes/quietArrow.d.ts +67 -0
  95. package/dist/_dts/theme-core/src/buttons/styles/recipes/staticRecipes.d.ts +649 -0
  96. package/dist/_dts/theme-core/src/buttons/styles/registry.d.ts +753 -0
  97. package/dist/_dts/theme-core/src/buttons/styles/selection.d.ts +69 -0
  98. package/dist/_dts/theme-core/src/site-styles/curatedSiteStyles.d.ts +26 -24
  99. package/dist/_dts/theme-core/src/site-styles/footerLooks.d.ts +19 -153
  100. package/dist/_dts/theme-core/src/site-styles/headerCtaVariants.d.ts +3 -1
  101. package/dist/_dts/theme-core/src/site-styles/previewCompiler.d.ts +8 -9
  102. package/dist/_dts/theme-core/src/site-styles/runtimeCompiler.d.ts +4 -4
  103. package/dist/_dts/ui/src/palette/hex.d.ts +3 -3
  104. package/dist/cli/index.mjs +1760 -1914
  105. package/dist/cli/init-docs/content/workflow-isr-revalidation.md +1 -1
  106. package/dist/client/{DiscountCodeDisclosure-DU1ygxU2.mjs → DiscountCodeDisclosure-C4xvTBux.mjs} +1 -1
  107. package/dist/client/{EventCombined.client-DB5zagCo.mjs → EventCombined.client-Cr7G131j.mjs} +2 -3
  108. package/dist/client/{EventListing.client-CTX9JHBL.mjs → EventListing.client-CAjbseNy.mjs} +2 -2
  109. package/dist/client/{LeafletStaticMap-BYImhgiB.mjs → LeafletStaticMap-BMlWfwz3.mjs} +1 -1
  110. package/dist/client/{MagicLinkForm-CRkw_M3l.mjs → MagicLinkForm-ChdFz96y.mjs} +1 -2
  111. package/dist/client/{StepIndicator-BnmVBJJh.mjs → StepIndicator-7D3GaBeM.mjs} +2 -2
  112. package/dist/client/{accordion-CeGreumd.mjs → accordion-D8JTs2Si.mjs} +1 -1
  113. package/dist/client/{bookingForm-CSzFNrpN.mjs → bookingForm-H3DCRBHs.mjs} +6 -7
  114. package/dist/client/bookings.mjs +3069 -1447
  115. package/dist/client/{carousel-B2jDtogf.mjs → carousel-DMr_QWlz.mjs} +1 -1
  116. package/dist/client/{cart-D-jZ-vaY.mjs → cart-DWMbfBdR.mjs} +3 -3
  117. package/dist/client/{checkout-Dza-LA7N.mjs → checkout-zmfdkXLx.mjs} +3 -3
  118. package/dist/client/client.mjs +1732 -1913
  119. package/dist/client/{colorStyles-CrgzoXsm.mjs → colorStyles-gcTh5yZT.mjs} +2 -12
  120. package/dist/client/{colorTokenParts-CMugwvX_.mjs → colorTokenParts-FSS3al6l.mjs} +31 -1
  121. package/dist/client/{courseRegistration-TYkNychb.mjs → courseRegistration-Z_mn18jy.mjs} +100 -37
  122. package/dist/client/{eventCalendar-BI_90SsT.mjs → eventCalendar-CYp0YJGF.mjs} +4 -5
  123. package/dist/client/{eventCombined-mNrpryE9.mjs → eventCombined-rKyYZUa9.mjs} +2 -2
  124. package/dist/client/{eventDetails-D4GVlCIY.mjs → eventDetails-X0ZaUuMj.mjs} +3 -3
  125. package/dist/client/{eventListing-YhKN018y.mjs → eventListing-W1SuL7EP.mjs} +2 -2
  126. package/dist/client/{eventRegistration-CdHGtilr.mjs → eventRegistration-ClWricui.mjs} +6 -7
  127. package/dist/client/{form-AcAL45rA.mjs → form--eKPwAAD.mjs} +1 -2
  128. package/dist/client/{headerSection-uEC52ueD.mjs → headerSection-BSeGr-FS.mjs} +2 -3
  129. package/dist/client/hooks.mjs +1554 -1871
  130. package/dist/client/{locationMap-_wJDSUYe.mjs → locationMap-BpOrtNYn.mjs} +8 -5
  131. package/dist/client/{media-BK0O3b-U.mjs → media-DoF0dcVx.mjs} +2 -2
  132. package/dist/client/{newsletterForm-DrArsb2B.mjs → newsletterForm-Bt7it7Dy.mjs} +1 -2
  133. package/dist/client/{pricing-BSYVghwo.mjs → pricing-BTjSDDr4.mjs} +1 -1
  134. package/dist/client/{productDetail-Dyk90sMs.mjs → productDetail-ICnVTcCg.mjs} +3 -3
  135. package/dist/client/{productList-BCKThwAI.mjs → productList-ZHr6D604.mjs} +3 -3
  136. package/dist/client/{propsDecoder-CkXxkdSR.mjs → propsDecoder-wWN0tBDF.mjs} +2351 -129
  137. package/dist/client/rendering/client.mjs +1664 -1824
  138. package/dist/client/rendering/islands.mjs +20 -20
  139. package/dist/client/rendering.mjs +1697 -1909
  140. package/dist/client/{shop-DeltVH10.mjs → shop-C1u_BUn0.mjs} +5 -6
  141. package/dist/client/{shop-commerce-BD9Jsm2Z.mjs → shop-commerce-CleZdRha.mjs} +5 -6
  142. package/dist/client/{shop-commerce-store-CmXD6FDS.mjs → shop-commerce-store-ycnoB2Ce.mjs} +1 -1
  143. package/dist/client/{shopShared-B8v-lH_F.mjs → shopShared-CPhByt2a.mjs} +3 -3
  144. package/dist/client/{useDiscountCodeUi-DbWJN8ld.mjs → useDiscountCodeUi-cStSQI6C.mjs} +1 -1
  145. package/dist/client/{useEventFilters-EJIMUnqT.mjs → useEventFilters-DKPN7hSY.mjs} +26 -27
  146. package/dist/client/{usePolling-Cmh14fc7.mjs → usePolling-BKAd6pkZ.mjs} +2 -2
  147. package/dist/preview-next/before-render.mjs +37 -6
  148. package/dist/preview-next/client/runtime.mjs +4 -4
  149. package/dist/preview-next/middleware.mjs +37 -6
  150. package/dist/{previewAuthStore-CTO7CRPk.mjs → previewAuthStore-BkT1D06A.mjs} +37 -6
  151. package/dist/{sdk-runtime-Dou0GAav.mjs → sdk-runtime-hu3wr3ng.mjs} +2 -2
  152. package/dist/{separator-CI5wZNXT.mjs → separator-Bln8izQ9.mjs} +1 -1
  153. package/dist/server/components.mjs +1472 -1788
  154. package/dist/server/config-validation.mjs +1559 -1876
  155. package/dist/server/config.mjs +1559 -1876
  156. package/dist/server/data.mjs +1554 -1871
  157. package/dist/server/index.mjs +38 -7
  158. package/dist/server/next/revalidate.mjs +53 -1
  159. package/dist/server/next.mjs +1584 -1869
  160. package/dist/server/page-converter.mjs +1543 -1860
  161. package/dist/server/prebuild.mjs +1 -1
  162. package/dist/server/rendering/server.mjs +1472 -1788
  163. package/dist/server/rendering.mjs +1472 -1788
  164. package/dist/server/routing.mjs +1600 -1891
  165. package/dist/server/server.mjs +1574 -1860
  166. package/dist/server/theme-bridge.mjs +1436 -1619
  167. package/dist/server/theme.mjs +1345 -1241
  168. package/dist/server/webhooks.mjs +54 -2
  169. package/package.json +1 -1
  170. package/dist/_dts/theme-core/src/buttons/personalities/brushed-wash.d.ts +0 -16
  171. package/dist/_dts/theme-core/src/buttons/personalities/confident-chip.d.ts +0 -16
  172. package/dist/_dts/theme-core/src/buttons/personalities/earth-pill.d.ts +0 -16
  173. package/dist/_dts/theme-core/src/buttons/personalities/editorial-link.d.ts +0 -16
  174. package/dist/_dts/theme-core/src/buttons/personalities/flat-ink.d.ts +0 -20
  175. package/dist/_dts/theme-core/src/buttons/personalities/index.d.ts +0 -172
  176. package/dist/_dts/theme-core/src/buttons/personalities/ink-stamp.d.ts +0 -16
  177. package/dist/_dts/theme-core/src/buttons/personalities/joy-stamp.d.ts +0 -22
  178. package/dist/_dts/theme-core/src/buttons/personalities/ornate-ritual.d.ts +0 -16
  179. package/dist/_dts/theme-core/src/buttons/personalities/pebble.d.ts +0 -16
  180. package/dist/_dts/theme-core/src/buttons/personalities/quiet-arrow.d.ts +0 -17
  181. package/dist/_dts/theme-core/src/buttons/personalities/showtime-pill.d.ts +0 -17
  182. package/dist/_dts/theme-core/src/buttons/personalities/soft-pill.d.ts +0 -16
  183. package/dist/_dts/theme-core/src/buttons/personalities/two-tone-pill.d.ts +0 -17
  184. package/dist/_dts/theme-core/src/buttons/personalities/types.d.ts +0 -285
  185. package/dist/client/buttons-DoFV7KDL.mjs +0 -2115
  186. package/dist/client/foregroundContract-xICPq59x.mjs +0 -31
@@ -3742,6 +3742,11 @@ function subtractDaysFromIso(isoString, dayCount) {
3742
3742
  return addDaysToIso(isoString, -dayCount);
3743
3743
  }
3744
3744
  //#endregion
3745
+ //#region ../core/src/assert-never.ts
3746
+ function assertNever$3(value, message = "Unexpected variant") {
3747
+ throw new Error(`${message}: ${String(value)}`);
3748
+ }
3749
+ //#endregion
3745
3750
  //#region ../core/src/currency-format.ts
3746
3751
  const CURRENCY_LOCALE_BY_CODE = {
3747
3752
  USD: "en-US",
@@ -4134,6 +4139,25 @@ function addLocalScheduleDays(date, days) {
4134
4139
  day: utcDate.getUTCDate()
4135
4140
  };
4136
4141
  }
4142
+ //#endregion
4143
+ //#region ../core/src/planner.ts
4144
+ /** Default clock for production callers that do not inject time explicitly. */
4145
+ const systemPlanningClock = { now: () => Date.now() };
4146
+ /** Resolve current time as a Date from an explicit Date, injected clock, or system clock. */
4147
+ function resolvePlanningNowDate(input = {}) {
4148
+ if (input.now !== void 0) return input.now;
4149
+ return new Date((input.clock ?? systemPlanningClock).now());
4150
+ }
4151
+ /**
4152
+ * Resolve a Date-compatible planner input.
4153
+ *
4154
+ * Precedence: bare Date, `{ now }`, `{ clock }`, then `systemPlanningClock`.
4155
+ * Explicit Date inputs are returned by reference, not cloned.
4156
+ */
4157
+ function resolvePlanningDateInput(input = {}) {
4158
+ if (input instanceof Date) return input;
4159
+ return resolvePlanningNowDate(input);
4160
+ }
4137
4161
  function clearWorkflowFeedback(state) {
4138
4162
  return {
4139
4163
  ...state,
@@ -7725,7 +7749,7 @@ const gradientDirectionSchema = z$1.enum([
7725
7749
  "to-br",
7726
7750
  "to-bl"
7727
7751
  ]);
7728
- const buttonBackgroundSchema = z$1.discriminatedUnion("type", [
7752
+ const buttonColourTreatmentSchema = z$1.discriminatedUnion("type", [
7729
7753
  z$1.object({
7730
7754
  type: z$1.literal("solid"),
7731
7755
  colorToken: opaqueButtonColourTokenSchema
@@ -7749,6 +7773,7 @@ const buttonBackgroundSchema = z$1.discriminatedUnion("type", [
7749
7773
  role: z$1.literal("link")
7750
7774
  })
7751
7775
  ]);
7776
+ const buttonBackgroundSchema = buttonColourTreatmentSchema;
7752
7777
  const effectApplicationSchema = z$1.object({
7753
7778
  effectId: z$1.string(),
7754
7779
  options: z$1.record(z$1.string(), z$1.json()).optional()
@@ -7977,298 +8002,429 @@ function themeButtonClassName(spec) {
7977
8002
  }
7978
8003
  buttonSizeNameSchema.options;
7979
8004
  //#endregion
7980
- //#region ../theme-core/src/site-styles/styleTags.ts
7981
- /**
7982
- * Descriptive style-axis metadata for button personalities and palette
7983
- * variants. Site Style picker shape is curated explicitly; these tags remain
7984
- * for catalog audit, docs, and future discovery tooling.
7985
- */
7986
- const STYLE_AXIS_VALUES = {
7987
- tone: [
7988
- "minimal",
7989
- "classic",
7990
- "bold"
7991
- ],
7992
- energy: ["calm", "vibrant"],
7993
- motion: [
7994
- "subtle",
7995
- "standard",
7996
- "expressive"
7997
- ],
7998
- era: [
7999
- "editorial",
8000
- "modern",
8001
- "organic",
8002
- "technical",
8003
- "retro"
8004
- ]
8005
- };
8006
- const styleTagsSchema = z$1.object({
8007
- tone: z$1.enum(STYLE_AXIS_VALUES.tone).optional(),
8008
- energy: z$1.enum(STYLE_AXIS_VALUES.energy).optional(),
8009
- motion: z$1.enum(STYLE_AXIS_VALUES.motion).optional(),
8010
- era: z$1.enum(STYLE_AXIS_VALUES.era).optional()
8011
- });
8012
- //#endregion
8013
- //#region ../theme-core/src/buttons/personalities/types.ts
8014
- function asButtonPersonalityId(value) {
8015
- if (value.length === 0) throw new Error("ButtonPersonalityId must be a non-empty string");
8016
- return value;
8005
+ //#region ../theme-core/src/buttons/styles/domain.ts
8006
+ const PUBLIC_BUTTON_ROLES = [
8007
+ "primary",
8008
+ "secondary",
8009
+ "outline",
8010
+ "ghost",
8011
+ "accent"
8012
+ ];
8013
+ function defineButtonStyleRecipe(recipe) {
8014
+ return recipe;
8017
8015
  }
8018
- const buttonPersonalityIdSchema = z$1.string().min(1).transform((value) => asButtonPersonalityId(value));
8019
- z$1.object({
8020
- id: buttonPersonalityIdSchema,
8021
- name: z$1.string().min(1),
8022
- description: z$1.string().min(1),
8023
- tags: styleTagsSchema,
8024
- buttonSystem: buttonSystemSchema
8025
- });
8026
- /**
8027
- * Helper for personality file modules to keep the call site short and
8028
- * type-safe. `as const satisfies ...` would also work but produces wider
8029
- * types at use sites.
8030
- */
8031
- function defineButtonPersonality(input) {
8016
+ function materializeButtonRoles(definitions) {
8017
+ const primary = materializeCompleteRole("primary", definitions.primary.appearance);
8018
+ const secondary = materializeCompleteRole("secondary", definitions.secondary.appearance);
8019
+ const completeRoles = {
8020
+ primary,
8021
+ secondary
8022
+ };
8032
8023
  return {
8033
- id: asButtonPersonalityId(input.id),
8034
- name: input.name,
8035
- description: input.description,
8036
- tags: input.tags,
8037
- buttonSystem: buttonSystemSchema.parse(input.buttonSystem)
8024
+ primary,
8025
+ secondary,
8026
+ outline: materializeSupportingRole("outline", definitions.outline, completeRoles),
8027
+ ghost: materializeSupportingRole("ghost", definitions.ghost, completeRoles),
8028
+ accent: materializeSupportingRole("accent", definitions.accent, completeRoles)
8029
+ };
8030
+ }
8031
+ function resolveButtonStyleRecipe(recipe, parameters) {
8032
+ const parsedParameters = recipe.parameterSchema.safeParse(parameters);
8033
+ if (!parsedParameters.success) return {
8034
+ kind: "error",
8035
+ error: {
8036
+ kind: "invalid-parameters",
8037
+ styleId: recipe.id,
8038
+ issues: parsedParameters.error.issues.map((issue) => issue.message)
8039
+ }
8040
+ };
8041
+ const definition = recipe.defineSystem(parsedParameters.data);
8042
+ const roles = materializeButtonRoles(definition.roles);
8043
+ const buttonSystem = {
8044
+ global: definition.global,
8045
+ variants: PUBLIC_BUTTON_ROLES.map((role) => roles[role]),
8046
+ ...definition.sizes ? { sizes: definition.sizes } : {}
8047
+ };
8048
+ const parsedButtonSystem = buttonSystemSchema.safeParse(buttonSystem);
8049
+ if (!parsedButtonSystem.success) return {
8050
+ kind: "error",
8051
+ error: {
8052
+ kind: "invalid-button-system",
8053
+ styleId: recipe.id,
8054
+ issues: parsedButtonSystem.error.issues.map((issue) => issue.message)
8055
+ }
8056
+ };
8057
+ return {
8058
+ kind: "resolved",
8059
+ buttonSystem: parsedButtonSystem.data
8060
+ };
8061
+ }
8062
+ function materializeSupportingRole(role, definition, completeRoles) {
8063
+ if (definition.kind === "complete") return materializeCompleteRole(role, definition.appearance);
8064
+ return {
8065
+ ...completeRoles[definition.from],
8066
+ ...definition.overrides,
8067
+ id: role,
8068
+ name: roleName(role),
8069
+ enabled: true,
8070
+ priority: 3
8071
+ };
8072
+ }
8073
+ function materializeCompleteRole(role, appearance) {
8074
+ return {
8075
+ ...appearance,
8076
+ id: role,
8077
+ name: roleName(role),
8078
+ enabled: true,
8079
+ priority: rolePriority(role)
8038
8080
  };
8039
8081
  }
8082
+ function roleName(role) {
8083
+ return role.charAt(0).toUpperCase() + role.slice(1);
8084
+ }
8085
+ function rolePriority(role) {
8086
+ if (role === "primary") return 1;
8087
+ if (role === "secondary") return 2;
8088
+ return 3;
8089
+ }
8040
8090
  //#endregion
8041
- //#region ../theme-core/src/buttons/personalities/brushed-wash.ts
8042
- /**
8043
- * Brushed Wash — gradient backgrounds that read as watercolour washes.
8044
- * Generous padding, soft shadows; pairs with vibrant organic brands.
8045
- */
8046
- const brushedWash = defineButtonPersonality({
8047
- id: "brushed-wash",
8048
- name: "Brushed Wash",
8049
- description: "Watercolour-gradient buttons with soft shadows and generous padding.",
8091
+ //#region ../theme-core/src/buttons/styles/selection.ts
8092
+ const BUTTON_STYLE_IDS = [
8093
+ "soft-pill",
8094
+ "earth-pill",
8095
+ "confident-chip",
8096
+ "editorial-link",
8097
+ "pebble",
8098
+ "ink-stamp",
8099
+ "flat-ink",
8100
+ "brushed-wash",
8101
+ "showtime-pill",
8102
+ "joy-stamp",
8103
+ "ornate-ritual",
8104
+ "quiet-arrow"
8105
+ ];
8106
+ const quietArrowParametersSchema = z$1.object({ secondaryTreatment: z$1.enum(["outlined", "solid-accent"]) }).strict();
8107
+ const emptyParametersSchema$1 = z$1.object({}).strict();
8108
+ function selectionBranch(styleId, parameters) {
8109
+ return z$1.object({
8110
+ styleId: z$1.literal(styleId),
8111
+ parameters
8112
+ }).strict();
8113
+ }
8114
+ z$1.discriminatedUnion("styleId", [
8115
+ selectionBranch("soft-pill", emptyParametersSchema$1),
8116
+ selectionBranch("earth-pill", emptyParametersSchema$1),
8117
+ selectionBranch("confident-chip", emptyParametersSchema$1),
8118
+ selectionBranch("editorial-link", emptyParametersSchema$1),
8119
+ selectionBranch("pebble", emptyParametersSchema$1),
8120
+ selectionBranch("ink-stamp", emptyParametersSchema$1),
8121
+ selectionBranch("flat-ink", emptyParametersSchema$1),
8122
+ selectionBranch("brushed-wash", emptyParametersSchema$1),
8123
+ selectionBranch("showtime-pill", emptyParametersSchema$1),
8124
+ selectionBranch("joy-stamp", emptyParametersSchema$1),
8125
+ selectionBranch("ornate-ritual", emptyParametersSchema$1),
8126
+ selectionBranch("quiet-arrow", quietArrowParametersSchema)
8127
+ ]);
8128
+ //#endregion
8129
+ //#region ../theme-core/src/buttons/styles/recipes/quietArrow.ts
8130
+ const quietArrowRecipe = defineButtonStyleRecipe({
8131
+ id: "quiet-arrow",
8132
+ accessibilityLabel: "Wide uppercase buttons with trailing arrows",
8050
8133
  tags: {
8051
8134
  tone: "classic",
8052
- energy: "vibrant",
8053
- motion: "standard",
8054
- era: "organic"
8135
+ energy: "calm",
8136
+ motion: "subtle",
8137
+ era: "editorial"
8055
8138
  },
8056
- buttonSystem: {
8139
+ parameterSchema: quietArrowParametersSchema,
8140
+ defaultParameters: { secondaryTreatment: "outlined" },
8141
+ controls: [{
8142
+ kind: "choice",
8143
+ parameter: "secondaryTreatment",
8144
+ label: "Secondary button treatment",
8145
+ options: [{
8146
+ value: "outlined",
8147
+ label: "Outlined"
8148
+ }, {
8149
+ value: "solid-accent",
8150
+ label: "Solid accent"
8151
+ }]
8152
+ }],
8153
+ defineSystem: (parameters) => ({
8057
8154
  global: {
8058
8155
  cornerStyle: "rounded",
8059
- shadow: "low",
8060
- textTransform: "none",
8156
+ textTransform: "uppercase",
8061
8157
  fontWeight: 500,
8062
8158
  borderWidth: 1,
8159
+ typography: "body",
8160
+ fontSize: "text-sm",
8161
+ letterSpacing: "loose",
8063
8162
  hoverTransform: "none",
8064
8163
  hoverColor: "none"
8065
8164
  },
8066
8165
  sizes: {
8067
8166
  sm: {
8068
- padding: "0.75rem 2rem",
8167
+ padding: "0.625rem 1.25rem",
8168
+ fontSize: "text-xs"
8169
+ },
8170
+ md: {
8171
+ padding: "0.8125rem 1.75rem",
8069
8172
  fontSize: "text-sm"
8070
8173
  },
8071
- md: { padding: "1rem 2.5rem" },
8072
8174
  lg: {
8073
- padding: "1.25rem 3rem",
8074
- fontSize: "text-lg"
8175
+ padding: "0.9375rem 2.125rem",
8176
+ fontSize: "text-sm"
8075
8177
  },
8076
8178
  xl: {
8077
- padding: "1.625rem 3.75rem",
8078
- fontSize: "text-xl"
8179
+ padding: "1.0625rem 2.5rem",
8180
+ fontSize: "text-base"
8079
8181
  }
8080
8182
  },
8081
- variants: [
8082
- {
8083
- id: "primary",
8084
- name: "Primary",
8085
- enabled: true,
8086
- priority: 1,
8087
- background: {
8088
- type: "gradient",
8089
- stops: ["primary", "accent"],
8090
- direction: "to-r",
8091
- foregroundToken: "primaryForeground"
8092
- },
8093
- borderRadius: "rounded-lg",
8094
- shadow: {
8095
- elevation: "low",
8096
- softness: "soft",
8097
- position: "bottom"
8183
+ roles: {
8184
+ primary: {
8185
+ kind: "complete",
8186
+ appearance: {
8187
+ background: {
8188
+ type: "solid",
8189
+ colorToken: unsafeAsOpaqueButtonColourToken("primary")
8190
+ },
8191
+ borderRadius: "rounded-xl",
8192
+ affordance: { iconId: "ArrowRight" },
8193
+ effects: { hover: [{
8194
+ effectId: "darken-background",
8195
+ options: { amount: 100 }
8196
+ }] }
8098
8197
  }
8099
8198
  },
8100
- {
8101
- id: "secondary",
8102
- name: "Secondary",
8103
- enabled: true,
8104
- priority: 2,
8105
- background: {
8106
- type: "gradient",
8107
- stops: ["secondary", "accent"],
8108
- direction: "to-br",
8109
- foregroundToken: "primaryForeground"
8110
- },
8111
- borderRadius: "rounded-lg",
8112
- shadow: {
8113
- elevation: "low",
8114
- softness: "soft",
8115
- position: "bottom"
8199
+ secondary: {
8200
+ kind: "complete",
8201
+ appearance: parameters.secondaryTreatment === "solid-accent" ? {
8202
+ background: {
8203
+ type: "solid",
8204
+ colorToken: unsafeAsOpaqueButtonColourToken("accent")
8205
+ },
8206
+ borderRadius: "rounded-xl",
8207
+ affordance: { iconId: "ArrowRight" },
8208
+ effects: { hover: [{
8209
+ effectId: "darken-background",
8210
+ options: { amount: 100 }
8211
+ }] }
8212
+ } : {
8213
+ background: {
8214
+ type: "surface",
8215
+ role: "link"
8216
+ },
8217
+ borderRadius: "rounded-xl",
8218
+ border: { widthClass: "border" },
8219
+ affordance: { iconId: "ArrowRight" },
8220
+ effects: { hover: [{
8221
+ effectId: "background-on-hover",
8222
+ options: { colorToken: "surfaceAlt" }
8223
+ }] }
8116
8224
  }
8117
8225
  },
8118
- {
8119
- id: "outline",
8120
- name: "Outline",
8121
- enabled: true,
8122
- priority: 3,
8123
- background: {
8124
- type: "surface",
8125
- role: "link"
8126
- },
8127
- borderRadius: "rounded-lg",
8128
- border: { widthClass: "border" },
8129
- effects: { hover: [{
8130
- effectId: "background-on-hover",
8131
- options: { colorToken: "surface" }
8132
- }] }
8226
+ outline: {
8227
+ kind: "complete",
8228
+ appearance: {
8229
+ background: {
8230
+ type: "surface",
8231
+ role: "link"
8232
+ },
8233
+ borderRadius: "rounded-xl",
8234
+ border: { widthClass: "border" }
8235
+ }
8133
8236
  },
8134
- {
8135
- id: "ghost",
8136
- name: "Ghost",
8137
- enabled: true,
8138
- priority: 3,
8139
- background: {
8140
- type: "surface",
8141
- role: "link"
8142
- },
8143
- borderRadius: "rounded-lg",
8144
- effects: { hover: [{
8145
- effectId: "background-on-hover",
8146
- options: { colorToken: "surface" }
8147
- }] }
8237
+ ghost: {
8238
+ kind: "complete",
8239
+ appearance: {
8240
+ background: {
8241
+ type: "surface",
8242
+ role: "link"
8243
+ },
8244
+ borderRadius: "rounded-xl"
8245
+ }
8246
+ },
8247
+ accent: {
8248
+ kind: "derive",
8249
+ from: "primary",
8250
+ overrides: { background: {
8251
+ type: "solid",
8252
+ colorToken: unsafeAsOpaqueButtonColourToken("accent")
8253
+ } }
8148
8254
  }
8149
- ]
8150
- }
8255
+ }
8256
+ })
8151
8257
  });
8152
8258
  //#endregion
8153
- //#region ../theme-core/src/buttons/personalities/confident-chip.ts
8154
- /**
8155
- * Confident Chip — moderate-radius solid buttons that read as competent and
8156
- * direct. Default for Clear Professional; works for any modern brand voice.
8157
- */
8158
- const confidentChip = defineButtonPersonality({
8159
- id: "confident-chip",
8160
- name: "Confident Chip",
8161
- description: "Mid-radius solid buttons with a crisp hover.",
8259
+ //#region ../theme-core/src/buttons/styles/recipes/defineStaticButtonStyleRecipe.ts
8260
+ const emptyParametersSchema = z$1.object({}).strict();
8261
+ function defineStaticButtonStyleRecipe(input) {
8262
+ const system = normalizeSystem(input.system);
8263
+ return defineButtonStyleRecipe({
8264
+ id: input.id,
8265
+ accessibilityLabel: input.accessibilityLabel,
8266
+ tags: input.tags,
8267
+ parameterSchema: emptyParametersSchema,
8268
+ defaultParameters: {},
8269
+ controls: [],
8270
+ defineSystem: () => system
8271
+ });
8272
+ }
8273
+ function normalizeSystem(input) {
8274
+ return {
8275
+ global: buttonGlobalSettingsSchema.parse(input.global),
8276
+ ...input.sizes ? { sizes: input.sizes } : {},
8277
+ roles: {
8278
+ primary: normalizeCompleteRole(input.roles.primary),
8279
+ secondary: normalizeCompleteRole(input.roles.secondary),
8280
+ outline: normalizeSupportingRole(input.roles.outline),
8281
+ ghost: normalizeSupportingRole(input.roles.ghost),
8282
+ accent: normalizeSupportingRole(input.roles.accent)
8283
+ }
8284
+ };
8285
+ }
8286
+ function normalizeCompleteRole(role) {
8287
+ return {
8288
+ kind: "complete",
8289
+ appearance: normalizeAppearance(role.appearance)
8290
+ };
8291
+ }
8292
+ function normalizeSupportingRole(role) {
8293
+ if (role.kind === "complete") return normalizeCompleteRole(role);
8294
+ return {
8295
+ kind: "derive",
8296
+ from: role.from,
8297
+ ...role.overrides ? { overrides: normalizeAppearanceOverrides(role.overrides) } : {}
8298
+ };
8299
+ }
8300
+ function normalizeAppearance(appearance) {
8301
+ const { background, hoverBackgroundToken, ...rest } = appearance;
8302
+ return {
8303
+ ...rest,
8304
+ background: buttonColourTreatmentSchema.parse(background),
8305
+ ...hoverBackgroundToken ? { hoverBackgroundToken: opaqueButtonColourTokenSchema.parse(hoverBackgroundToken) } : {}
8306
+ };
8307
+ }
8308
+ function normalizeAppearanceOverrides(overrides) {
8309
+ const { background, hoverBackgroundToken, ...rest } = overrides;
8310
+ return {
8311
+ ...rest,
8312
+ ...background ? { background: buttonColourTreatmentSchema.parse(background) } : {},
8313
+ ...hoverBackgroundToken ? { hoverBackgroundToken: opaqueButtonColourTokenSchema.parse(hoverBackgroundToken) } : {}
8314
+ };
8315
+ }
8316
+ //#endregion
8317
+ //#region ../theme-core/src/buttons/styles/recipes/staticRecipes.ts
8318
+ const softPillRecipe = defineStaticButtonStyleRecipe({
8319
+ id: "soft-pill",
8320
+ accessibilityLabel: "Rounded pill buttons with a gentle hover lift",
8162
8321
  tags: {
8163
8322
  tone: "classic",
8164
8323
  energy: "calm",
8165
- motion: "standard",
8166
- era: "modern"
8324
+ motion: "subtle",
8325
+ era: "organic"
8167
8326
  },
8168
- buttonSystem: {
8327
+ system: {
8169
8328
  global: {
8170
- cornerStyle: "rounded",
8171
- shadow: "medium",
8329
+ cornerStyle: "pill",
8330
+ shadow: "low",
8172
8331
  textTransform: "none",
8173
- fontWeight: 600,
8174
- borderWidth: 1,
8175
- hoverTransform: "none",
8332
+ fontWeight: 500,
8333
+ borderWidth: 2,
8334
+ hoverTransform: "lift",
8176
8335
  hoverColor: "none"
8177
8336
  },
8178
- variants: [
8179
- {
8180
- id: "primary",
8181
- name: "Primary",
8182
- enabled: true,
8183
- priority: 1,
8184
- background: {
8185
- type: "solid",
8186
- colorToken: "primary"
8187
- },
8188
- borderRadius: "rounded-md",
8189
- shadow: {
8190
- elevation: "medium",
8191
- softness: "soft",
8192
- position: "bottom"
8193
- },
8194
- effects: {
8195
- hover: [{
8196
- effectId: "darken-background",
8197
- options: { amount: 100 }
8198
- }],
8199
- active: [{
8200
- effectId: "scale-down",
8201
- options: { scale: "0.97" }
8202
- }]
8337
+ roles: {
8338
+ primary: {
8339
+ kind: "complete",
8340
+ appearance: {
8341
+ background: {
8342
+ type: "solid",
8343
+ colorToken: "primary"
8344
+ },
8345
+ borderRadius: "rounded-full",
8346
+ shadow: {
8347
+ elevation: "low",
8348
+ softness: "soft",
8349
+ position: "bottom"
8350
+ },
8351
+ effects: {
8352
+ hover: [{
8353
+ effectId: "lift",
8354
+ options: { translateY: "-1px" }
8355
+ }],
8356
+ active: [{
8357
+ effectId: "scale-down",
8358
+ options: { scale: "0.98" }
8359
+ }]
8360
+ }
8203
8361
  }
8204
8362
  },
8205
- {
8206
- id: "secondary",
8207
- name: "Secondary",
8208
- enabled: true,
8209
- priority: 2,
8210
- background: {
8211
- type: "solid",
8212
- colorToken: "surface"
8213
- },
8214
- borderRadius: "rounded-md",
8215
- border: { widthClass: "border" },
8216
- effects: { hover: [{
8217
- effectId: "background-on-hover",
8218
- options: { colorToken: "background" }
8219
- }] }
8363
+ secondary: {
8364
+ kind: "complete",
8365
+ appearance: {
8366
+ background: {
8367
+ type: "surface",
8368
+ role: "link"
8369
+ },
8370
+ borderRadius: "rounded-full",
8371
+ border: { widthClass: "border-2" },
8372
+ effects: { hover: [{
8373
+ effectId: "background-on-hover",
8374
+ options: { colorToken: "surface" }
8375
+ }] }
8376
+ }
8220
8377
  },
8221
- {
8222
- id: "outline",
8223
- name: "Outline",
8224
- enabled: true,
8225
- priority: 3,
8226
- background: {
8227
- type: "surface",
8228
- role: "link"
8229
- },
8230
- borderRadius: "rounded-md",
8231
- border: { widthClass: "border" },
8232
- effects: { hover: [{
8233
- effectId: "background-on-hover",
8234
- options: { colorToken: "surface" }
8235
- }] }
8378
+ outline: {
8379
+ kind: "complete",
8380
+ appearance: {
8381
+ background: {
8382
+ type: "surface",
8383
+ role: "link"
8384
+ },
8385
+ borderRadius: "rounded-full",
8386
+ border: { widthClass: "border" },
8387
+ effects: { hover: [{
8388
+ effectId: "background-on-hover",
8389
+ options: { colorToken: "surface" }
8390
+ }] }
8391
+ }
8236
8392
  },
8237
- {
8238
- id: "ghost",
8239
- name: "Ghost",
8240
- enabled: true,
8241
- priority: 3,
8242
- background: {
8243
- type: "surface",
8244
- role: "link"
8245
- },
8246
- borderRadius: "rounded-md",
8247
- effects: { hover: [{
8248
- effectId: "background-on-hover",
8249
- options: { colorToken: "surface" }
8250
- }] }
8393
+ ghost: {
8394
+ kind: "complete",
8395
+ appearance: {
8396
+ background: {
8397
+ type: "surface",
8398
+ role: "link"
8399
+ },
8400
+ borderRadius: "rounded-full",
8401
+ effects: { hover: [{
8402
+ effectId: "background-on-hover",
8403
+ options: { colorToken: "surface" }
8404
+ }] }
8405
+ }
8406
+ },
8407
+ accent: {
8408
+ kind: "derive",
8409
+ from: "primary",
8410
+ overrides: { background: {
8411
+ type: "solid",
8412
+ colorToken: "accent"
8413
+ } }
8251
8414
  }
8252
- ]
8415
+ }
8253
8416
  }
8254
8417
  });
8255
- //#endregion
8256
- //#region ../theme-core/src/buttons/personalities/earth-pill.ts
8257
- /**
8258
- * Earth Pill - warm studio pills with compact, primary-coloured ghost CTAs.
8259
- * Built for grounded editorial wellness styles such as Soft Earth.
8260
- */
8261
- const earthPill = defineButtonPersonality({
8418
+ const earthPillRecipe = defineStaticButtonStyleRecipe({
8262
8419
  id: "earth-pill",
8263
- name: "Earth Pill",
8264
- description: "Compact terracotta pills with quiet primary-coloured ghost CTAs.",
8420
+ accessibilityLabel: "Compact rounded buttons with a quiet hover lift",
8265
8421
  tags: {
8266
8422
  tone: "classic",
8267
8423
  energy: "calm",
8268
8424
  motion: "subtle",
8269
8425
  era: "organic"
8270
8426
  },
8271
- buttonSystem: {
8427
+ system: {
8272
8428
  global: {
8273
8429
  cornerStyle: "pill",
8274
8430
  shadow: "low",
@@ -8295,576 +8451,547 @@ const earthPill = defineButtonPersonality({
8295
8451
  fontSize: "text-xl"
8296
8452
  }
8297
8453
  },
8298
- variants: [
8299
- {
8300
- id: "primary",
8301
- name: "Primary",
8302
- enabled: true,
8303
- priority: 1,
8304
- background: {
8305
- type: "solid",
8306
- colorToken: "primary"
8307
- },
8308
- borderRadius: "rounded-full",
8309
- shadow: {
8310
- elevation: "low",
8311
- softness: "soft",
8312
- position: "bottom"
8313
- },
8314
- effects: {
8315
- hover: [{
8316
- effectId: "lift",
8317
- options: { translateY: "-1px" }
8318
- }, {
8319
- effectId: "background-on-hover",
8320
- options: { colorToken: "accent" }
8321
- }],
8322
- active: [{
8323
- effectId: "scale-down",
8324
- options: { scale: "0.98" }
8325
- }]
8454
+ roles: {
8455
+ primary: {
8456
+ kind: "complete",
8457
+ appearance: {
8458
+ background: {
8459
+ type: "solid",
8460
+ colorToken: "primary"
8461
+ },
8462
+ borderRadius: "rounded-full",
8463
+ shadow: {
8464
+ elevation: "low",
8465
+ softness: "soft",
8466
+ position: "bottom"
8467
+ },
8468
+ effects: {
8469
+ hover: [{
8470
+ effectId: "lift",
8471
+ options: { translateY: "-1px" }
8472
+ }, {
8473
+ effectId: "background-on-hover",
8474
+ options: { colorToken: "accent" }
8475
+ }],
8476
+ active: [{
8477
+ effectId: "scale-down",
8478
+ options: { scale: "0.98" }
8479
+ }]
8480
+ }
8326
8481
  }
8327
8482
  },
8328
- {
8329
- id: "secondary",
8330
- name: "Secondary",
8331
- enabled: true,
8332
- priority: 2,
8333
- background: {
8334
- type: "surface",
8335
- role: "link"
8336
- },
8337
- borderRadius: "rounded-full",
8338
- border: { widthClass: "border" },
8339
- effects: { hover: [{
8340
- effectId: "background-on-hover",
8341
- options: { colorToken: "muted" }
8342
- }] }
8483
+ secondary: {
8484
+ kind: "complete",
8485
+ appearance: {
8486
+ background: {
8487
+ type: "surface",
8488
+ role: "link"
8489
+ },
8490
+ borderRadius: "rounded-full",
8491
+ border: { widthClass: "border" },
8492
+ effects: { hover: [{
8493
+ effectId: "background-on-hover",
8494
+ options: { colorToken: "muted" }
8495
+ }] }
8496
+ }
8343
8497
  },
8344
- {
8345
- id: "outline",
8346
- name: "Outline",
8347
- enabled: true,
8348
- priority: 3,
8349
- background: {
8350
- type: "surface",
8351
- role: "link"
8352
- },
8353
- borderRadius: "rounded-full",
8354
- border: { widthClass: "border" },
8355
- effects: { hover: [{
8356
- effectId: "background-on-hover",
8357
- options: { colorToken: "surfaceAlt" }
8358
- }] }
8498
+ outline: {
8499
+ kind: "complete",
8500
+ appearance: {
8501
+ background: {
8502
+ type: "surface",
8503
+ role: "link"
8504
+ },
8505
+ borderRadius: "rounded-full",
8506
+ border: { widthClass: "border" },
8507
+ effects: { hover: [{
8508
+ effectId: "background-on-hover",
8509
+ options: { colorToken: "surfaceAlt" }
8510
+ }] }
8511
+ }
8359
8512
  },
8360
- {
8361
- id: "ghost",
8362
- name: "Ghost",
8363
- enabled: true,
8364
- priority: 3,
8365
- background: {
8366
- type: "surface",
8367
- role: "link"
8368
- },
8369
- borderRadius: "rounded-full",
8370
- shadow: {
8371
- elevation: "none",
8372
- softness: null,
8373
- position: "bottom"
8374
- },
8375
- sizes: {
8376
- sm: {
8377
- padding: "0.25rem 0",
8378
- fontSize: "text-sm"
8513
+ ghost: {
8514
+ kind: "complete",
8515
+ appearance: {
8516
+ background: {
8517
+ type: "surface",
8518
+ role: "link"
8379
8519
  },
8380
- md: { padding: "0.375rem 0" },
8381
- lg: {
8382
- padding: "0.5rem 0",
8383
- fontSize: "text-lg"
8520
+ borderRadius: "rounded-full",
8521
+ shadow: {
8522
+ elevation: "none",
8523
+ softness: null,
8524
+ position: "bottom"
8384
8525
  },
8385
- xl: {
8386
- padding: "0.625rem 0",
8387
- fontSize: "text-xl"
8526
+ sizes: {
8527
+ sm: {
8528
+ padding: "0.25rem 0",
8529
+ fontSize: "text-sm"
8530
+ },
8531
+ md: { padding: "0.375rem 0" },
8532
+ lg: {
8533
+ padding: "0.5rem 0",
8534
+ fontSize: "text-lg"
8535
+ },
8536
+ xl: {
8537
+ padding: "0.625rem 0",
8538
+ fontSize: "text-xl"
8539
+ }
8388
8540
  }
8389
8541
  }
8542
+ },
8543
+ accent: {
8544
+ kind: "derive",
8545
+ from: "primary",
8546
+ overrides: { background: {
8547
+ type: "solid",
8548
+ colorToken: "accent"
8549
+ } }
8390
8550
  }
8391
- ]
8551
+ }
8392
8552
  }
8393
8553
  });
8394
- //#endregion
8395
- //#region ../theme-core/src/buttons/personalities/editorial-link.ts
8396
- /**
8397
- * Editorial Link — all variants are link-style with a growing underline.
8398
- * Default for Warm Editorial and Quiet Luxury; reads as refined and quiet.
8399
- */
8400
- const editorialLink = defineButtonPersonality({
8401
- id: "editorial-link",
8402
- name: "Editorial Link",
8403
- description: "Link-style with a growing underline. Refined, typographic.",
8554
+ const confidentChipRecipe = defineStaticButtonStyleRecipe({
8555
+ id: "confident-chip",
8556
+ accessibilityLabel: "Moderately rounded solid buttons with a crisp hover",
8404
8557
  tags: {
8405
- tone: "minimal",
8558
+ tone: "classic",
8406
8559
  energy: "calm",
8407
- motion: "subtle",
8408
- era: "editorial"
8560
+ motion: "standard",
8561
+ era: "modern"
8409
8562
  },
8410
- buttonSystem: {
8563
+ system: {
8411
8564
  global: {
8412
- cornerStyle: "square",
8565
+ cornerStyle: "rounded",
8566
+ shadow: "medium",
8413
8567
  textTransform: "none",
8414
- fontWeight: 500,
8568
+ fontWeight: 600,
8415
8569
  borderWidth: 1,
8416
8570
  hoverTransform: "none",
8417
8571
  hoverColor: "none"
8418
8572
  },
8419
- variants: [
8420
- {
8421
- id: "primary",
8422
- name: "Primary",
8423
- enabled: true,
8424
- priority: 1,
8425
- background: {
8426
- type: "surface",
8427
- role: "link"
8428
- },
8429
- borderRadius: "rounded-none",
8430
- sizes: {
8431
- sm: { padding: "0.25rem 0" },
8432
- md: { padding: "0.5rem 0" },
8433
- lg: { padding: "0.625rem 0" },
8434
- xl: { padding: "0.75rem 0" }
8435
- },
8436
- effects: { base: [{
8437
- effectId: "border-bottom-grow",
8438
- options: {
8439
- height: 2,
8440
- colorToken: "text",
8441
- origin: "left"
8573
+ roles: {
8574
+ primary: {
8575
+ kind: "complete",
8576
+ appearance: {
8577
+ background: {
8578
+ type: "solid",
8579
+ colorToken: "primary"
8580
+ },
8581
+ borderRadius: "rounded-md",
8582
+ shadow: {
8583
+ elevation: "medium",
8584
+ softness: "soft",
8585
+ position: "bottom"
8586
+ },
8587
+ effects: {
8588
+ hover: [{
8589
+ effectId: "darken-background",
8590
+ options: { amount: 100 }
8591
+ }],
8592
+ active: [{
8593
+ effectId: "scale-down",
8594
+ options: { scale: "0.97" }
8595
+ }]
8442
8596
  }
8443
- }] }
8597
+ }
8444
8598
  },
8445
- {
8446
- id: "secondary",
8447
- name: "Secondary",
8448
- enabled: true,
8449
- priority: 2,
8450
- background: {
8451
- type: "surface",
8452
- role: "link"
8453
- },
8454
- borderRadius: "rounded-none",
8455
- sizes: {
8456
- sm: { padding: "0.25rem 0" },
8457
- md: { padding: "0.5rem 0" },
8458
- lg: { padding: "0.625rem 0" },
8459
- xl: { padding: "0.75rem 0" }
8460
- },
8461
- effects: { base: [{
8462
- effectId: "border-bottom-grow",
8463
- options: {
8464
- height: 1,
8465
- colorToken: "mutedText",
8466
- origin: "left"
8467
- }
8468
- }] }
8599
+ secondary: {
8600
+ kind: "complete",
8601
+ appearance: {
8602
+ background: {
8603
+ type: "solid",
8604
+ colorToken: "surface"
8605
+ },
8606
+ borderRadius: "rounded-md",
8607
+ border: { widthClass: "border" },
8608
+ effects: { hover: [{
8609
+ effectId: "background-on-hover",
8610
+ options: { colorToken: "background" }
8611
+ }] }
8612
+ }
8469
8613
  },
8470
- {
8471
- id: "outline",
8472
- name: "Outline",
8473
- enabled: true,
8474
- priority: 3,
8475
- background: {
8476
- type: "surface",
8477
- role: "link"
8478
- },
8479
- borderRadius: "rounded-none",
8480
- sizes: {
8481
- sm: { padding: "0.25rem 0" },
8482
- md: { padding: "0.5rem 0" },
8483
- lg: { padding: "0.625rem 0" },
8484
- xl: { padding: "0.75rem 0" }
8485
- },
8486
- effects: { base: [{
8487
- effectId: "border-bottom-grow",
8488
- options: {
8489
- height: 1,
8490
- colorToken: "accent",
8491
- origin: "left"
8492
- }
8493
- }] }
8614
+ outline: {
8615
+ kind: "complete",
8616
+ appearance: {
8617
+ background: {
8618
+ type: "surface",
8619
+ role: "link"
8620
+ },
8621
+ borderRadius: "rounded-md",
8622
+ border: { widthClass: "border" },
8623
+ effects: { hover: [{
8624
+ effectId: "background-on-hover",
8625
+ options: { colorToken: "surface" }
8626
+ }] }
8627
+ }
8494
8628
  },
8495
- {
8496
- id: "ghost",
8497
- name: "Ghost",
8498
- enabled: true,
8499
- priority: 3,
8500
- background: {
8501
- type: "surface",
8502
- role: "link"
8503
- },
8504
- borderRadius: "rounded-none",
8505
- sizes: {
8506
- sm: { padding: "0.25rem 0" },
8507
- md: { padding: "0.5rem 0" },
8508
- lg: { padding: "0.625rem 0" },
8509
- xl: { padding: "0.75rem 0" }
8629
+ ghost: {
8630
+ kind: "complete",
8631
+ appearance: {
8632
+ background: {
8633
+ type: "surface",
8634
+ role: "link"
8635
+ },
8636
+ borderRadius: "rounded-md",
8637
+ effects: { hover: [{
8638
+ effectId: "background-on-hover",
8639
+ options: { colorToken: "surface" }
8640
+ }] }
8510
8641
  }
8642
+ },
8643
+ accent: {
8644
+ kind: "derive",
8645
+ from: "primary",
8646
+ overrides: { background: {
8647
+ type: "solid",
8648
+ colorToken: "accent"
8649
+ } }
8511
8650
  }
8512
- ]
8651
+ }
8513
8652
  }
8514
8653
  });
8515
- //#endregion
8516
- //#region ../theme-core/src/buttons/personalities/flat-ink.ts
8517
- /**
8518
- * Flat Ink — a solid ink primary, a hairline ghost secondary, and a single
8519
- * amber accent climax, all completely flat (no drop shadow) with a modest
8520
- * radius. Reads as grounded, editorial and clinician-precise: the Fulcrum
8521
- * physiotherapy voice. The primary shifts to the secondary (moss) on hover and
8522
- * every variant lifts 2px; the amber accent is meant to be used sparingly as
8523
- * the page's one conversion climax.
8524
- */
8525
- const flatInk = defineButtonPersonality({
8526
- id: "flat-ink",
8527
- name: "Flat Ink",
8528
- description: "Flat solid-ink primary, hairline ghost secondary, and a single amber accent climax — modest radius, no shadow.",
8654
+ const editorialSizes = {
8655
+ sm: { padding: "0.25rem 0" },
8656
+ md: { padding: "0.5rem 0" },
8657
+ lg: { padding: "0.625rem 0" },
8658
+ xl: { padding: "0.75rem 0" }
8659
+ };
8660
+ const editorialLinkRecipe = defineStaticButtonStyleRecipe({
8661
+ id: "editorial-link",
8662
+ accessibilityLabel: "Flat typographic buttons with an underline treatment",
8529
8663
  tags: {
8530
- tone: "classic",
8664
+ tone: "minimal",
8531
8665
  energy: "calm",
8532
8666
  motion: "subtle",
8533
8667
  era: "editorial"
8534
8668
  },
8535
- buttonSystem: {
8669
+ system: {
8536
8670
  global: {
8537
- cornerStyle: "rounded",
8671
+ cornerStyle: "square",
8538
8672
  textTransform: "none",
8539
- fontWeight: 600,
8673
+ fontWeight: 500,
8540
8674
  borderWidth: 1,
8541
- typography: "body",
8542
- letterSpacing: "tight",
8543
- paddingPreset: "default",
8544
- hoverTransform: "lift",
8675
+ hoverTransform: "none",
8545
8676
  hoverColor: "none"
8546
8677
  },
8547
- sizes: {
8548
- sm: {
8549
- padding: "0.625rem 1.125rem",
8550
- fontSize: "text-sm"
8678
+ roles: {
8679
+ primary: {
8680
+ kind: "complete",
8681
+ appearance: {
8682
+ background: {
8683
+ type: "surface",
8684
+ role: "link"
8685
+ },
8686
+ borderRadius: "rounded-none",
8687
+ sizes: editorialSizes,
8688
+ effects: { base: [{
8689
+ effectId: "border-bottom-grow",
8690
+ options: {
8691
+ height: 2,
8692
+ colorToken: "text",
8693
+ origin: "left"
8694
+ }
8695
+ }] }
8696
+ }
8551
8697
  },
8552
- md: { padding: "0.8125rem 1.375rem" },
8553
- lg: {
8554
- padding: "0.9375rem 1.625rem",
8555
- fontSize: "text-lg"
8698
+ secondary: {
8699
+ kind: "complete",
8700
+ appearance: {
8701
+ background: {
8702
+ type: "surface",
8703
+ role: "link"
8704
+ },
8705
+ borderRadius: "rounded-none",
8706
+ sizes: editorialSizes,
8707
+ effects: { base: [{
8708
+ effectId: "border-bottom-grow",
8709
+ options: {
8710
+ height: 1,
8711
+ colorToken: "mutedText",
8712
+ origin: "left"
8713
+ }
8714
+ }] }
8715
+ }
8556
8716
  },
8557
- xl: {
8558
- padding: "1.0625rem 2rem",
8559
- fontSize: "text-xl"
8560
- }
8561
- },
8562
- variants: [
8563
- {
8564
- id: "primary",
8565
- name: "Primary",
8566
- enabled: true,
8567
- priority: 1,
8568
- background: {
8569
- type: "solid",
8570
- colorToken: "primary"
8571
- },
8572
- borderRadius: "rounded-lg",
8573
- affordance: { iconId: "ArrowRight" },
8574
- effects: { hover: [{
8575
- effectId: "lift",
8576
- options: { translateY: "-2px" }
8577
- }, {
8578
- effectId: "background-on-hover",
8579
- options: { colorToken: "secondary" }
8580
- }] }
8717
+ outline: {
8718
+ kind: "complete",
8719
+ appearance: {
8720
+ background: {
8721
+ type: "surface",
8722
+ role: "link"
8723
+ },
8724
+ borderRadius: "rounded-none",
8725
+ sizes: editorialSizes,
8726
+ effects: { base: [{
8727
+ effectId: "border-bottom-grow",
8728
+ options: {
8729
+ height: 1,
8730
+ colorToken: "accent",
8731
+ origin: "left"
8732
+ }
8733
+ }] }
8734
+ }
8581
8735
  },
8582
- {
8583
- id: "secondary",
8584
- name: "Secondary",
8585
- enabled: true,
8586
- priority: 2,
8587
- background: {
8588
- type: "surface",
8589
- role: "link"
8590
- },
8591
- borderRadius: "rounded-lg",
8592
- border: { widthClass: "border" },
8593
- effects: { hover: [{
8594
- effectId: "lift",
8595
- options: { translateY: "-2px" }
8596
- }, {
8597
- effectId: "background-on-hover",
8598
- options: { colorToken: "surfaceAlt" }
8599
- }] }
8736
+ ghost: {
8737
+ kind: "complete",
8738
+ appearance: {
8739
+ background: {
8740
+ type: "surface",
8741
+ role: "link"
8742
+ },
8743
+ borderRadius: "rounded-none",
8744
+ sizes: editorialSizes
8745
+ }
8600
8746
  },
8601
- {
8602
- id: "accent",
8603
- name: "Accent",
8604
- enabled: true,
8605
- priority: 3,
8606
- background: {
8607
- type: "solid",
8608
- colorToken: "accent"
8609
- },
8610
- borderRadius: "rounded-lg",
8611
- affordance: { iconId: "ArrowRight" },
8612
- effects: { hover: [{
8613
- effectId: "lift",
8614
- options: { translateY: "-2px" }
8615
- }] }
8747
+ accent: {
8748
+ kind: "derive",
8749
+ from: "primary",
8750
+ overrides: { effects: { base: [{
8751
+ effectId: "border-bottom-grow",
8752
+ options: {
8753
+ height: 2,
8754
+ colorToken: "accent",
8755
+ origin: "left"
8756
+ }
8757
+ }] } }
8616
8758
  }
8617
- ]
8759
+ }
8618
8760
  }
8619
8761
  });
8620
- //#endregion
8621
- //#region ../theme-core/src/buttons/personalities/ink-stamp.ts
8622
- /**
8623
- * Ink Stamp — solid earthy fills with a bordered, hand-stamped block-print
8624
- * feel. Reads as bold and handmade; suits expressive craft brands.
8625
- */
8626
- const inkStamp = defineButtonPersonality({
8627
- id: "ink-stamp",
8628
- name: "Ink Stamp",
8629
- description: "Hand-stamped block-print buttons with a slight lift on hover.",
8762
+ const pebbleRecipe = defineStaticButtonStyleRecipe({
8763
+ id: "pebble",
8764
+ accessibilityLabel: "Generously padded pill buttons",
8630
8765
  tags: {
8631
- tone: "bold",
8632
- energy: "vibrant",
8766
+ tone: "classic",
8767
+ energy: "calm",
8633
8768
  motion: "standard",
8634
8769
  era: "organic"
8635
8770
  },
8636
- buttonSystem: {
8771
+ system: {
8637
8772
  global: {
8638
- cornerStyle: "rounded",
8639
- textTransform: "uppercase",
8640
- fontWeight: 600,
8641
- borderWidth: 1,
8773
+ cornerStyle: "pill",
8774
+ textTransform: "none",
8775
+ fontWeight: 500,
8776
+ borderWidth: 2,
8642
8777
  hoverTransform: "none",
8643
- hoverColor: "none",
8644
- letterSpacing: "loose"
8778
+ hoverColor: "none"
8645
8779
  },
8646
8780
  sizes: {
8647
8781
  sm: {
8648
- padding: "0.5rem 1.25rem",
8649
- fontSize: "text-xs"
8650
- },
8651
- md: {
8652
- padding: "0.75rem 1.75rem",
8782
+ padding: "0.625rem 1.75rem",
8653
8783
  fontSize: "text-sm"
8654
8784
  },
8785
+ md: { padding: "0.875rem 2.25rem" },
8655
8786
  lg: {
8656
- padding: "0.875rem 2.25rem",
8657
- fontSize: "text-base"
8658
- },
8659
- xl: {
8660
8787
  padding: "1.125rem 2.75rem",
8661
8788
  fontSize: "text-lg"
8789
+ },
8790
+ xl: {
8791
+ padding: "1.5rem 3.5rem",
8792
+ fontSize: "text-xl"
8662
8793
  }
8663
8794
  },
8664
- variants: [
8665
- {
8666
- id: "primary",
8667
- name: "Primary",
8668
- enabled: true,
8669
- priority: 1,
8670
- background: {
8671
- type: "solid",
8672
- colorToken: "primary"
8673
- },
8674
- borderRadius: "rounded-md",
8675
- effects: {
8676
- hover: [{
8677
- effectId: "drop-shadow",
8678
- options: { intensity: "medium" }
8679
- }, {
8680
- effectId: "scale-up",
8681
- options: { scale: "1.02" }
8682
- }],
8683
- active: [{
8684
- effectId: "scale-down",
8685
- options: { scale: "0.98" }
8686
- }]
8795
+ roles: {
8796
+ primary: {
8797
+ kind: "complete",
8798
+ appearance: {
8799
+ background: {
8800
+ type: "solid",
8801
+ colorToken: "primary"
8802
+ },
8803
+ borderRadius: "rounded-full",
8804
+ effects: { hover: [{
8805
+ effectId: "darken-background",
8806
+ options: { amount: 100 }
8807
+ }] }
8687
8808
  }
8688
8809
  },
8689
- {
8690
- id: "secondary",
8691
- name: "Secondary",
8692
- enabled: true,
8693
- priority: 2,
8694
- background: {
8695
- type: "surface",
8696
- role: "link"
8697
- },
8698
- borderRadius: "rounded-md",
8699
- border: { widthClass: "border-2" },
8700
- effects: { hover: [{
8701
- effectId: "background-on-hover",
8702
- options: { colorToken: "surface" }
8703
- }] }
8704
- },
8705
- {
8706
- id: "outline",
8707
- name: "Outline",
8708
- enabled: true,
8709
- priority: 3,
8710
- background: {
8711
- type: "surface",
8712
- role: "link"
8713
- },
8714
- borderRadius: "rounded-md",
8715
- border: { widthClass: "border-2" },
8716
- effects: { hover: [{
8717
- effectId: "background-on-hover",
8718
- options: { colorToken: "surface" }
8719
- }] }
8720
- },
8721
- {
8722
- id: "ghost",
8723
- name: "Ghost",
8724
- enabled: true,
8725
- priority: 3,
8726
- background: {
8727
- type: "surface",
8728
- role: "link"
8729
- },
8730
- borderRadius: "rounded-none",
8731
- sizes: {
8732
- sm: {
8733
- padding: "0.375rem 0",
8734
- fontSize: "text-xs"
8810
+ secondary: {
8811
+ kind: "complete",
8812
+ appearance: {
8813
+ background: {
8814
+ type: "surface",
8815
+ role: "link"
8735
8816
  },
8736
- md: {
8737
- padding: "0.5rem 0",
8738
- fontSize: "text-sm"
8817
+ borderRadius: "rounded-full",
8818
+ border: { widthClass: "border-2" },
8819
+ effects: { hover: [{
8820
+ effectId: "background-on-hover",
8821
+ options: { colorToken: "accent" }
8822
+ }] }
8823
+ }
8824
+ },
8825
+ outline: {
8826
+ kind: "complete",
8827
+ appearance: {
8828
+ background: {
8829
+ type: "surface",
8830
+ role: "link"
8739
8831
  },
8740
- lg: {
8741
- padding: "0.625rem 0",
8742
- fontSize: "text-base"
8832
+ borderRadius: "rounded-full",
8833
+ border: { widthClass: "border" },
8834
+ effects: { hover: [{
8835
+ effectId: "background-on-hover",
8836
+ options: { colorToken: "surface" }
8837
+ }] }
8838
+ }
8839
+ },
8840
+ ghost: {
8841
+ kind: "complete",
8842
+ appearance: {
8843
+ background: {
8844
+ type: "surface",
8845
+ role: "link"
8743
8846
  },
8744
- xl: {
8745
- padding: "0.75rem 0",
8746
- fontSize: "text-lg"
8747
- }
8748
- },
8749
- effects: { base: [{
8750
- effectId: "border-bottom-grow",
8751
- options: {
8752
- height: 1,
8753
- colorToken: "accent",
8754
- origin: "left"
8847
+ borderRadius: "rounded-full",
8848
+ sizes: {
8849
+ sm: {
8850
+ padding: "0.375rem 0.875rem",
8851
+ fontSize: "text-sm"
8852
+ },
8853
+ md: { padding: "0.5rem 1rem" },
8854
+ lg: {
8855
+ padding: "0.625rem 1.25rem",
8856
+ fontSize: "text-lg"
8857
+ },
8858
+ xl: {
8859
+ padding: "0.75rem 1.5rem",
8860
+ fontSize: "text-xl"
8861
+ }
8755
8862
  }
8756
- }] }
8863
+ }
8864
+ },
8865
+ accent: {
8866
+ kind: "derive",
8867
+ from: "primary",
8868
+ overrides: { background: {
8869
+ type: "solid",
8870
+ colorToken: "accent"
8871
+ } }
8757
8872
  }
8758
- ]
8873
+ }
8759
8874
  }
8760
8875
  });
8761
- //#endregion
8762
- //#region ../theme-core/src/buttons/personalities/joy-stamp.ts
8763
- /**
8764
- * Joy Stamp — neo-brutalist pills with a hard, brand-coloured offset shadow
8765
- * that collapses as the button presses into it on hover. Authored for the
8766
- * Original Joy site style: a cream "stamp" shadow under the solid primary, and
8767
- * a brown shadow under the cream outline. Reads as bold, warm, and handmade.
8768
- *
8769
- * The coloured offset shadow relies on the per-variant `shadow.colorToken`
8770
- * extension; the press is the shared `press-down` effect, which matches its
8771
- * translate to each variant's own shadow offset.
8772
- */
8773
- const joyStamp = defineButtonPersonality({
8774
- id: "joy-stamp",
8775
- name: "Joy Stamp",
8776
- description: "Pill buttons with a hard, brand-coloured offset shadow that presses in on hover.",
8876
+ const brushedWashRecipe = defineStaticButtonStyleRecipe({
8877
+ id: "brushed-wash",
8878
+ accessibilityLabel: "Generously padded gradient buttons with soft shadows",
8777
8879
  tags: {
8778
- tone: "bold",
8880
+ tone: "classic",
8779
8881
  energy: "vibrant",
8780
- motion: "expressive",
8882
+ motion: "standard",
8781
8883
  era: "organic"
8782
8884
  },
8783
- buttonSystem: {
8885
+ system: {
8784
8886
  global: {
8785
- cornerStyle: "pill",
8786
- shadow: "high",
8887
+ cornerStyle: "rounded",
8888
+ shadow: "low",
8787
8889
  textTransform: "none",
8788
- typography: "heading",
8789
- fontWeight: 700,
8790
- fontSize: "text-lg",
8791
- borderWidth: 2,
8890
+ fontWeight: 500,
8891
+ borderWidth: 1,
8792
8892
  hoverTransform: "none",
8793
8893
  hoverColor: "none"
8794
8894
  },
8795
8895
  sizes: {
8796
8896
  sm: {
8797
- padding: "0.625rem 1.25rem",
8798
- fontSize: "text-base"
8799
- },
8800
- md: {
8801
- padding: "0.875rem 1.75rem",
8802
- fontSize: "text-lg"
8897
+ padding: "0.75rem 2rem",
8898
+ fontSize: "text-sm"
8803
8899
  },
8900
+ md: { padding: "1rem 2.5rem" },
8804
8901
  lg: {
8805
- padding: "1.125rem 2.25rem",
8806
- fontSize: "text-xl"
8902
+ padding: "1.25rem 3rem",
8903
+ fontSize: "text-lg"
8807
8904
  },
8808
8905
  xl: {
8809
- padding: "1.25rem 2.75rem",
8810
- fontSize: "text-2xl"
8906
+ padding: "1.625rem 3.75rem",
8907
+ fontSize: "text-xl"
8811
8908
  }
8812
8909
  },
8813
- variants: [{
8814
- id: "primary",
8815
- name: "Primary",
8816
- enabled: true,
8817
- priority: 1,
8818
- background: {
8819
- type: "solid",
8820
- colorToken: "primary"
8910
+ roles: {
8911
+ primary: {
8912
+ kind: "complete",
8913
+ appearance: {
8914
+ background: {
8915
+ type: "gradient",
8916
+ stops: ["primary", "accent"],
8917
+ direction: "to-r",
8918
+ foregroundToken: "primaryForeground"
8919
+ },
8920
+ borderRadius: "rounded-lg",
8921
+ shadow: {
8922
+ elevation: "low",
8923
+ softness: "soft",
8924
+ position: "bottom"
8925
+ }
8926
+ }
8821
8927
  },
8822
- borderRadius: "rounded-full",
8823
- shadow: {
8824
- elevation: "high",
8825
- softness: "hard",
8826
- position: "bottom-right",
8827
- colorToken: "text"
8928
+ secondary: {
8929
+ kind: "complete",
8930
+ appearance: {
8931
+ background: {
8932
+ type: "gradient",
8933
+ stops: ["secondary", "accent"],
8934
+ direction: "to-br",
8935
+ foregroundToken: "primaryForeground"
8936
+ },
8937
+ borderRadius: "rounded-lg",
8938
+ shadow: {
8939
+ elevation: "low",
8940
+ softness: "soft",
8941
+ position: "bottom"
8942
+ }
8943
+ }
8828
8944
  },
8829
- effects: { hover: [{ effectId: "press-down" }] }
8830
- }, {
8831
- id: "secondary",
8832
- name: "Secondary",
8833
- enabled: true,
8834
- priority: 2,
8835
- background: {
8836
- type: "surface",
8837
- role: "link"
8945
+ outline: {
8946
+ kind: "complete",
8947
+ appearance: {
8948
+ background: {
8949
+ type: "surface",
8950
+ role: "link"
8951
+ },
8952
+ borderRadius: "rounded-lg",
8953
+ border: { widthClass: "border" },
8954
+ effects: { hover: [{
8955
+ effectId: "background-on-hover",
8956
+ options: { colorToken: "surface" }
8957
+ }] }
8958
+ }
8838
8959
  },
8839
- borderRadius: "rounded-full",
8840
- border: { widthClass: "border-2" },
8841
- shadow: {
8842
- elevation: "high",
8843
- softness: "hard",
8844
- position: "bottom-right",
8845
- colorToken: "secondary"
8960
+ ghost: {
8961
+ kind: "complete",
8962
+ appearance: {
8963
+ background: {
8964
+ type: "surface",
8965
+ role: "link"
8966
+ },
8967
+ borderRadius: "rounded-lg",
8968
+ effects: { hover: [{
8969
+ effectId: "background-on-hover",
8970
+ options: { colorToken: "surface" }
8971
+ }] }
8972
+ }
8846
8973
  },
8847
- effects: { hover: [{ effectId: "press-down" }] }
8848
- }]
8974
+ accent: {
8975
+ kind: "derive",
8976
+ from: "primary",
8977
+ overrides: { background: {
8978
+ type: "solid",
8979
+ colorToken: "accent"
8980
+ } }
8981
+ }
8982
+ }
8849
8983
  }
8850
8984
  });
8851
- //#endregion
8852
- //#region ../theme-core/src/buttons/personalities/ornate-ritual.ts
8853
- /**
8854
- * Ornate Ritual — framed, ceremonial buttons with jewel-toned fills and
8855
- * deliberate spacing. Designed for music, ceremony, and soulful event sites.
8856
- */
8857
- const ornateRitual = defineButtonPersonality({
8858
- id: "ornate-ritual",
8859
- name: "Ornate Ritual",
8860
- description: "Ceremonial framed buttons with jewel-toned fills and quiet color-change hover states.",
8985
+ const inkStampRecipe = defineStaticButtonStyleRecipe({
8986
+ id: "ink-stamp",
8987
+ accessibilityLabel: "Uppercase block-print buttons with a playful hover",
8861
8988
  tags: {
8862
- tone: "classic",
8989
+ tone: "bold",
8863
8990
  energy: "vibrant",
8864
- motion: "subtle",
8991
+ motion: "standard",
8865
8992
  era: "organic"
8866
8993
  },
8867
- buttonSystem: {
8994
+ system: {
8868
8995
  global: {
8869
8996
  cornerStyle: "rounded",
8870
8997
  textTransform: "uppercase",
@@ -8876,359 +9003,256 @@ const ornateRitual = defineButtonPersonality({
8876
9003
  },
8877
9004
  sizes: {
8878
9005
  sm: {
8879
- padding: "0.5625rem 1.25rem",
9006
+ padding: "0.5rem 1.25rem",
8880
9007
  fontSize: "text-xs"
8881
9008
  },
8882
9009
  md: {
8883
9010
  padding: "0.75rem 1.75rem",
8884
- fontSize: "text-xs"
9011
+ fontSize: "text-sm"
8885
9012
  },
8886
9013
  lg: {
8887
9014
  padding: "0.875rem 2.25rem",
8888
- fontSize: "text-sm"
9015
+ fontSize: "text-base"
8889
9016
  },
8890
9017
  xl: {
8891
- padding: "1rem 2.75rem",
8892
- fontSize: "text-base"
9018
+ padding: "1.125rem 2.75rem",
9019
+ fontSize: "text-lg"
8893
9020
  }
8894
9021
  },
8895
- variants: [
8896
- {
8897
- id: "primary",
8898
- name: "Primary",
8899
- enabled: true,
8900
- priority: 1,
8901
- background: {
8902
- type: "solid",
8903
- colorToken: "primary"
8904
- },
8905
- borderRadius: "rounded-md",
8906
- border: { widthClass: "border" },
8907
- effects: {
8908
- hover: [{
9022
+ roles: {
9023
+ primary: {
9024
+ kind: "complete",
9025
+ appearance: {
9026
+ background: {
9027
+ type: "solid",
9028
+ colorToken: "primary"
9029
+ },
9030
+ borderRadius: "rounded-md",
9031
+ effects: {
9032
+ hover: [{
9033
+ effectId: "drop-shadow",
9034
+ options: { intensity: "medium" }
9035
+ }, {
9036
+ effectId: "scale-up",
9037
+ options: { scale: "1.02" }
9038
+ }],
9039
+ active: [{
9040
+ effectId: "scale-down",
9041
+ options: { scale: "0.98" }
9042
+ }]
9043
+ }
9044
+ }
9045
+ },
9046
+ secondary: {
9047
+ kind: "complete",
9048
+ appearance: {
9049
+ background: {
9050
+ type: "surface",
9051
+ role: "link"
9052
+ },
9053
+ borderRadius: "rounded-md",
9054
+ border: { widthClass: "border-2" },
9055
+ effects: { hover: [{
8909
9056
  effectId: "background-on-hover",
8910
- options: { colorToken: "primary-100" }
8911
- }],
8912
- active: [{
8913
- effectId: "scale-down",
8914
- options: { scale: "0.98" }
8915
- }]
9057
+ options: { colorToken: "surface" }
9058
+ }] }
8916
9059
  }
8917
9060
  },
8918
- {
8919
- id: "secondary",
8920
- name: "Secondary",
8921
- enabled: true,
8922
- priority: 2,
8923
- background: {
8924
- type: "solid",
8925
- colorToken: "secondary"
8926
- },
8927
- borderRadius: "rounded-md",
8928
- border: { widthClass: "border" },
8929
- effects: { hover: [{
8930
- effectId: "background-on-hover",
8931
- options: { colorToken: "surface" }
8932
- }] }
8933
- },
8934
- {
8935
- id: "outline",
8936
- name: "Outline",
8937
- enabled: true,
8938
- priority: 3,
8939
- background: {
8940
- type: "surface",
8941
- role: "link"
8942
- },
8943
- borderRadius: "rounded-md",
8944
- border: { widthClass: "border" },
8945
- effects: { hover: [{
8946
- effectId: "background-on-hover",
8947
- options: { colorToken: "muted" }
8948
- }] }
8949
- },
8950
- {
8951
- id: "ghost",
8952
- name: "Ghost",
8953
- enabled: true,
8954
- priority: 3,
8955
- background: {
8956
- type: "surface",
8957
- role: "link"
8958
- },
8959
- borderRadius: "rounded-none",
8960
- sizes: {
8961
- sm: {
8962
- padding: "0.375rem 0",
8963
- fontSize: "text-xs"
9061
+ outline: {
9062
+ kind: "complete",
9063
+ appearance: {
9064
+ background: {
9065
+ type: "surface",
9066
+ role: "link"
8964
9067
  },
8965
- md: {
8966
- padding: "0.5rem 0",
8967
- fontSize: "text-sm"
8968
- },
8969
- lg: {
8970
- padding: "0.625rem 0",
8971
- fontSize: "text-base"
8972
- },
8973
- xl: {
8974
- padding: "0.75rem 0",
8975
- fontSize: "text-lg"
8976
- }
8977
- },
8978
- effects: { base: [{
8979
- effectId: "border-bottom-grow",
8980
- options: {
8981
- height: 1,
8982
- colorToken: "accent",
8983
- origin: "left"
8984
- }
8985
- }] }
8986
- }
8987
- ]
8988
- }
8989
- });
8990
- //#endregion
8991
- //#region ../theme-core/src/buttons/personalities/pebble.ts
8992
- /**
8993
- * Pebble — large pill primaries with generous padding. Reads as spa-luxurious
8994
- * and natural; pairs well with wellness/yoga/dance brands.
8995
- */
8996
- const pebble = defineButtonPersonality({
8997
- id: "pebble",
8998
- name: "Pebble",
8999
- description: "Generous pill buttons with a quiet colour-softening hover.",
9000
- tags: {
9001
- tone: "classic",
9002
- energy: "calm",
9003
- motion: "standard",
9004
- era: "organic"
9005
- },
9006
- buttonSystem: {
9007
- global: {
9008
- cornerStyle: "pill",
9009
- textTransform: "none",
9010
- fontWeight: 500,
9011
- borderWidth: 2,
9012
- hoverTransform: "none",
9013
- hoverColor: "none"
9014
- },
9015
- sizes: {
9016
- sm: {
9017
- padding: "0.625rem 1.75rem",
9018
- fontSize: "text-sm"
9019
- },
9020
- md: { padding: "0.875rem 2.25rem" },
9021
- lg: {
9022
- padding: "1.125rem 2.75rem",
9023
- fontSize: "text-lg"
9024
- },
9025
- xl: {
9026
- padding: "1.5rem 3.5rem",
9027
- fontSize: "text-xl"
9028
- }
9029
- },
9030
- variants: [
9031
- {
9032
- id: "primary",
9033
- name: "Primary",
9034
- enabled: true,
9035
- priority: 1,
9036
- background: {
9037
- type: "solid",
9038
- colorToken: "primary"
9039
- },
9040
- borderRadius: "rounded-full",
9041
- effects: { hover: [{
9042
- effectId: "darken-background",
9043
- options: { amount: 100 }
9044
- }] }
9045
- },
9046
- {
9047
- id: "secondary",
9048
- name: "Secondary",
9049
- enabled: true,
9050
- priority: 2,
9051
- background: {
9052
- type: "surface",
9053
- role: "link"
9054
- },
9055
- borderRadius: "rounded-full",
9056
- border: { widthClass: "border-2" },
9057
- effects: { hover: [{
9058
- effectId: "background-on-hover",
9059
- options: { colorToken: "accent" }
9060
- }] }
9061
- },
9062
- {
9063
- id: "outline",
9064
- name: "Outline",
9065
- enabled: true,
9066
- priority: 3,
9067
- background: {
9068
- type: "surface",
9069
- role: "link"
9070
- },
9071
- borderRadius: "rounded-full",
9072
- border: { widthClass: "border" },
9073
- effects: { hover: [{
9074
- effectId: "background-on-hover",
9075
- options: { colorToken: "surface" }
9076
- }] }
9068
+ borderRadius: "rounded-md",
9069
+ border: { widthClass: "border-2" },
9070
+ effects: { hover: [{
9071
+ effectId: "background-on-hover",
9072
+ options: { colorToken: "surface" }
9073
+ }] }
9074
+ }
9077
9075
  },
9078
- {
9079
- id: "ghost",
9080
- name: "Ghost",
9081
- enabled: true,
9082
- priority: 3,
9083
- background: {
9084
- type: "surface",
9085
- role: "link"
9086
- },
9087
- borderRadius: "rounded-full",
9088
- sizes: {
9089
- sm: {
9090
- padding: "0.375rem 0.875rem",
9091
- fontSize: "text-sm"
9076
+ ghost: {
9077
+ kind: "complete",
9078
+ appearance: {
9079
+ background: {
9080
+ type: "surface",
9081
+ role: "link"
9092
9082
  },
9093
- md: { padding: "0.5rem 1rem" },
9094
- lg: {
9095
- padding: "0.625rem 1.25rem",
9096
- fontSize: "text-lg"
9083
+ borderRadius: "rounded-none",
9084
+ sizes: {
9085
+ sm: {
9086
+ padding: "0.375rem 0",
9087
+ fontSize: "text-xs"
9088
+ },
9089
+ md: {
9090
+ padding: "0.5rem 0",
9091
+ fontSize: "text-sm"
9092
+ },
9093
+ lg: {
9094
+ padding: "0.625rem 0",
9095
+ fontSize: "text-base"
9096
+ },
9097
+ xl: {
9098
+ padding: "0.75rem 0",
9099
+ fontSize: "text-lg"
9100
+ }
9097
9101
  },
9098
- xl: {
9099
- padding: "0.75rem 1.5rem",
9100
- fontSize: "text-xl"
9101
- }
9102
+ effects: { base: [{
9103
+ effectId: "border-bottom-grow",
9104
+ options: {
9105
+ height: 1,
9106
+ colorToken: "accent",
9107
+ origin: "left"
9108
+ }
9109
+ }] }
9102
9110
  }
9111
+ },
9112
+ accent: {
9113
+ kind: "derive",
9114
+ from: "primary",
9115
+ overrides: { background: {
9116
+ type: "solid",
9117
+ colorToken: "accent"
9118
+ } }
9103
9119
  }
9104
- ]
9120
+ }
9105
9121
  }
9106
9122
  });
9107
- //#endregion
9108
- //#region ../theme-core/src/buttons/personalities/quiet-arrow.ts
9109
- /**
9110
- * Quiet Arrow — wide, flat editorial buttons for grounded service businesses.
9111
- * Primary and secondary keep stable semantic colours; surrounding surfaces are
9112
- * responsible for contrast rather than reversing the button roles.
9113
- */
9114
- const quietArrow = defineButtonPersonality({
9115
- id: "quiet-arrow",
9116
- name: "Quiet Arrow",
9117
- description: "Wide, flat arrow buttons with a solid primary and a primary-colour outlined secondary.",
9123
+ const flatInkRecipe = defineStaticButtonStyleRecipe({
9124
+ id: "flat-ink",
9125
+ accessibilityLabel: "Flat rounded buttons with arrow affordances",
9118
9126
  tags: {
9119
9127
  tone: "classic",
9120
9128
  energy: "calm",
9121
9129
  motion: "subtle",
9122
9130
  era: "editorial"
9123
9131
  },
9124
- buttonSystem: {
9132
+ system: {
9125
9133
  global: {
9126
9134
  cornerStyle: "rounded",
9127
- textTransform: "uppercase",
9128
- fontWeight: 500,
9135
+ textTransform: "none",
9136
+ fontWeight: 600,
9129
9137
  borderWidth: 1,
9130
9138
  typography: "body",
9131
- fontSize: "text-sm",
9132
- letterSpacing: "loose",
9133
- hoverTransform: "none",
9139
+ letterSpacing: "tight",
9140
+ paddingPreset: "default",
9141
+ hoverTransform: "lift",
9134
9142
  hoverColor: "none"
9135
9143
  },
9136
9144
  sizes: {
9137
9145
  sm: {
9138
- padding: "0.625rem 1.25rem",
9139
- fontSize: "text-xs"
9140
- },
9141
- md: {
9142
- padding: "0.8125rem 1.75rem",
9146
+ padding: "0.625rem 1.125rem",
9143
9147
  fontSize: "text-sm"
9144
9148
  },
9149
+ md: { padding: "0.8125rem 1.375rem" },
9145
9150
  lg: {
9146
- padding: "0.9375rem 2.125rem",
9147
- fontSize: "text-sm"
9151
+ padding: "0.9375rem 1.625rem",
9152
+ fontSize: "text-lg"
9148
9153
  },
9149
9154
  xl: {
9150
- padding: "1.0625rem 2.5rem",
9151
- fontSize: "text-base"
9155
+ padding: "1.0625rem 2rem",
9156
+ fontSize: "text-xl"
9152
9157
  }
9153
9158
  },
9154
- variants: [
9155
- {
9156
- id: "primary",
9157
- name: "Primary",
9158
- enabled: true,
9159
- priority: 1,
9160
- background: {
9161
- type: "solid",
9162
- colorToken: "primary"
9163
- },
9164
- borderRadius: "rounded-xl",
9165
- affordance: { iconId: "ArrowRight" },
9166
- effects: { hover: [{
9167
- effectId: "darken-background",
9168
- options: { amount: 100 }
9169
- }] }
9159
+ roles: {
9160
+ primary: {
9161
+ kind: "complete",
9162
+ appearance: {
9163
+ background: {
9164
+ type: "solid",
9165
+ colorToken: "primary"
9166
+ },
9167
+ borderRadius: "rounded-lg",
9168
+ affordance: { iconId: "ArrowRight" },
9169
+ effects: { hover: [{
9170
+ effectId: "lift",
9171
+ options: { translateY: "-2px" }
9172
+ }, {
9173
+ effectId: "background-on-hover",
9174
+ options: { colorToken: "secondary" }
9175
+ }] }
9176
+ }
9170
9177
  },
9171
- {
9172
- id: "secondary",
9173
- name: "Secondary",
9174
- enabled: true,
9175
- priority: 2,
9176
- background: {
9177
- type: "surface",
9178
- role: "link"
9179
- },
9180
- borderRadius: "rounded-xl",
9181
- border: { widthClass: "border" },
9182
- affordance: { iconId: "ArrowRight" },
9183
- effects: { hover: [{
9184
- effectId: "background-on-hover",
9185
- options: { colorToken: "surfaceAlt" }
9186
- }] }
9178
+ secondary: {
9179
+ kind: "complete",
9180
+ appearance: {
9181
+ background: {
9182
+ type: "surface",
9183
+ role: "link"
9184
+ },
9185
+ borderRadius: "rounded-lg",
9186
+ border: { widthClass: "border" },
9187
+ effects: { hover: [{
9188
+ effectId: "lift",
9189
+ options: { translateY: "-2px" }
9190
+ }, {
9191
+ effectId: "background-on-hover",
9192
+ options: { colorToken: "surfaceAlt" }
9193
+ }] }
9194
+ }
9187
9195
  },
9188
- {
9189
- id: "outline",
9190
- name: "Outline",
9191
- enabled: true,
9192
- priority: 3,
9193
- background: {
9194
- type: "surface",
9195
- role: "link"
9196
- },
9197
- borderRadius: "rounded-xl",
9198
- border: { widthClass: "border" }
9196
+ outline: {
9197
+ kind: "complete",
9198
+ appearance: {
9199
+ background: {
9200
+ type: "surface",
9201
+ role: "link"
9202
+ },
9203
+ borderRadius: "rounded-lg",
9204
+ border: { widthClass: "border" },
9205
+ effects: { hover: [{
9206
+ effectId: "lift",
9207
+ options: { translateY: "-2px" }
9208
+ }, {
9209
+ effectId: "background-on-hover",
9210
+ options: { colorToken: "surfaceAlt" }
9211
+ }] }
9212
+ }
9199
9213
  },
9200
- {
9201
- id: "ghost",
9202
- name: "Ghost",
9203
- enabled: true,
9204
- priority: 3,
9205
- background: {
9206
- type: "surface",
9207
- role: "link"
9208
- },
9209
- borderRadius: "rounded-xl"
9214
+ ghost: {
9215
+ kind: "complete",
9216
+ appearance: {
9217
+ background: {
9218
+ type: "surface",
9219
+ role: "link"
9220
+ },
9221
+ borderRadius: "rounded-lg",
9222
+ effects: { hover: [{
9223
+ effectId: "lift",
9224
+ options: { translateY: "-2px" }
9225
+ }] }
9226
+ }
9227
+ },
9228
+ accent: {
9229
+ kind: "complete",
9230
+ appearance: {
9231
+ background: {
9232
+ type: "solid",
9233
+ colorToken: "accent"
9234
+ },
9235
+ borderRadius: "rounded-lg",
9236
+ affordance: { iconId: "ArrowRight" },
9237
+ effects: { hover: [{
9238
+ effectId: "lift",
9239
+ options: { translateY: "-2px" }
9240
+ }] }
9241
+ }
9210
9242
  }
9211
- ]
9243
+ }
9212
9244
  }
9213
9245
  });
9214
- //#endregion
9215
- //#region ../theme-core/src/buttons/personalities/showtime-pill.ts
9216
- /**
9217
- * Showtime Pill — bold, spacious, fully rounded buttons with playful pop
9218
- * motion. Suits energetic schools, performers, festivals, and community brands
9219
- * that need calls to action to feel loud without becoming visually chaotic.
9220
- */
9221
- const showtimePill = defineButtonPersonality({
9246
+ const showtimePillRecipe = defineStaticButtonStyleRecipe({
9222
9247
  id: "showtime-pill",
9223
- name: "Showtime Pill",
9224
- description: "Bold pill buttons with uppercase labels, generous padding, and a playful pop.",
9248
+ accessibilityLabel: "Bold uppercase pill buttons with playful movement",
9225
9249
  tags: {
9226
9250
  tone: "bold",
9227
9251
  energy: "vibrant",
9228
9252
  motion: "expressive",
9229
9253
  era: "modern"
9230
9254
  },
9231
- buttonSystem: {
9255
+ system: {
9232
9256
  global: {
9233
9257
  cornerStyle: "pill",
9234
9258
  shadow: "low",
@@ -9259,272 +9283,376 @@ const showtimePill = defineButtonPersonality({
9259
9283
  fontSize: "1.125rem"
9260
9284
  }
9261
9285
  },
9262
- variants: [
9263
- {
9264
- id: "primary",
9265
- name: "Primary",
9266
- enabled: true,
9267
- priority: 1,
9268
- background: {
9269
- type: "solid",
9270
- colorToken: "primary"
9271
- },
9272
- borderRadius: "rounded-full",
9273
- effects: { hover: [{ effectId: "pop" }] }
9274
- },
9275
- {
9276
- id: "secondary",
9277
- name: "Secondary",
9278
- enabled: true,
9279
- priority: 2,
9280
- background: {
9281
- type: "surface",
9282
- role: "link"
9283
- },
9284
- borderRadius: "rounded-full",
9285
- border: { widthClass: "border-2" },
9286
- effects: { hover: [{ effectId: "pop" }] }
9286
+ roles: {
9287
+ primary: {
9288
+ kind: "complete",
9289
+ appearance: {
9290
+ background: {
9291
+ type: "solid",
9292
+ colorToken: "primary"
9293
+ },
9294
+ borderRadius: "rounded-full",
9295
+ effects: { hover: [{ effectId: "pop" }] }
9296
+ }
9287
9297
  },
9288
- {
9289
- id: "hero",
9290
- name: "Hero",
9291
- enabled: true,
9292
- priority: 1,
9293
- background: {
9294
- type: "solid",
9295
- colorToken: "secondary"
9296
- },
9297
- borderRadius: "rounded-full",
9298
- effects: { hover: [{ effectId: "pop" }] }
9298
+ secondary: {
9299
+ kind: "complete",
9300
+ appearance: {
9301
+ background: {
9302
+ type: "surface",
9303
+ role: "link"
9304
+ },
9305
+ borderRadius: "rounded-full",
9306
+ border: { widthClass: "border-2" },
9307
+ effects: { hover: [{ effectId: "pop" }] }
9308
+ }
9299
9309
  },
9300
- {
9301
- id: "header",
9302
- name: "Header",
9303
- enabled: true,
9304
- priority: 2,
9305
- background: {
9306
- type: "solid",
9307
- colorToken: "background"
9308
- },
9309
- borderRadius: "rounded-full",
9310
- border: { widthClass: "border-2" },
9311
- effects: { hover: [{
9312
- effectId: "background-on-hover",
9313
- options: { colorToken: "surface" }
9314
- }] }
9310
+ outline: {
9311
+ kind: "complete",
9312
+ appearance: {
9313
+ background: {
9314
+ type: "surface",
9315
+ role: "link"
9316
+ },
9317
+ borderRadius: "rounded-full",
9318
+ border: { widthClass: "border-2" },
9319
+ effects: { hover: [{
9320
+ effectId: "background-on-hover",
9321
+ options: { colorToken: "background" }
9322
+ }] }
9323
+ }
9315
9324
  },
9316
- {
9317
- id: "outline",
9318
- name: "Outline",
9319
- enabled: true,
9320
- priority: 3,
9321
- background: {
9322
- type: "surface",
9323
- role: "link"
9324
- },
9325
- borderRadius: "rounded-full",
9326
- border: { widthClass: "border-2" },
9327
- effects: { hover: [{
9328
- effectId: "background-on-hover",
9329
- options: { colorToken: "background" }
9330
- }] }
9325
+ ghost: {
9326
+ kind: "complete",
9327
+ appearance: {
9328
+ background: {
9329
+ type: "surface",
9330
+ role: "link"
9331
+ },
9332
+ borderRadius: "rounded-full",
9333
+ effects: { hover: [{
9334
+ effectId: "background-on-hover",
9335
+ options: { colorToken: "surface" }
9336
+ }] }
9337
+ }
9331
9338
  },
9332
- {
9333
- id: "ghost",
9334
- name: "Ghost",
9335
- enabled: true,
9336
- priority: 3,
9337
- background: {
9338
- type: "surface",
9339
- role: "link"
9340
- },
9341
- borderRadius: "rounded-full",
9342
- effects: { hover: [{
9343
- effectId: "background-on-hover",
9344
- options: { colorToken: "surface" }
9345
- }] }
9339
+ accent: {
9340
+ kind: "complete",
9341
+ appearance: {
9342
+ background: {
9343
+ type: "solid",
9344
+ colorToken: "secondary"
9345
+ },
9346
+ borderRadius: "rounded-full",
9347
+ effects: { hover: [{ effectId: "pop" }] }
9348
+ }
9346
9349
  }
9347
- ]
9350
+ }
9348
9351
  }
9349
9352
  });
9350
- //#endregion
9351
- //#region ../theme-core/src/buttons/personalities/soft-pill.ts
9352
- /**
9353
- * Soft Pill — rounded-full pill buttons with a hover lift. The default for
9354
- * Calm Studio and Modern Wellness; reads as quietly modern and welcoming.
9355
- */
9356
- const softPill = defineButtonPersonality({
9357
- id: "soft-pill",
9358
- name: "Soft Pill",
9359
- description: "Rounded pills with a gentle lift on hover. Quietly modern.",
9353
+ const joyStampRecipe = defineStaticButtonStyleRecipe({
9354
+ id: "joy-stamp",
9355
+ accessibilityLabel: "Bold pill buttons with a hard offset shadow",
9360
9356
  tags: {
9361
- tone: "classic",
9362
- energy: "calm",
9363
- motion: "subtle",
9357
+ tone: "bold",
9358
+ energy: "vibrant",
9359
+ motion: "expressive",
9364
9360
  era: "organic"
9365
9361
  },
9366
- buttonSystem: {
9362
+ system: {
9367
9363
  global: {
9368
9364
  cornerStyle: "pill",
9369
- shadow: "low",
9365
+ shadow: "high",
9370
9366
  textTransform: "none",
9371
- fontWeight: 500,
9367
+ typography: "heading",
9368
+ fontWeight: 700,
9369
+ fontSize: "text-lg",
9372
9370
  borderWidth: 2,
9373
- hoverTransform: "lift",
9371
+ hoverTransform: "none",
9374
9372
  hoverColor: "none"
9375
9373
  },
9376
- variants: [
9377
- {
9378
- id: "primary",
9379
- name: "Primary",
9380
- enabled: true,
9381
- priority: 1,
9382
- background: {
9383
- type: "solid",
9384
- colorToken: "primary"
9385
- },
9386
- borderRadius: "rounded-full",
9387
- shadow: {
9388
- elevation: "low",
9389
- softness: "soft",
9390
- position: "bottom"
9391
- },
9392
- effects: {
9393
- hover: [{
9394
- effectId: "lift",
9395
- options: { translateY: "-1px" }
9396
- }],
9397
- active: [{
9398
- effectId: "scale-down",
9399
- options: { scale: "0.98" }
9400
- }]
9374
+ sizes: {
9375
+ sm: {
9376
+ padding: "0.625rem 1.25rem",
9377
+ fontSize: "text-base"
9378
+ },
9379
+ md: {
9380
+ padding: "0.875rem 1.75rem",
9381
+ fontSize: "text-lg"
9382
+ },
9383
+ lg: {
9384
+ padding: "1.125rem 2.25rem",
9385
+ fontSize: "text-xl"
9386
+ },
9387
+ xl: {
9388
+ padding: "1.25rem 2.75rem",
9389
+ fontSize: "text-2xl"
9390
+ }
9391
+ },
9392
+ roles: {
9393
+ primary: {
9394
+ kind: "complete",
9395
+ appearance: {
9396
+ background: {
9397
+ type: "solid",
9398
+ colorToken: "primary"
9399
+ },
9400
+ borderRadius: "rounded-full",
9401
+ shadow: {
9402
+ elevation: "high",
9403
+ softness: "hard",
9404
+ position: "bottom-right",
9405
+ colorToken: "text"
9406
+ },
9407
+ effects: { hover: [{ effectId: "press-down" }] }
9401
9408
  }
9402
9409
  },
9403
- {
9404
- id: "secondary",
9405
- name: "Secondary",
9406
- enabled: true,
9407
- priority: 2,
9408
- background: {
9409
- type: "surface",
9410
- role: "link"
9411
- },
9412
- borderRadius: "rounded-full",
9413
- border: { widthClass: "border-2" },
9414
- effects: { hover: [{
9415
- effectId: "background-on-hover",
9416
- options: { colorToken: "surface" }
9417
- }] }
9410
+ secondary: {
9411
+ kind: "complete",
9412
+ appearance: {
9413
+ background: {
9414
+ type: "surface",
9415
+ role: "link"
9416
+ },
9417
+ borderRadius: "rounded-full",
9418
+ border: { widthClass: "border-2" },
9419
+ shadow: {
9420
+ elevation: "high",
9421
+ softness: "hard",
9422
+ position: "bottom-right",
9423
+ colorToken: "secondary"
9424
+ },
9425
+ effects: { hover: [{ effectId: "press-down" }] }
9426
+ }
9418
9427
  },
9419
- {
9420
- id: "outline",
9421
- name: "Outline",
9422
- enabled: true,
9423
- priority: 3,
9424
- background: {
9425
- type: "surface",
9426
- role: "link"
9427
- },
9428
- borderRadius: "rounded-full",
9429
- border: { widthClass: "border" },
9430
- effects: { hover: [{
9431
- effectId: "background-on-hover",
9432
- options: { colorToken: "surface" }
9433
- }] }
9428
+ outline: {
9429
+ kind: "derive",
9430
+ from: "secondary"
9434
9431
  },
9435
- {
9436
- id: "ghost",
9437
- name: "Ghost",
9438
- enabled: true,
9439
- priority: 3,
9440
- background: {
9441
- type: "surface",
9442
- role: "link"
9443
- },
9444
- borderRadius: "rounded-full",
9445
- effects: { hover: [{
9446
- effectId: "background-on-hover",
9447
- options: { colorToken: "surface" }
9448
- }] }
9432
+ ghost: {
9433
+ kind: "complete",
9434
+ appearance: {
9435
+ background: {
9436
+ type: "surface",
9437
+ role: "link"
9438
+ },
9439
+ borderRadius: "rounded-full",
9440
+ effects: { hover: [{ effectId: "press-down" }] }
9441
+ }
9442
+ },
9443
+ accent: {
9444
+ kind: "derive",
9445
+ from: "primary",
9446
+ overrides: { background: {
9447
+ type: "solid",
9448
+ colorToken: "accent"
9449
+ } }
9449
9450
  }
9450
- ]
9451
+ }
9451
9452
  }
9452
9453
  });
9453
- //#endregion
9454
- //#region ../theme-core/src/buttons/personalities/two-tone-pill.ts
9455
- const solidSecondary = {
9456
- id: "secondary",
9457
- name: "Secondary",
9458
- enabled: true,
9459
- priority: 2,
9460
- background: {
9461
- type: "solid",
9462
- colorToken: unsafeAsOpaqueButtonColourToken("secondary")
9454
+ const ornateRitualRecipe = defineStaticButtonStyleRecipe({
9455
+ id: "ornate-ritual",
9456
+ accessibilityLabel: "Uppercase framed buttons with restrained movement",
9457
+ tags: {
9458
+ tone: "classic",
9459
+ energy: "vibrant",
9460
+ motion: "subtle",
9461
+ era: "organic"
9463
9462
  },
9464
- borderRadius: "rounded-full",
9465
- shadow: {
9466
- elevation: "none",
9467
- softness: null,
9468
- position: "bottom"
9469
- },
9470
- effects: {
9471
- hover: [{
9472
- effectId: "background-on-hover-alpha",
9473
- options: {
9474
- colorToken: "secondary",
9475
- opacity: 88
9463
+ system: {
9464
+ global: {
9465
+ cornerStyle: "rounded",
9466
+ textTransform: "uppercase",
9467
+ fontWeight: 600,
9468
+ borderWidth: 1,
9469
+ hoverTransform: "none",
9470
+ hoverColor: "none",
9471
+ letterSpacing: "loose"
9472
+ },
9473
+ sizes: {
9474
+ sm: {
9475
+ padding: "0.5625rem 1.25rem",
9476
+ fontSize: "text-xs"
9477
+ },
9478
+ md: {
9479
+ padding: "0.75rem 1.75rem",
9480
+ fontSize: "text-xs"
9481
+ },
9482
+ lg: {
9483
+ padding: "0.875rem 2.25rem",
9484
+ fontSize: "text-sm"
9485
+ },
9486
+ xl: {
9487
+ padding: "1rem 2.75rem",
9488
+ fontSize: "text-base"
9476
9489
  }
9477
- }],
9478
- active: [{
9479
- effectId: "scale-down",
9480
- options: { scale: "0.98" }
9481
- }]
9490
+ },
9491
+ roles: {
9492
+ primary: {
9493
+ kind: "complete",
9494
+ appearance: {
9495
+ background: {
9496
+ type: "solid",
9497
+ colorToken: "primary"
9498
+ },
9499
+ borderRadius: "rounded-md",
9500
+ border: { widthClass: "border" },
9501
+ effects: {
9502
+ hover: [{
9503
+ effectId: "background-on-hover",
9504
+ options: { colorToken: "primary-100" }
9505
+ }],
9506
+ active: [{
9507
+ effectId: "scale-down",
9508
+ options: { scale: "0.98" }
9509
+ }]
9510
+ }
9511
+ }
9512
+ },
9513
+ secondary: {
9514
+ kind: "complete",
9515
+ appearance: {
9516
+ background: {
9517
+ type: "solid",
9518
+ colorToken: "secondary"
9519
+ },
9520
+ borderRadius: "rounded-md",
9521
+ border: { widthClass: "border" },
9522
+ effects: { hover: [{
9523
+ effectId: "background-on-hover",
9524
+ options: { colorToken: "surface" }
9525
+ }] }
9526
+ }
9527
+ },
9528
+ outline: {
9529
+ kind: "complete",
9530
+ appearance: {
9531
+ background: {
9532
+ type: "surface",
9533
+ role: "link"
9534
+ },
9535
+ borderRadius: "rounded-md",
9536
+ border: { widthClass: "border" },
9537
+ effects: { hover: [{
9538
+ effectId: "background-on-hover",
9539
+ options: { colorToken: "muted" }
9540
+ }] }
9541
+ }
9542
+ },
9543
+ ghost: {
9544
+ kind: "complete",
9545
+ appearance: {
9546
+ background: {
9547
+ type: "surface",
9548
+ role: "link"
9549
+ },
9550
+ borderRadius: "rounded-none",
9551
+ sizes: {
9552
+ sm: {
9553
+ padding: "0.375rem 0",
9554
+ fontSize: "text-xs"
9555
+ },
9556
+ md: {
9557
+ padding: "0.5rem 0",
9558
+ fontSize: "text-sm"
9559
+ },
9560
+ lg: {
9561
+ padding: "0.625rem 0",
9562
+ fontSize: "text-base"
9563
+ },
9564
+ xl: {
9565
+ padding: "0.75rem 0",
9566
+ fontSize: "text-lg"
9567
+ }
9568
+ },
9569
+ effects: { base: [{
9570
+ effectId: "border-bottom-grow",
9571
+ options: {
9572
+ height: 1,
9573
+ colorToken: "accent",
9574
+ origin: "left"
9575
+ }
9576
+ }] }
9577
+ }
9578
+ },
9579
+ accent: {
9580
+ kind: "derive",
9581
+ from: "primary",
9582
+ overrides: { background: {
9583
+ type: "solid",
9584
+ colorToken: "accent"
9585
+ } }
9586
+ }
9587
+ }
9482
9588
  }
9483
- };
9589
+ });
9484
9590
  //#endregion
9485
- //#region ../theme-core/src/buttons/personalities/index.ts
9486
- const buttonPersonalities = [
9487
- softPill,
9488
- defineButtonPersonality({
9489
- id: "two-tone-pill",
9490
- name: "Two-tone Pill",
9491
- description: "Rounded pills with distinct solid primary and secondary brand colours.",
9492
- tags: softPill.tags,
9493
- buttonSystem: {
9494
- ...softPill.buttonSystem,
9495
- variants: softPill.buttonSystem.variants.map((variant) => variant.id === "secondary" ? solidSecondary : variant)
9496
- }
9497
- }),
9498
- earthPill,
9499
- confidentChip,
9500
- editorialLink,
9501
- pebble,
9502
- inkStamp,
9503
- flatInk,
9504
- brushedWash,
9505
- showtimePill,
9506
- joyStamp,
9507
- ornateRitual,
9508
- quietArrow
9591
+ //#region ../theme-core/src/buttons/styles/registry.ts
9592
+ const buttonStyleRecipes = [
9593
+ softPillRecipe,
9594
+ earthPillRecipe,
9595
+ confidentChipRecipe,
9596
+ editorialLinkRecipe,
9597
+ pebbleRecipe,
9598
+ inkStampRecipe,
9599
+ flatInkRecipe,
9600
+ brushedWashRecipe,
9601
+ showtimePillRecipe,
9602
+ joyStampRecipe,
9603
+ ornateRitualRecipe,
9604
+ quietArrowRecipe
9509
9605
  ];
9510
- new Map(buttonPersonalities.map((p) => [p.id, p]));
9511
- buttonPersonalities.map((personality) => ({
9512
- id: personality.id,
9513
- name: personality.name,
9514
- description: personality.description,
9515
- category: presetCategory(personality),
9516
- buttonSystem: personality.buttonSystem,
9517
- compatibility: {
9518
- ...personality.tags.tone ? { tones: [personality.tags.tone] } : {},
9519
- ...personality.tags.energy ? { energies: [personality.tags.energy] } : {}
9520
- }
9521
- }));
9522
- function presetCategory(personality) {
9523
- switch (personality.tags.era) {
9606
+ const recipesById = new Map(buttonStyleRecipes.map((recipe) => [recipe.id, recipe]));
9607
+ function findButtonStyleRecipe(id) {
9608
+ const recipe = recipesById.get(id);
9609
+ if (recipe === void 0) throw new Error(`Button style registry is missing known style ${id}`);
9610
+ return recipe;
9611
+ }
9612
+ function resolveButtonStyleSelection(selection) {
9613
+ switch (selection.styleId) {
9614
+ case "soft-pill": return resolveButtonStyleRecipe(softPillRecipe, selection.parameters);
9615
+ case "earth-pill": return resolveButtonStyleRecipe(earthPillRecipe, selection.parameters);
9616
+ case "confident-chip": return resolveButtonStyleRecipe(confidentChipRecipe, selection.parameters);
9617
+ case "editorial-link": return resolveButtonStyleRecipe(editorialLinkRecipe, selection.parameters);
9618
+ case "pebble": return resolveButtonStyleRecipe(pebbleRecipe, selection.parameters);
9619
+ case "ink-stamp": return resolveButtonStyleRecipe(inkStampRecipe, selection.parameters);
9620
+ case "flat-ink": return resolveButtonStyleRecipe(flatInkRecipe, selection.parameters);
9621
+ case "brushed-wash": return resolveButtonStyleRecipe(brushedWashRecipe, selection.parameters);
9622
+ case "showtime-pill": return resolveButtonStyleRecipe(showtimePillRecipe, selection.parameters);
9623
+ case "joy-stamp": return resolveButtonStyleRecipe(joyStampRecipe, selection.parameters);
9624
+ case "ornate-ritual": return resolveButtonStyleRecipe(ornateRitualRecipe, selection.parameters);
9625
+ case "quiet-arrow": return resolveButtonStyleRecipe(quietArrowRecipe, selection.parameters);
9626
+ }
9627
+ }
9628
+ function defaultButtonStyleSelectionFor(styleId) {
9629
+ return {
9630
+ styleId,
9631
+ parameters: findButtonStyleRecipe(styleId).defaultParameters
9632
+ };
9633
+ }
9634
+ if (buttonStyleRecipes.map((recipe) => recipe.id).length !== BUTTON_STYLE_IDS.length || BUTTON_STYLE_IDS.some((id) => !recipesById.has(id))) throw new Error("Button style registry and ButtonStyleId catalog are out of sync");
9635
+ buttonStyleRecipes.map((recipe) => {
9636
+ const resolved = resolveButtonStyleSelection(defaultButtonStyleSelectionFor(recipe.id));
9637
+ if (resolved.kind === "error") throw new Error(`Invalid registered button style ${recipe.id}: ${resolved.error.issues.join(", ")}`);
9638
+ return {
9639
+ id: recipe.id,
9640
+ name: recipe.accessibilityLabel,
9641
+ description: recipe.accessibilityLabel,
9642
+ category: presetCategory(recipe),
9643
+ buttonSystem: resolved.buttonSystem,
9644
+ compatibility: {
9645
+ ...recipe.tags.tone ? { tones: [recipe.tags.tone] } : {},
9646
+ ...recipe.tags.energy ? { energies: [recipe.tags.energy] } : {}
9647
+ }
9648
+ };
9649
+ });
9650
+ function presetCategory(recipe) {
9651
+ switch (recipe.tags.era) {
9524
9652
  case "retro": return "retro";
9525
9653
  case "editorial": return "minimal";
9526
9654
  case "organic": return "artistic";
9527
- case "modern": return personality.tags.tone === "bold" ? "modern" : "technical";
9655
+ case "modern": return recipe.tags.tone === "bold" ? "modern" : "technical";
9528
9656
  case "technical": return "technical";
9529
9657
  case void 0: return "modern";
9530
9658
  }
@@ -10868,6 +10996,43 @@ function consumeSearchParam(paramName) {
10868
10996
  return value;
10869
10997
  }
10870
10998
  //#endregion
10999
+ //#region ../blocks/src/system/runtime/shared/utils/validation.ts
11000
+ const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
11001
+ /**
11002
+ * Validate registration form data.
11003
+ *
11004
+ * Only `name` and `email` are required fields. The `phone` and `notes` fields
11005
+ * are optional and not validated by this function.
11006
+ *
11007
+ * @param data - Form data to validate
11008
+ * @returns Object with field errors (empty if valid)
11009
+ */
11010
+ function validateFormData$1(data) {
11011
+ const errors = {};
11012
+ if (!data.name.trim()) errors.name = "Name is required";
11013
+ if (!data.email.trim()) errors.email = "Email is required";
11014
+ else if (!EMAIL_REGEX.test(data.email.trim())) errors.email = "Please enter a valid email";
11015
+ return errors;
11016
+ }
11017
+ //#endregion
11018
+ //#region ../blocks/src/system/runtime/shared/utils/wizardSteps.ts
11019
+ /**
11020
+ * Shared Wizard Step Utilities
11021
+ *
11022
+ * Helpers for wizard step navigation in registration blocks.
11023
+ */
11024
+ /**
11025
+ * Get the 1-based step number for a step in the wizard.
11026
+ *
11027
+ * @param step - The step to find
11028
+ * @param wizardSteps - Array of wizard steps
11029
+ * @returns 1-based step number, or 1 if step not found
11030
+ */
11031
+ function getStepNumber(step, wizardSteps) {
11032
+ const index = wizardSteps.indexOf(step);
11033
+ return index >= 0 ? index + 1 : 1;
11034
+ }
11035
+ //#endregion
10871
11036
  //#region ../blocks/src/system/runtime/nodes/booking-form.client.tsx
10872
11037
  const PORTAL_REBOOK_QUERY_PARAM = "rebook";
10873
11038
  const PORTAL_REBOOK_QUERY_VALUE = "1";
@@ -13337,14 +13502,14 @@ const eventRegistrationWizardDeps = {
13337
13502
  useEventRegistrationSubmit,
13338
13503
  useDiscountCodeUi
13339
13504
  };
13340
- function parseCurrencyInputToCents(value) {
13505
+ function parseCurrencyInputToCents$1(value) {
13341
13506
  const trimmed = value.trim();
13342
13507
  if (!trimmed) return null;
13343
13508
  const amount = Number(trimmed);
13344
13509
  if (!Number.isFinite(amount) || amount < 0) return null;
13345
13510
  return Math.round(amount * 100);
13346
13511
  }
13347
- function formatCentsAsCurrencyInput(amount) {
13512
+ function formatCentsAsCurrencyInput$1(amount) {
13348
13513
  const major = amount / 100;
13349
13514
  return Number.isInteger(major) ? String(major) : major.toFixed(2);
13350
13515
  }
@@ -13405,7 +13570,7 @@ function useEventRegistrationWizardWithDeps(options, deps) {
13405
13570
  const [selectedTicketLines, setSelectedTicketLines] = useState([]);
13406
13571
  const [selectedSlidingAmountInput, setSelectedSlidingAmountInput] = useState(() => {
13407
13572
  const slidingScale = getSlidingScaleForEvent(preSelectedEvent ?? null);
13408
- return slidingScale?.suggestedAmount !== null && slidingScale?.suggestedAmount !== void 0 ? formatCentsAsCurrencyInput(slidingScale.suggestedAmount) : "";
13573
+ return slidingScale?.suggestedAmount !== null && slidingScale?.suggestedAmount !== void 0 ? formatCentsAsCurrencyInput$1(slidingScale.suggestedAmount) : "";
13409
13574
  });
13410
13575
  const [registrationResult, setRegistrationResult] = useState(null);
13411
13576
  const [errorMessage, setErrorMessage] = useState("");
@@ -13478,7 +13643,7 @@ function useEventRegistrationWizardWithDeps(options, deps) {
13478
13643
  occurrenceId,
13479
13644
  attendanceMode: selectedAttendanceMode,
13480
13645
  ticketSelections,
13481
- selectedSlidingAmount: parseCurrencyInputToCents(selectedSlidingAmountInput),
13646
+ selectedSlidingAmount: parseCurrencyInputToCents$1(selectedSlidingAmountInput),
13482
13647
  email: formData.email
13483
13648
  });
13484
13649
  const existingRegistrationResult = deps.useExistingEventRegistration({
@@ -13630,7 +13795,7 @@ function useEventRegistrationWizardWithDeps(options, deps) {
13630
13795
  selectedAttendanceMode,
13631
13796
  singleSelectedTicketTypeId
13632
13797
  ]);
13633
- const selectedSlidingAmount = useMemo(() => parseCurrencyInputToCents(selectedSlidingAmountInput), [selectedSlidingAmountInput]);
13798
+ const selectedSlidingAmount = useMemo(() => parseCurrencyInputToCents$1(selectedSlidingAmountInput), [selectedSlidingAmountInput]);
13634
13799
  const selectedEventSlidingScale = useMemo(() => getSlidingScaleForEvent(selectedEvent ?? preSelectedEvent), [selectedEvent, preSelectedEvent]);
13635
13800
  const slidingScaleSelectionMessage = getSlidingScaleSelectionMessage({
13636
13801
  slidingScale: selectedEventSlidingScale,
@@ -13884,7 +14049,7 @@ function useEventRegistrationWizardWithDeps(options, deps) {
13884
14049
  const handleEventSelect = (event) => {
13885
14050
  setSelectedEvent(event);
13886
14051
  const slidingScale = getSlidingScaleForEvent(event);
13887
- setSelectedSlidingAmountInput(slidingScale?.suggestedAmount !== null && slidingScale?.suggestedAmount !== void 0 ? formatCentsAsCurrencyInput(slidingScale.suggestedAmount) : "");
14052
+ setSelectedSlidingAmountInput(slidingScale?.suggestedAmount !== null && slidingScale?.suggestedAmount !== void 0 ? formatCentsAsCurrencyInput$1(slidingScale.suggestedAmount) : "");
13888
14053
  setSelectedAttendanceMode((event.deliveryMode ?? "in_person") === "hybrid" ? null : (event.deliveryMode ?? "in_person") === "online" ? "online" : "in_person");
13889
14054
  setSelectedTicketLines([]);
13890
14055
  dispatchFlow({
@@ -14128,82 +14293,1421 @@ function useEventRegistrationControllerWithDeps(options, deps) {
14128
14293
  }, [directPaymentOptions, wizard]);
14129
14294
  const next = useCallback(async () => {
14130
14295
  switch (wizard.step) {
14131
- case "select-event":
14132
- if (!wizard.selectedEvent) return {
14133
- ok: false,
14134
- reason: "blocked",
14135
- message: "Select an event to continue."
14136
- };
14137
- wizard.setStep("select-tickets");
14138
- return { ok: true };
14139
- case "select-tickets":
14140
- if (wizard.ticketTypeSelectionRequired && !wizard.hasValidTicketSelection) return {
14296
+ case "select-event":
14297
+ if (!wizard.selectedEvent) return {
14298
+ ok: false,
14299
+ reason: "blocked",
14300
+ message: "Select an event to continue."
14301
+ };
14302
+ wizard.setStep("select-tickets");
14303
+ return { ok: true };
14304
+ case "select-tickets":
14305
+ if (wizard.ticketTypeSelectionRequired && !wizard.hasValidTicketSelection) return {
14306
+ ok: false,
14307
+ reason: "blocked",
14308
+ message: "Choose at least one available ticket to continue."
14309
+ };
14310
+ wizard.setStep("enter-details");
14311
+ return { ok: true };
14312
+ case "enter-details": {
14313
+ const result = await wizard.handleDetailsComplete();
14314
+ return result.ok ? { ok: true } : result;
14315
+ }
14316
+ case "select-payment": {
14317
+ const result = await submit({ redirect: "manual" });
14318
+ return result.ok ? { ok: true } : {
14319
+ ok: false,
14320
+ reason: result.reason === "submission-failed" ? "submission-failed" : "blocked",
14321
+ message: result.message
14322
+ };
14323
+ }
14324
+ case "error":
14325
+ case "success":
14326
+ case "submitting":
14327
+ case "redirecting":
14328
+ case "verifying-payment":
14329
+ case "verifying-timeout":
14330
+ case "payment-failed": return {
14331
+ ok: false,
14332
+ reason: "blocked",
14333
+ message: "This registration step cannot continue."
14334
+ };
14335
+ }
14336
+ }, [submit, wizard]);
14337
+ const back = useCallback(() => {
14338
+ switch (wizard.step) {
14339
+ case "enter-details":
14340
+ wizard.setStep("select-tickets");
14341
+ return;
14342
+ case "select-payment":
14343
+ wizard.setStep("enter-details");
14344
+ return;
14345
+ case "select-tickets":
14346
+ if (!wizard.hasPreSelectedContext) wizard.setStep("select-event");
14347
+ return;
14348
+ case "select-event":
14349
+ case "error":
14350
+ case "success":
14351
+ case "submitting":
14352
+ case "redirecting":
14353
+ case "verifying-payment":
14354
+ case "verifying-timeout":
14355
+ case "payment-failed": return;
14356
+ }
14357
+ }, [wizard]);
14358
+ const updateAttendee = useCallback((values) => {
14359
+ wizard.setFormData(values);
14360
+ }, [wizard]);
14361
+ const updateGuests = useCallback((guests) => {
14362
+ wizard.setGuests(guests);
14363
+ }, [wizard]);
14364
+ const selectPayment = useCallback((selection) => {
14365
+ wizard.handlePaymentSelectionChange(normalizeEventRegistrationDirectPaymentSelection({
14366
+ options: directPaymentOptions,
14367
+ selection: toInternalPaymentSelection(selection)
14368
+ }));
14369
+ }, [directPaymentOptions, wizard]);
14370
+ const selectedTicketTypeId = getSingleSelectedTicketTypeId(wizard.selectedTicketLines);
14371
+ const spamProtection = options.spamProtectionEnabled ?? isSpamProtectionEnabled() ? {
14372
+ enabled: true,
14373
+ provider: "altcha",
14374
+ challenge: {
14375
+ url: getChallengeUrl(),
14376
+ auto: "onfocus"
14377
+ },
14378
+ solution: wizard.altchaSolution
14379
+ } : { enabled: false };
14380
+ const payment = resolvePaymentModel(wizard, directPaymentOptions);
14381
+ return {
14382
+ flow: {
14383
+ status: resolveHeadlessEventRegistrationStatus(wizard),
14384
+ view: resolveHeadlessEventRegistrationView(wizard),
14385
+ steps,
14386
+ currentStep,
14387
+ events: {
14388
+ items: wizard.events,
14389
+ scoped: wizard.scopedEvents,
14390
+ selected: wizard.selectedEvent,
14391
+ preSelected: wizard.preSelectedEvent,
14392
+ hasPreSelectedContext: wizard.hasPreSelectedContext,
14393
+ target: wizard.registrationTarget,
14394
+ occurrenceContext: wizard.occurrenceContext,
14395
+ contentEntry: wizard.contentEntry
14396
+ },
14397
+ tickets: {
14398
+ count: wizard.ticketsCount,
14399
+ max: wizard.maxTicketsNum,
14400
+ selections: wizard.ticketSelections,
14401
+ summaryLines: wizard.ticketSelectionSummaryLines,
14402
+ subtotalCents: wizard.ticketSelectionSubtotalCents,
14403
+ selectedTicketTypeId,
14404
+ selectedTicketType: wizard.selectedTicketType,
14405
+ visibleTicketTypes: wizard.visibleTicketTypes,
14406
+ selectionRequired: wizard.ticketTypeSelectionRequired,
14407
+ hasValidSelection: wizard.hasValidTicketSelection,
14408
+ eventRequiresPayment: wizard.eventRequiresPayment,
14409
+ selectedAttendanceMode: wizard.selectedAttendanceMode
14410
+ },
14411
+ attendee: {
14412
+ values: wizard.formData,
14413
+ errors: wizard.errors
14414
+ },
14415
+ guests: {
14416
+ required: wizard.requireGuestDetails,
14417
+ items: wizard.guests,
14418
+ errors: wizard.guestErrors,
14419
+ shake: wizard.guestSectionShake
14420
+ },
14421
+ payment,
14422
+ discount: {
14423
+ code: wizard.discountCode,
14424
+ isOpen: wizard.discountUiOpen,
14425
+ available: wizard.discountsAvailable,
14426
+ manualCodeEntryAllowed: wizard.manualCodeEntryAllowed,
14427
+ loading: wizard.discountsAvailableLoading,
14428
+ autoAppliedDiscount: wizard.autoAppliedDiscount
14429
+ },
14430
+ portalAuth: {
14431
+ state: wizard.portalAuthState,
14432
+ profile: wizard.portalProfile,
14433
+ isAuthenticated: wizard.portalAuthState === "authenticated",
14434
+ showMagicLinkModal: wizard.showMagicLinkModal
14435
+ },
14436
+ existingRegistration: {
14437
+ state: resolveExistingRegistrationState(wizard),
14438
+ registration: wizard.existingRegistration
14439
+ },
14440
+ waitlistClaim: wizard.waitlistClaim,
14441
+ spamProtection,
14442
+ terminal: {
14443
+ result: wizard.registrationResult,
14444
+ errorMessage: wizard.errorMessage,
14445
+ verification: wizard.verification
14446
+ },
14447
+ actions: {
14448
+ selectEvent: wizard.handleEventSelect,
14449
+ selectAttendanceMode: wizard.setSelectedAttendanceMode,
14450
+ selectTicketType: (ticketTypeId) => {
14451
+ wizard.setSelectedTicketLines(ticketTypeId ? [{
14452
+ ticketTypeId,
14453
+ quantity: 1
14454
+ }] : []);
14455
+ },
14456
+ setTicketSelections: wizard.setSelectedTicketLines,
14457
+ setTicketsCount: wizard.setTicketsCount,
14458
+ updateAttendee,
14459
+ updateGuests,
14460
+ selectPayment,
14461
+ setDiscountCode: wizard.setDiscountCode,
14462
+ setDiscountOpen: wizard.setDiscountUiOpen,
14463
+ setAltchaSolution: wizard.setAltchaSolution,
14464
+ requestLogin: () => wizard.setShowMagicLinkModal(true),
14465
+ logout: wizard.handleLogout,
14466
+ next,
14467
+ back,
14468
+ submit,
14469
+ retry: wizard.handleRetry
14470
+ }
14471
+ },
14472
+ wizard
14473
+ };
14474
+ }
14475
+ function useEventRegistrationWizardControllerWithDeps(options, deps) {
14476
+ const maxTicketsNum = normalizeEventRegistrationMaxTickets(options.maxTickets);
14477
+ return { wizard: deps.useEventRegistrationWizard({
14478
+ anchorId: options.anchorId ?? "event-registration",
14479
+ occurrenceContext: options.occurrenceContext ?? null,
14480
+ contentEntry: options.contentEntry ?? null,
14481
+ events: [...options.events ?? []],
14482
+ maxTicketsNum
14483
+ }) };
14484
+ }
14485
+ function resolveExistingRegistrationState(wizard) {
14486
+ switch (wizard.existingRegistrationState.tag) {
14487
+ case "idle": return {
14488
+ tag: "idle",
14489
+ value: wizard.existingRegistration
14490
+ };
14491
+ case "loading": return {
14492
+ tag: "loading",
14493
+ value: wizard.existingRegistration
14494
+ };
14495
+ case "resolved": return {
14496
+ tag: "loaded",
14497
+ value: wizard.existingRegistration
14498
+ };
14499
+ }
14500
+ }
14501
+ function resolvePaymentModel(wizard, directPaymentOptions) {
14502
+ const internalSelection = normalizeEventRegistrationDirectPaymentSelection({
14503
+ options: directPaymentOptions,
14504
+ selection: wizard.paymentSelection
14505
+ });
14506
+ const selection = toHeadlessPaymentSelection(internalSelection);
14507
+ const directPaymentSelectionState = resolveEventRegistrationDirectPaymentSelection({
14508
+ options: directPaymentOptions,
14509
+ selection: internalSelection
14510
+ });
14511
+ if (wizard.step !== "select-payment") return {
14512
+ kind: "hidden",
14513
+ selection,
14514
+ directPaymentOptions,
14515
+ directPaymentSelectionState,
14516
+ stepState: wizard.paymentStepState
14517
+ };
14518
+ return {
14519
+ kind: wizard.paymentStepState.status,
14520
+ selection,
14521
+ directPaymentOptions,
14522
+ directPaymentSelectionState,
14523
+ stepState: wizard.paymentStepState,
14524
+ eligibility: wizard.eligibility,
14525
+ eligibilityError: wizard.eligibilityError
14526
+ };
14527
+ }
14528
+ function resolveHeadlessDirectPaymentOptions(wizard) {
14529
+ const eligibility = wizard.eligibility;
14530
+ if (!eligibility?.eligible || !eligibility.canPayDirectly || !eligibility.price) return [];
14531
+ const event = wizard.selectedEvent ?? wizard.preSelectedEvent;
14532
+ return buildEventRegistrationPaymentOptionsViewModel({
14533
+ eventTitle: event?.title ?? "Event",
14534
+ paymentTerms: event?.paymentTerms,
14535
+ totalPriceCents: eligibility.price,
14536
+ currency: eligibility.currency,
14537
+ startsAt: wizard.occurrenceContext?.startsAt ?? event?.startsAt
14538
+ }).options;
14539
+ }
14540
+ //#endregion
14541
+ //#region ../blocks/src/system/blocks/courses/shared/pricing.ts
14542
+ function isSaleWindowOpen(option, now) {
14543
+ if (!option.isActive) return false;
14544
+ if (option.saleStartsAt && new Date(option.saleStartsAt) > now) return false;
14545
+ if (option.saleEndsAt && new Date(option.saleEndsAt) < now) return false;
14546
+ return true;
14547
+ }
14548
+ function toExactOrFreeDisplay(amount, currency) {
14549
+ if (amount <= 0) return {
14550
+ kind: "free",
14551
+ currency
14552
+ };
14553
+ return {
14554
+ kind: "exact",
14555
+ amount,
14556
+ currency
14557
+ };
14558
+ }
14559
+ function getSelectableCoursePricingOptionsForNow(course, now) {
14560
+ if (!course.pricing || course.pricing.pricingStrategy !== "tiered") return [];
14561
+ return [...course.pricing.options].filter((option) => isSaleWindowOpen(option, now)).sort((left, right) => {
14562
+ if (left.sortOrder !== right.sortOrder) return left.sortOrder - right.sortOrder;
14563
+ if (left.priceAmount !== right.priceAmount) return left.priceAmount - right.priceAmount;
14564
+ return left.name.localeCompare(right.name);
14565
+ });
14566
+ }
14567
+ function resolveCoursePricingState(course, selectedPricingOptionId, selectedSlidingAmountOrNowInput, nowInput = {}) {
14568
+ const selectedSlidingAmount = typeof selectedSlidingAmountOrNowInput === "number" || selectedSlidingAmountOrNowInput === null ? selectedSlidingAmountOrNowInput : null;
14569
+ const effectiveNowInput = typeof selectedSlidingAmountOrNowInput === "object" && selectedSlidingAmountOrNowInput !== null ? selectedSlidingAmountOrNowInput : nowInput;
14570
+ const currency = course.pricing?.currency ?? course.currency;
14571
+ if (!course.pricing) {
14572
+ const legacyAmount = course.priceCents ?? 0;
14573
+ return {
14574
+ currency,
14575
+ display: toExactOrFreeDisplay(legacyAmount, currency),
14576
+ selectableOptions: [],
14577
+ requiresSelection: false,
14578
+ selectedOption: null,
14579
+ slidingScale: null,
14580
+ selectedSlidingAmount: null,
14581
+ selectedPriceAmount: legacyAmount,
14582
+ selectedRequiresPayment: legacyAmount > 0,
14583
+ hasPaidSelection: legacyAmount > 0,
14584
+ hasFreeSelection: legacyAmount <= 0
14585
+ };
14586
+ }
14587
+ switch (course.pricing.pricingStrategy) {
14588
+ case "free": return {
14589
+ currency,
14590
+ display: {
14591
+ kind: "free",
14592
+ currency
14593
+ },
14594
+ selectableOptions: [],
14595
+ requiresSelection: false,
14596
+ selectedOption: null,
14597
+ slidingScale: null,
14598
+ selectedSlidingAmount: null,
14599
+ selectedPriceAmount: 0,
14600
+ selectedRequiresPayment: false,
14601
+ hasPaidSelection: false,
14602
+ hasFreeSelection: true
14603
+ };
14604
+ case "single": {
14605
+ const amount = course.pricing.singlePriceAmount ?? 0;
14606
+ return {
14607
+ currency,
14608
+ display: toExactOrFreeDisplay(amount, currency),
14609
+ selectableOptions: [],
14610
+ requiresSelection: false,
14611
+ selectedOption: null,
14612
+ slidingScale: null,
14613
+ selectedSlidingAmount: null,
14614
+ selectedPriceAmount: amount,
14615
+ selectedRequiresPayment: amount > 0,
14616
+ hasPaidSelection: amount > 0,
14617
+ hasFreeSelection: amount <= 0
14618
+ };
14619
+ }
14620
+ case "tiered": {
14621
+ const selectableOptions = getSelectableCoursePricingOptionsForNow(course, resolvePlanningDateInput(effectiveNowInput));
14622
+ const selectedOption = (selectedPricingOptionId ? selectableOptions.find((option) => option.id === selectedPricingOptionId) : null) ?? (selectableOptions.length === 1 ? selectableOptions[0] ?? null : null);
14623
+ const lowestAmount = selectableOptions.reduce((lowest, option) => {
14624
+ if (lowest === null || option.priceAmount < lowest) return option.priceAmount;
14625
+ return lowest;
14626
+ }, null);
14627
+ return {
14628
+ currency,
14629
+ display: lowestAmount === null ? {
14630
+ kind: "unavailable",
14631
+ currency
14632
+ } : selectableOptions.length > 1 && lowestAmount > 0 ? {
14633
+ kind: "from",
14634
+ amount: lowestAmount,
14635
+ currency
14636
+ } : toExactOrFreeDisplay(lowestAmount, currency),
14637
+ selectableOptions,
14638
+ requiresSelection: selectableOptions.length > 1,
14639
+ selectedOption,
14640
+ slidingScale: null,
14641
+ selectedSlidingAmount: null,
14642
+ selectedPriceAmount: selectedOption?.priceAmount ?? null,
14643
+ selectedRequiresPayment: (selectedOption?.priceAmount ?? 0) > 0,
14644
+ hasPaidSelection: selectableOptions.some((option) => option.priceAmount > 0),
14645
+ hasFreeSelection: selectableOptions.some((option) => option.priceAmount === 0)
14646
+ };
14647
+ }
14648
+ case "sliding_scale": {
14649
+ const minAmount = course.pricing.slidingMinAmount ?? null;
14650
+ const maxAmount = course.pricing.slidingMaxAmount ?? null;
14651
+ if (minAmount === null || maxAmount === null || minAmount < 0 || maxAmount < minAmount) return {
14652
+ currency,
14653
+ display: {
14654
+ kind: "unavailable",
14655
+ currency
14656
+ },
14657
+ selectableOptions: [],
14658
+ requiresSelection: true,
14659
+ selectedOption: null,
14660
+ slidingScale: null,
14661
+ selectedSlidingAmount: null,
14662
+ selectedPriceAmount: null,
14663
+ selectedRequiresPayment: false,
14664
+ hasPaidSelection: false,
14665
+ hasFreeSelection: false
14666
+ };
14667
+ const normalizedSelectedAmount = typeof selectedSlidingAmount === "number" && Number.isFinite(selectedSlidingAmount) ? selectedSlidingAmount : null;
14668
+ return {
14669
+ currency,
14670
+ display: {
14671
+ kind: "range",
14672
+ minAmount,
14673
+ maxAmount,
14674
+ currency
14675
+ },
14676
+ selectableOptions: [],
14677
+ requiresSelection: normalizedSelectedAmount === null,
14678
+ selectedOption: null,
14679
+ slidingScale: {
14680
+ minAmount,
14681
+ maxAmount,
14682
+ suggestedAmount: course.pricing.slidingSuggestedAmount ?? null
14683
+ },
14684
+ selectedSlidingAmount: normalizedSelectedAmount,
14685
+ selectedPriceAmount: normalizedSelectedAmount,
14686
+ selectedRequiresPayment: (normalizedSelectedAmount ?? 0) > 0,
14687
+ hasPaidSelection: maxAmount > 0,
14688
+ hasFreeSelection: minAmount === 0
14689
+ };
14690
+ }
14691
+ }
14692
+ }
14693
+ //#endregion
14694
+ //#region ../blocks/src/system/runtime/nodes/course-registration/utils.ts
14695
+ /**
14696
+ * Anchor ID for the registration block, used for scroll targeting.
14697
+ */
14698
+ const COURSE_REGISTRATION_ANCHOR_ID = "course-registration";
14699
+ //#endregion
14700
+ //#region ../blocks/src/system/runtime/nodes/course-registration/wizardSteps.ts
14701
+ /**
14702
+ * Step labels for the wizard indicator
14703
+ */
14704
+ const STEP_LABELS = {
14705
+ "select-course": "Course",
14706
+ "enter-details": "Details",
14707
+ "booking-mode": "Method",
14708
+ "select-payment": "Payment",
14709
+ submitting: "Booking",
14710
+ redirecting: "Redirecting",
14711
+ "verifying-payment": "Verifying",
14712
+ "verifying-timeout": "Timeout",
14713
+ "payment-failed": "Failed",
14714
+ success: "Complete",
14715
+ waitlist: "Waitlist",
14716
+ error: "Error"
14717
+ };
14718
+ /**
14719
+ * Determine which wizard steps to show based on context and eligibility.
14720
+ *
14721
+ * @param hasPreSelectedCourse - Whether course is pre-selected (skips course selection)
14722
+ * @param requiresPayment - Whether the course requires payment
14723
+ * @param hasEligibleMemberships - Whether user has eligible memberships
14724
+ * @param bookingMode - Current booking mode selection
14725
+ * @param isMembershipFastTrack - Whether user qualifies for single-step membership flow
14726
+ * @returns Array of wizard steps to display
14727
+ */
14728
+ function getWizardStepsForContext(hasPreSelectedCourse, requiresPayment, hasEligibleMemberships, bookingMode, isMembershipFastTrack) {
14729
+ if (isMembershipFastTrack) return hasPreSelectedCourse ? ["enter-details"] : ["select-course", "enter-details"];
14730
+ if (!requiresPayment) return hasPreSelectedCourse ? ["enter-details"] : ["select-course", "enter-details"];
14731
+ if (hasEligibleMemberships) {
14732
+ const skipPayment = bookingMode === "membership";
14733
+ if (hasPreSelectedCourse) return skipPayment ? ["enter-details", "booking-mode"] : [
14734
+ "enter-details",
14735
+ "booking-mode",
14736
+ "select-payment"
14737
+ ];
14738
+ return skipPayment ? [
14739
+ "select-course",
14740
+ "enter-details",
14741
+ "booking-mode"
14742
+ ] : [
14743
+ "select-course",
14744
+ "enter-details",
14745
+ "booking-mode",
14746
+ "select-payment"
14747
+ ];
14748
+ }
14749
+ return hasPreSelectedCourse ? ["enter-details", "select-payment"] : [
14750
+ "select-course",
14751
+ "enter-details",
14752
+ "select-payment"
14753
+ ];
14754
+ }
14755
+ /**
14756
+ * Get labels for a list of wizard steps
14757
+ */
14758
+ function getStepLabels(steps) {
14759
+ return steps.map((step) => STEP_LABELS[step] ?? step);
14760
+ }
14761
+ //#endregion
14762
+ //#region ../blocks/src/system/runtime/nodes/course-page-surface-scope.ts
14763
+ /**
14764
+ * Resolves native course-page ownership without falling back to entry or slug
14765
+ * coincidence when a page-surface subject is present but invalid.
14766
+ */
14767
+ function resolveCoursePageSurfaceScope(courses, subjectValue) {
14768
+ if (subjectValue === null || subjectValue === void 0) return { kind: "absent" };
14769
+ const parsed = parseEventPageSurfaceSubject(subjectValue);
14770
+ if (!parsed.ok) return {
14771
+ kind: "unsupported",
14772
+ reason: parsed.error.kind
14773
+ };
14774
+ const seriesIds = eventSeriesIdsForSubject(parsed.value);
14775
+ const allowedSeriesIds = new Set(seriesIds);
14776
+ return {
14777
+ kind: "scoped",
14778
+ subject: parsed.value,
14779
+ courses: courses.filter((course) => allowedSeriesIds.has(course.seriesId))
14780
+ };
14781
+ }
14782
+ function eventSeriesIdsForSubject(subject) {
14783
+ switch (subject.kind) {
14784
+ case "eventSeries": return [subject.eventSeriesId];
14785
+ case "eventOffering": return subject.availableEventSeriesIds;
14786
+ default: return assertNever$3(subject);
14787
+ }
14788
+ }
14789
+ //#endregion
14790
+ //#region ../blocks/src/system/runtime/nodes/course-registration/useCourseRegistrationWizard.ts
14791
+ function parseCurrencyInputToCents(value) {
14792
+ const trimmed = value.trim();
14793
+ if (!trimmed) return null;
14794
+ const amount = Number(trimmed);
14795
+ if (!Number.isFinite(amount) || amount < 0) return null;
14796
+ return Math.round(amount * 100);
14797
+ }
14798
+ function formatCentsAsCurrencyInput(amount) {
14799
+ const major = amount / 100;
14800
+ return Number.isInteger(major) ? String(major) : major.toFixed(2);
14801
+ }
14802
+ function shouldFetchCourseEligibility(course, hasPaidSelection, portalAuthState, hasWaitlistClaimToken) {
14803
+ if (!course || portalAuthState === "checking") return false;
14804
+ if (hasWaitlistClaimToken) return false;
14805
+ const needsMembershipEligibility = portalAuthState === "authenticated" && hasPaidSelection && course.acceptsMemberships;
14806
+ const needsWaitlistAvailabilityCheck = course.availableSpots !== null && course.availableSpots <= 0;
14807
+ return needsMembershipEligibility || needsWaitlistAvailabilityCheck;
14808
+ }
14809
+ /**
14810
+ * Extracts session_id from URL, checking both query params and hash fragment.
14811
+ *
14812
+ * Stripe appends session_id to the success URL, but if our URL contains a hash
14813
+ * (e.g., #course-registration), Stripe ends up appending session_id after the hash:
14814
+ * Expected: ?enrollment=success&session_id=cs_test_...#course-registration
14815
+ * Actual: ?enrollment=success#course-registration&session_id=cs_test_...
14816
+ *
14817
+ * This helper checks both locations to handle this edge case.
14818
+ */
14819
+ function getSessionIdFromUrl() {
14820
+ const fromSearch = new URLSearchParams(window.location.search).get("session_id");
14821
+ if (fromSearch) return fromSearch;
14822
+ return window.location.hash.match(/[?&]session_id=([^&]+)/)?.[1] ?? null;
14823
+ }
14824
+ function getMissingCoursePricingSelectionMessage(pricing) {
14825
+ if (pricing?.requiresSelection && !pricing.selectedOption) {
14826
+ if (pricing.slidingScale) return "Choose an amount to continue.";
14827
+ return "Choose a pricing option to continue.";
14828
+ }
14829
+ if (pricing?.slidingScale && pricing.selectedSlidingAmount !== null && pricing.selectedSlidingAmount < pricing.slidingScale.minAmount) return "Choose an amount within the sliding scale.";
14830
+ if (pricing?.slidingScale && pricing.selectedSlidingAmount !== null && pricing.selectedSlidingAmount > pricing.slidingScale.maxAmount) return "Choose an amount within the sliding scale.";
14831
+ return null;
14832
+ }
14833
+ function buildCoursePricingOptionPayload(pricing) {
14834
+ if (pricing?.selectedOption) return { pricingOptionId: pricing.selectedOption.id };
14835
+ if (pricing?.slidingScale && pricing.selectedSlidingAmount !== null) return { selectedSlidingAmount: pricing.selectedSlidingAmount };
14836
+ return {};
14837
+ }
14838
+ function resolveHasCoursePayment(pricing) {
14839
+ if (!pricing) return false;
14840
+ if (pricing.selectedOption) return pricing.selectedRequiresPayment;
14841
+ return pricing.hasPaidSelection;
14842
+ }
14843
+ function useCourseRegistrationWizard(props, options = {}) {
14844
+ const api = useBlockApi();
14845
+ const { contentEntry, courses, entrySubrouteContext, subject } = props;
14846
+ const scopedCourses = useMemo(() => {
14847
+ const allCourses = courses ?? [];
14848
+ if (entrySubrouteContext?.kind === "course-run") {
14849
+ const matched = allCourses.find((c) => c.id === entrySubrouteContext.run.courseId) ?? null;
14850
+ if (matched) return [matched];
14851
+ }
14852
+ const pageSurfaceScope = resolveCoursePageSurfaceScope(allCourses, subject);
14853
+ if (pageSurfaceScope.kind === "unsupported") return [];
14854
+ if (pageSurfaceScope.kind === "scoped") return [...pageSurfaceScope.courses];
14855
+ if (contentEntry?.id) {
14856
+ const matchedCourses = allCourses.filter((c) => c.contentEntryId === contentEntry.id);
14857
+ if (matchedCourses.length > 0) return matchedCourses;
14858
+ }
14859
+ if (contentEntry?.slug) {
14860
+ const matchedCourse = allCourses.find((c) => c.slug === contentEntry.slug);
14861
+ if (matchedCourse) return [matchedCourse];
14862
+ }
14863
+ return allCourses;
14864
+ }, [
14865
+ courses,
14866
+ contentEntry?.id,
14867
+ contentEntry?.slug,
14868
+ entrySubrouteContext,
14869
+ subject
14870
+ ]);
14871
+ const [step, setStep] = useState(() => {
14872
+ const enrollment = new URLSearchParams(window.location.search).get("enrollment");
14873
+ const sessionId = getSessionIdFromUrl();
14874
+ if (enrollment === "success" && sessionId) return "verifying-payment";
14875
+ return scopedCourses.length === 1 ? "enter-details" : "select-course";
14876
+ });
14877
+ const [selectedCourse, setSelectedCourse] = useState(scopedCourses.length === 1 ? scopedCourses[0] ?? null : null);
14878
+ const [selectedPricingOptionId, setSelectedPricingOptionId] = useState(null);
14879
+ const [selectedSlidingAmountInput, setSelectedSlidingAmountInput] = useState("");
14880
+ const [formData, setFormData] = useState({
14881
+ name: "",
14882
+ email: "",
14883
+ phone: "",
14884
+ notes: ""
14885
+ });
14886
+ const [errors, setErrors] = useState({});
14887
+ const [enrollmentResult, setEnrollmentResult] = useState(null);
14888
+ const [errorMessage, setErrorMessage] = useState("");
14889
+ const [portalAuthState, setPortalAuthState] = useState("checking");
14890
+ const [altchaSolution, setAltchaSolution] = useState(null);
14891
+ const showSpamProtection = isSpamProtectionEnabled();
14892
+ const [eligibilityLoading, setEligibilityLoading] = useState(false);
14893
+ const [eligibilityError, setEligibilityError] = useState(null);
14894
+ const [courseEligibility, setCourseEligibility] = useState(null);
14895
+ const [eligibleMemberships, setEligibleMemberships] = useState([]);
14896
+ const [paymentChoice, setPaymentChoice] = useState("checkout");
14897
+ const [selectedMembershipId, setSelectedMembershipId] = useState(null);
14898
+ const [bookingMode, setBookingMode] = useState(null);
14899
+ const waitlistClaim = useWaitlistClaim();
14900
+ const selectedCoursePricing = useMemo(() => selectedCourse ? resolveCoursePricingState(selectedCourse, selectedPricingOptionId, parseCurrencyInputToCents(selectedSlidingAmountInput)) : null, [
14901
+ selectedCourse,
14902
+ selectedPricingOptionId,
14903
+ selectedSlidingAmountInput
14904
+ ]);
14905
+ const missingPricingSelectionMessage = getMissingCoursePricingSelectionMessage(selectedCoursePricing);
14906
+ const selectedCoursePricingPayload = buildCoursePricingOptionPayload(selectedCoursePricing);
14907
+ const showSoldOutCourseError = () => {
14908
+ setErrorMessage(courseEligibility?.reason ?? "This course is fully booked.");
14909
+ setStep("error");
14910
+ return true;
14911
+ };
14912
+ useEffect(() => {
14913
+ let cancelled = false;
14914
+ async function checkPortalAuth() {
14915
+ if (!(api.config.auth.type === "bearer" || api.config.authOverrides?.portal?.type === "bearer")) {
14916
+ setPortalAuthState("unauthenticated");
14917
+ return;
14918
+ }
14919
+ setPortalAuthState("checking");
14920
+ try {
14921
+ const me = await api.getPortalProfile();
14922
+ if (cancelled) return;
14923
+ setPortalAuthState("authenticated");
14924
+ setFormData((prev) => ({
14925
+ ...prev,
14926
+ email: me.profile.email,
14927
+ name: prev.name || me.profile.name,
14928
+ phone: prev.phone || me.profile.phone || ""
14929
+ }));
14930
+ } catch {
14931
+ if (cancelled) return;
14932
+ setPortalAuthState("unauthenticated");
14933
+ }
14934
+ }
14935
+ checkPortalAuth();
14936
+ return () => {
14937
+ cancelled = true;
14938
+ };
14939
+ }, [api]);
14940
+ useEffect(() => {
14941
+ const claim = waitlistClaim.claim;
14942
+ if (!claim || claim.subject.kind !== "course") return;
14943
+ const courseId = claim.subject.courseId;
14944
+ const matchedCourse = scopedCourses.find((course) => course.id === courseId) ?? null;
14945
+ if (!matchedCourse) {
14946
+ setErrorMessage("This waitlist claim is for a different course.");
14947
+ setStep("error");
14948
+ return;
14949
+ }
14950
+ setSelectedCourse(matchedCourse);
14951
+ setFormData((prev) => ({
14952
+ ...prev,
14953
+ email: claim.prefill.email,
14954
+ name: claim.prefill.name,
14955
+ phone: claim.prefill.phone ?? "",
14956
+ notes: claim.prefill.notes ?? ""
14957
+ }));
14958
+ if (step === "select-course") setStep("enter-details");
14959
+ }, [
14960
+ waitlistClaim.claim,
14961
+ scopedCourses,
14962
+ step
14963
+ ]);
14964
+ useEffect(() => {
14965
+ if (waitlistClaim.requiresPortalLogin) return;
14966
+ if (!waitlistClaim.error) return;
14967
+ setErrorMessage(waitlistClaim.error);
14968
+ setStep("error");
14969
+ }, [waitlistClaim.error, waitlistClaim.requiresPortalLogin]);
14970
+ useEffect(() => {
14971
+ if (!selectedCourse) {
14972
+ setSelectedPricingOptionId(null);
14973
+ setSelectedSlidingAmountInput("");
14974
+ return;
14975
+ }
14976
+ const defaultPricing = resolveCoursePricingState(selectedCourse, null);
14977
+ setSelectedSlidingAmountInput(defaultPricing.slidingScale?.suggestedAmount !== null && defaultPricing.slidingScale?.suggestedAmount !== void 0 ? formatCentsAsCurrencyInput(defaultPricing.slidingScale.suggestedAmount) : "");
14978
+ setSelectedPricingOptionId((previous) => {
14979
+ if (previous && defaultPricing.selectableOptions.some((option) => option.id === previous)) return previous;
14980
+ return defaultPricing.selectedOption?.id ?? null;
14981
+ });
14982
+ }, [selectedCourse]);
14983
+ useEffect(() => {
14984
+ let cancelled = false;
14985
+ async function fetchEligibility() {
14986
+ setCourseEligibility(null);
14987
+ setEligibleMemberships([]);
14988
+ setSelectedMembershipId(null);
14989
+ setEligibilityError(null);
14990
+ setPaymentChoice("checkout");
14991
+ const course = selectedCourse;
14992
+ if (!shouldFetchCourseEligibility(course, selectedCoursePricing?.hasPaidSelection ?? false, portalAuthState, waitlistClaim.token !== null)) return;
14993
+ if (!course) return;
14994
+ setEligibilityLoading(true);
14995
+ try {
14996
+ const res = await api.checkCourseEligibility({ courseId: course.id });
14997
+ if (cancelled) return;
14998
+ setCourseEligibility(res);
14999
+ const memberships = res.eligibleMemberships ?? [];
15000
+ setEligibleMemberships(memberships);
15001
+ if (portalAuthState === "authenticated" && memberships.length > 0) {
15002
+ setPaymentChoice("membership");
15003
+ if (memberships.length === 1) setSelectedMembershipId(memberships[0]?.customerMembershipId ?? null);
15004
+ }
15005
+ } catch (error) {
15006
+ if (cancelled) return;
15007
+ setEligibilityError(error instanceof Error ? error.message : "Failed to check membership eligibility");
15008
+ } finally {
15009
+ if (!cancelled) setEligibilityLoading(false);
15010
+ }
15011
+ }
15012
+ fetchEligibility();
15013
+ return () => {
15014
+ cancelled = true;
15015
+ };
15016
+ }, [
15017
+ api,
15018
+ selectedCourse,
15019
+ selectedCoursePricing?.hasPaidSelection,
15020
+ portalAuthState,
15021
+ waitlistClaim.token
15022
+ ]);
15023
+ useEffect(() => {
15024
+ let cancelled = false;
15025
+ async function verifyAfterCheckout() {
15026
+ const enrollment = new URLSearchParams(window.location.search).get("enrollment");
15027
+ const sessionId = getSessionIdFromUrl();
15028
+ if (enrollment === "cancelled") {
15029
+ scrollToBlock(COURSE_REGISTRATION_ANCHOR_ID);
15030
+ setErrorMessage("Payment was cancelled. You can try again.");
15031
+ setStep("error");
15032
+ return;
15033
+ }
15034
+ if (enrollment !== "success" || !sessionId) return;
15035
+ scrollToBlock(COURSE_REGISTRATION_ANCHOR_ID);
15036
+ setStep("verifying-payment");
15037
+ const startedAt = Date.now();
15038
+ const timeoutMs = 3e4;
15039
+ while (!cancelled) {
15040
+ try {
15041
+ const status = await api.getCourseRegistrationStatus({ sessionId });
15042
+ if (cancelled) return;
15043
+ if (status.status === "enrolled" || status.status === "completed") {
15044
+ setEnrollmentResult({
15045
+ status: "enrolled",
15046
+ usedMembership: false
15047
+ });
15048
+ setStep("success");
15049
+ return;
15050
+ }
15051
+ if (status.status === "waitlist") {
15052
+ setEnrollmentResult({
15053
+ status: "waitlist",
15054
+ usedMembership: false
15055
+ });
15056
+ setStep("waitlist");
15057
+ return;
15058
+ }
15059
+ if (status.status === "cancelled") {
15060
+ setErrorMessage("Payment did not complete. Please try again.");
15061
+ setStep("error");
15062
+ return;
15063
+ }
15064
+ } catch (error) {
15065
+ setErrorMessage(error instanceof Error ? error.message : "Failed to verify payment");
15066
+ }
15067
+ if (Date.now() - startedAt > timeoutMs) {
15068
+ setErrorMessage("Payment is still processing. Please check your email for confirmation.");
15069
+ setStep("error");
15070
+ return;
15071
+ }
15072
+ await new Promise((r) => setTimeout(r, 1500));
15073
+ }
15074
+ }
15075
+ verifyAfterCheckout();
15076
+ return () => {
15077
+ cancelled = true;
15078
+ };
15079
+ }, [api]);
15080
+ const handleValidateForm = () => {
15081
+ const newErrors = validateFormData$1(formData);
15082
+ setErrors(newErrors);
15083
+ return Object.keys(newErrors).length === 0;
15084
+ };
15085
+ const handleCourseSelect = (course) => {
15086
+ setSelectedCourse(course);
15087
+ setStep("enter-details");
15088
+ };
15089
+ const handleSubmit = async () => {
15090
+ if (!selectedCourse) return {
15091
+ kind: "blocked",
15092
+ message: "Choose an available set of dates to continue."
15093
+ };
15094
+ if (!handleValidateForm()) return { kind: "validation-failed" };
15095
+ if (missingPricingSelectionMessage) {
15096
+ setErrorMessage(missingPricingSelectionMessage);
15097
+ setStep("error");
15098
+ return {
15099
+ kind: "blocked",
15100
+ message: missingPricingSelectionMessage
15101
+ };
15102
+ }
15103
+ const requiresPayment = selectedCoursePricing?.selectedRequiresPayment ?? false;
15104
+ const canUseMembership = requiresPayment && selectedCourse.acceptsMemberships && portalAuthState === "authenticated" && eligibleMemberships.length > 0;
15105
+ if (showSpamProtection && !altchaSolution) {
15106
+ const message = "Please complete the verification check.";
15107
+ setErrorMessage(message);
15108
+ setStep("error");
15109
+ return {
15110
+ kind: "blocked",
15111
+ message
15112
+ };
15113
+ }
15114
+ if (!waitlistClaim.claim && courseEligibility?.isFull && !courseEligibility.canJoinWaitlist && showSoldOutCourseError()) return {
15115
+ kind: "blocked",
15116
+ message: courseEligibility.reason ?? "This course is fully booked."
15117
+ };
15118
+ if (courseEligibility?.canJoinWaitlist) {
15119
+ setStep("submitting");
15120
+ try {
15121
+ const result = await api.enrollInCourse({
15122
+ courseId: selectedCourse.id,
15123
+ ...selectedCoursePricingPayload,
15124
+ email: formData.email.trim().toLowerCase(),
15125
+ name: formData.name.trim(),
15126
+ phone: formData.phone.trim() || void 0,
15127
+ formData: { notes: formData.notes.trim() || void 0 },
15128
+ altchaSolution,
15129
+ paymentMethod: "free",
15130
+ claimToken: waitlistClaim.token ?? void 0
15131
+ });
15132
+ setEnrollmentResult({
15133
+ status: result.status,
15134
+ waitlistPosition: result.waitlistPosition ?? void 0,
15135
+ usedMembership: false
15136
+ });
15137
+ setStep(result.status === "waitlist" ? "waitlist" : "success");
15138
+ return { kind: "state-updated" };
15139
+ } catch (error) {
15140
+ const message = error instanceof Error ? error.message : "Booking failed";
15141
+ setErrorMessage(message);
15142
+ setStep("error");
15143
+ return {
15144
+ kind: "failed",
15145
+ message
15146
+ };
15147
+ }
15148
+ }
15149
+ if (requiresPayment && canUseMembership) {
15150
+ setStep("select-payment");
15151
+ return { kind: "state-updated" };
15152
+ }
15153
+ if (requiresPayment) {
15154
+ setStep("submitting");
15155
+ try {
15156
+ const { successUrl, cancelUrl } = buildCheckoutReturnUrls("enrollment", COURSE_REGISTRATION_ANCHOR_ID);
15157
+ const checkoutResult = await api.createCourseCheckout({
15158
+ courseId: selectedCourse.id,
15159
+ ...selectedCoursePricingPayload,
15160
+ email: formData.email.trim().toLowerCase(),
15161
+ name: formData.name.trim(),
15162
+ phone: formData.phone.trim() || void 0,
15163
+ formData: { notes: formData.notes.trim() || void 0 },
15164
+ altchaSolution,
15165
+ discountCode: discountCode.trim() ? discountCode.trim() : void 0,
15166
+ successUrl,
15167
+ cancelUrl,
15168
+ claimToken: waitlistClaim.token ?? void 0
15169
+ });
15170
+ if (checkoutResult.bypassed) {
15171
+ setEnrollmentResult({
15172
+ status: "enrolled",
15173
+ waitlistPosition: void 0,
15174
+ usedMembership: false
15175
+ });
15176
+ setStep("success");
15177
+ return { kind: "state-updated" };
15178
+ }
15179
+ if (!checkoutResult.checkoutUrl) throw new Error("Checkout URL missing");
15180
+ setStep("redirecting");
15181
+ if (options.onCheckoutRedirect) options.onCheckoutRedirect(checkoutResult.checkoutUrl);
15182
+ else window.location.href = checkoutResult.checkoutUrl;
15183
+ return {
15184
+ kind: "checkout-redirect",
15185
+ checkoutUrl: checkoutResult.checkoutUrl
15186
+ };
15187
+ } catch (error) {
15188
+ const message = error instanceof Error ? error.message : "Checkout failed";
15189
+ setErrorMessage(message);
15190
+ setStep("error");
15191
+ return {
15192
+ kind: "failed",
15193
+ message
15194
+ };
15195
+ }
15196
+ }
15197
+ setStep("submitting");
15198
+ try {
15199
+ const result = await api.enrollInCourse({
15200
+ courseId: selectedCourse.id,
15201
+ ...selectedCoursePricingPayload,
15202
+ email: formData.email.trim().toLowerCase(),
15203
+ name: formData.name.trim(),
15204
+ phone: formData.phone.trim() || void 0,
15205
+ formData: { notes: formData.notes.trim() || void 0 },
15206
+ altchaSolution,
15207
+ paymentMethod: "free",
15208
+ claimToken: waitlistClaim.token ?? void 0
15209
+ });
15210
+ setEnrollmentResult({
15211
+ status: result.status,
15212
+ waitlistPosition: result.waitlistPosition ?? void 0,
15213
+ usedMembership: result.paymentMethod === "membership"
15214
+ });
15215
+ setStep(result.status === "waitlist" ? "waitlist" : "success");
15216
+ return { kind: "state-updated" };
15217
+ } catch (error) {
15218
+ const message = error instanceof Error ? error.message : "Booking failed";
15219
+ setErrorMessage(message);
15220
+ setStep("error");
15221
+ return {
15222
+ kind: "failed",
15223
+ message
15224
+ };
15225
+ }
15226
+ };
15227
+ const handlePaymentContinue = async () => {
15228
+ if (!selectedCourse) return {
15229
+ kind: "blocked",
15230
+ message: "Choose an available set of dates to continue."
15231
+ };
15232
+ if (!handleValidateForm()) return { kind: "validation-failed" };
15233
+ if (missingPricingSelectionMessage) {
15234
+ setErrorMessage(missingPricingSelectionMessage);
15235
+ setStep("error");
15236
+ return {
15237
+ kind: "blocked",
15238
+ message: missingPricingSelectionMessage
15239
+ };
15240
+ }
15241
+ const requiresPayment = selectedCoursePricing?.selectedRequiresPayment ?? false;
15242
+ if (!waitlistClaim.claim && courseEligibility?.isFull && !courseEligibility.canJoinWaitlist && showSoldOutCourseError()) return {
15243
+ kind: "blocked",
15244
+ message: courseEligibility.reason ?? "This course is fully booked."
15245
+ };
15246
+ if (!requiresPayment) {
15247
+ setStep("enter-details");
15248
+ return { kind: "state-updated" };
15249
+ }
15250
+ if (showSpamProtection && !altchaSolution) {
15251
+ const message = "Please complete the verification check.";
15252
+ setErrorMessage(message);
15253
+ setStep("error");
15254
+ return {
15255
+ kind: "blocked",
15256
+ message
15257
+ };
15258
+ }
15259
+ if (paymentChoice === "membership") {
15260
+ if (!selectedMembershipId) {
15261
+ const message = "Select a membership to sign up.";
15262
+ setErrorMessage(message);
15263
+ setStep("error");
15264
+ return {
15265
+ kind: "blocked",
15266
+ message
15267
+ };
15268
+ }
15269
+ setStep("submitting");
15270
+ try {
15271
+ const result = await api.enrollInCourse({
15272
+ courseId: selectedCourse.id,
15273
+ ...selectedCoursePricingPayload,
15274
+ email: formData.email.trim().toLowerCase(),
15275
+ name: formData.name.trim(),
15276
+ phone: formData.phone.trim() || void 0,
15277
+ formData: { notes: formData.notes.trim() || void 0 },
15278
+ altchaSolution,
15279
+ paymentMethod: "membership",
15280
+ customerMembershipId: selectedMembershipId,
15281
+ claimToken: waitlistClaim.token ?? void 0
15282
+ });
15283
+ setEnrollmentResult({
15284
+ status: result.status,
15285
+ waitlistPosition: result.waitlistPosition ?? void 0,
15286
+ usedMembership: true
15287
+ });
15288
+ setStep(result.status === "waitlist" ? "waitlist" : "success");
15289
+ return { kind: "state-updated" };
15290
+ } catch (error) {
15291
+ const message = error instanceof Error ? error.message : "Booking failed";
15292
+ setErrorMessage(message);
15293
+ setStep("error");
15294
+ return {
15295
+ kind: "failed",
15296
+ message
15297
+ };
15298
+ }
15299
+ }
15300
+ setStep("submitting");
15301
+ try {
15302
+ const { successUrl, cancelUrl } = buildCheckoutReturnUrls("enrollment", COURSE_REGISTRATION_ANCHOR_ID);
15303
+ const checkoutResult = await api.createCourseCheckout({
15304
+ courseId: selectedCourse.id,
15305
+ ...selectedCoursePricingPayload,
15306
+ email: formData.email.trim().toLowerCase(),
15307
+ name: formData.name.trim(),
15308
+ phone: formData.phone.trim() || void 0,
15309
+ formData: { notes: formData.notes.trim() || void 0 },
15310
+ altchaSolution,
15311
+ discountCode: discountCode.trim() ? discountCode.trim() : void 0,
15312
+ successUrl,
15313
+ cancelUrl,
15314
+ claimToken: waitlistClaim.token ?? void 0
15315
+ });
15316
+ if (checkoutResult.bypassed) {
15317
+ setEnrollmentResult({
15318
+ status: "enrolled",
15319
+ waitlistPosition: void 0,
15320
+ usedMembership: false
15321
+ });
15322
+ setStep("success");
15323
+ return { kind: "state-updated" };
15324
+ }
15325
+ if (!checkoutResult.checkoutUrl) throw new Error("Checkout URL missing");
15326
+ setStep("redirecting");
15327
+ if (options.onCheckoutRedirect) options.onCheckoutRedirect(checkoutResult.checkoutUrl);
15328
+ else window.location.href = checkoutResult.checkoutUrl;
15329
+ return {
15330
+ kind: "checkout-redirect",
15331
+ checkoutUrl: checkoutResult.checkoutUrl
15332
+ };
15333
+ } catch (error) {
15334
+ const message = error instanceof Error ? error.message : "Checkout failed";
15335
+ setErrorMessage(message);
15336
+ setStep("error");
15337
+ return {
15338
+ kind: "failed",
15339
+ message
15340
+ };
15341
+ }
15342
+ };
15343
+ const handleRetry = () => {
15344
+ setStep(scopedCourses.length === 1 ? "enter-details" : "select-course");
15345
+ setErrorMessage("");
15346
+ };
15347
+ const handleBookingModeChange = (mode) => {
15348
+ setBookingMode(mode);
15349
+ if (mode === "membership") setPaymentChoice("membership");
15350
+ else setPaymentChoice("checkout");
15351
+ };
15352
+ const discountUi = useDiscountCodeUi({
15353
+ api,
15354
+ discountContext: useMemo(() => {
15355
+ if (!selectedCourse) return null;
15356
+ if (!selectedCoursePricing?.selectedRequiresPayment) return null;
15357
+ if (paymentChoice !== "checkout") return null;
15358
+ return {
15359
+ type: "course",
15360
+ courseId: selectedCourse.id
15361
+ };
15362
+ }, [
15363
+ selectedCourse,
15364
+ selectedCoursePricing?.selectedRequiresPayment,
15365
+ paymentChoice
15366
+ ]),
15367
+ subtotalCents: selectedCoursePricing?.selectedPriceAmount ?? void 0
15368
+ });
15369
+ const discountCode = discountUi.discountCode;
15370
+ const hasCoursePayment = resolveHasCoursePayment(selectedCoursePricing);
15371
+ const canUseMembership = hasCoursePayment && selectedCourse?.acceptsMemberships === true && portalAuthState === "authenticated" && eligibleMemberships.length > 0;
15372
+ const hasPreSelectedCourse = scopedCourses.length === 1;
15373
+ const isMembershipFastTrack = canUseMembership && eligibleMemberships.length === 1 && bookingMode === "membership";
15374
+ const wizardSteps = useMemo(() => getWizardStepsForContext(hasPreSelectedCourse, hasCoursePayment, canUseMembership, bookingMode, isMembershipFastTrack), [
15375
+ hasPreSelectedCourse,
15376
+ hasCoursePayment,
15377
+ canUseMembership,
15378
+ bookingMode,
15379
+ isMembershipFastTrack
15380
+ ]);
15381
+ const currentStepNumber = getStepNumber(step, wizardSteps);
15382
+ const totalSteps = wizardSteps.length;
15383
+ const stepLabels = getStepLabels(wizardSteps);
15384
+ const canShowPortalAuthSidebarOnCurrentStep = step !== "success" && step !== "waitlist" && step !== "error";
15385
+ const showPortalAuthSidebar = portalAuthState === "unauthenticated" && waitlistClaim.requiresPortalLogin && canShowPortalAuthSidebarOnCurrentStep || portalAuthState === "unauthenticated" && hasCoursePayment && selectedCourse?.acceptsMemberships === true && step !== "select-course" && canShowPortalAuthSidebarOnCurrentStep;
15386
+ return {
15387
+ api,
15388
+ scopedCourses,
15389
+ step,
15390
+ setStep,
15391
+ selectedCourse,
15392
+ selectedPricingOptionId,
15393
+ setSelectedPricingOptionId,
15394
+ selectedSlidingAmountInput,
15395
+ setSelectedSlidingAmountInput,
15396
+ selectedCoursePricing,
15397
+ setSelectedCourse,
15398
+ formData,
15399
+ setFormData,
15400
+ errors,
15401
+ enrollmentResult,
15402
+ errorMessage,
15403
+ portalAuthState,
15404
+ handleLogout: portalAuthState === "authenticated" ? () => logoutPortal(COURSE_REGISTRATION_ANCHOR_ID) : void 0,
15405
+ altchaSolution,
15406
+ setAltchaSolution,
15407
+ showSpamProtection,
15408
+ eligibilityLoading,
15409
+ eligibilityError,
15410
+ courseEligibility,
15411
+ eligibleMemberships,
15412
+ paymentChoice,
15413
+ setPaymentChoice,
15414
+ selectedMembershipId,
15415
+ setSelectedMembershipId,
15416
+ discountUiOpen: discountUi.discountUiOpen,
15417
+ setDiscountUiOpen: discountUi.setDiscountUiOpen,
15418
+ discountCode: discountUi.discountCode,
15419
+ setDiscountCode: discountUi.setDiscountCode,
15420
+ discountsAvailable: discountUi.discountsAvailable,
15421
+ manualCodeEntryAllowed: discountUi.manualCodeEntryAllowed,
15422
+ discountsAvailableLoading: discountUi.discountsAvailableLoading,
15423
+ canUseMembership,
15424
+ waitlistClaim,
15425
+ bookingMode,
15426
+ wizardSteps,
15427
+ currentStepNumber,
15428
+ totalSteps,
15429
+ stepLabels,
15430
+ showPortalAuthSidebar,
15431
+ hasPreSelectedCourse,
15432
+ hasCoursePayment,
15433
+ handleCourseSelect,
15434
+ handleSubmit,
15435
+ handlePaymentContinue,
15436
+ handleRetry,
15437
+ handleBookingModeChange
15438
+ };
15439
+ }
15440
+ //#endregion
15441
+ //#region ../blocks/src/system/runtime/nodes/course-registration/whole-series-registration.headless.planning.ts
15442
+ const STEP_IDS = {
15443
+ "select-course": "select-run",
15444
+ "enter-details": "enter-details",
15445
+ "booking-mode": "select-booking-method",
15446
+ "select-payment": "select-payment"
15447
+ };
15448
+ function toPublicStepId(step) {
15449
+ switch (step) {
15450
+ case "select-course":
15451
+ case "enter-details":
15452
+ case "booking-mode":
15453
+ case "select-payment": return STEP_IDS[step];
15454
+ case "submitting":
15455
+ case "redirecting":
15456
+ case "verifying-payment":
15457
+ case "verifying-timeout":
15458
+ case "payment-failed":
15459
+ case "success":
15460
+ case "waitlist":
15461
+ case "error": return null;
15462
+ }
15463
+ }
15464
+ function deriveWholeSeriesRegistrationSteps(input) {
15465
+ const currentId = toPublicStepId(input.step);
15466
+ const currentIndex = currentId ? input.wizardSteps.findIndex((step) => toPublicStepId(step) === currentId) : input.step === "success" || input.step === "waitlist" ? input.wizardSteps.length : -1;
15467
+ return input.wizardSteps.flatMap((step, index) => {
15468
+ const id = toPublicStepId(step);
15469
+ if (!id) return [];
15470
+ const state = index < currentIndex ? "complete" : index === currentIndex ? "active" : currentIndex >= 0 ? "available" : "blocked";
15471
+ return [{
15472
+ id,
15473
+ state,
15474
+ index,
15475
+ canEnter: state === "active" || state === "complete"
15476
+ }];
15477
+ });
15478
+ }
15479
+ function resolveWholeSeriesRegistrationStatus(wizard) {
15480
+ switch (wizard.step) {
15481
+ case "submitting": return { tag: "submitting" };
15482
+ case "redirecting": return { tag: "redirecting" };
15483
+ case "verifying-payment": return { tag: "verifying-payment" };
15484
+ case "verifying-timeout": return {
15485
+ tag: "verifying-timeout",
15486
+ message: wizard.errorMessage
15487
+ };
15488
+ case "payment-failed": return {
15489
+ tag: "payment-failed",
15490
+ message: wizard.errorMessage
15491
+ };
15492
+ case "success": return wizard.enrollmentResult ? {
15493
+ tag: "success",
15494
+ result: wizard.enrollmentResult
15495
+ } : {
15496
+ tag: "error",
15497
+ message: "Registration completed without a result."
15498
+ };
15499
+ case "waitlist": return wizard.enrollmentResult ? {
15500
+ tag: "waitlist",
15501
+ result: wizard.enrollmentResult
15502
+ } : {
15503
+ tag: "error",
15504
+ message: "Waitlist registration completed without a result."
15505
+ };
15506
+ case "error": return {
15507
+ tag: "error",
15508
+ message: wizard.errorMessage
15509
+ };
15510
+ case "select-course":
15511
+ case "enter-details":
15512
+ case "booking-mode":
15513
+ case "select-payment": return { tag: "ready" };
15514
+ }
15515
+ }
15516
+ function resolveWholeSeriesRegistrationView(wizard) {
15517
+ switch (wizard.step) {
15518
+ case "submitting":
15519
+ case "redirecting":
15520
+ case "verifying-payment": return {
15521
+ kind: "verifying",
15522
+ message: "Completing your sign-up."
15523
+ };
15524
+ case "verifying-timeout":
15525
+ case "payment-failed":
15526
+ case "error": return {
15527
+ kind: "error",
15528
+ message: wizard.errorMessage
15529
+ };
15530
+ case "success": return wizard.enrollmentResult ? {
15531
+ kind: "success",
15532
+ result: wizard.enrollmentResult
15533
+ } : {
15534
+ kind: "error",
15535
+ message: "Registration completed without a result."
15536
+ };
15537
+ case "waitlist": return wizard.enrollmentResult ? {
15538
+ kind: "waitlist",
15539
+ result: wizard.enrollmentResult
15540
+ } : {
15541
+ kind: "error",
15542
+ message: "Waitlist registration completed without a result."
15543
+ };
15544
+ case "select-course":
15545
+ case "enter-details":
15546
+ case "booking-mode":
15547
+ case "select-payment":
15548
+ if (wizard.scopedRunsCount === 0) return {
15549
+ kind: "empty",
15550
+ message: "There are no dates available to sign up for."
15551
+ };
15552
+ if (wizard.selectedRun && !wizard.selectedRun.enrollmentOpen) return {
15553
+ kind: "unavailable",
15554
+ message: "Sign-up is closed."
15555
+ };
15556
+ return { kind: "form" };
15557
+ }
15558
+ }
15559
+ function resolveWholeSeriesPricingSelection(pricing) {
15560
+ if (!pricing) return { tag: "not-required" };
15561
+ if (pricing.display.kind === "unavailable") return { tag: "unavailable" };
15562
+ if (pricing.slidingScale && pricing.selectedSlidingAmount !== null) {
15563
+ if (pricing.selectedSlidingAmount < pricing.slidingScale.minAmount || pricing.selectedSlidingAmount > pricing.slidingScale.maxAmount) return {
15564
+ tag: "invalid",
15565
+ message: "Choose an amount within the available range."
15566
+ };
15567
+ }
15568
+ if (pricing.requiresSelection && !pricing.selectedOption && pricing.selectedSlidingAmount === null) return { tag: "required" };
15569
+ return pricing.requiresSelection || pricing.selectedOption || pricing.selectedSlidingAmount !== null ? { tag: "valid" } : { tag: "not-required" };
15570
+ }
15571
+ function resolveWholeSeriesPaymentSelection(input) {
15572
+ if (!input.required) return { method: "none" };
15573
+ if (input.paymentChoice === "membership") return {
15574
+ method: "membership",
15575
+ membershipId: input.selectedMembershipId
15576
+ };
15577
+ return { method: "card" };
15578
+ }
15579
+ //#endregion
15580
+ //#region ../blocks/src/system/runtime/hooks/useHeadlessWholeSeriesRegistrationFlow.ts
15581
+ const wholeSeriesRegistrationControllerDeps = { useCourseRegistrationWizard };
15582
+ function createWholeSeriesRegistrationBlockApiConfig(input) {
15583
+ return {
15584
+ baseUrl: input.apiBaseUrl,
15585
+ auth: input.previewToken ? {
15586
+ type: "bearer",
15587
+ token: input.previewToken
15588
+ } : { type: "none" },
15589
+ siteId: input.siteId,
15590
+ isPreview: input.stage === "preview",
15591
+ ...input.portalToken ? { authOverrides: { portal: {
15592
+ type: "bearer",
15593
+ token: input.portalToken
15594
+ } } } : {}
15595
+ };
15596
+ }
15597
+ function HeadlessWholeSeriesRegistrationProvider$1({ config, children }) {
15598
+ return React.createElement(BlockApiProvider, {
15599
+ config,
15600
+ children
15601
+ });
15602
+ }
15603
+ function useHeadlessWholeSeriesRegistrationFlow(options) {
15604
+ return useWholeSeriesRegistrationControllerWithDeps(options, wholeSeriesRegistrationControllerDeps);
15605
+ }
15606
+ function useWholeSeriesRegistrationControllerWithDeps(options, deps) {
15607
+ const wizard = deps.useCourseRegistrationWizard({
15608
+ courses: (options.runs ?? []).map(toPublicCourse),
15609
+ contentEntry: options.contentEntry ?? null,
15610
+ subject: options.subject ?? null
15611
+ }, { onCheckoutRedirect: () => {} });
15612
+ const steps = useMemo(() => deriveWholeSeriesRegistrationSteps({
15613
+ wizardSteps: wizard.wizardSteps,
15614
+ step: wizard.step
15615
+ }), [wizard.step, wizard.wizardSteps]);
15616
+ const currentStep = steps.find((step) => step.state === "active") ?? null;
15617
+ const pricingSelection = resolveWholeSeriesPricingSelection(wizard.selectedCoursePricing);
15618
+ const paymentSelection = resolveWholeSeriesPaymentSelection({
15619
+ paymentChoice: wizard.paymentChoice,
15620
+ selectedMembershipId: wizard.selectedMembershipId,
15621
+ required: wizard.selectedCoursePricing?.selectedRequiresPayment ?? false
15622
+ });
15623
+ const updateRegistrant = useCallback((values) => wizard.setFormData(values), [wizard]);
15624
+ const setSlidingAmountCents = useCallback((amount) => {
15625
+ wizard.setSelectedSlidingAmountInput(amount === null || !Number.isFinite(amount) || amount < 0 ? "" : String(amount / 100));
15626
+ }, [wizard]);
15627
+ const selectPayment = useCallback((selection) => {
15628
+ switch (selection.method) {
15629
+ case "none":
15630
+ wizard.handleBookingModeChange(null);
15631
+ wizard.setSelectedMembershipId(null);
15632
+ return;
15633
+ case "card":
15634
+ wizard.handleBookingModeChange("card");
15635
+ wizard.setSelectedMembershipId(null);
15636
+ return;
15637
+ case "membership":
15638
+ wizard.handleBookingModeChange("membership");
15639
+ wizard.setSelectedMembershipId(selection.membershipId);
15640
+ return;
15641
+ }
15642
+ }, [wizard]);
15643
+ const runCurrentAction = useCallback(async () => {
15644
+ const blocked = resolveActionBlocker(wizard);
15645
+ if (blocked) return blocked;
15646
+ switch (wizard.step) {
15647
+ case "select-course":
15648
+ if (!wizard.selectedCourse) return {
14141
15649
  ok: false,
14142
15650
  reason: "blocked",
14143
- message: "Choose at least one available ticket to continue."
15651
+ message: "Choose an available set of dates to continue."
14144
15652
  };
14145
15653
  wizard.setStep("enter-details");
14146
- return { ok: true };
14147
- case "enter-details": {
14148
- const result = await wizard.handleDetailsComplete();
14149
- return result.ok ? { ok: true } : result;
14150
- }
14151
- case "select-payment": {
14152
- const result = await submit({ redirect: "manual" });
14153
- return result.ok ? { ok: true } : {
14154
- ok: false,
14155
- reason: result.reason === "submission-failed" ? "submission-failed" : "blocked",
14156
- message: result.message
15654
+ return {
15655
+ ok: true,
15656
+ outcome: { kind: "state-updated" }
14157
15657
  };
14158
- }
14159
- case "error":
14160
- case "success":
15658
+ case "enter-details": return toHeadlessActionResult(await wizard.handleSubmit());
15659
+ case "booking-mode":
15660
+ case "select-payment": return toHeadlessActionResult(await wizard.handlePaymentContinue());
14161
15661
  case "submitting":
14162
15662
  case "redirecting":
14163
15663
  case "verifying-payment":
14164
15664
  case "verifying-timeout":
14165
- case "payment-failed": return {
15665
+ case "payment-failed":
15666
+ case "success":
15667
+ case "waitlist":
15668
+ case "error": return {
14166
15669
  ok: false,
14167
15670
  reason: "blocked",
14168
15671
  message: "This registration step cannot continue."
14169
15672
  };
14170
15673
  }
14171
- }, [submit, wizard]);
15674
+ }, [pricingSelection, wizard]);
14172
15675
  const back = useCallback(() => {
14173
15676
  switch (wizard.step) {
14174
15677
  case "enter-details":
14175
- wizard.setStep("select-tickets");
15678
+ if (!wizard.hasPreSelectedCourse) wizard.setStep("select-course");
14176
15679
  return;
14177
- case "select-payment":
15680
+ case "booking-mode":
14178
15681
  wizard.setStep("enter-details");
14179
15682
  return;
14180
- case "select-tickets":
14181
- if (!wizard.hasPreSelectedContext) wizard.setStep("select-event");
15683
+ case "select-payment":
15684
+ wizard.setStep(wizard.canUseMembership ? "booking-mode" : "enter-details");
14182
15685
  return;
14183
- case "select-event":
14184
- case "error":
14185
- case "success":
15686
+ case "select-course":
14186
15687
  case "submitting":
14187
15688
  case "redirecting":
14188
15689
  case "verifying-payment":
14189
15690
  case "verifying-timeout":
14190
- case "payment-failed": return;
15691
+ case "payment-failed":
15692
+ case "success":
15693
+ case "waitlist":
15694
+ case "error": return;
14191
15695
  }
14192
15696
  }, [wizard]);
14193
- const updateAttendee = useCallback((values) => {
14194
- wizard.setFormData(values);
14195
- }, [wizard]);
14196
- const updateGuests = useCallback((guests) => {
14197
- wizard.setGuests(guests);
14198
- }, [wizard]);
14199
- const selectPayment = useCallback((selection) => {
14200
- wizard.handlePaymentSelectionChange(normalizeEventRegistrationDirectPaymentSelection({
14201
- options: directPaymentOptions,
14202
- selection: toInternalPaymentSelection(selection)
14203
- }));
14204
- }, [directPaymentOptions, wizard]);
14205
- const selectedTicketTypeId = getSingleSelectedTicketTypeId(wizard.selectedTicketLines);
14206
- const spamProtection = options.spamProtectionEnabled ?? isSpamProtectionEnabled() ? {
15697
+ const payment = {
15698
+ required: wizard.selectedCoursePricing?.selectedRequiresPayment ?? false,
15699
+ selection: paymentSelection,
15700
+ canUseMembership: wizard.canUseMembership,
15701
+ eligibleMemberships: wizard.eligibleMemberships.map((membership) => ({
15702
+ id: membership.customerMembershipId,
15703
+ name: membership.membershipName
15704
+ })),
15705
+ eligibility: wizard.eligibilityLoading ? { tag: "loading" } : wizard.eligibilityError ? {
15706
+ tag: "error",
15707
+ message: wizard.eligibilityError
15708
+ } : wizard.courseEligibility || wizard.portalAuthState !== "checking" ? { tag: "loaded" } : { tag: "idle" }
15709
+ };
15710
+ const spamProtection = wizard.showSpamProtection ? {
14207
15711
  enabled: true,
14208
15712
  provider: "altcha",
14209
15713
  challenge: {
@@ -14212,165 +15716,280 @@ function useEventRegistrationControllerWithDeps(options, deps) {
14212
15716
  },
14213
15717
  solution: wizard.altchaSolution
14214
15718
  } : { enabled: false };
14215
- const payment = resolvePaymentModel(wizard, directPaymentOptions);
15719
+ const selectedRun = wizard.selectedCourse;
15720
+ const isFull = wizard.courseEligibility?.isFull ?? (selectedRun?.availableSpots !== null && selectedRun?.availableSpots !== void 0 ? selectedRun.availableSpots <= 0 : false);
14216
15721
  return {
14217
- flow: {
14218
- status: resolveHeadlessEventRegistrationStatus(wizard),
14219
- view: resolveHeadlessEventRegistrationView(wizard),
14220
- steps,
14221
- currentStep,
14222
- events: {
14223
- items: wizard.events,
14224
- scoped: wizard.scopedEvents,
14225
- selected: wizard.selectedEvent,
14226
- preSelected: wizard.preSelectedEvent,
14227
- hasPreSelectedContext: wizard.hasPreSelectedContext,
14228
- target: wizard.registrationTarget,
14229
- occurrenceContext: wizard.occurrenceContext,
14230
- contentEntry: wizard.contentEntry
14231
- },
14232
- tickets: {
14233
- count: wizard.ticketsCount,
14234
- max: wizard.maxTicketsNum,
14235
- selections: wizard.ticketSelections,
14236
- summaryLines: wizard.ticketSelectionSummaryLines,
14237
- subtotalCents: wizard.ticketSelectionSubtotalCents,
14238
- selectedTicketTypeId,
14239
- selectedTicketType: wizard.selectedTicketType,
14240
- visibleTicketTypes: wizard.visibleTicketTypes,
14241
- selectionRequired: wizard.ticketTypeSelectionRequired,
14242
- hasValidSelection: wizard.hasValidTicketSelection,
14243
- eventRequiresPayment: wizard.eventRequiresPayment,
14244
- selectedAttendanceMode: wizard.selectedAttendanceMode
14245
- },
14246
- attendee: {
14247
- values: wizard.formData,
14248
- errors: wizard.errors
14249
- },
14250
- guests: {
14251
- required: wizard.requireGuestDetails,
14252
- items: wizard.guests,
14253
- errors: wizard.guestErrors,
14254
- shake: wizard.guestSectionShake
14255
- },
14256
- payment,
14257
- discount: {
14258
- code: wizard.discountCode,
14259
- isOpen: wizard.discountUiOpen,
14260
- available: wizard.discountsAvailable,
14261
- manualCodeEntryAllowed: wizard.manualCodeEntryAllowed,
14262
- loading: wizard.discountsAvailableLoading,
14263
- autoAppliedDiscount: wizard.autoAppliedDiscount
14264
- },
14265
- portalAuth: {
14266
- state: wizard.portalAuthState,
14267
- profile: wizard.portalProfile,
14268
- isAuthenticated: wizard.portalAuthState === "authenticated",
14269
- showMagicLinkModal: wizard.showMagicLinkModal
14270
- },
14271
- existingRegistration: {
14272
- state: resolveExistingRegistrationState(wizard),
14273
- registration: wizard.existingRegistration
14274
- },
14275
- waitlistClaim: wizard.waitlistClaim,
14276
- spamProtection,
14277
- terminal: {
14278
- result: wizard.registrationResult,
14279
- errorMessage: wizard.errorMessage,
14280
- verification: wizard.verification
14281
- },
14282
- actions: {
14283
- selectEvent: wizard.handleEventSelect,
14284
- selectAttendanceMode: wizard.setSelectedAttendanceMode,
14285
- selectTicketType: (ticketTypeId) => {
14286
- wizard.setSelectedTicketLines(ticketTypeId ? [{
14287
- ticketTypeId,
14288
- quantity: 1
14289
- }] : []);
14290
- },
14291
- setTicketSelections: wizard.setSelectedTicketLines,
14292
- setTicketsCount: wizard.setTicketsCount,
14293
- updateAttendee,
14294
- updateGuests,
14295
- selectPayment,
14296
- setDiscountCode: wizard.setDiscountCode,
14297
- setDiscountOpen: wizard.setDiscountUiOpen,
14298
- setAltchaSolution: wizard.setAltchaSolution,
14299
- requestLogin: () => wizard.setShowMagicLinkModal(true),
14300
- logout: wizard.handleLogout,
14301
- next,
14302
- back,
14303
- submit,
14304
- retry: wizard.handleRetry
14305
- }
15722
+ status: resolveWholeSeriesRegistrationStatus(wizard),
15723
+ view: resolveWholeSeriesRegistrationView({
15724
+ step: wizard.step,
15725
+ scopedRunsCount: wizard.scopedCourses.length,
15726
+ selectedRun,
15727
+ enrollmentResult: wizard.enrollmentResult,
15728
+ errorMessage: wizard.errorMessage
15729
+ }),
15730
+ steps,
15731
+ currentStep,
15732
+ runs: {
15733
+ items: (options.runs ?? []).map((run) => toWholeSeriesRegistrationRun(toPublicCourse(run))),
15734
+ scoped: wizard.scopedCourses.map(toWholeSeriesRegistrationRun),
15735
+ selected: selectedRun ? toWholeSeriesRegistrationRun(selectedRun) : null,
15736
+ hasPreSelectedRun: wizard.hasPreSelectedCourse,
15737
+ contentEntry: options.contentEntry ?? null,
15738
+ subject: options.subject ?? null
15739
+ },
15740
+ registrant: {
15741
+ values: wizard.formData,
15742
+ errors: wizard.errors
15743
+ },
15744
+ pricing: {
15745
+ ...toWholeSeriesRegistrationPricing(wizard.selectedCoursePricing),
15746
+ selectedOptionId: wizard.selectedPricingOptionId,
15747
+ selection: pricingSelection
14306
15748
  },
14307
- wizard
14308
- };
14309
- }
14310
- function useEventRegistrationWizardControllerWithDeps(options, deps) {
14311
- const maxTicketsNum = normalizeEventRegistrationMaxTickets(options.maxTickets);
14312
- return { wizard: deps.useEventRegistrationWizard({
14313
- anchorId: options.anchorId ?? "event-registration",
14314
- occurrenceContext: options.occurrenceContext ?? null,
14315
- contentEntry: options.contentEntry ?? null,
14316
- events: [...options.events ?? []],
14317
- maxTicketsNum
14318
- }) };
15749
+ payment,
15750
+ discount: {
15751
+ code: wizard.discountCode,
15752
+ isOpen: wizard.discountUiOpen,
15753
+ available: wizard.discountsAvailable,
15754
+ manualCodeEntryAllowed: wizard.manualCodeEntryAllowed,
15755
+ loading: wizard.discountsAvailableLoading
15756
+ },
15757
+ availability: {
15758
+ enrollmentOpen: selectedRun?.enrollmentOpen ?? false,
15759
+ availableSpots: selectedRun?.availableSpots ?? null,
15760
+ isFull,
15761
+ canJoinWaitlist: wizard.courseEligibility?.canJoinWaitlist ?? false,
15762
+ message: wizard.courseEligibility?.reason ?? null
15763
+ },
15764
+ portalAuth: {
15765
+ state: wizard.portalAuthState,
15766
+ isAuthenticated: wizard.portalAuthState === "authenticated"
15767
+ },
15768
+ waitlistClaim: resolveWaitlistClaim(wizard),
15769
+ spamProtection,
15770
+ terminal: {
15771
+ result: wizard.enrollmentResult,
15772
+ errorMessage: wizard.errorMessage
15773
+ },
15774
+ actions: {
15775
+ selectRun: (runId) => {
15776
+ const run = wizard.scopedCourses.find((candidate) => candidate.seriesId === runId) ?? null;
15777
+ if (run) wizard.handleCourseSelect(run);
15778
+ },
15779
+ updateRegistrant,
15780
+ selectPricingOption: wizard.setSelectedPricingOptionId,
15781
+ setSlidingAmountCents,
15782
+ selectPayment,
15783
+ setDiscountCode: wizard.setDiscountCode,
15784
+ setDiscountOpen: wizard.setDiscountUiOpen,
15785
+ setAltchaSolution: wizard.setAltchaSolution,
15786
+ continue: runCurrentAction,
15787
+ back,
15788
+ submit: runCurrentAction,
15789
+ retry: wizard.handleRetry,
15790
+ logout: () => wizard.handleLogout?.()
15791
+ }
15792
+ };
15793
+ }
15794
+ function toHeadlessActionResult(result) {
15795
+ switch (result.kind) {
15796
+ case "state-updated": return {
15797
+ ok: true,
15798
+ outcome: result
15799
+ };
15800
+ case "checkout-redirect": return {
15801
+ ok: true,
15802
+ outcome: result
15803
+ };
15804
+ case "validation-failed": return {
15805
+ ok: false,
15806
+ reason: "validation-failed",
15807
+ message: "Check the highlighted fields and try again."
15808
+ };
15809
+ case "blocked": return {
15810
+ ok: false,
15811
+ reason: "blocked",
15812
+ message: result.message
15813
+ };
15814
+ case "failed": return {
15815
+ ok: false,
15816
+ reason: "failed",
15817
+ message: result.message
15818
+ };
15819
+ }
14319
15820
  }
14320
- function resolveExistingRegistrationState(wizard) {
14321
- switch (wizard.existingRegistrationState.tag) {
14322
- case "idle": return {
14323
- tag: "idle",
14324
- value: wizard.existingRegistration
15821
+ function toPublicCourse(run) {
15822
+ const pricing = toCoursePricing(run.pricing);
15823
+ return {
15824
+ id: run.registrationTarget.courseId,
15825
+ seriesId: run.runId,
15826
+ title: run.title,
15827
+ slug: run.slug,
15828
+ description: run.description,
15829
+ teamMembers: run.teamMembers ? [...run.teamMembers] : void 0,
15830
+ enrollmentOpen: run.registrationOpen,
15831
+ pricing,
15832
+ priceCents: run.pricing.pricingStrategy === "single" ? run.pricing.amountCents : null,
15833
+ currency: run.pricing.currency,
15834
+ paymentTerms: run.pricing.paymentTerms,
15835
+ enrollmentCapacity: run.capacity,
15836
+ acceptsMemberships: run.acceptsMemberships,
15837
+ status: run.lifecycleStatus,
15838
+ sessionsCount: run.includedDates.length,
15839
+ enrolledCount: run.registeredCount,
15840
+ availableSpots: run.availableSpots,
15841
+ sessions: run.includedDates.map((date) => ({
15842
+ ...date,
15843
+ overrides: date.overrides ? { ...date.overrides } : date.overrides
15844
+ })),
15845
+ venue: run.venue,
15846
+ contentEntryId: run.contentEntryId
15847
+ };
15848
+ }
15849
+ function toCoursePricing(pricing) {
15850
+ const common = {
15851
+ bookingRequirement: pricing.bookingRequirement,
15852
+ pricingStrategy: pricing.pricingStrategy,
15853
+ currency: pricing.currency,
15854
+ paymentTerms: pricing.paymentTerms
15855
+ };
15856
+ switch (pricing.pricingStrategy) {
15857
+ case "free": return {
15858
+ ...common,
15859
+ singlePriceAmount: null,
15860
+ options: []
14325
15861
  };
14326
- case "loading": return {
14327
- tag: "loading",
14328
- value: wizard.existingRegistration
15862
+ case "single": return {
15863
+ ...common,
15864
+ singlePriceAmount: pricing.amountCents,
15865
+ options: []
14329
15866
  };
14330
- case "resolved": return {
14331
- tag: "loaded",
14332
- value: wizard.existingRegistration
15867
+ case "tiered": return {
15868
+ ...common,
15869
+ singlePriceAmount: null,
15870
+ options: pricing.options
15871
+ };
15872
+ case "sliding_scale": return {
15873
+ ...common,
15874
+ singlePriceAmount: null,
15875
+ slidingMinAmount: pricing.minAmountCents,
15876
+ slidingMaxAmount: pricing.maxAmountCents,
15877
+ slidingSuggestedAmount: pricing.suggestedAmountCents,
15878
+ options: []
14333
15879
  };
14334
15880
  }
14335
15881
  }
14336
- function resolvePaymentModel(wizard, directPaymentOptions) {
14337
- const internalSelection = normalizeEventRegistrationDirectPaymentSelection({
14338
- options: directPaymentOptions,
14339
- selection: wizard.paymentSelection
14340
- });
14341
- const selection = toHeadlessPaymentSelection(internalSelection);
14342
- const directPaymentSelectionState = resolveEventRegistrationDirectPaymentSelection({
14343
- options: directPaymentOptions,
14344
- selection: internalSelection
14345
- });
14346
- if (wizard.step !== "select-payment") return {
14347
- kind: "hidden",
14348
- selection,
14349
- directPaymentOptions,
14350
- directPaymentSelectionState,
14351
- stepState: wizard.paymentStepState
15882
+ function toWholeSeriesRegistrationRun(run) {
15883
+ return {
15884
+ id: run.seriesId,
15885
+ title: run.title,
15886
+ slug: run.slug,
15887
+ description: run.description,
15888
+ registrationOpen: run.enrollmentOpen,
15889
+ capacity: run.enrollmentCapacity,
15890
+ registeredCount: run.enrolledCount,
15891
+ availableSpots: run.availableSpots,
15892
+ includedDates: run.sessions.map((session) => ({
15893
+ id: session.id,
15894
+ startsAt: session.startsAt,
15895
+ endsAt: session.endsAt,
15896
+ timeZone: session.timeZone
15897
+ })),
15898
+ venue: run.venue
15899
+ };
15900
+ }
15901
+ function toWholeSeriesRegistrationPricing(pricing) {
15902
+ if (!pricing) return {
15903
+ currency: null,
15904
+ display: null,
15905
+ options: [],
15906
+ slidingScale: null,
15907
+ selectedAmountCents: null,
15908
+ selectedPriceCents: null,
15909
+ requiresPayment: false
14352
15910
  };
14353
15911
  return {
14354
- kind: wizard.paymentStepState.status,
14355
- selection,
14356
- directPaymentOptions,
14357
- directPaymentSelectionState,
14358
- stepState: wizard.paymentStepState,
14359
- eligibility: wizard.eligibility,
14360
- eligibilityError: wizard.eligibilityError
15912
+ currency: pricing.currency,
15913
+ display: toWholeSeriesPriceDisplay(pricing.display),
15914
+ options: pricing.selectableOptions.map((option) => ({
15915
+ id: option.id,
15916
+ name: option.name,
15917
+ description: option.description,
15918
+ amountCents: option.priceAmount
15919
+ })),
15920
+ slidingScale: pricing.slidingScale ? {
15921
+ minAmountCents: pricing.slidingScale.minAmount,
15922
+ maxAmountCents: pricing.slidingScale.maxAmount,
15923
+ suggestedAmountCents: pricing.slidingScale.suggestedAmount
15924
+ } : null,
15925
+ selectedAmountCents: pricing.selectedSlidingAmount,
15926
+ selectedPriceCents: pricing.selectedPriceAmount,
15927
+ requiresPayment: pricing.selectedRequiresPayment
15928
+ };
15929
+ }
15930
+ function toWholeSeriesPriceDisplay(display) {
15931
+ switch (display.kind) {
15932
+ case "free": return display;
15933
+ case "exact": return {
15934
+ kind: "exact",
15935
+ amountCents: display.amount,
15936
+ currency: display.currency
15937
+ };
15938
+ case "from": return {
15939
+ kind: "from",
15940
+ amountCents: display.amount,
15941
+ currency: display.currency
15942
+ };
15943
+ case "range": return {
15944
+ kind: "range",
15945
+ minAmountCents: display.minAmount,
15946
+ maxAmountCents: display.maxAmount,
15947
+ currency: display.currency
15948
+ };
15949
+ case "unavailable": return display;
15950
+ }
15951
+ }
15952
+ function resolveWaitlistClaim(wizard) {
15953
+ const waitlistClaim = wizard.waitlistClaim;
15954
+ if (waitlistClaim.isLoading) return { tag: "loading" };
15955
+ if (waitlistClaim.requiresPortalLogin) return {
15956
+ tag: "login-required",
15957
+ message: waitlistClaim.loginMessage ?? "Log in to continue with your waitlist place."
15958
+ };
15959
+ if (waitlistClaim.error) return {
15960
+ tag: "error",
15961
+ message: waitlistClaim.error
15962
+ };
15963
+ if (waitlistClaim.claim) return {
15964
+ tag: "ready",
15965
+ expiresAt: waitlistClaim.claim.expiresAt,
15966
+ prefill: {
15967
+ email: waitlistClaim.claim.prefill.email,
15968
+ name: waitlistClaim.claim.prefill.name,
15969
+ phone: waitlistClaim.claim.prefill.phone,
15970
+ notes: waitlistClaim.claim.prefill.notes
15971
+ }
14361
15972
  };
15973
+ return { tag: "none" };
14362
15974
  }
14363
- function resolveHeadlessDirectPaymentOptions(wizard) {
14364
- const eligibility = wizard.eligibility;
14365
- if (!eligibility?.eligible || !eligibility.canPayDirectly || !eligibility.price) return [];
14366
- const event = wizard.selectedEvent ?? wizard.preSelectedEvent;
14367
- return buildEventRegistrationPaymentOptionsViewModel({
14368
- eventTitle: event?.title ?? "Event",
14369
- paymentTerms: event?.paymentTerms,
14370
- totalPriceCents: eligibility.price,
14371
- currency: eligibility.currency,
14372
- startsAt: wizard.occurrenceContext?.startsAt ?? event?.startsAt
14373
- }).options;
15975
+ function resolveActionBlocker(wizard) {
15976
+ if (!wizard.selectedCourse) return null;
15977
+ if (!wizard.selectedCourse.enrollmentOpen) return {
15978
+ ok: false,
15979
+ reason: "blocked",
15980
+ message: "Sign-up is closed."
15981
+ };
15982
+ if (wizard.eligibilityLoading || wizard.portalAuthState === "checking") return {
15983
+ ok: false,
15984
+ reason: "blocked",
15985
+ message: "Availability is still being checked."
15986
+ };
15987
+ if (wizard.courseEligibility?.isFull && !wizard.courseEligibility.canJoinWaitlist) return {
15988
+ ok: false,
15989
+ reason: "blocked",
15990
+ message: wizard.courseEligibility.reason ?? "There are no places available."
15991
+ };
15992
+ return null;
14374
15993
  }
14375
15994
  //#endregion
14376
15995
  //#region ../blocks/src/system/runtime/hooks/useBookingFormConfig.state.ts
@@ -14581,8 +16200,11 @@ const HeadlessBookingProvider = (props) => {
14581
16200
  const HeadlessEventRegistrationProvider = (props) => {
14582
16201
  return React$1.createElement(HeadlessEventRegistrationProvider$1, props);
14583
16202
  };
16203
+ const HeadlessWholeSeriesRegistrationProvider = (props) => {
16204
+ return React$1.createElement(HeadlessWholeSeriesRegistrationProvider$1, props);
16205
+ };
14584
16206
  const HeadlessBookingPaymentSelectionStep = (props) => {
14585
16207
  return React$1.createElement(HeadlessBookingPaymentSelectionStep$1, props);
14586
16208
  };
14587
16209
  //#endregion
14588
- export { BOOKING_FORM_DATA_RESERVED_KEY_VALUES, BookingFormClient, AltchaWidget as HeadlessBookingAltchaWidget, AltchaWidget as HeadlessEventRegistrationAltchaWidget, HeadlessBookingPaymentSelectionStep, HeadlessBookingProvider, HeadlessEventRegistrationProvider, createBookingBlockApiConfig, createEventRegistrationBlockApiConfig, getAppointmentBookingOverrideInput, getEventRegistrationOverrideInput, useBookingFormConfig, useHeadlessBookingFlow, useHeadlessEventRegistrationFlow };
16210
+ export { BOOKING_FORM_DATA_RESERVED_KEY_VALUES, BookingFormClient, AltchaWidget as HeadlessBookingAltchaWidget, AltchaWidget as HeadlessEventRegistrationAltchaWidget, AltchaWidget as HeadlessWholeSeriesRegistrationAltchaWidget, HeadlessBookingPaymentSelectionStep, HeadlessBookingProvider, HeadlessEventRegistrationProvider, HeadlessWholeSeriesRegistrationProvider, createBookingBlockApiConfig, createEventRegistrationBlockApiConfig, createWholeSeriesRegistrationBlockApiConfig, getAppointmentBookingOverrideInput, getEventRegistrationOverrideInput, useBookingFormConfig, useHeadlessBookingFlow, useHeadlessEventRegistrationFlow, useHeadlessWholeSeriesRegistrationFlow };