@medalsocial/booking 0.0.0-placeholder.0 → 0.2.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 (266) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +6 -0
  4. package/README.md +220 -1
  5. package/dist/core/age.d.mts +34 -0
  6. package/dist/core/age.mjs +95 -0
  7. package/dist/core/age.mjs.map +1 -0
  8. package/dist/core/attempt-store.d.mts +111 -0
  9. package/dist/core/attempt-store.mjs +159 -0
  10. package/dist/core/attempt-store.mjs.map +1 -0
  11. package/dist/core/categories.d.mts +27 -0
  12. package/dist/core/categories.mjs +42 -0
  13. package/dist/core/categories.mjs.map +1 -0
  14. package/dist/core/clock.d.mts +33 -0
  15. package/dist/core/clock.mjs +251 -0
  16. package/dist/core/clock.mjs.map +1 -0
  17. package/dist/core/config.d.mts +178 -0
  18. package/dist/core/config.mjs +394 -0
  19. package/dist/core/config.mjs.map +1 -0
  20. package/dist/core/consent.d.mts +14 -0
  21. package/dist/core/consent.mjs +17 -0
  22. package/dist/core/consent.mjs.map +1 -0
  23. package/dist/core/deep-link.d.mts +40 -0
  24. package/dist/core/deep-link.mjs +160 -0
  25. package/dist/core/deep-link.mjs.map +1 -0
  26. package/dist/core/display-name.d.mts +18 -0
  27. package/dist/core/display-name.mjs +30 -0
  28. package/dist/core/display-name.mjs.map +1 -0
  29. package/dist/core/draft-store.d.mts +89 -0
  30. package/dist/core/draft-store.mjs +131 -0
  31. package/dist/core/draft-store.mjs.map +1 -0
  32. package/dist/core/dto.d.mts +46 -0
  33. package/dist/core/dto.mjs +157 -0
  34. package/dist/core/dto.mjs.map +1 -0
  35. package/dist/core/ics.d.mts +63 -0
  36. package/dist/core/ics.mjs +130 -0
  37. package/dist/core/ics.mjs.map +1 -0
  38. package/dist/core/index.d.mts +35 -0
  39. package/dist/core/index.mjs +32 -0
  40. package/dist/core/labels.d.mts +88 -0
  41. package/dist/core/labels.mjs +93 -0
  42. package/dist/core/labels.mjs.map +1 -0
  43. package/dist/core/machine.d.mts +645 -0
  44. package/dist/core/machine.mjs +988 -0
  45. package/dist/core/machine.mjs.map +1 -0
  46. package/dist/core/money.d.mts +12 -0
  47. package/dist/core/money.mjs +51 -0
  48. package/dist/core/money.mjs.map +1 -0
  49. package/dist/core/next-available.d.mts +20 -0
  50. package/dist/core/next-available.mjs +36 -0
  51. package/dist/core/next-available.mjs.map +1 -0
  52. package/dist/core/next-free.d.mts +25 -0
  53. package/dist/core/next-free.mjs +44 -0
  54. package/dist/core/next-free.mjs.map +1 -0
  55. package/dist/core/party-slots.d.mts +79 -0
  56. package/dist/core/party-slots.mjs +229 -0
  57. package/dist/core/party-slots.mjs.map +1 -0
  58. package/dist/core/paths.d.mts +11 -0
  59. package/dist/core/paths.mjs +56 -0
  60. package/dist/core/paths.mjs.map +1 -0
  61. package/dist/core/phone.d.mts +17 -0
  62. package/dist/core/phone.mjs +58 -0
  63. package/dist/core/phone.mjs.map +1 -0
  64. package/dist/core/portal/cookies.d.mts +29 -0
  65. package/dist/core/portal/dto.d.mts +140 -0
  66. package/dist/core/portal/dto.mjs +142 -0
  67. package/dist/core/portal/dto.mjs.map +1 -0
  68. package/dist/core/portal/family.d.mts +39 -0
  69. package/dist/core/portal/family.mjs +39 -0
  70. package/dist/core/portal/family.mjs.map +1 -0
  71. package/dist/core/portal/login-input.d.mts +29 -0
  72. package/dist/core/portal/login-input.mjs +29 -0
  73. package/dist/core/portal/login-input.mjs.map +1 -0
  74. package/dist/core/portal/next-path.d.mts +12 -0
  75. package/dist/core/portal/next-path.mjs +98 -0
  76. package/dist/core/portal/next-path.mjs.map +1 -0
  77. package/dist/core/portal/return-path.d.mts +7 -0
  78. package/dist/core/portal/return-path.mjs +49 -0
  79. package/dist/core/portal/return-path.mjs.map +1 -0
  80. package/dist/core/portal/session-cookie.d.mts +27 -0
  81. package/dist/core/portal/session-cookie.mjs +30 -0
  82. package/dist/core/portal/session-cookie.mjs.map +1 -0
  83. package/dist/core/portal/vipps-link.d.mts +56 -0
  84. package/dist/core/portal/vipps-link.mjs +136 -0
  85. package/dist/core/portal/vipps-link.mjs.map +1 -0
  86. package/dist/core/portal/vipps-return.d.mts +39 -0
  87. package/dist/core/portal/vipps-return.mjs +69 -0
  88. package/dist/core/portal/vipps-return.mjs.map +1 -0
  89. package/dist/core/rebook-store.d.mts +28 -0
  90. package/dist/core/rebook-store.mjs +60 -0
  91. package/dist/core/rebook-store.mjs.map +1 -0
  92. package/dist/core/restore-gate.d.mts +40 -0
  93. package/dist/core/restore-gate.mjs +96 -0
  94. package/dist/core/restore-gate.mjs.map +1 -0
  95. package/dist/core/stores.d.mts +8 -0
  96. package/dist/core/stores.mjs +23 -0
  97. package/dist/core/stores.mjs.map +1 -0
  98. package/dist/core/types.d.mts +245 -0
  99. package/dist/core/visit.d.mts +26 -0
  100. package/dist/core/visit.mjs +40 -0
  101. package/dist/core/visit.mjs.map +1 -0
  102. package/dist/core/wire.d.mts +37 -0
  103. package/dist/next/cache/memory.d.mts +14 -0
  104. package/dist/next/cache/memory.mjs +51 -0
  105. package/dist/next/cache/memory.mjs.map +1 -0
  106. package/dist/next/cache/next-data.d.mts +6 -0
  107. package/dist/next/cache/next-data.mjs +35 -0
  108. package/dist/next/cache/next-data.mjs.map +1 -0
  109. package/dist/next/cache/noop.d.mts +6 -0
  110. package/dist/next/cache/noop.mjs +15 -0
  111. package/dist/next/cache/noop.mjs.map +1 -0
  112. package/dist/next/cache/workers.d.mts +29 -0
  113. package/dist/next/cache/workers.mjs +74 -0
  114. package/dist/next/cache/workers.mjs.map +1 -0
  115. package/dist/next/catalogue.d.mts +38 -0
  116. package/dist/next/catalogue.mjs +215 -0
  117. package/dist/next/catalogue.mjs.map +1 -0
  118. package/dist/next/handler.d.mts +11 -0
  119. package/dist/next/handler.mjs +150 -0
  120. package/dist/next/handler.mjs.map +1 -0
  121. package/dist/next/index.d.mts +17 -0
  122. package/dist/next/index.mjs +14 -0
  123. package/dist/next/loaders.d.mts +79 -0
  124. package/dist/next/loaders.mjs +195 -0
  125. package/dist/next/loaders.mjs.map +1 -0
  126. package/dist/next/manage-dto.d.mts +6 -0
  127. package/dist/next/manage-dto.mjs +28 -0
  128. package/dist/next/manage-dto.mjs.map +1 -0
  129. package/dist/next/medal.d.mts +105 -0
  130. package/dist/next/medal.mjs +155 -0
  131. package/dist/next/medal.mjs.map +1 -0
  132. package/dist/next/options.d.mts +158 -0
  133. package/dist/next/options.mjs +65 -0
  134. package/dist/next/options.mjs.map +1 -0
  135. package/dist/next/portal/guardian.d.mts +42 -0
  136. package/dist/next/portal/guardian.mjs +76 -0
  137. package/dist/next/portal/guardian.mjs.map +1 -0
  138. package/dist/next/portal/medal-portal.d.mts +161 -0
  139. package/dist/next/portal/medal-portal.mjs +825 -0
  140. package/dist/next/portal/medal-portal.mjs.map +1 -0
  141. package/dist/next/portal/session.d.mts +38 -0
  142. package/dist/next/portal/session.mjs +40 -0
  143. package/dist/next/portal/session.mjs.map +1 -0
  144. package/dist/next/portal/vipps-flash.d.mts +14 -0
  145. package/dist/next/portal/vipps-flash.mjs +41 -0
  146. package/dist/next/portal/vipps-flash.mjs.map +1 -0
  147. package/dist/next/portal.d.mts +205 -0
  148. package/dist/next/portal.mjs +469 -0
  149. package/dist/next/portal.mjs.map +1 -0
  150. package/dist/next/request.d.mts +51 -0
  151. package/dist/next/request.mjs +123 -0
  152. package/dist/next/request.mjs.map +1 -0
  153. package/dist/next/routes/catalogue-routes.mjs +344 -0
  154. package/dist/next/routes/catalogue-routes.mjs.map +1 -0
  155. package/dist/next/routes/create.mjs +460 -0
  156. package/dist/next/routes/create.mjs.map +1 -0
  157. package/dist/next/routes/manage.mjs +191 -0
  158. package/dist/next/routes/manage.mjs.map +1 -0
  159. package/dist/next/routes/portal-routes.mjs +312 -0
  160. package/dist/next/routes/portal-routes.mjs.map +1 -0
  161. package/dist/next/routes/shared.mjs +162 -0
  162. package/dist/next/routes/shared.mjs.map +1 -0
  163. package/dist/next/routes/vipps-return.mjs +166 -0
  164. package/dist/next/routes/vipps-return.mjs.map +1 -0
  165. package/dist/next/runtime.d.mts +49 -0
  166. package/dist/next/runtime.mjs +94 -0
  167. package/dist/next/runtime.mjs.map +1 -0
  168. package/dist/next/seed.d.mts +71 -0
  169. package/dist/next/seed.mjs +495 -0
  170. package/dist/next/seed.mjs.map +1 -0
  171. package/dist/next/server.d.mts +26 -0
  172. package/dist/next/server.mjs +63 -0
  173. package/dist/next/server.mjs.map +1 -0
  174. package/dist/react/BookingLink.d.mts +61 -0
  175. package/dist/react/BookingLink.mjs +102 -0
  176. package/dist/react/BookingLink.mjs.map +1 -0
  177. package/dist/react/BookingWizard.d.mts +49 -0
  178. package/dist/react/BookingWizard.mjs +598 -0
  179. package/dist/react/BookingWizard.mjs.map +1 -0
  180. package/dist/react/LoginPageQuery.d.mts +37 -0
  181. package/dist/react/LoginPageQuery.mjs +106 -0
  182. package/dist/react/LoginPageQuery.mjs.map +1 -0
  183. package/dist/react/LoginSheet.d.mts +49 -0
  184. package/dist/react/LoginSheet.mjs +98 -0
  185. package/dist/react/LoginSheet.mjs.map +1 -0
  186. package/dist/react/ManageBooking.d.mts +50 -0
  187. package/dist/react/ManageBooking.mjs +197 -0
  188. package/dist/react/ManageBooking.mjs.map +1 -0
  189. package/dist/react/PortalDashboard.d.mts +10 -0
  190. package/dist/react/Provider.d.mts +82 -0
  191. package/dist/react/Provider.mjs +132 -0
  192. package/dist/react/Provider.mjs.map +1 -0
  193. package/dist/react/actions.d.mts +133 -0
  194. package/dist/react/actions.mjs +90 -0
  195. package/dist/react/actions.mjs.map +1 -0
  196. package/dist/react/index.d.mts +28 -0
  197. package/dist/react/index.mjs +20 -0
  198. package/dist/react/kit.d.mts +54 -0
  199. package/dist/react/kit.mjs +127 -0
  200. package/dist/react/kit.mjs.map +1 -0
  201. package/dist/react/labels/index.d.mts +23 -0
  202. package/dist/react/labels/index.mjs +44 -0
  203. package/dist/react/labels/index.mjs.map +1 -0
  204. package/dist/react/labels/login.d.mts +10 -0
  205. package/dist/react/labels/login.mjs +13 -0
  206. package/dist/react/labels/login.mjs.map +1 -0
  207. package/dist/react/labels/manage.d.mts +15 -0
  208. package/dist/react/labels/manage.mjs +19 -0
  209. package/dist/react/labels/manage.mjs.map +1 -0
  210. package/dist/react/labels/portal.d.mts +25 -0
  211. package/dist/react/labels/portal.mjs +41 -0
  212. package/dist/react/labels/portal.mjs.map +1 -0
  213. package/dist/react/labels/screens.d.mts +11 -0
  214. package/dist/react/labels/screens.mjs +721 -0
  215. package/dist/react/labels/screens.mjs.map +1 -0
  216. package/dist/react/labels/wizard.d.mts +76 -0
  217. package/dist/react/labels/wizard.mjs +97 -0
  218. package/dist/react/labels/wizard.mjs.map +1 -0
  219. package/dist/react/link/index.d.mts +5 -0
  220. package/dist/react/link/index.mjs +5 -0
  221. package/dist/react/portal/SessionTouch.d.mts +19 -0
  222. package/dist/react/portal/SessionTouch.mjs +40 -0
  223. package/dist/react/portal/SessionTouch.mjs.map +1 -0
  224. package/dist/react/portal/age-prompt.d.mts +10 -0
  225. package/dist/react/portal/age-prompt.mjs +21 -0
  226. package/dist/react/portal/age-prompt.mjs.map +1 -0
  227. package/dist/react/portal/book-again.d.mts +18 -0
  228. package/dist/react/portal/book-again.mjs +21 -0
  229. package/dist/react/portal/book-again.mjs.map +1 -0
  230. package/dist/react/portal/child-summary.d.mts +17 -0
  231. package/dist/react/portal/child-summary.mjs +45 -0
  232. package/dist/react/portal/child-summary.mjs.map +1 -0
  233. package/dist/react/portal/dashboard.d.mts +74 -0
  234. package/dist/react/portal/dashboard.mjs +204 -0
  235. package/dist/react/portal/dashboard.mjs.map +1 -0
  236. package/dist/react/portal/leave.d.mts +17 -0
  237. package/dist/react/portal/leave.mjs +21 -0
  238. package/dist/react/portal/leave.mjs.map +1 -0
  239. package/dist/react/portal/sections.d.mts +12 -0
  240. package/dist/react/portal/sections.mjs +48 -0
  241. package/dist/react/portal/sections.mjs.map +1 -0
  242. package/dist/react/portal/tabs.d.mts +23 -0
  243. package/dist/react/portal/tabs.mjs +36 -0
  244. package/dist/react/portal/tabs.mjs.map +1 -0
  245. package/dist/react/portal/vipps-flash.d.mts +11 -0
  246. package/dist/react/portal/vipps-flash.mjs +18 -0
  247. package/dist/react/portal/vipps-flash.mjs.map +1 -0
  248. package/dist/react/portal/wired.mjs +202 -0
  249. package/dist/react/portal/wired.mjs.map +1 -0
  250. package/dist/react/screen-labels.mjs +14 -0
  251. package/dist/react/screen-labels.mjs.map +1 -0
  252. package/dist/react/shared.d.mts +13 -0
  253. package/dist/react/shared.mjs +7 -0
  254. package/dist/react/use-next-free.d.mts +19 -0
  255. package/dist/react/use-next-free.mjs +56 -0
  256. package/dist/react/use-next-free.mjs.map +1 -0
  257. package/dist/react/useBooking.d.mts +179 -0
  258. package/dist/react/useBooking.mjs +1331 -0
  259. package/dist/react/useBooking.mjs.map +1 -0
  260. package/dist/react/wizard/adapters.mjs +189 -0
  261. package/dist/react/wizard/adapters.mjs.map +1 -0
  262. package/dist/react/wizard/index.d.mts +11 -0
  263. package/dist/react/wizard/index.mjs +6 -0
  264. package/dist/react/wizard-error.mjs +29 -0
  265. package/dist/react/wizard-error.mjs.map +1 -0
  266. package/package.json +126 -5
@@ -0,0 +1,598 @@
1
+ 'use client';
2
+ import { fill, fillParts, labelText } from "../core/labels.mjs";
3
+ import { visitServicesOf } from "../core/visit.mjs";
4
+ import { SELF_KEY } from "../core/machine.mjs";
5
+ import { useBookingKit } from "./Provider.mjs";
6
+ import { screenLabels } from "./screen-labels.mjs";
7
+ import { LoginSheet as LoginSheet$1 } from "./LoginSheet.mjs";
8
+ import { useBooking } from "./useBooking.mjs";
9
+ import { addedChildEntries, confirmationProps, detailsLines, detailsScreenBase, familyEntries, guestChoices, partyPeople, partySizeWord, serviceFitsFor, serviceScreenBase, weekendNoteFor } from "./wizard/adapters.mjs";
10
+ import { forMedaScreen, wizardErrorText } from "./wizard-error.mjs";
11
+ import { BookingButton, Confirmation, DetailsScreen, LiveStatus, ServiceScreen, StylistScreen, SummaryBar, TakenToast, TimeScreen, TimeScreenSkeleton, WhoScreen } from "@medalsocial/meda/booking";
12
+ import { Suspense, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
13
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
14
+ import { ArrowLeft } from "lucide-react";
15
+ //#region src/react/BookingWizard.tsx
16
+ /**
17
+ * `<BookingWizard>` — the booking flow, from «who is it for?» to the
18
+ * confirmation, drawn with the `@medalsocial/meda/booking` screens over
19
+ * `useBooking()`.
20
+ *
21
+ * The hook owns the machine, the fetches and the stores; this file owns the
22
+ * page: which step is on screen (ONE at a time — the details step's nonce and
23
+ * the submit flag depend on it), focus and scroll on every move, the restore
24
+ * gate's pre-hydration script and skeleton, the header, and what each screen
25
+ * is handed (prices, the weekend note, ages, the possessive, the calendar
26
+ * file) out of the site's config.
27
+ */
28
+ /** The wizard's root, for the inline restore gate to find before hydration. */
29
+ const WIZARD_ROOT_ID = "booking-wizard";
30
+ /** The time step's heading, which a lost slot sends the visitor back to. */
31
+ const TIME_HEADING_ID = "booking-time-heading";
32
+ /** The heading each screen focuses when the visitor arrives on it. */
33
+ const STEP_HEADINGS = {
34
+ who: "booking-who-heading",
35
+ service: "booking-service-heading",
36
+ when: "booking-stylist-heading",
37
+ details: "booking-details-heading",
38
+ confirmed: "booking-confirmed-heading"
39
+ };
40
+ const STEPS = [
41
+ "who",
42
+ "service",
43
+ "when",
44
+ "details"
45
+ ];
46
+ /** Smooth, or instant for a visitor who asked for reduced motion. */
47
+ function scrollToTop(target) {
48
+ if (!target || typeof target.scrollIntoView !== "function") return;
49
+ const reduced = typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
50
+ target.scrollIntoView({
51
+ behavior: reduced ? "auto" : "smooth",
52
+ block: "start"
53
+ });
54
+ }
55
+ /**
56
+ * The wizard, behind its own Suspense boundary: `useSearchParams` throws to
57
+ * the nearest one during a static prerender, and without ours that is the
58
+ * root layout's — the whole page would ship empty until hydration.
59
+ */
60
+ function BookingWizard(props) {
61
+ return /* @__PURE__ */ jsx(Suspense, {
62
+ fallback: null,
63
+ children: /* @__PURE__ */ jsx(BookingWizardShell, { ...props })
64
+ });
65
+ }
66
+ function BookingWizardShell(props) {
67
+ const resolved = useBookingKit(props, props.contact);
68
+ const booking = useBooking(props);
69
+ const { kit } = booking;
70
+ const { labels } = kit;
71
+ const { state, restore, confirmed, onEvent } = {
72
+ ...booking,
73
+ onEvent: props.onEvent
74
+ };
75
+ /**
76
+ * On every change of screen: the wizard's top into view and focus on the
77
+ * new step's heading. Never on the first screen, and never for what the
78
+ * restore gate reveals — that is the page arriving, not the visitor moving.
79
+ */
80
+ const rootRef = useRef(null);
81
+ const screenKey = confirmed !== null ? "confirmed" : state.step;
82
+ const shownScreen = useRef(null);
83
+ const takenSlotTs = booking.slots.takenSlotTs;
84
+ const timeStepReady = booking.slots.ready;
85
+ const focusTimeWhenReady = useRef(false);
86
+ useEffect(() => {
87
+ if (restore.restoring) return;
88
+ const previous = shownScreen.current;
89
+ shownScreen.current = screenKey;
90
+ if (previous === screenKey) return;
91
+ onEvent?.({
92
+ type: "step",
93
+ step: screenKey
94
+ });
95
+ if (previous === null) return;
96
+ const toTimes = screenKey === "when" && takenSlotTs !== null;
97
+ const headingId = toTimes ? TIME_HEADING_ID : STEP_HEADINGS[screenKey];
98
+ const heading = document.getElementById(headingId);
99
+ scrollToTop(toTimes ? heading : rootRef.current);
100
+ heading?.focus({ preventScroll: true });
101
+ focusTimeWhenReady.current = toTimes && !timeStepReady;
102
+ }, [
103
+ screenKey,
104
+ restore.restoring,
105
+ takenSlotTs,
106
+ timeStepReady,
107
+ onEvent
108
+ ]);
109
+ /** After a login from the sheet the row that opened it is gone: focus the step's heading. */
110
+ const focusAfterSignIn = useRef(false);
111
+ const signedIn = booking.login.signedIn;
112
+ useEffect(() => {
113
+ if (signedIn === null || !focusAfterSignIn.current) return;
114
+ focusAfterSignIn.current = false;
115
+ document.getElementById(STEP_HEADINGS[state.step])?.focus({ preventScroll: true });
116
+ }, [signedIn, state.step]);
117
+ useEffect(() => {
118
+ if (!timeStepReady || !focusTimeWhenReady.current) return;
119
+ focusTimeWhenReady.current = false;
120
+ const active = document.activeElement;
121
+ if (active === null || active === document.body) document.getElementById(TIME_HEADING_ID)?.focus({ preventScroll: true });
122
+ }, [timeStepReady]);
123
+ const lastError = useRef(state.error);
124
+ useEffect(() => {
125
+ if (state.error !== null && state.error !== lastError.current) onEvent?.({
126
+ type: "submit_error",
127
+ code: state.error
128
+ });
129
+ lastError.current = state.error;
130
+ }, [state.error, onEvent]);
131
+ const confirmedCount = confirmed?.bookings.length ?? 0;
132
+ useEffect(() => {
133
+ if (confirmedCount > 0) onEvent?.({
134
+ type: "submit_ok",
135
+ bookings: confirmedCount
136
+ });
137
+ }, [confirmedCount, onEvent]);
138
+ useLayoutEffect(() => {
139
+ if (!restore.hydrated) return;
140
+ rootRef.current?.removeAttribute("data-restoring");
141
+ rootRef.current?.setAttribute("data-hydrated", "");
142
+ }, [restore.hydrated]);
143
+ const [arrivalStep, setArrivalStep] = useState(restore.restoring ? null : state.step);
144
+ if (!restore.restoring && arrivalStep === null) setArrivalStep(state.step);
145
+ const [stepsMovedOnce, setStepsMovedOnce] = useState(false);
146
+ const movedNow = arrivalStep !== null && !restore.restoring && state.step !== arrivalStep;
147
+ if (movedNow && !stepsMovedOnce) setStepsMovedOnce(true);
148
+ const stepsMoved = stepsMovedOnce || movedNow;
149
+ const restoreSkeleton = /* @__PURE__ */ jsx(RestoreSkeleton, { text: labelText(labels["wizard.restoring"]) });
150
+ /**
151
+ * The root both screens share: the scroll target, and the element the
152
+ * inline restore gate marks. The script and the hidden skeleton exist only
153
+ * in the server's HTML and the hydration pass that adopts it.
154
+ */
155
+ const shell = (content) => /* @__PURE__ */ jsxs("div", {
156
+ id: WIZARD_ROOT_ID,
157
+ ref: rootRef,
158
+ className: resolved.classNames.wizard?.root ?? "group scroll-mt-28",
159
+ suppressHydrationWarning: true,
160
+ children: [!restore.hydrated && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: { __html: kit.restoreGate.restoreGateScript(WIZARD_ROOT_ID, restore.linked, restore.resuming) } }), /* @__PURE__ */ jsx("div", {
161
+ className: "hidden group-data-[restoring]:block",
162
+ children: restoreSkeleton
163
+ })] }), restore.restoring && restore.hydrated ? restoreSkeleton : /* @__PURE__ */ jsx("div", {
164
+ className: "group-data-[restoring]:hidden",
165
+ children: content
166
+ })]
167
+ });
168
+ if (confirmed !== null) return shell(/* @__PURE__ */ jsx(ConfirmationStep, {
169
+ booking,
170
+ resolved,
171
+ siteUrl: props.siteUrl
172
+ }));
173
+ const onSignedIn = (who) => {
174
+ focusAfterSignIn.current = true;
175
+ booking.login.signIn(who);
176
+ };
177
+ const multiSelect = state.step === "service" && kit.config.party.maxServicesPerPerson > 1;
178
+ const loginOffered = props.actions !== void 0 && kit.config.portal.enabled;
179
+ const loginRow = loginOffered ? /* @__PURE__ */ jsx(LoginRow, {
180
+ booking,
181
+ actions: props.actions,
182
+ overrides: props,
183
+ onSignedIn
184
+ }) : null;
185
+ return shell(/* @__PURE__ */ jsxs("div", {
186
+ className: "space-y-8",
187
+ children: [
188
+ /* @__PURE__ */ jsx(WizardHeader, {
189
+ kit,
190
+ state,
191
+ submitting: booking.submitting,
192
+ onGo: (step) => booking.dispatch({
193
+ type: "goToStep",
194
+ step
195
+ })
196
+ }),
197
+ state.error !== null && state.step !== "details" && state.step !== "when" && !multiSelect && /* @__PURE__ */ jsx("p", {
198
+ role: "alert",
199
+ className: resolved.classNames.wizard?.alert ?? "rounded-lg border border-destructive/40 bg-destructive/10 px-5 py-3 text-sm",
200
+ children: wizardErrorText(labels, state.error, kit.config.party.maxServicesPerPerson)
201
+ }),
202
+ state.step === "details" && loginRow,
203
+ loginOffered && booking.login.vippsConfirm !== null && signedIn === null && /* @__PURE__ */ jsx(LoginSheet$1, {
204
+ ...overridesOf(props),
205
+ actions: props.actions,
206
+ presentation: "sheet",
207
+ trigger: false,
208
+ vippsConfirm: booking.login.vippsConfirm,
209
+ resumePath: booking.login.resumePath,
210
+ onSignedIn
211
+ }),
212
+ /* @__PURE__ */ jsxs("div", {
213
+ "data-booking-step": state.step,
214
+ "data-animate": stepsMoved ? "" : void 0,
215
+ className: "space-y-8",
216
+ children: [
217
+ state.step === "who" && /* @__PURE__ */ jsx(WhoStep, {
218
+ booking,
219
+ resolved,
220
+ loginRow
221
+ }),
222
+ state.step === "service" && /* @__PURE__ */ jsx(ServiceStep, {
223
+ booking,
224
+ resolved,
225
+ multiSelect
226
+ }),
227
+ state.step === "when" && /* @__PURE__ */ jsx(WhenStep, {
228
+ booking,
229
+ resolved
230
+ }),
231
+ state.step === "details" && /* @__PURE__ */ jsx(DetailsStep, {
232
+ booking,
233
+ resolved
234
+ })
235
+ ]
236
+ }, state.step),
237
+ !multiSelect && /* @__PURE__ */ jsx(SummaryBar, {
238
+ line: booking.derived.summary,
239
+ canAdvance: booking.derived.canAdvance,
240
+ step: state.step,
241
+ onNext: booking.next,
242
+ labels: screenLabels(labels),
243
+ classNames: resolved.classNames.summary
244
+ })
245
+ ]
246
+ }));
247
+ }
248
+ /** The overrides a nested booking component inherits from the wizard's props. */
249
+ function overridesOf(props) {
250
+ return {
251
+ config: props.config,
252
+ labels: props.labels,
253
+ classNames: props.classNames,
254
+ components: props.components
255
+ };
256
+ }
257
+ function WhoStep({ booking, resolved, loginRow }) {
258
+ const { kit, state, people } = booking;
259
+ const guests = useMemo(() => guestChoices(kit), [kit]);
260
+ return /* @__PURE__ */ jsx(WhoScreen, {
261
+ labels: screenLabels(kit.labels),
262
+ format: kit.format,
263
+ people: state.people,
264
+ family: people.family === null ? null : familyEntries(kit, people.family, people.ageDayKey),
265
+ guestChoices: guests,
266
+ addedChildren: addedChildEntries(kit, state.people, people.ageDayKey),
267
+ maxPeople: kit.wizard.maxPeople,
268
+ selfKey: SELF_KEY,
269
+ isGuestSeat: kit.wizard.isGuestSeat,
270
+ onChoose: people.choosePeople,
271
+ onAddChild: people.addChild,
272
+ loginRow,
273
+ currentYear: Number(kit.clock.dayKey(booking.slots.fromTs).slice(0, 4)),
274
+ classNames: resolved.classNames.who,
275
+ components: resolved.components.PersonCard ? { PersonCard: resolved.components.PersonCard } : void 0
276
+ });
277
+ }
278
+ function ServiceStep({ booking, resolved, multiSelect }) {
279
+ const { kit, state, people, catalogue } = booking;
280
+ const ageOf = (person) => people.personAge(person, people.ageDayKey);
281
+ const single = state.people.length === 1 ? state.people[0] : null;
282
+ const pickFor = (index, service, oneTap) => {
283
+ const visit = state.choices[index] ? [state.choices[index], ...state.extras[index]] : [];
284
+ if (!multiSelect || visit.length === 0 || visit.length === 1 && visit[0].id === service.id) oneTap();
285
+ else if (!visit.some((ticked) => ticked.id === service.id)) booking.toggleServiceFor(index, service);
286
+ };
287
+ return /* @__PURE__ */ jsx(ServiceScreen, {
288
+ labels: screenLabels(kit.labels),
289
+ format: kit.format,
290
+ services: catalogue.services,
291
+ ...serviceScreenBase(kit),
292
+ serviceFits: serviceFitsFor(kit, state.people.map(ageOf)),
293
+ initialCategory: catalogue.deepLinkCategory,
294
+ onPick: (service) => pickFor(0, service, () => booking.pickService(service)),
295
+ suggestion: single ? people.suggestionFor(single) : null,
296
+ chosenId: state.choices[0]?.id ?? null,
297
+ childName: single ? single.name ?? null : null,
298
+ party: state.people.length > 1 ? {
299
+ people: partyPeople(kit, state.people, ageOf, people.suggestionFor),
300
+ choices: state.choices,
301
+ onPickFor: (index, service) => pickFor(index, service, () => booking.pickServiceFor(index, service)),
302
+ onRemove: (index) => booking.dispatch({
303
+ type: "choosePeople",
304
+ people: state.people.filter((_, position) => position !== index)
305
+ })
306
+ } : void 0,
307
+ selection: multiSelect ? {
308
+ lists: state.choices.map((first, index) => first ? [first, ...state.extras[index]] : []),
309
+ onToggle: booking.toggleServiceFor,
310
+ onContinue: booking.continueFromService,
311
+ total: state.items.length === 0 ? null : {
312
+ minutes: kit.wizard.visitMinutes(state.items, state.partyMode),
313
+ priceOre: booking.derived.total
314
+ },
315
+ canContinue: booking.derived.canAdvance,
316
+ notice: state.error === null ? null : labelText(wizardErrorText(kit.labels, state.error, kit.config.party.maxServicesPerPerson)),
317
+ labels: kit.labels
318
+ } : void 0,
319
+ classNames: resolved.classNames.service,
320
+ components: resolved.components.ServiceCard ? { ServiceCard: resolved.components.ServiceCard } : void 0
321
+ });
322
+ }
323
+ function WhenStep({ booking, resolved }) {
324
+ const { kit, state, catalogue, slots, schedule } = booking;
325
+ const { labels, format, wizard, config } = kit;
326
+ const party = booking.derived.party;
327
+ const items = state.items;
328
+ const phone = config.contact.phone;
329
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
330
+ /* @__PURE__ */ jsx(StylistScreen, {
331
+ labels: screenLabels(labels),
332
+ format,
333
+ serviceIds: items.flatMap((item) => visitServicesOf(item).map((service) => service.id)),
334
+ resources: catalogue.resources,
335
+ loading: !catalogue.resourcesKnown,
336
+ nextAvailableLoading: slots.nextAvailableLoading,
337
+ selectedResourceId: state.resourceId,
338
+ nextAvailableTs: slots.nextAvailableTs,
339
+ pendingName: catalogue.chosenStylistName,
340
+ notice: catalogue.stylistNotice,
341
+ onPick: booking.pickResource,
342
+ party: party ? {
343
+ mode: state.partyMode,
344
+ size: items.length,
345
+ sizeWord: partySizeWord(labels, items.length),
346
+ minutes: {
347
+ sequential: wizard.visitMinutes(items, "sequential"),
348
+ parallel: wizard.visitMinutes(items, "parallel")
349
+ },
350
+ onMode: (mode) => booking.dispatch({
351
+ type: "setPartyMode",
352
+ mode
353
+ })
354
+ } : void 0,
355
+ classNames: resolved.classNames.stylist,
356
+ components: resolved.components.StylistCard ? { StylistCard: resolved.components.StylistCard } : void 0
357
+ }),
358
+ slots.ready ? /* @__PURE__ */ jsx(TimeScreen, {
359
+ labels: screenLabels(labels),
360
+ format,
361
+ dayparts: kit.dayparts,
362
+ monthView: true,
363
+ slots: slots.single,
364
+ days: slots.days,
365
+ openDays: schedule.openDays,
366
+ stylistName: catalogue.chosenStylistName,
367
+ takenSlotTs: slots.takenSlotTs,
368
+ phone,
369
+ weekendNote: (dayTs) => weekendNoteFor(kit, party ? items : items.slice(0, 1), dayTs),
370
+ onPick: booking.pickSlot,
371
+ party: party ? {
372
+ items,
373
+ mode: state.partyMode,
374
+ slots: slots.party,
375
+ chipEndTs: (slot) => wizard.visitEndTs(items, slot.startTs, "sequential"),
376
+ alternativeFor: state.partyMode === "sequential" ? (dayTs) => kit.partySlots.partyAlternative({
377
+ sequential: slots.party,
378
+ parallel: slots.alternatives,
379
+ dayTs
380
+ }) : void 0,
381
+ resolveStylistName: catalogue.resolveStylistName,
382
+ onPick: booking.pickPartySlot
383
+ } : void 0,
384
+ classNames: resolved.classNames.time,
385
+ components: timeComponents(resolved)
386
+ }) : slots.failed ? /* @__PURE__ */ jsx(SlotsUnavailable, {
387
+ kit,
388
+ onRetry: booking.refresh
389
+ }) : /* @__PURE__ */ jsx(TimeScreenSkeleton, {
390
+ labels: screenLabels(labels),
391
+ days: slots.days.length,
392
+ monthView: true,
393
+ surchargeRow: items.some((item) => visitServicesOf(item).some((service) => service.weekendSurchargePct > 0))
394
+ }),
395
+ /* @__PURE__ */ jsx(TakenToast, {
396
+ labels: screenLabels(labels),
397
+ takenSlotTs: slots.takenSlotTs
398
+ })
399
+ ] });
400
+ }
401
+ function timeComponents({ components }) {
402
+ if (!components.DayChip && !components.TimeChip) return void 0;
403
+ return {
404
+ DayChip: components.DayChip,
405
+ TimeChip: components.TimeChip
406
+ };
407
+ }
408
+ function DetailsStep({ booking, resolved }) {
409
+ const { kit, state } = booking;
410
+ const guardian = booking.login.guardian;
411
+ const lines = useMemo(() => detailsLines(kit, state), [kit, state]);
412
+ return /* @__PURE__ */ jsx(DetailsScreen, {
413
+ state: forMedaScreen(state),
414
+ onChange: booking.dispatch,
415
+ onSubmit: booking.submit,
416
+ lines,
417
+ totalOre: booking.derived.total,
418
+ ...detailsScreenBase(kit),
419
+ submitting: booking.submitting,
420
+ submissionNonce: booking.submissionNonce,
421
+ family: guardian?.family,
422
+ guardianPhone: guardian?.phone ?? null,
423
+ format: kit.format,
424
+ labels: screenLabels(kit.labels),
425
+ classNames: resolved.classNames.details,
426
+ components: resolved.components.FamilyChip ? { FamilyChip: resolved.components.FamilyChip } : void 0
427
+ });
428
+ }
429
+ function ConfirmationStep({ booking, resolved, siteUrl }) {
430
+ const { kit } = booking;
431
+ const [now] = useState(() => Date.now());
432
+ const confirmation = booking.confirmed;
433
+ return /* @__PURE__ */ jsx(Confirmation, {
434
+ ...confirmationProps(kit, confirmation, siteUrl),
435
+ onStartOver: booking.startOver,
436
+ now,
437
+ format: kit.format,
438
+ labels: screenLabels(kit.labels),
439
+ classNames: resolved.classNames.confirmation,
440
+ components: resolved.components.PartyLine ? { PartyLine: resolved.components.PartyLine } : void 0
441
+ });
442
+ }
443
+ /**
444
+ * «Have an account? Log in» — or, once the parent is known, nothing at all
445
+ * for one who arrived logged in, and one line saying so for one who logged in
446
+ * from the sheet.
447
+ */
448
+ function LoginRow({ booking, actions, overrides, onSignedIn }) {
449
+ const { arrivedAs, signedIn, resumePath } = booking.login;
450
+ const { labels } = booking.kit;
451
+ if (signedIn === null && arrivedAs !== null) return null;
452
+ const who = signedIn === null ? null : signedIn.guardian ?? arrivedAs;
453
+ const name = who === null ? null : [who.firstName, who.lastName].filter(Boolean).join(" ").trim() || who.email;
454
+ const confirmation = signedIn === null ? null : name === null ? labelText(labels["wizard.signedIn"]) : fill(labels["wizard.signedInAs"], { name });
455
+ return /* @__PURE__ */ jsxs("div", { children: [signedIn === null && /* @__PURE__ */ jsx(LoginSheet$1, {
456
+ ...overridesOf(overrides),
457
+ actions,
458
+ presentation: "sheet",
459
+ resumePath,
460
+ onSignedIn
461
+ }), /* @__PURE__ */ jsx(LiveStatus, {
462
+ text: confirmation,
463
+ className: "text-sm text-muted-foreground empty:hidden"
464
+ })] });
465
+ }
466
+ /** The neutral shell the restore gate shows, the header's and a step's shape. */
467
+ function RestoreSkeleton({ text }) {
468
+ return /* @__PURE__ */ jsxs("div", {
469
+ "data-testid": "restore-skeleton",
470
+ children: [/* @__PURE__ */ jsx(LiveStatus, { text }), /* @__PURE__ */ jsxs("div", {
471
+ "aria-busy": "true",
472
+ className: "space-y-8",
473
+ children: [/* @__PURE__ */ jsxs("div", {
474
+ "aria-hidden": "true",
475
+ className: "space-y-3",
476
+ children: [/* @__PURE__ */ jsxs("div", {
477
+ className: "flex items-center gap-3",
478
+ children: [/* @__PURE__ */ jsx("span", { className: "size-9 shrink-0 animate-pulse rounded-full bg-muted" }), /* @__PURE__ */ jsxs("span", {
479
+ className: "flex flex-col gap-1.5",
480
+ children: [/* @__PURE__ */ jsx("span", { className: "h-4 w-24 animate-pulse rounded bg-muted" }), /* @__PURE__ */ jsx("span", { className: "h-3 w-36 animate-pulse rounded bg-muted" })]
481
+ })]
482
+ }), /* @__PURE__ */ jsx("span", { className: "block h-1.5 w-full animate-pulse rounded-full bg-muted" })]
483
+ }), /* @__PURE__ */ jsxs("div", {
484
+ "aria-hidden": "true",
485
+ className: "space-y-6",
486
+ children: [/* @__PURE__ */ jsx("span", { className: "block h-8 w-56 animate-pulse rounded bg-muted md:h-9" }), /* @__PURE__ */ jsx("div", {
487
+ className: "space-y-2",
488
+ children: Array.from({ length: 4 }, (_, index) => /* @__PURE__ */ jsx("span", { className: "block h-24 animate-pulse rounded-lg bg-muted" }, index))
489
+ })]
490
+ })]
491
+ })]
492
+ });
493
+ }
494
+ /**
495
+ * Where the visitor is, the way back, and how much is left: a back arrow, the
496
+ * title and «step N of 4», and one segment per step that IS the button to it —
497
+ * tappable only where the machine's `canGoToStep` says so, and frozen while a
498
+ * submission is in flight.
499
+ */
500
+ function WizardHeader({ kit, state, submitting, onGo }) {
501
+ const { labels, wizard } = kit;
502
+ const stepLabel = (step) => labelText(labels[`wizard.step.${step}`]);
503
+ const index = STEPS.indexOf(state.step);
504
+ const previous = index > 0 ? STEPS[index - 1] : null;
505
+ const canGoBack = previous !== null && !submitting && wizard.canGoToStep(state, previous);
506
+ return /* @__PURE__ */ jsxs("div", {
507
+ className: "space-y-3",
508
+ children: [/* @__PURE__ */ jsxs("div", {
509
+ className: "flex items-center gap-3",
510
+ children: [canGoBack ? /* @__PURE__ */ jsx("button", {
511
+ type: "button",
512
+ "aria-label": fill(labels["wizard.back"], { label: stepLabel(previous) }),
513
+ onClick: () => onGo(previous),
514
+ className: "flex size-9 shrink-0 items-center justify-center rounded-full border border-border text-foreground transition-colors hover:bg-muted",
515
+ children: /* @__PURE__ */ jsx(ArrowLeft, {
516
+ "aria-hidden": "true",
517
+ className: "size-4"
518
+ })
519
+ }) : /* @__PURE__ */ jsx("span", {
520
+ "aria-hidden": "true",
521
+ className: "size-9 shrink-0"
522
+ }), /* @__PURE__ */ jsxs("p", {
523
+ className: "min-w-0",
524
+ children: [/* @__PURE__ */ jsx("span", {
525
+ className: "block font-sans text-sm font-bold",
526
+ children: labels["wizard.title"]
527
+ }), /* @__PURE__ */ jsx("span", {
528
+ className: "block truncate text-xs text-muted-foreground",
529
+ children: fillParts(labels["wizard.progress"], {
530
+ step: index + 1,
531
+ total: STEPS.length,
532
+ label: stepLabel(state.step)
533
+ })
534
+ })]
535
+ })]
536
+ }), /* @__PURE__ */ jsx("ol", {
537
+ "aria-label": labelText(labels["wizard.progressLabel"]),
538
+ className: "flex items-center gap-1.5",
539
+ children: STEPS.map((step, position) => {
540
+ const isCurrent = step === state.step;
541
+ const reachable = !submitting && !isCurrent && wizard.canGoToStep(state, step);
542
+ return /* @__PURE__ */ jsx("li", {
543
+ className: "flex-1",
544
+ children: /* @__PURE__ */ jsx("button", {
545
+ type: "button",
546
+ disabled: !reachable,
547
+ "aria-current": isCurrent ? "step" : void 0,
548
+ onClick: () => onGo(step),
549
+ className: `block h-1.5 w-full rounded-full transition-colors ${position <= index ? "bg-primary" : "bg-border"} ${reachable ? "cursor-pointer" : "cursor-default"}`,
550
+ children: /* @__PURE__ */ jsx("span", {
551
+ className: "sr-only",
552
+ children: stepLabel(step)
553
+ })
554
+ })
555
+ }, step);
556
+ })
557
+ })]
558
+ });
559
+ }
560
+ /**
561
+ * The openings could not be read — this site's failure, not a full day —
562
+ * with the telephone as the rescue and a retry as the cheaper first try.
563
+ */
564
+ function SlotsUnavailable({ kit, onRetry }) {
565
+ const { labels, config, format } = kit;
566
+ const phone = config.contact.phone;
567
+ return /* @__PURE__ */ jsxs("div", {
568
+ className: "space-y-3",
569
+ children: [/* @__PURE__ */ jsxs("p", {
570
+ role: "alert",
571
+ className: "rounded-lg border border-border bg-card px-5 py-4 text-sm",
572
+ children: [
573
+ labels["wizard.slotsUnavailable.lead"],
574
+ phone ? /* @__PURE__ */ jsx("a", {
575
+ href: format.telHref(phone),
576
+ "aria-label": fill(labels["wizard.slotsUnavailable.callAria"], { phone }),
577
+ className: "font-semibold text-primary underline underline-offset-4",
578
+ children: fillParts(labels["wizard.slotsUnavailable.call"], { phone })
579
+ }) : labels["wizard.slotsUnavailable.callPlain"],
580
+ labels["wizard.slotsUnavailable.suffix"]
581
+ ]
582
+ }), /* @__PURE__ */ jsx(BookingButtonOutline, {
583
+ onClick: onRetry,
584
+ children: labels["wizard.retry"]
585
+ })]
586
+ });
587
+ }
588
+ function BookingButtonOutline({ onClick, children }) {
589
+ return /* @__PURE__ */ jsx(BookingButton, {
590
+ variant: "outline",
591
+ onClick,
592
+ children
593
+ });
594
+ }
595
+ //#endregion
596
+ export { BookingWizard };
597
+
598
+ //# sourceMappingURL=BookingWizard.mjs.map