@medalsocial/booking 0.2.1 → 0.3.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 (53) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +21 -3
  3. package/dist/core/attempt-store.d.mts +1 -0
  4. package/dist/core/attempt-store.mjs +13 -0
  5. package/dist/core/attempt-store.mjs.map +1 -1
  6. package/dist/core/config.d.mts +37 -0
  7. package/dist/core/config.mjs +29 -0
  8. package/dist/core/config.mjs.map +1 -1
  9. package/dist/core/machine.d.mts +27 -1
  10. package/dist/core/machine.mjs +52 -4
  11. package/dist/core/machine.mjs.map +1 -1
  12. package/dist/next/medal.d.mts +8 -1
  13. package/dist/next/medal.mjs +16 -5
  14. package/dist/next/medal.mjs.map +1 -1
  15. package/dist/next/portal/medal-portal.mjs +1 -62
  16. package/dist/next/portal/medal-portal.mjs.map +1 -1
  17. package/dist/next/redact.mjs +76 -0
  18. package/dist/next/redact.mjs.map +1 -0
  19. package/dist/next/routes/create.mjs +67 -9
  20. package/dist/next/routes/create.mjs.map +1 -1
  21. package/dist/next/routes/shared.mjs +3 -0
  22. package/dist/next/routes/shared.mjs.map +1 -1
  23. package/dist/react/BookingWizard.mjs +139 -11
  24. package/dist/react/BookingWizard.mjs.map +1 -1
  25. package/dist/react/LoginSheet.d.mts +19 -1
  26. package/dist/react/LoginSheet.mjs +15 -1
  27. package/dist/react/LoginSheet.mjs.map +1 -1
  28. package/dist/react/Provider.d.mts +2 -1
  29. package/dist/react/Provider.mjs.map +1 -1
  30. package/dist/react/actions.d.mts +16 -5
  31. package/dist/react/actions.mjs +5 -1
  32. package/dist/react/actions.mjs.map +1 -1
  33. package/dist/react/index.d.mts +2 -2
  34. package/dist/react/kit.d.mts +6 -1
  35. package/dist/react/kit.mjs +4 -3
  36. package/dist/react/kit.mjs.map +1 -1
  37. package/dist/react/labels/login.d.mts +2 -0
  38. package/dist/react/labels/login.mjs +4 -2
  39. package/dist/react/labels/login.mjs.map +1 -1
  40. package/dist/react/labels/screens.mjs +58 -2
  41. package/dist/react/labels/screens.mjs.map +1 -1
  42. package/dist/react/labels/wizard.d.mts +8 -0
  43. package/dist/react/labels/wizard.mjs +6 -0
  44. package/dist/react/labels/wizard.mjs.map +1 -1
  45. package/dist/react/screen-labels.mjs +5 -0
  46. package/dist/react/screen-labels.mjs.map +1 -1
  47. package/dist/react/useBooking.d.mts +19 -1
  48. package/dist/react/useBooking.mjs +269 -42
  49. package/dist/react/useBooking.mjs.map +1 -1
  50. package/dist/react/wizard/adapters.mjs +66 -3
  51. package/dist/react/wizard/adapters.mjs.map +1 -1
  52. package/dist/react/wizard/index.d.mts +2 -2
  53. package/package.json +4 -4
@@ -6,9 +6,9 @@ import { useBookingKit } from "./Provider.mjs";
6
6
  import { screenLabels } from "./screen-labels.mjs";
7
7
  import { LoginSheet as LoginSheet$1 } from "./LoginSheet.mjs";
8
8
  import { useBooking } from "./useBooking.mjs";
9
- import { addedChildEntries, confirmationProps, detailsLines, detailsScreenBase, familyEntries, guestChoices, partyPeople, partySizeWord, serviceFitsFor, serviceScreenBase, weekendNoteFor } from "./wizard/adapters.mjs";
9
+ import { GUEST_ADULT_SEAT, addedChildEntries, confirmationProps, detailsLines, detailsScreenBase, familyEntries, guestChoices, partyPeople, partySizeWord, recapProps, serviceFitsFor, serviceScreenBase, stylistFace, weekendNoteFor } from "./wizard/adapters.mjs";
10
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";
11
+ import { BookingButton, BookingRecap, Confirmation, DetailsScreen, LiveStatus, ServiceScreen, StylistScreen, SummaryBar, TakenToast, TimeScreen, TimeScreenSkeleton, WhoScreen } from "@medalsocial/meda/booking";
12
12
  import { Suspense, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
13
13
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
14
14
  import { ArrowLeft } from "lucide-react";
@@ -65,9 +65,17 @@ function BookingWizard(props) {
65
65
  }
66
66
  function BookingWizardShell(props) {
67
67
  const resolved = useBookingKit(props, props.contact);
68
- const booking = useBooking(props);
68
+ const loginOffered = props.actions !== void 0 && resolved.kit.config.portal.enabled;
69
+ const booking = useBooking({
70
+ ...props,
71
+ loginOffered
72
+ });
69
73
  const { kit } = booking;
70
74
  const { labels } = kit;
75
+ const unreachableLogin = kit.config.account.required && !loginOffered;
76
+ useEffect(() => {
77
+ if (unreachableLogin && typeof process !== "undefined" && process.env.NODE_ENV !== "production") console.warn("[booking] account.required without login actions: nobody can log in to book.");
78
+ }, [unreachableLogin]);
71
79
  const { state, restore, confirmed, onEvent } = {
72
80
  ...booking,
73
81
  onEvent: props.onEvent
@@ -114,6 +122,28 @@ function BookingWizardShell(props) {
114
122
  focusAfterSignIn.current = false;
115
123
  document.getElementById(STEP_HEADINGS[state.step])?.focus({ preventScroll: true });
116
124
  }, [signedIn, state.step]);
125
+ /**
126
+ * A 401 put the gate back over the form: focus its heading (the details
127
+ * step's id), where the notice beside it says why. Once per loss, and only
128
+ * once the gate is actually on screen — a replay answers behind the skeleton.
129
+ */
130
+ const sessionLost = booking.login.sessionLost;
131
+ const sessionLossFocused = useRef(false);
132
+ useEffect(() => {
133
+ if (!sessionLost) {
134
+ sessionLossFocused.current = false;
135
+ return;
136
+ }
137
+ if (sessionLossFocused.current || restore.restoring || state.step !== "details") return;
138
+ sessionLossFocused.current = true;
139
+ const heading = document.getElementById(STEP_HEADINGS.details);
140
+ scrollToTop(rootRef.current);
141
+ heading?.focus({ preventScroll: true });
142
+ }, [
143
+ sessionLost,
144
+ restore.restoring,
145
+ state.step
146
+ ]);
117
147
  useEffect(() => {
118
148
  if (!timeStepReady || !focusTimeWhenReady.current) return;
119
149
  focusTimeWhenReady.current = false;
@@ -175,8 +205,13 @@ function BookingWizardShell(props) {
175
205
  booking.login.signIn(who);
176
206
  };
177
207
  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, {
208
+ /**
209
+ * `account.required`: every booking is a logged-in parent's. The login is
210
+ * not offered along the way — it IS «Bekreft», for a parent not logged in.
211
+ */
212
+ const accountRequired = kit.config.account.required && loginOffered;
213
+ const gated = accountRequired && state.step === "details" && !booking.login.loggedIn;
214
+ const loginRow = loginOffered && !accountRequired ? /* @__PURE__ */ jsx(LoginRow, {
180
215
  booking,
181
216
  actions: props.actions,
182
217
  overrides: props,
@@ -199,8 +234,12 @@ function BookingWizardShell(props) {
199
234
  className: resolved.classNames.wizard?.alert ?? "rounded-lg border border-destructive/40 bg-destructive/10 px-5 py-3 text-sm",
200
235
  children: wizardErrorText(labels, state.error, kit.config.party.maxServicesPerPerson)
201
236
  }),
237
+ state.step === "details" && !gated && kit.config.screens.recap && /* @__PURE__ */ jsx(RecapStep, {
238
+ booking,
239
+ resolved
240
+ }),
202
241
  state.step === "details" && loginRow,
203
- loginOffered && booking.login.vippsConfirm !== null && signedIn === null && /* @__PURE__ */ jsx(LoginSheet$1, {
242
+ loginOffered && booking.login.vippsConfirm !== null && signedIn === null && !gated && /* @__PURE__ */ jsx(LoginSheet$1, {
204
243
  ...overridesOf(props),
205
244
  actions: props.actions,
206
245
  presentation: "sheet",
@@ -228,17 +267,24 @@ function BookingWizardShell(props) {
228
267
  booking,
229
268
  resolved
230
269
  }),
231
- state.step === "details" && /* @__PURE__ */ jsx(DetailsStep, {
270
+ state.step === "details" && (gated ? /* @__PURE__ */ jsx(AccountGate, {
271
+ booking,
272
+ resolved,
273
+ actions: props.actions,
274
+ overrides: props,
275
+ onSignedIn
276
+ }) : /* @__PURE__ */ jsx(DetailsStep, {
232
277
  booking,
233
278
  resolved
234
- })
279
+ }))
235
280
  ]
236
281
  }, state.step),
237
282
  !multiSelect && /* @__PURE__ */ jsx(SummaryBar, {
238
- line: booking.derived.summary,
283
+ ...kit.config.screens.summaryDetail ? booking.derived.summaryParts : { line: booking.derived.summary },
239
284
  canAdvance: booking.derived.canAdvance,
240
285
  step: state.step,
241
286
  onNext: booking.next,
287
+ hideNextWhenDisabled: kit.config.screens.hideDisabledNext,
242
288
  labels: screenLabels(labels),
243
289
  classNames: resolved.classNames.summary
244
290
  })
@@ -269,6 +315,10 @@ function WhoStep({ booking, resolved, loginRow }) {
269
315
  isGuestSeat: kit.wizard.isGuestSeat,
270
316
  onChoose: people.choosePeople,
271
317
  onAddChild: people.addChild,
318
+ guestParty: kit.config.screens.guestParty ? {
319
+ child: kit.wizard.guestChild,
320
+ adult: GUEST_ADULT_SEAT
321
+ } : void 0,
272
322
  loginRow,
273
323
  currentYear: Number(kit.clock.dayKey(booking.slots.fromTs).slice(0, 4)),
274
324
  classNames: resolved.classNames.who,
@@ -289,7 +339,7 @@ function ServiceStep({ booking, resolved, multiSelect }) {
289
339
  format: kit.format,
290
340
  services: catalogue.services,
291
341
  ...serviceScreenBase(kit),
292
- serviceFits: serviceFitsFor(kit, state.people.map(ageOf)),
342
+ serviceFits: serviceFitsFor(kit, state.people.map(ageOf), state.people),
293
343
  initialCategory: catalogue.deepLinkCategory,
294
344
  onPick: (service) => pickFor(0, service, () => booking.pickService(service)),
295
345
  suggestion: single ? people.suggestionFor(single) : null,
@@ -339,6 +389,8 @@ function WhenStep({ booking, resolved }) {
339
389
  pendingName: catalogue.chosenStylistName,
340
390
  notice: catalogue.stylistNotice,
341
391
  onPick: booking.pickResource,
392
+ firstAvailableFaces: config.screens.firstAvailableFaces,
393
+ edgeFade: config.screens.stylistEdgeFade,
342
394
  party: party ? {
343
395
  mode: state.partyMode,
344
396
  size: items.length,
@@ -368,6 +420,8 @@ function WhenStep({ booking, resolved }) {
368
420
  phone,
369
421
  weekendNote: (dayTs) => weekendNoteFor(kit, party ? items : items.slice(0, 1), dayTs),
370
422
  onPick: booking.pickSlot,
423
+ soonest: config.screens.soonest ? { resolveStylist: (resourceId) => stylistFace(catalogue.resources, resourceId) } : void 0,
424
+ dayFullness: config.screens.dayFullness,
371
425
  party: party ? {
372
426
  items,
373
427
  mode: state.partyMode,
@@ -398,6 +452,36 @@ function WhenStep({ booking, resolved }) {
398
452
  })
399
453
  ] });
400
454
  }
455
+ /**
456
+ * The details step's recap (`config.screens.recap`): the day, hours, who and
457
+ * total of the booking the form below submits, «edit» back to the time step,
458
+ * and a swap to another stylist free at that minute.
459
+ */
460
+ function RecapStep({ booking, resolved }) {
461
+ const { kit, state, catalogue, slots } = booking;
462
+ const recap = useMemo(() => recapProps(kit, state, catalogue.resources, slots.single), [
463
+ kit,
464
+ state,
465
+ catalogue.resources,
466
+ slots.single
467
+ ]);
468
+ return /* @__PURE__ */ jsx(BookingRecap, {
469
+ lines: recap.lines,
470
+ totalOre: booking.derived.total,
471
+ format: kit.format,
472
+ labels: screenLabels(kit.labels),
473
+ onEdit: () => booking.dispatch({
474
+ type: "goToStep",
475
+ step: "when"
476
+ }),
477
+ alternatives: recap.alternatives,
478
+ onSwap: (resourceId) => booking.pickSlot({
479
+ startTs: state.startTs,
480
+ resourceId
481
+ }),
482
+ classNames: resolved.classNames.recap
483
+ });
484
+ }
401
485
  function timeComponents({ components }) {
402
486
  if (!components.DayChip && !components.TimeChip) return void 0;
403
487
  return {
@@ -420,12 +504,56 @@ function DetailsStep({ booking, resolved }) {
420
504
  submissionNonce: booking.submissionNonce,
421
505
  family: guardian?.family,
422
506
  guardianPhone: guardian?.phone ?? null,
507
+ emailReadOnly: kit.config.account.required && guardian !== null,
423
508
  format: kit.format,
424
509
  labels: screenLabels(kit.labels),
425
510
  classNames: resolved.classNames.details,
426
511
  components: resolved.components.FamilyChip ? { FamilyChip: resolved.components.FamilyChip } : void 0
427
512
  });
428
513
  }
514
+ /**
515
+ * «Bekreft» under `account.required` for a parent not logged in: «Nesten
516
+ * ferdig», a line, and the login — Vipps, or e-mail — in place of the form.
517
+ * The heading carries the details step's id, so the focus a step change moves
518
+ * lands here, and the form's heading takes it over once the parent is in.
519
+ */
520
+ function AccountGate({ booking, resolved, actions, overrides, onSignedIn }) {
521
+ const { labels } = booking.kit;
522
+ const { classNames } = resolved;
523
+ const slot = (base, extra) => [base, extra].filter(Boolean).join(" ");
524
+ return /* @__PURE__ */ jsxs("section", {
525
+ "aria-labelledby": STEP_HEADINGS.details,
526
+ className: slot("space-y-6", classNames.details?.root),
527
+ children: [
528
+ /* @__PURE__ */ jsx("h2", {
529
+ id: STEP_HEADINGS.details,
530
+ tabIndex: -1,
531
+ className: slot("font-sans text-2xl font-bold outline-none md:text-3xl", classNames.details?.heading),
532
+ children: labelText(labels["wizard.account.heading"])
533
+ }),
534
+ /* @__PURE__ */ jsx(LiveStatus, {
535
+ text: booking.login.sessionLost ? labelText(labels["wizard.account.sessionLost"]) : null,
536
+ className: "text-sm font-medium empty:hidden"
537
+ }),
538
+ /* @__PURE__ */ jsx("p", {
539
+ className: "text-muted-foreground",
540
+ children: labelText(labels["wizard.account.intro"])
541
+ }),
542
+ /* @__PURE__ */ jsx("p", {
543
+ className: slot("rounded-lg border border-border bg-card px-4 py-3 text-sm font-medium", classNames.summary?.line),
544
+ children: booking.derived.summary
545
+ }),
546
+ /* @__PURE__ */ jsx(LoginSheet$1, {
547
+ ...overridesOf(overrides),
548
+ actions,
549
+ presentation: "gate",
550
+ resumePath: booking.login.resumePath,
551
+ vippsConfirm: booking.login.vippsConfirm,
552
+ onSignedIn
553
+ })
554
+ ]
555
+ });
556
+ }
429
557
  function ConfirmationStep({ booking, resolved, siteUrl }) {
430
558
  const { kit } = booking;
431
559
  const [now] = useState(() => Date.now());
@@ -449,7 +577,7 @@ function LoginRow({ booking, actions, overrides, onSignedIn }) {
449
577
  const { arrivedAs, signedIn, resumePath } = booking.login;
450
578
  const { labels } = booking.kit;
451
579
  if (signedIn === null && arrivedAs !== null) return null;
452
- const who = signedIn === null ? null : signedIn.guardian ?? arrivedAs;
580
+ const who = signedIn?.guardian ?? null;
453
581
  const name = who === null ? null : [who.firstName, who.lastName].filter(Boolean).join(" ").trim() || who.email;
454
582
  const confirmation = signedIn === null ? null : name === null ? labelText(labels["wizard.signedIn"]) : fill(labels["wizard.signedInAs"], { name });
455
583
  return /* @__PURE__ */ jsxs("div", { children: [signedIn === null && /* @__PURE__ */ jsx(LoginSheet$1, {
@@ -1 +1 @@
1
- {"version":3,"file":"BookingWizard.mjs","names":["LoginSheet"],"sources":["../../src/react/BookingWizard.tsx"],"sourcesContent":["/**\n * `<BookingWizard>` — the booking flow, from «who is it for?» to the\n * confirmation, drawn with the `@medalsocial/meda/booking` screens over\n * `useBooking()`.\n *\n * The hook owns the machine, the fetches and the stores; this file owns the\n * page: which step is on screen (ONE at a time — the details step's nonce and\n * the submit flag depend on it), focus and scroll on every move, the restore\n * gate's pre-hydration script and skeleton, the header, and what each screen\n * is handed (prices, the weekend note, ages, the possessive, the calendar\n * file) out of the site's config.\n */\n\nimport {\n BookingButton,\n Confirmation,\n DetailsScreen,\n LiveStatus,\n ServiceScreen,\n StylistScreen,\n SummaryBar,\n TakenToast,\n TimeScreen,\n TimeScreenSkeleton,\n WhoScreen,\n} from '@medalsocial/meda/booking';\nimport { ArrowLeft } from 'lucide-react';\nimport {\n type ReactNode,\n Suspense,\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport { fill, fillParts, labelText } from '../core/labels';\nimport { SELF_KEY, type WizardPerson, type WizardState, type WizardStep } from '../core/machine';\nimport type { BookingServiceDto } from '../core/types';\nimport { visitServicesOf } from '../core/visit';\nimport type { PortalActions } from './actions';\nimport type { BookingKit } from './kit';\nimport { LoginSheet } from './LoginSheet';\nimport type { BookingOverrides, ResolvedBooking } from './Provider';\nimport { useBookingKit } from './Provider';\nimport { screenLabels } from './screen-labels';\nimport {\n type BookingConfirmation,\n type BookingController,\n type BookingSeed,\n useBooking,\n} from './useBooking';\nimport {\n addedChildEntries,\n confirmationProps,\n detailsLines,\n detailsScreenBase,\n familyEntries,\n guestChoices,\n partyPeople,\n partySizeWord,\n serviceFitsFor,\n serviceScreenBase,\n weekendNoteFor,\n} from './wizard/adapters';\nimport { forMedaScreen, wizardErrorText } from './wizard-error';\n\nexport interface BookingWizardProps extends BookingOverrides {\n /** What the page prefetched (the `/next` loader's seed). */\n seed: BookingSeed;\n /** This request's phone and address, over `config.contact` (the loader's `contact`). */\n contact?: { phone?: string | null; address?: string | null };\n /** The parent the page found a portal session for; `null` for a guest. */\n guardian?: BookingGuardianProp;\n /**\n * The app's server actions for the login sheet (Decision 8). Without them —\n * or with `config.portal.enabled` off — no login is offered.\n */\n actions?: Pick<PortalActions, 'startLogin' | 'startVipps'>;\n /** How many days the window covers. Default `config.window.rangeDays`. */\n rangeDays?: number;\n /**\n * The site's origin (`https://…`), so the manage links inside the calendar\n * file are absolute. Absent, they stay root-relative.\n */\n siteUrl?: string;\n /** A monitoring tap: the step the visitor is on, and a submission's outcome. */\n onEvent?: (event: BookingWizardEvent) => void;\n}\n\ntype BookingGuardianProp = Parameters<typeof useBooking>[0]['guardian'];\n\nexport type BookingWizardEvent =\n | { type: 'step'; step: WizardStep | 'confirmed' }\n | { type: 'submit_ok'; bookings: number }\n | { type: 'submit_error'; code: NonNullable<WizardState['error']> };\n\n/** The wizard's root, for the inline restore gate to find before hydration. */\nconst WIZARD_ROOT_ID = 'booking-wizard';\n\n/** The time step's heading, which a lost slot sends the visitor back to. */\nconst TIME_HEADING_ID = 'booking-time-heading';\n\n/** The heading each screen focuses when the visitor arrives on it. */\nconst STEP_HEADINGS: Record<WizardStep | 'confirmed', string> = {\n who: 'booking-who-heading',\n service: 'booking-service-heading',\n when: 'booking-stylist-heading',\n details: 'booking-details-heading',\n confirmed: 'booking-confirmed-heading',\n};\n\nconst STEPS: readonly WizardStep[] = ['who', 'service', 'when', 'details'];\n\n/** Smooth, or instant for a visitor who asked for reduced motion. */\nfunction scrollToTop(target: HTMLElement | null): void {\n if (!target || typeof target.scrollIntoView !== 'function') return;\n const reduced =\n typeof window.matchMedia === 'function' &&\n window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n target.scrollIntoView({ behavior: reduced ? 'auto' : 'smooth', block: 'start' });\n}\n\n/**\n * The wizard, behind its own Suspense boundary: `useSearchParams` throws to\n * the nearest one during a static prerender, and without ours that is the\n * root layout's — the whole page would ship empty until hydration.\n */\nexport function BookingWizard(props: BookingWizardProps) {\n return (\n <Suspense fallback={null}>\n <BookingWizardShell {...props} />\n </Suspense>\n );\n}\n\nfunction BookingWizardShell(props: BookingWizardProps) {\n const resolved = useBookingKit(props, props.contact);\n const booking = useBooking(props);\n const { kit } = booking;\n const { labels } = kit;\n const { state, restore, confirmed, onEvent } = { ...booking, onEvent: props.onEvent };\n\n /**\n * On every change of screen: the wizard's top into view and focus on the\n * new step's heading. Never on the first screen, and never for what the\n * restore gate reveals — that is the page arriving, not the visitor moving.\n */\n const rootRef = useRef<HTMLDivElement>(null);\n const screenKey: WizardStep | 'confirmed' = confirmed !== null ? 'confirmed' : state.step;\n const shownScreen = useRef<WizardStep | 'confirmed' | null>(null);\n const takenSlotTs = booking.slots.takenSlotTs;\n const timeStepReady = booking.slots.ready;\n const focusTimeWhenReady = useRef(false);\n useEffect(() => {\n if (restore.restoring) return;\n const previous = shownScreen.current;\n shownScreen.current = screenKey;\n if (previous === screenKey) return;\n onEvent?.({ type: 'step', step: screenKey });\n if (previous === null) return;\n // A lost slot sends the visitor back for the ALTERNATIVES, on the time half.\n const toTimes = screenKey === 'when' && takenSlotTs !== null;\n const headingId = toTimes ? TIME_HEADING_ID : STEP_HEADINGS[screenKey];\n const heading = document.getElementById(headingId);\n scrollToTop(toTimes ? heading : rootRef.current);\n heading?.focus({ preventScroll: true });\n // The heading focused now may be the skeleton's; focus follows the real one.\n focusTimeWhenReady.current = toTimes && !timeStepReady;\n }, [screenKey, restore.restoring, takenSlotTs, timeStepReady, onEvent]);\n\n /** After a login from the sheet the row that opened it is gone: focus the step's heading. */\n const focusAfterSignIn = useRef(false);\n const signedIn = booking.login.signedIn;\n useEffect(() => {\n if (signedIn === null || !focusAfterSignIn.current) return;\n focusAfterSignIn.current = false;\n document.getElementById(STEP_HEADINGS[state.step])?.focus({ preventScroll: true });\n }, [signedIn, state.step]);\n useEffect(() => {\n if (!timeStepReady || !focusTimeWhenReady.current) return;\n focusTimeWhenReady.current = false;\n const active = document.activeElement;\n if (active === null || active === document.body) {\n document.getElementById(TIME_HEADING_ID)?.focus({ preventScroll: true });\n }\n }, [timeStepReady]);\n\n // The monitoring tap for a submission's outcome.\n const lastError = useRef(state.error);\n useEffect(() => {\n if (state.error !== null && state.error !== lastError.current) {\n onEvent?.({ type: 'submit_error', code: state.error });\n }\n lastError.current = state.error;\n }, [state.error, onEvent]);\n const confirmedCount = confirmed?.bookings.length ?? 0;\n useEffect(() => {\n if (confirmedCount > 0) onEvent?.({ type: 'submit_ok', bookings: confirmedCount });\n }, [confirmedCount, onEvent]);\n\n // React owns the root from here on: drop the pre-hydration mark the inline\n // script may have set, and say so, so the script's fallback timer stands down.\n useLayoutEffect(() => {\n if (!restore.hydrated) return;\n rootRef.current?.removeAttribute('data-restoring');\n rootRef.current?.setAttribute('data-hydrated', '');\n }, [restore.hydrated]);\n\n // Steps animate in only once the visitor has moved between them.\n const [arrivalStep, setArrivalStep] = useState<WizardStep | null>(\n restore.restoring ? null : state.step\n );\n if (!restore.restoring && arrivalStep === null) setArrivalStep(state.step);\n const [stepsMovedOnce, setStepsMovedOnce] = useState(false);\n const movedNow = arrivalStep !== null && !restore.restoring && state.step !== arrivalStep;\n if (movedNow && !stepsMovedOnce) setStepsMovedOnce(true);\n const stepsMoved = stepsMovedOnce || movedNow;\n\n const restoreSkeleton = <RestoreSkeleton text={labelText(labels['wizard.restoring'])} />;\n /**\n * The root both screens share: the scroll target, and the element the\n * inline restore gate marks. The script and the hidden skeleton exist only\n * in the server's HTML and the hydration pass that adopts it.\n */\n const shell = (content: ReactNode) => (\n <div\n id={WIZARD_ROOT_ID}\n ref={rootRef}\n className={resolved.classNames.wizard?.root ?? 'group scroll-mt-28'}\n suppressHydrationWarning\n >\n {!restore.hydrated && (\n <>\n <script\n // A fixed script built from constants, run before hydration.\n // biome-ignore lint/security/noDangerouslySetInnerHtml: the restore gate must run before React does.\n dangerouslySetInnerHTML={{\n __html: kit.restoreGate.restoreGateScript(\n WIZARD_ROOT_ID,\n restore.linked,\n restore.resuming\n ),\n }}\n />\n <div className=\"hidden group-data-[restoring]:block\">{restoreSkeleton}</div>\n </>\n )}\n {restore.restoring && restore.hydrated ? (\n restoreSkeleton\n ) : (\n <div className=\"group-data-[restoring]:hidden\">{content}</div>\n )}\n </div>\n );\n\n if (confirmed !== null) {\n return shell(\n <ConfirmationStep booking={booking} resolved={resolved} siteUrl={props.siteUrl} />\n );\n }\n\n const onSignedIn = (who: Parameters<BookingController['login']['signIn']>[0]) => {\n focusAfterSignIn.current = true;\n booking.login.signIn(who);\n };\n // The multi-select service step carries its own total bar and refusal notice.\n const multiSelect = state.step === 'service' && kit.config.party.maxServicesPerPerson > 1;\n const loginOffered = props.actions !== undefined && kit.config.portal.enabled;\n const loginRow = loginOffered ? (\n <LoginRow\n booking={booking}\n actions={props.actions as NonNullable<BookingWizardProps['actions']>}\n overrides={props}\n onSignedIn={onSignedIn}\n />\n ) : null;\n\n return shell(\n <div className=\"space-y-8\">\n <WizardHeader\n kit={kit}\n state={state}\n submitting={booking.submitting}\n onGo={(step) => booking.dispatch({ type: 'goToStep', step })}\n />\n\n {/* The machine's error, on the steps that have nowhere else to put it. */}\n {state.error !== null &&\n state.step !== 'details' &&\n state.step !== 'when' &&\n !multiSelect && (\n <p\n role=\"alert\"\n className={\n resolved.classNames.wizard?.alert ??\n 'rounded-lg border border-destructive/40 bg-destructive/10 px-5 py-3 text-sm'\n }\n >\n {wizardErrorText(labels, state.error, kit.config.party.maxServicesPerPerson)}\n </p>\n )}\n\n {state.step === 'details' && loginRow}\n\n {loginOffered && booking.login.vippsConfirm !== null && signedIn === null && (\n <LoginSheet\n {...overridesOf(props)}\n actions={props.actions as NonNullable<BookingWizardProps['actions']>}\n presentation=\"sheet\"\n trigger={false}\n vippsConfirm={booking.login.vippsConfirm}\n resumePath={booking.login.resumePath}\n onSignedIn={onSignedIn}\n />\n )}\n\n {/* One box per step, keyed by it, so a step arriving is a mount. */}\n <div\n key={state.step}\n data-booking-step={state.step}\n data-animate={stepsMoved ? '' : undefined}\n className=\"space-y-8\"\n >\n {state.step === 'who' && (\n <WhoStep booking={booking} resolved={resolved} loginRow={loginRow} />\n )}\n {state.step === 'service' && (\n <ServiceStep booking={booking} resolved={resolved} multiSelect={multiSelect} />\n )}\n {state.step === 'when' && <WhenStep booking={booking} resolved={resolved} />}\n {state.step === 'details' && <DetailsStep booking={booking} resolved={resolved} />}\n </div>\n\n {!multiSelect && (\n <SummaryBar\n line={booking.derived.summary}\n canAdvance={booking.derived.canAdvance}\n step={state.step}\n onNext={booking.next}\n labels={screenLabels(labels)}\n classNames={resolved.classNames.summary}\n />\n )}\n </div>\n );\n}\n\n/** The overrides a nested booking component inherits from the wizard's props. */\nfunction overridesOf(props: BookingOverrides): BookingOverrides {\n return {\n config: props.config,\n labels: props.labels,\n classNames: props.classNames,\n components: props.components,\n };\n}\n\ninterface StepProps {\n booking: BookingController;\n resolved: ResolvedBooking;\n}\n\nfunction WhoStep({ booking, resolved, loginRow }: StepProps & { loginRow: ReactNode }) {\n const { kit, state, people } = booking;\n const guests = useMemo(() => guestChoices(kit), [kit]);\n return (\n <WhoScreen\n labels={screenLabels(kit.labels)}\n format={kit.format}\n people={state.people}\n family={people.family === null ? null : familyEntries(kit, people.family, people.ageDayKey)}\n guestChoices={guests}\n addedChildren={addedChildEntries(kit, state.people, people.ageDayKey)}\n maxPeople={kit.wizard.maxPeople}\n selfKey={SELF_KEY}\n isGuestSeat={kit.wizard.isGuestSeat}\n onChoose={people.choosePeople}\n onAddChild={people.addChild}\n loginRow={loginRow}\n currentYear={Number(kit.clock.dayKey(booking.slots.fromTs).slice(0, 4))}\n classNames={resolved.classNames.who}\n components={\n resolved.components.PersonCard ? { PersonCard: resolved.components.PersonCard } : undefined\n }\n />\n );\n}\n\nfunction ServiceStep({ booking, resolved, multiSelect }: StepProps & { multiSelect: boolean }) {\n const { kit, state, people, catalogue } = booking;\n const ageOf = (person: WizardPerson) => people.personAge(person, people.ageDayKey);\n const single = state.people.length === 1 ? state.people[0] : null;\n // «Samme som sist» stays a one-tap shortcut while the person's visit is\n // nothing, or just that service (a returning child is seated with it already\n // ticked). Once they have ticked something else, a one-tap pick would REPLACE\n // their visit — and, alone, jump to the time step — so the suggestion joins\n // the visit instead, and never unticks a service already in it.\n const pickFor = (index: number, service: BookingServiceDto, oneTap: () => void) => {\n const visit = state.choices[index] ? [state.choices[index], ...state.extras[index]] : [];\n if (!multiSelect || visit.length === 0 || (visit.length === 1 && visit[0].id === service.id)) {\n oneTap();\n } else if (!visit.some((ticked) => ticked.id === service.id)) {\n booking.toggleServiceFor(index, service);\n }\n };\n return (\n <ServiceScreen\n labels={screenLabels(kit.labels)}\n format={kit.format}\n services={catalogue.services}\n {...serviceScreenBase(kit)}\n serviceFits={serviceFitsFor(kit, state.people.map(ageOf))}\n initialCategory={catalogue.deepLinkCategory}\n onPick={(service) => pickFor(0, service, () => booking.pickService(service))}\n suggestion={single ? people.suggestionFor(single) : null}\n chosenId={state.choices[0]?.id ?? null}\n childName={single ? (single.name ?? null) : null}\n party={\n state.people.length > 1\n ? {\n people: partyPeople(kit, state.people, ageOf, people.suggestionFor),\n choices: state.choices,\n onPickFor: (index, service) =>\n pickFor(index, service, () => booking.pickServiceFor(index, service)),\n onRemove: (index) =>\n booking.dispatch({\n type: 'choosePeople',\n people: state.people.filter((_, position) => position !== index),\n }),\n }\n : undefined\n }\n selection={\n multiSelect\n ? {\n // Index = seated person; meda reads a missing list (nobody seated) as empty.\n lists: state.choices.map((first, index) =>\n first ? [first, ...state.extras[index]] : []\n ),\n onToggle: booking.toggleServiceFor,\n onContinue: booking.continueFromService,\n total:\n state.items.length === 0\n ? null\n : {\n minutes: kit.wizard.visitMinutes(state.items, state.partyMode),\n priceOre: booking.derived.total,\n },\n canContinue: booking.derived.canAdvance,\n notice:\n state.error === null\n ? null\n : labelText(\n wizardErrorText(\n kit.labels,\n state.error,\n kit.config.party.maxServicesPerPerson\n )\n ),\n labels: kit.labels,\n }\n : undefined\n }\n classNames={resolved.classNames.service}\n components={\n resolved.components.ServiceCard\n ? { ServiceCard: resolved.components.ServiceCard }\n : undefined\n }\n />\n );\n}\n\nfunction WhenStep({ booking, resolved }: StepProps) {\n const { kit, state, catalogue, slots, schedule } = booking;\n const { labels, format, wizard, config } = kit;\n const party = booking.derived.party;\n const items = state.items;\n const phone = config.contact.phone;\n return (\n <>\n <StylistScreen\n labels={screenLabels(labels)}\n format={format}\n // Not deduplicated: a named stylist has to cover every service of every\n // line — a person's extras included, as their visit is one stylist's.\n serviceIds={items.flatMap((item) => visitServicesOf(item).map((service) => service.id))}\n resources={catalogue.resources}\n loading={!catalogue.resourcesKnown}\n nextAvailableLoading={slots.nextAvailableLoading}\n selectedResourceId={state.resourceId}\n nextAvailableTs={slots.nextAvailableTs}\n pendingName={catalogue.chosenStylistName}\n notice={catalogue.stylistNotice}\n onPick={booking.pickResource}\n party={\n party\n ? {\n mode: state.partyMode,\n size: items.length,\n sizeWord: partySizeWord(labels, items.length),\n minutes: {\n sequential: wizard.visitMinutes(items, 'sequential'),\n parallel: wizard.visitMinutes(items, 'parallel'),\n },\n onMode: (mode) => booking.dispatch({ type: 'setPartyMode', mode }),\n }\n : undefined\n }\n classNames={resolved.classNames.stylist}\n components={\n resolved.components.StylistCard\n ? { StylistCard: resolved.components.StylistCard }\n : undefined\n }\n />\n\n {slots.ready ? (\n <TimeScreen\n labels={screenLabels(labels)}\n format={format}\n dayparts={kit.dayparts}\n monthView\n slots={slots.single}\n days={slots.days}\n openDays={schedule.openDays}\n stylistName={catalogue.chosenStylistName}\n takenSlotTs={slots.takenSlotTs}\n phone={phone}\n weekendNote={(dayTs) => weekendNoteFor(kit, party ? items : items.slice(0, 1), dayTs)}\n onPick={booking.pickSlot}\n party={\n party\n ? {\n items,\n mode: state.partyMode,\n slots: slots.party,\n chipEndTs: (slot) => wizard.visitEndTs(items, slot.startTs, 'sequential'),\n // Only a back-to-back family is offered the simultaneous rescue.\n alternativeFor:\n state.partyMode === 'sequential'\n ? (dayTs) =>\n kit.partySlots.partyAlternative({\n sequential: slots.party,\n parallel: slots.alternatives,\n dayTs,\n })\n : undefined,\n resolveStylistName: catalogue.resolveStylistName,\n onPick: booking.pickPartySlot,\n }\n : undefined\n }\n classNames={resolved.classNames.time}\n components={timeComponents(resolved)}\n />\n ) : slots.failed ? (\n <SlotsUnavailable kit={kit} onRetry={booking.refresh} />\n ) : (\n // The step's own shape, so nothing below it moves when it fills in.\n <TimeScreenSkeleton\n labels={screenLabels(labels)}\n days={slots.days.length}\n monthView\n surchargeRow={items.some((item) =>\n visitServicesOf(item).some((service) => service.weekendSurchargePct > 0)\n )}\n />\n )}\n\n {/* Once, outside the skeleton-or-step switch, so a re-read cannot remount it. */}\n <TakenToast labels={screenLabels(labels)} takenSlotTs={slots.takenSlotTs} />\n </>\n );\n}\n\nfunction timeComponents({ components }: ResolvedBooking) {\n if (!components.DayChip && !components.TimeChip) return undefined;\n return { DayChip: components.DayChip, TimeChip: components.TimeChip };\n}\n\nfunction DetailsStep({ booking, resolved }: StepProps) {\n const { kit, state } = booking;\n const guardian = booking.login.guardian;\n const lines = useMemo(() => detailsLines(kit, state), [kit, state]);\n return (\n <DetailsScreen\n state={forMedaScreen(state)}\n onChange={booking.dispatch}\n onSubmit={booking.submit}\n lines={lines}\n totalOre={booking.derived.total}\n {...detailsScreenBase(kit)}\n submitting={booking.submitting}\n submissionNonce={booking.submissionNonce}\n family={guardian?.family}\n guardianPhone={guardian?.phone ?? null}\n format={kit.format}\n labels={screenLabels(kit.labels)}\n classNames={resolved.classNames.details}\n components={\n resolved.components.FamilyChip ? { FamilyChip: resolved.components.FamilyChip } : undefined\n }\n />\n );\n}\n\nfunction ConfirmationStep({ booking, resolved, siteUrl }: StepProps & { siteUrl?: string }) {\n const { kit } = booking;\n // Read once, so «today» cannot become «tomorrow» halfway down the card.\n const [now] = useState(() => Date.now());\n const confirmation = booking.confirmed as BookingConfirmation;\n return (\n <Confirmation\n {...confirmationProps(kit, confirmation, siteUrl)}\n onStartOver={booking.startOver}\n now={now}\n format={kit.format}\n labels={screenLabels(kit.labels)}\n classNames={resolved.classNames.confirmation}\n components={\n resolved.components.PartyLine ? { PartyLine: resolved.components.PartyLine } : undefined\n }\n />\n );\n}\n\n/**\n * «Have an account? Log in» — or, once the parent is known, nothing at all\n * for one who arrived logged in, and one line saying so for one who logged in\n * from the sheet.\n */\nfunction LoginRow({\n booking,\n actions,\n overrides,\n onSignedIn,\n}: {\n booking: BookingController;\n actions: NonNullable<BookingWizardProps['actions']>;\n overrides: BookingOverrides;\n onSignedIn: (guardian: Parameters<BookingController['login']['signIn']>[0]) => void;\n}) {\n const { arrivedAs, signedIn, resumePath } = booking.login;\n const { labels } = booking.kit;\n if (signedIn === null && arrivedAs !== null) return null;\n const who = signedIn === null ? null : (signedIn.guardian ?? arrivedAs);\n const name =\n who === null\n ? null\n : [who.firstName, who.lastName].filter(Boolean).join(' ').trim() || who.email;\n const confirmation =\n signedIn === null\n ? null\n : name === null\n ? labelText(labels['wizard.signedIn'])\n : fill(labels['wizard.signedInAs'], { name });\n return (\n <div>\n {signedIn === null && (\n <LoginSheet\n {...overridesOf(overrides)}\n actions={actions}\n presentation=\"sheet\"\n resumePath={resumePath}\n onSignedIn={onSignedIn}\n />\n )}\n <LiveStatus text={confirmation} className=\"text-sm text-muted-foreground empty:hidden\" />\n </div>\n );\n}\n\n/** The neutral shell the restore gate shows, the header's and a step's shape. */\nfunction RestoreSkeleton({ text }: { text: string }) {\n return (\n <div data-testid=\"restore-skeleton\">\n {/* Outside the busy subtree: assistive tech may hold back what is inside it. */}\n <LiveStatus text={text} />\n <div aria-busy=\"true\" className=\"space-y-8\">\n <div aria-hidden=\"true\" className=\"space-y-3\">\n <div className=\"flex items-center gap-3\">\n <span className=\"size-9 shrink-0 animate-pulse rounded-full bg-muted\" />\n <span className=\"flex flex-col gap-1.5\">\n <span className=\"h-4 w-24 animate-pulse rounded bg-muted\" />\n <span className=\"h-3 w-36 animate-pulse rounded bg-muted\" />\n </span>\n </div>\n <span className=\"block h-1.5 w-full animate-pulse rounded-full bg-muted\" />\n </div>\n <div aria-hidden=\"true\" className=\"space-y-6\">\n <span className=\"block h-8 w-56 animate-pulse rounded bg-muted md:h-9\" />\n <div className=\"space-y-2\">\n {Array.from({ length: 4 }, (_, index) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: four identical placeholders.\n <span key={index} className=\"block h-24 animate-pulse rounded-lg bg-muted\" />\n ))}\n </div>\n </div>\n </div>\n </div>\n );\n}\n\n/**\n * Where the visitor is, the way back, and how much is left: a back arrow, the\n * title and «step N of 4», and one segment per step that IS the button to it —\n * tappable only where the machine's `canGoToStep` says so, and frozen while a\n * submission is in flight.\n */\nfunction WizardHeader({\n kit,\n state,\n submitting,\n onGo,\n}: {\n kit: BookingKit;\n state: WizardState;\n submitting: boolean;\n onGo: (step: WizardStep) => void;\n}) {\n const { labels, wizard } = kit;\n const stepLabel = (step: WizardStep) => labelText(labels[`wizard.step.${step}`]);\n const index = STEPS.indexOf(state.step);\n const previous = index > 0 ? STEPS[index - 1] : null;\n const canGoBack = previous !== null && !submitting && wizard.canGoToStep(state, previous);\n\n return (\n <div className=\"space-y-3\">\n <div className=\"flex items-center gap-3\">\n {canGoBack ? (\n <button\n type=\"button\"\n aria-label={fill(labels['wizard.back'], { label: stepLabel(previous) })}\n onClick={() => onGo(previous)}\n className=\"flex size-9 shrink-0 items-center justify-center rounded-full border border-border text-foreground transition-colors hover:bg-muted\"\n >\n <ArrowLeft aria-hidden=\"true\" className=\"size-4\" />\n </button>\n ) : (\n // Held rather than dropped, so the title does not shuffle sideways.\n <span aria-hidden=\"true\" className=\"size-9 shrink-0\" />\n )}\n <p className=\"min-w-0\">\n <span className=\"block font-sans text-sm font-bold\">{labels['wizard.title']}</span>\n <span className=\"block truncate text-xs text-muted-foreground\">\n {fillParts(labels['wizard.progress'], {\n step: index + 1,\n total: STEPS.length,\n label: stepLabel(state.step),\n })}\n </span>\n </p>\n </div>\n\n <ol\n aria-label={labelText(labels['wizard.progressLabel'])}\n className=\"flex items-center gap-1.5\"\n >\n {STEPS.map((step, position) => {\n const isCurrent = step === state.step;\n const reachable = !submitting && !isCurrent && wizard.canGoToStep(state, step);\n return (\n <li key={step} className=\"flex-1\">\n <button\n type=\"button\"\n disabled={!reachable}\n aria-current={isCurrent ? 'step' : undefined}\n onClick={() => onGo(step)}\n className={`block h-1.5 w-full rounded-full transition-colors ${\n position <= index ? 'bg-primary' : 'bg-border'\n } ${reachable ? 'cursor-pointer' : 'cursor-default'}`}\n >\n <span className=\"sr-only\">{stepLabel(step)}</span>\n </button>\n </li>\n );\n })}\n </ol>\n </div>\n );\n}\n\n/**\n * The openings could not be read — this site's failure, not a full day —\n * with the telephone as the rescue and a retry as the cheaper first try.\n */\nfunction SlotsUnavailable({ kit, onRetry }: { kit: BookingKit; onRetry: () => void }) {\n const { labels, config, format } = kit;\n const phone = config.contact.phone;\n return (\n <div className=\"space-y-3\">\n <p role=\"alert\" className=\"rounded-lg border border-border bg-card px-5 py-4 text-sm\">\n {labels['wizard.slotsUnavailable.lead']}\n {phone ? (\n <a\n href={format.telHref(phone)}\n aria-label={fill(labels['wizard.slotsUnavailable.callAria'], { phone })}\n className=\"font-semibold text-primary underline underline-offset-4\"\n >\n {fillParts(labels['wizard.slotsUnavailable.call'], { phone })}\n </a>\n ) : (\n labels['wizard.slotsUnavailable.callPlain']\n )}\n {labels['wizard.slotsUnavailable.suffix']}\n </p>\n <BookingButtonOutline onClick={onRetry}>{labels['wizard.retry']}</BookingButtonOutline>\n </div>\n );\n}\n\nfunction BookingButtonOutline({ onClick, children }: { onClick: () => void; children: ReactNode }) {\n return (\n <BookingButton variant=\"outline\" onClick={onClick}>\n {children}\n </BookingButton>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"BookingWizard.mjs","names":["LoginSheet"],"sources":["../../src/react/BookingWizard.tsx"],"sourcesContent":["/**\n * `<BookingWizard>` — the booking flow, from «who is it for?» to the\n * confirmation, drawn with the `@medalsocial/meda/booking` screens over\n * `useBooking()`.\n *\n * The hook owns the machine, the fetches and the stores; this file owns the\n * page: which step is on screen (ONE at a time — the details step's nonce and\n * the submit flag depend on it), focus and scroll on every move, the restore\n * gate's pre-hydration script and skeleton, the header, and what each screen\n * is handed (prices, the weekend note, ages, the possessive, the calendar\n * file) out of the site's config.\n */\n\nimport {\n BookingButton,\n BookingRecap,\n Confirmation,\n DetailsScreen,\n LiveStatus,\n ServiceScreen,\n StylistScreen,\n SummaryBar,\n TakenToast,\n TimeScreen,\n TimeScreenSkeleton,\n WhoScreen,\n} from '@medalsocial/meda/booking';\nimport { ArrowLeft } from 'lucide-react';\nimport {\n type ReactNode,\n Suspense,\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport { fill, fillParts, labelText } from '../core/labels';\nimport { SELF_KEY, type WizardPerson, type WizardState, type WizardStep } from '../core/machine';\nimport type { BookingServiceDto } from '../core/types';\nimport { visitServicesOf } from '../core/visit';\nimport type { PortalActions } from './actions';\nimport type { BookingKit } from './kit';\nimport { LoginSheet } from './LoginSheet';\nimport type { BookingOverrides, ResolvedBooking } from './Provider';\nimport { useBookingKit } from './Provider';\nimport { screenLabels } from './screen-labels';\nimport {\n type BookingConfirmation,\n type BookingController,\n type BookingSeed,\n useBooking,\n} from './useBooking';\nimport {\n addedChildEntries,\n confirmationProps,\n detailsLines,\n detailsScreenBase,\n familyEntries,\n GUEST_ADULT_SEAT,\n guestChoices,\n partyPeople,\n partySizeWord,\n recapProps,\n serviceFitsFor,\n serviceScreenBase,\n stylistFace,\n weekendNoteFor,\n} from './wizard/adapters';\nimport { forMedaScreen, wizardErrorText } from './wizard-error';\n\nexport interface BookingWizardProps extends BookingOverrides {\n /** What the page prefetched (the `/next` loader's seed). */\n seed: BookingSeed;\n /** This request's phone and address, over `config.contact` (the loader's `contact`). */\n contact?: { phone?: string | null; address?: string | null };\n /** The parent the page found a portal session for; `null` for a guest. */\n guardian?: BookingGuardianProp;\n /**\n * The app's server actions for the login sheet (Decision 8). Without them —\n * or with `config.portal.enabled` off — no login is offered.\n */\n actions?: Pick<PortalActions, 'startLogin' | 'startVipps'>;\n /** How many days the window covers. Default `config.window.rangeDays`. */\n rangeDays?: number;\n /**\n * The site's origin (`https://…`), so the manage links inside the calendar\n * file are absolute. Absent, they stay root-relative.\n */\n siteUrl?: string;\n /** A monitoring tap: the step the visitor is on, and a submission's outcome. */\n onEvent?: (event: BookingWizardEvent) => void;\n}\n\ntype BookingGuardianProp = Parameters<typeof useBooking>[0]['guardian'];\n\nexport type BookingWizardEvent =\n | { type: 'step'; step: WizardStep | 'confirmed' }\n | { type: 'submit_ok'; bookings: number }\n | { type: 'submit_error'; code: NonNullable<WizardState['error']> };\n\n/** The bundler's build mode, which it inlines; absent outside a bundler. */\ndeclare const process: { env: { NODE_ENV?: string } } | undefined;\n\n/** The wizard's root, for the inline restore gate to find before hydration. */\nconst WIZARD_ROOT_ID = 'booking-wizard';\n\n/** The time step's heading, which a lost slot sends the visitor back to. */\nconst TIME_HEADING_ID = 'booking-time-heading';\n\n/** The heading each screen focuses when the visitor arrives on it. */\nconst STEP_HEADINGS: Record<WizardStep | 'confirmed', string> = {\n who: 'booking-who-heading',\n service: 'booking-service-heading',\n when: 'booking-stylist-heading',\n details: 'booking-details-heading',\n confirmed: 'booking-confirmed-heading',\n};\n\nconst STEPS: readonly WizardStep[] = ['who', 'service', 'when', 'details'];\n\n/** Smooth, or instant for a visitor who asked for reduced motion. */\nfunction scrollToTop(target: HTMLElement | null): void {\n if (!target || typeof target.scrollIntoView !== 'function') return;\n const reduced =\n typeof window.matchMedia === 'function' &&\n window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n target.scrollIntoView({ behavior: reduced ? 'auto' : 'smooth', block: 'start' });\n}\n\n/**\n * The wizard, behind its own Suspense boundary: `useSearchParams` throws to\n * the nearest one during a static prerender, and without ours that is the\n * root layout's — the whole page would ship empty until hydration.\n */\nexport function BookingWizard(props: BookingWizardProps) {\n return (\n <Suspense fallback={null}>\n <BookingWizardShell {...props} />\n </Suspense>\n );\n}\n\nfunction BookingWizardShell(props: BookingWizardProps) {\n const resolved = useBookingKit(props, props.contact);\n const loginOffered = props.actions !== undefined && resolved.kit.config.portal.enabled;\n const booking = useBooking({ ...props, loginOffered });\n const { kit } = booking;\n const { labels } = kit;\n\n // `account.required` with no way to log in: every «Bekreft» would be refused.\n const unreachableLogin = kit.config.account.required && !loginOffered;\n useEffect(() => {\n if (\n unreachableLogin &&\n typeof process !== 'undefined' &&\n process.env.NODE_ENV !== 'production'\n ) {\n console.warn('[booking] account.required without login actions: nobody can log in to book.');\n }\n }, [unreachableLogin]);\n const { state, restore, confirmed, onEvent } = { ...booking, onEvent: props.onEvent };\n\n /**\n * On every change of screen: the wizard's top into view and focus on the\n * new step's heading. Never on the first screen, and never for what the\n * restore gate reveals — that is the page arriving, not the visitor moving.\n */\n const rootRef = useRef<HTMLDivElement>(null);\n const screenKey: WizardStep | 'confirmed' = confirmed !== null ? 'confirmed' : state.step;\n const shownScreen = useRef<WizardStep | 'confirmed' | null>(null);\n const takenSlotTs = booking.slots.takenSlotTs;\n const timeStepReady = booking.slots.ready;\n const focusTimeWhenReady = useRef(false);\n useEffect(() => {\n if (restore.restoring) return;\n const previous = shownScreen.current;\n shownScreen.current = screenKey;\n if (previous === screenKey) return;\n onEvent?.({ type: 'step', step: screenKey });\n if (previous === null) return;\n // A lost slot sends the visitor back for the ALTERNATIVES, on the time half.\n const toTimes = screenKey === 'when' && takenSlotTs !== null;\n const headingId = toTimes ? TIME_HEADING_ID : STEP_HEADINGS[screenKey];\n const heading = document.getElementById(headingId);\n scrollToTop(toTimes ? heading : rootRef.current);\n heading?.focus({ preventScroll: true });\n // The heading focused now may be the skeleton's; focus follows the real one.\n focusTimeWhenReady.current = toTimes && !timeStepReady;\n }, [screenKey, restore.restoring, takenSlotTs, timeStepReady, onEvent]);\n\n /** After a login from the sheet the row that opened it is gone: focus the step's heading. */\n const focusAfterSignIn = useRef(false);\n const signedIn = booking.login.signedIn;\n useEffect(() => {\n if (signedIn === null || !focusAfterSignIn.current) return;\n focusAfterSignIn.current = false;\n document.getElementById(STEP_HEADINGS[state.step])?.focus({ preventScroll: true });\n }, [signedIn, state.step]);\n /**\n * A 401 put the gate back over the form: focus its heading (the details\n * step's id), where the notice beside it says why. Once per loss, and only\n * once the gate is actually on screen — a replay answers behind the skeleton.\n */\n const sessionLost = booking.login.sessionLost;\n const sessionLossFocused = useRef(false);\n useEffect(() => {\n if (!sessionLost) {\n sessionLossFocused.current = false;\n return;\n }\n if (sessionLossFocused.current || restore.restoring || state.step !== 'details') return;\n sessionLossFocused.current = true;\n const heading = document.getElementById(STEP_HEADINGS.details);\n scrollToTop(rootRef.current);\n heading?.focus({ preventScroll: true });\n }, [sessionLost, restore.restoring, state.step]);\n useEffect(() => {\n if (!timeStepReady || !focusTimeWhenReady.current) return;\n focusTimeWhenReady.current = false;\n const active = document.activeElement;\n if (active === null || active === document.body) {\n document.getElementById(TIME_HEADING_ID)?.focus({ preventScroll: true });\n }\n }, [timeStepReady]);\n\n // The monitoring tap for a submission's outcome.\n const lastError = useRef(state.error);\n useEffect(() => {\n if (state.error !== null && state.error !== lastError.current) {\n onEvent?.({ type: 'submit_error', code: state.error });\n }\n lastError.current = state.error;\n }, [state.error, onEvent]);\n const confirmedCount = confirmed?.bookings.length ?? 0;\n useEffect(() => {\n if (confirmedCount > 0) onEvent?.({ type: 'submit_ok', bookings: confirmedCount });\n }, [confirmedCount, onEvent]);\n\n // React owns the root from here on: drop the pre-hydration mark the inline\n // script may have set, and say so, so the script's fallback timer stands down.\n useLayoutEffect(() => {\n if (!restore.hydrated) return;\n rootRef.current?.removeAttribute('data-restoring');\n rootRef.current?.setAttribute('data-hydrated', '');\n }, [restore.hydrated]);\n\n // Steps animate in only once the visitor has moved between them.\n const [arrivalStep, setArrivalStep] = useState<WizardStep | null>(\n restore.restoring ? null : state.step\n );\n if (!restore.restoring && arrivalStep === null) setArrivalStep(state.step);\n const [stepsMovedOnce, setStepsMovedOnce] = useState(false);\n const movedNow = arrivalStep !== null && !restore.restoring && state.step !== arrivalStep;\n if (movedNow && !stepsMovedOnce) setStepsMovedOnce(true);\n const stepsMoved = stepsMovedOnce || movedNow;\n\n const restoreSkeleton = <RestoreSkeleton text={labelText(labels['wizard.restoring'])} />;\n /**\n * The root both screens share: the scroll target, and the element the\n * inline restore gate marks. The script and the hidden skeleton exist only\n * in the server's HTML and the hydration pass that adopts it.\n */\n const shell = (content: ReactNode) => (\n <div\n id={WIZARD_ROOT_ID}\n ref={rootRef}\n className={resolved.classNames.wizard?.root ?? 'group scroll-mt-28'}\n suppressHydrationWarning\n >\n {!restore.hydrated && (\n <>\n <script\n // A fixed script built from constants, run before hydration.\n // biome-ignore lint/security/noDangerouslySetInnerHtml: the restore gate must run before React does.\n dangerouslySetInnerHTML={{\n __html: kit.restoreGate.restoreGateScript(\n WIZARD_ROOT_ID,\n restore.linked,\n restore.resuming\n ),\n }}\n />\n <div className=\"hidden group-data-[restoring]:block\">{restoreSkeleton}</div>\n </>\n )}\n {restore.restoring && restore.hydrated ? (\n restoreSkeleton\n ) : (\n <div className=\"group-data-[restoring]:hidden\">{content}</div>\n )}\n </div>\n );\n\n if (confirmed !== null) {\n return shell(\n <ConfirmationStep booking={booking} resolved={resolved} siteUrl={props.siteUrl} />\n );\n }\n\n const onSignedIn = (who: Parameters<BookingController['login']['signIn']>[0]) => {\n focusAfterSignIn.current = true;\n booking.login.signIn(who);\n };\n // The multi-select service step carries its own total bar and refusal notice.\n const multiSelect = state.step === 'service' && kit.config.party.maxServicesPerPerson > 1;\n /**\n * `account.required`: every booking is a logged-in parent's. The login is\n * not offered along the way — it IS «Bekreft», for a parent not logged in.\n */\n const accountRequired = kit.config.account.required && loginOffered;\n const gated = accountRequired && state.step === 'details' && !booking.login.loggedIn;\n const loginRow =\n loginOffered && !accountRequired ? (\n <LoginRow\n booking={booking}\n actions={props.actions as NonNullable<BookingWizardProps['actions']>}\n overrides={props}\n onSignedIn={onSignedIn}\n />\n ) : null;\n\n return shell(\n <div className=\"space-y-8\">\n <WizardHeader\n kit={kit}\n state={state}\n submitting={booking.submitting}\n onGo={(step) => booking.dispatch({ type: 'goToStep', step })}\n />\n\n {/* The machine's error, on the steps that have nowhere else to put it. */}\n {state.error !== null &&\n state.step !== 'details' &&\n state.step !== 'when' &&\n !multiSelect && (\n <p\n role=\"alert\"\n className={\n resolved.classNames.wizard?.alert ??\n 'rounded-lg border border-destructive/40 bg-destructive/10 px-5 py-3 text-sm'\n }\n >\n {wizardErrorText(labels, state.error, kit.config.party.maxServicesPerPerson)}\n </p>\n )}\n\n {/* What is being booked, first: above the login offer and the form. */}\n {state.step === 'details' && !gated && kit.config.screens.recap && (\n <RecapStep booking={booking} resolved={resolved} />\n )}\n\n {state.step === 'details' && loginRow}\n\n {/* The gate takes a Vipps confirm code itself, in place of the form. */}\n {loginOffered && booking.login.vippsConfirm !== null && signedIn === null && !gated && (\n <LoginSheet\n {...overridesOf(props)}\n actions={props.actions as NonNullable<BookingWizardProps['actions']>}\n presentation=\"sheet\"\n trigger={false}\n vippsConfirm={booking.login.vippsConfirm}\n resumePath={booking.login.resumePath}\n onSignedIn={onSignedIn}\n />\n )}\n\n {/* One box per step, keyed by it, so a step arriving is a mount. */}\n <div\n key={state.step}\n data-booking-step={state.step}\n data-animate={stepsMoved ? '' : undefined}\n className=\"space-y-8\"\n >\n {state.step === 'who' && (\n <WhoStep booking={booking} resolved={resolved} loginRow={loginRow} />\n )}\n {state.step === 'service' && (\n <ServiceStep booking={booking} resolved={resolved} multiSelect={multiSelect} />\n )}\n {state.step === 'when' && <WhenStep booking={booking} resolved={resolved} />}\n {state.step === 'details' &&\n (gated ? (\n <AccountGate\n booking={booking}\n resolved={resolved}\n actions={props.actions as NonNullable<BookingWizardProps['actions']>}\n overrides={props}\n onSignedIn={onSignedIn}\n />\n ) : (\n <DetailsStep booking={booking} resolved={resolved} />\n ))}\n </div>\n\n {!multiSelect && (\n <SummaryBar\n {...(kit.config.screens.summaryDetail\n ? booking.derived.summaryParts\n : { line: booking.derived.summary })}\n canAdvance={booking.derived.canAdvance}\n step={state.step}\n onNext={booking.next}\n hideNextWhenDisabled={kit.config.screens.hideDisabledNext}\n labels={screenLabels(labels)}\n classNames={resolved.classNames.summary}\n />\n )}\n </div>\n );\n}\n\n/** The overrides a nested booking component inherits from the wizard's props. */\nfunction overridesOf(props: BookingOverrides): BookingOverrides {\n return {\n config: props.config,\n labels: props.labels,\n classNames: props.classNames,\n components: props.components,\n };\n}\n\ninterface StepProps {\n booking: BookingController;\n resolved: ResolvedBooking;\n}\n\nfunction WhoStep({ booking, resolved, loginRow }: StepProps & { loginRow: ReactNode }) {\n const { kit, state, people } = booking;\n const guests = useMemo(() => guestChoices(kit), [kit]);\n return (\n <WhoScreen\n labels={screenLabels(kit.labels)}\n format={kit.format}\n people={state.people}\n family={people.family === null ? null : familyEntries(kit, people.family, people.ageDayKey)}\n guestChoices={guests}\n addedChildren={addedChildEntries(kit, state.people, people.ageDayKey)}\n maxPeople={kit.wizard.maxPeople}\n selfKey={SELF_KEY}\n isGuestSeat={kit.wizard.isGuestSeat}\n onChoose={people.choosePeople}\n onAddChild={people.addChild}\n guestParty={\n kit.config.screens.guestParty\n ? { child: kit.wizard.guestChild, adult: GUEST_ADULT_SEAT }\n : undefined\n }\n loginRow={loginRow}\n currentYear={Number(kit.clock.dayKey(booking.slots.fromTs).slice(0, 4))}\n classNames={resolved.classNames.who}\n components={\n resolved.components.PersonCard ? { PersonCard: resolved.components.PersonCard } : undefined\n }\n />\n );\n}\n\nfunction ServiceStep({ booking, resolved, multiSelect }: StepProps & { multiSelect: boolean }) {\n const { kit, state, people, catalogue } = booking;\n const ageOf = (person: WizardPerson) => people.personAge(person, people.ageDayKey);\n const single = state.people.length === 1 ? state.people[0] : null;\n // «Samme som sist» stays a one-tap shortcut while the person's visit is\n // nothing, or just that service (a returning child is seated with it already\n // ticked). Once they have ticked something else, a one-tap pick would REPLACE\n // their visit — and, alone, jump to the time step — so the suggestion joins\n // the visit instead, and never unticks a service already in it.\n const pickFor = (index: number, service: BookingServiceDto, oneTap: () => void) => {\n const visit = state.choices[index] ? [state.choices[index], ...state.extras[index]] : [];\n if (!multiSelect || visit.length === 0 || (visit.length === 1 && visit[0].id === service.id)) {\n oneTap();\n } else if (!visit.some((ticked) => ticked.id === service.id)) {\n booking.toggleServiceFor(index, service);\n }\n };\n return (\n <ServiceScreen\n labels={screenLabels(kit.labels)}\n format={kit.format}\n services={catalogue.services}\n {...serviceScreenBase(kit)}\n serviceFits={serviceFitsFor(kit, state.people.map(ageOf), state.people)}\n initialCategory={catalogue.deepLinkCategory}\n onPick={(service) => pickFor(0, service, () => booking.pickService(service))}\n suggestion={single ? people.suggestionFor(single) : null}\n chosenId={state.choices[0]?.id ?? null}\n childName={single ? (single.name ?? null) : null}\n party={\n state.people.length > 1\n ? {\n people: partyPeople(kit, state.people, ageOf, people.suggestionFor),\n choices: state.choices,\n onPickFor: (index, service) =>\n pickFor(index, service, () => booking.pickServiceFor(index, service)),\n onRemove: (index) =>\n booking.dispatch({\n type: 'choosePeople',\n people: state.people.filter((_, position) => position !== index),\n }),\n }\n : undefined\n }\n selection={\n multiSelect\n ? {\n // Index = seated person; meda reads a missing list (nobody seated) as empty.\n lists: state.choices.map((first, index) =>\n first ? [first, ...state.extras[index]] : []\n ),\n onToggle: booking.toggleServiceFor,\n onContinue: booking.continueFromService,\n total:\n state.items.length === 0\n ? null\n : {\n minutes: kit.wizard.visitMinutes(state.items, state.partyMode),\n priceOre: booking.derived.total,\n },\n canContinue: booking.derived.canAdvance,\n notice:\n state.error === null\n ? null\n : labelText(\n wizardErrorText(\n kit.labels,\n state.error,\n kit.config.party.maxServicesPerPerson\n )\n ),\n labels: kit.labels,\n }\n : undefined\n }\n classNames={resolved.classNames.service}\n components={\n resolved.components.ServiceCard\n ? { ServiceCard: resolved.components.ServiceCard }\n : undefined\n }\n />\n );\n}\n\nfunction WhenStep({ booking, resolved }: StepProps) {\n const { kit, state, catalogue, slots, schedule } = booking;\n const { labels, format, wizard, config } = kit;\n const party = booking.derived.party;\n const items = state.items;\n const phone = config.contact.phone;\n return (\n <>\n <StylistScreen\n labels={screenLabels(labels)}\n format={format}\n // Not deduplicated: a named stylist has to cover every service of every\n // line — a person's extras included, as their visit is one stylist's.\n serviceIds={items.flatMap((item) => visitServicesOf(item).map((service) => service.id))}\n resources={catalogue.resources}\n loading={!catalogue.resourcesKnown}\n nextAvailableLoading={slots.nextAvailableLoading}\n selectedResourceId={state.resourceId}\n nextAvailableTs={slots.nextAvailableTs}\n pendingName={catalogue.chosenStylistName}\n notice={catalogue.stylistNotice}\n onPick={booking.pickResource}\n firstAvailableFaces={config.screens.firstAvailableFaces}\n edgeFade={config.screens.stylistEdgeFade}\n party={\n party\n ? {\n mode: state.partyMode,\n size: items.length,\n sizeWord: partySizeWord(labels, items.length),\n minutes: {\n sequential: wizard.visitMinutes(items, 'sequential'),\n parallel: wizard.visitMinutes(items, 'parallel'),\n },\n onMode: (mode) => booking.dispatch({ type: 'setPartyMode', mode }),\n }\n : undefined\n }\n classNames={resolved.classNames.stylist}\n components={\n resolved.components.StylistCard\n ? { StylistCard: resolved.components.StylistCard }\n : undefined\n }\n />\n\n {slots.ready ? (\n <TimeScreen\n labels={screenLabels(labels)}\n format={format}\n dayparts={kit.dayparts}\n monthView\n slots={slots.single}\n days={slots.days}\n openDays={schedule.openDays}\n stylistName={catalogue.chosenStylistName}\n takenSlotTs={slots.takenSlotTs}\n phone={phone}\n weekendNote={(dayTs) => weekendNoteFor(kit, party ? items : items.slice(0, 1), dayTs)}\n onPick={booking.pickSlot}\n soonest={\n config.screens.soonest\n ? {\n resolveStylist: (resourceId) => stylistFace(catalogue.resources, resourceId),\n }\n : undefined\n }\n dayFullness={config.screens.dayFullness}\n party={\n party\n ? {\n items,\n mode: state.partyMode,\n slots: slots.party,\n chipEndTs: (slot) => wizard.visitEndTs(items, slot.startTs, 'sequential'),\n // Only a back-to-back family is offered the simultaneous rescue.\n alternativeFor:\n state.partyMode === 'sequential'\n ? (dayTs) =>\n kit.partySlots.partyAlternative({\n sequential: slots.party,\n parallel: slots.alternatives,\n dayTs,\n })\n : undefined,\n resolveStylistName: catalogue.resolveStylistName,\n onPick: booking.pickPartySlot,\n }\n : undefined\n }\n classNames={resolved.classNames.time}\n components={timeComponents(resolved)}\n />\n ) : slots.failed ? (\n <SlotsUnavailable kit={kit} onRetry={booking.refresh} />\n ) : (\n // The step's own shape, so nothing below it moves when it fills in.\n <TimeScreenSkeleton\n labels={screenLabels(labels)}\n days={slots.days.length}\n monthView\n surchargeRow={items.some((item) =>\n visitServicesOf(item).some((service) => service.weekendSurchargePct > 0)\n )}\n />\n )}\n\n {/* Once, outside the skeleton-or-step switch, so a re-read cannot remount it. */}\n <TakenToast labels={screenLabels(labels)} takenSlotTs={slots.takenSlotTs} />\n </>\n );\n}\n\n/**\n * The details step's recap (`config.screens.recap`): the day, hours, who and\n * total of the booking the form below submits, «edit» back to the time step,\n * and a swap to another stylist free at that minute.\n */\nfunction RecapStep({ booking, resolved }: StepProps) {\n const { kit, state, catalogue, slots } = booking;\n const recap = useMemo(\n () => recapProps(kit, state, catalogue.resources, slots.single),\n [kit, state, catalogue.resources, slots.single]\n );\n return (\n <BookingRecap\n lines={recap.lines}\n totalOre={booking.derived.total}\n format={kit.format}\n labels={screenLabels(kit.labels)}\n onEdit={() => booking.dispatch({ type: 'goToStep', step: 'when' })}\n alternatives={recap.alternatives}\n // Alternatives exist only for a timed visit, so there is always a start to keep.\n onSwap={(resourceId) => booking.pickSlot({ startTs: state.startTs as number, resourceId })}\n classNames={resolved.classNames.recap}\n />\n );\n}\n\nfunction timeComponents({ components }: ResolvedBooking) {\n if (!components.DayChip && !components.TimeChip) return undefined;\n return { DayChip: components.DayChip, TimeChip: components.TimeChip };\n}\n\nfunction DetailsStep({ booking, resolved }: StepProps) {\n const { kit, state } = booking;\n const guardian = booking.login.guardian;\n const lines = useMemo(() => detailsLines(kit, state), [kit, state]);\n return (\n <DetailsScreen\n state={forMedaScreen(state)}\n onChange={booking.dispatch}\n onSubmit={booking.submit}\n lines={lines}\n totalOre={booking.derived.total}\n {...detailsScreenBase(kit)}\n submitting={booking.submitting}\n submissionNonce={booking.submissionNonce}\n family={guardian?.family}\n guardianPhone={guardian?.phone ?? null}\n // The address they logged in with: the booking is theirs, by that address.\n emailReadOnly={kit.config.account.required && guardian !== null}\n format={kit.format}\n labels={screenLabels(kit.labels)}\n classNames={resolved.classNames.details}\n components={\n resolved.components.FamilyChip ? { FamilyChip: resolved.components.FamilyChip } : undefined\n }\n />\n );\n}\n\n/**\n * «Bekreft» under `account.required` for a parent not logged in: «Nesten\n * ferdig», a line, and the login — Vipps, or e-mail — in place of the form.\n * The heading carries the details step's id, so the focus a step change moves\n * lands here, and the form's heading takes it over once the parent is in.\n */\nfunction AccountGate({\n booking,\n resolved,\n actions,\n overrides,\n onSignedIn,\n}: StepProps & {\n actions: NonNullable<BookingWizardProps['actions']>;\n overrides: BookingOverrides;\n onSignedIn: (guardian: Parameters<BookingController['login']['signIn']>[0]) => void;\n}) {\n const { labels } = booking.kit;\n const { classNames } = resolved;\n // The details screen's own root and heading slots, after the same defaults.\n const slot = (base: string, extra: string | undefined) => [base, extra].filter(Boolean).join(' ');\n return (\n <section\n aria-labelledby={STEP_HEADINGS.details}\n className={slot('space-y-6', classNames.details?.root)}\n >\n <h2\n id={STEP_HEADINGS.details}\n tabIndex={-1}\n className={slot(\n 'font-sans text-2xl font-bold outline-none md:text-3xl',\n classNames.details?.heading\n )}\n >\n {labelText(labels['wizard.account.heading'])}\n </h2>\n <LiveStatus\n text={booking.login.sessionLost ? labelText(labels['wizard.account.sessionLost']) : null}\n className=\"text-sm font-medium empty:hidden\"\n />\n <p className=\"text-muted-foreground\">{labelText(labels['wizard.account.intro'])}</p>\n {/* What the login is for: the hour held, in the summary bar's own words and slot. */}\n <p\n className={slot(\n 'rounded-lg border border-border bg-card px-4 py-3 text-sm font-medium',\n classNames.summary?.line\n )}\n >\n {booking.derived.summary}\n </p>\n <LoginSheet\n {...overridesOf(overrides)}\n actions={actions}\n presentation=\"gate\"\n resumePath={booking.login.resumePath}\n vippsConfirm={booking.login.vippsConfirm}\n onSignedIn={onSignedIn}\n />\n </section>\n );\n}\n\nfunction ConfirmationStep({ booking, resolved, siteUrl }: StepProps & { siteUrl?: string }) {\n const { kit } = booking;\n // Read once, so «today» cannot become «tomorrow» halfway down the card.\n const [now] = useState(() => Date.now());\n const confirmation = booking.confirmed as BookingConfirmation;\n return (\n <Confirmation\n {...confirmationProps(kit, confirmation, siteUrl)}\n onStartOver={booking.startOver}\n now={now}\n format={kit.format}\n labels={screenLabels(kit.labels)}\n classNames={resolved.classNames.confirmation}\n components={\n resolved.components.PartyLine ? { PartyLine: resolved.components.PartyLine } : undefined\n }\n />\n );\n}\n\n/**\n * «Have an account? Log in» — or, once the parent is known, nothing at all\n * for one who arrived logged in, and one line saying so for one who logged in\n * from the sheet.\n */\nfunction LoginRow({\n booking,\n actions,\n overrides,\n onSignedIn,\n}: {\n booking: BookingController;\n actions: NonNullable<BookingWizardProps['actions']>;\n overrides: BookingOverrides;\n onSignedIn: (guardian: Parameters<BookingController['login']['signIn']>[0]) => void;\n}) {\n const { arrivedAs, signedIn, resumePath } = booking.login;\n const { labels } = booking.kit;\n if (signedIn === null && arrivedAs !== null) return null;\n // Named only by the login itself: an unreadable profile is «Logget inn»,\n // never the name of whoever the page arrived as.\n const who = signedIn?.guardian ?? null;\n const name =\n who === null\n ? null\n : [who.firstName, who.lastName].filter(Boolean).join(' ').trim() || who.email;\n const confirmation =\n signedIn === null\n ? null\n : name === null\n ? labelText(labels['wizard.signedIn'])\n : fill(labels['wizard.signedInAs'], { name });\n return (\n <div>\n {signedIn === null && (\n <LoginSheet\n {...overridesOf(overrides)}\n actions={actions}\n presentation=\"sheet\"\n resumePath={resumePath}\n onSignedIn={onSignedIn}\n />\n )}\n <LiveStatus text={confirmation} className=\"text-sm text-muted-foreground empty:hidden\" />\n </div>\n );\n}\n\n/** The neutral shell the restore gate shows, the header's and a step's shape. */\nfunction RestoreSkeleton({ text }: { text: string }) {\n return (\n <div data-testid=\"restore-skeleton\">\n {/* Outside the busy subtree: assistive tech may hold back what is inside it. */}\n <LiveStatus text={text} />\n <div aria-busy=\"true\" className=\"space-y-8\">\n <div aria-hidden=\"true\" className=\"space-y-3\">\n <div className=\"flex items-center gap-3\">\n <span className=\"size-9 shrink-0 animate-pulse rounded-full bg-muted\" />\n <span className=\"flex flex-col gap-1.5\">\n <span className=\"h-4 w-24 animate-pulse rounded bg-muted\" />\n <span className=\"h-3 w-36 animate-pulse rounded bg-muted\" />\n </span>\n </div>\n <span className=\"block h-1.5 w-full animate-pulse rounded-full bg-muted\" />\n </div>\n <div aria-hidden=\"true\" className=\"space-y-6\">\n <span className=\"block h-8 w-56 animate-pulse rounded bg-muted md:h-9\" />\n <div className=\"space-y-2\">\n {Array.from({ length: 4 }, (_, index) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: four identical placeholders.\n <span key={index} className=\"block h-24 animate-pulse rounded-lg bg-muted\" />\n ))}\n </div>\n </div>\n </div>\n </div>\n );\n}\n\n/**\n * Where the visitor is, the way back, and how much is left: a back arrow, the\n * title and «step N of 4», and one segment per step that IS the button to it —\n * tappable only where the machine's `canGoToStep` says so, and frozen while a\n * submission is in flight.\n */\nfunction WizardHeader({\n kit,\n state,\n submitting,\n onGo,\n}: {\n kit: BookingKit;\n state: WizardState;\n submitting: boolean;\n onGo: (step: WizardStep) => void;\n}) {\n const { labels, wizard } = kit;\n const stepLabel = (step: WizardStep) => labelText(labels[`wizard.step.${step}`]);\n const index = STEPS.indexOf(state.step);\n const previous = index > 0 ? STEPS[index - 1] : null;\n const canGoBack = previous !== null && !submitting && wizard.canGoToStep(state, previous);\n\n return (\n <div className=\"space-y-3\">\n <div className=\"flex items-center gap-3\">\n {canGoBack ? (\n <button\n type=\"button\"\n aria-label={fill(labels['wizard.back'], { label: stepLabel(previous) })}\n onClick={() => onGo(previous)}\n className=\"flex size-9 shrink-0 items-center justify-center rounded-full border border-border text-foreground transition-colors hover:bg-muted\"\n >\n <ArrowLeft aria-hidden=\"true\" className=\"size-4\" />\n </button>\n ) : (\n // Held rather than dropped, so the title does not shuffle sideways.\n <span aria-hidden=\"true\" className=\"size-9 shrink-0\" />\n )}\n <p className=\"min-w-0\">\n <span className=\"block font-sans text-sm font-bold\">{labels['wizard.title']}</span>\n <span className=\"block truncate text-xs text-muted-foreground\">\n {fillParts(labels['wizard.progress'], {\n step: index + 1,\n total: STEPS.length,\n label: stepLabel(state.step),\n })}\n </span>\n </p>\n </div>\n\n <ol\n aria-label={labelText(labels['wizard.progressLabel'])}\n className=\"flex items-center gap-1.5\"\n >\n {STEPS.map((step, position) => {\n const isCurrent = step === state.step;\n const reachable = !submitting && !isCurrent && wizard.canGoToStep(state, step);\n return (\n <li key={step} className=\"flex-1\">\n <button\n type=\"button\"\n disabled={!reachable}\n aria-current={isCurrent ? 'step' : undefined}\n onClick={() => onGo(step)}\n className={`block h-1.5 w-full rounded-full transition-colors ${\n position <= index ? 'bg-primary' : 'bg-border'\n } ${reachable ? 'cursor-pointer' : 'cursor-default'}`}\n >\n <span className=\"sr-only\">{stepLabel(step)}</span>\n </button>\n </li>\n );\n })}\n </ol>\n </div>\n );\n}\n\n/**\n * The openings could not be read — this site's failure, not a full day —\n * with the telephone as the rescue and a retry as the cheaper first try.\n */\nfunction SlotsUnavailable({ kit, onRetry }: { kit: BookingKit; onRetry: () => void }) {\n const { labels, config, format } = kit;\n const phone = config.contact.phone;\n return (\n <div className=\"space-y-3\">\n <p role=\"alert\" className=\"rounded-lg border border-border bg-card px-5 py-4 text-sm\">\n {labels['wizard.slotsUnavailable.lead']}\n {phone ? (\n <a\n href={format.telHref(phone)}\n aria-label={fill(labels['wizard.slotsUnavailable.callAria'], { phone })}\n className=\"font-semibold text-primary underline underline-offset-4\"\n >\n {fillParts(labels['wizard.slotsUnavailable.call'], { phone })}\n </a>\n ) : (\n labels['wizard.slotsUnavailable.callPlain']\n )}\n {labels['wizard.slotsUnavailable.suffix']}\n </p>\n <BookingButtonOutline onClick={onRetry}>{labels['wizard.retry']}</BookingButtonOutline>\n </div>\n );\n}\n\nfunction BookingButtonOutline({ onClick, children }: { onClick: () => void; children: ReactNode }) {\n return (\n <BookingButton variant=\"outline\" onClick={onClick}>\n {children}\n </BookingButton>\n );\n}\n"],"mappings":"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"}
@@ -4,7 +4,7 @@ import { BookingOverrides } from "./Provider.mjs";
4
4
  import { VippsConfirm } from "@medalsocial/meda/booking";
5
5
  //#region src/react/LoginSheet.d.ts
6
6
  /**
7
- * The site's one login — Vipps, or a six-digit code by e-mail — in the two
7
+ * The site's one login — Vipps, or a six-digit code by e-mail — in the three
8
8
  * places a visitor takes it, over meda's `LoginSheet` / `LoginPanel`.
9
9
  *
10
10
  * - `presentation="sheet"` is the booking wizard's: an offer the wizard never
@@ -14,6 +14,16 @@ import { VippsConfirm } from "@medalsocial/meda/booking";
14
14
  * `router.replace` to `returnPath` or the portal — the destination is a
15
15
  * server page that reads the fresh cookie on the way in, and Back should not
16
16
  * land on a login whose job is done.
17
+ * - `presentation="gate"` is the wizard's «Bekreft» under `account.required`:
18
+ * the panel in place of the form, never navigating. Vipps lands on the
19
+ * wizard's resume URL; a good code hands the parent back through
20
+ * `onSignedIn`, like the sheet.
21
+ *
22
+ * Where Vipps is offered, the page and the gate draw e-mail as a second button
23
+ * under it («Fortsett med Vipps» / «Fortsett med e-post») that opens the form.
24
+ * The sheet keeps its form open under Vipps: meda's `LoginSheet` does not take
25
+ * `emailCollapsed` (and under `account.required` the wizard draws no sheet but
26
+ * a Vipps confirm, which opens on the code).
17
27
  *
18
28
  * THE CODE IS CHECKED BY `fetch`, NOT BY A SERVER ACTION
19
29
  * (`POST <portalApi>/login/verify`): in Next 16 an action that sets a cookie
@@ -37,6 +47,14 @@ export type LoginSheetProps = BookingOverrides & {
37
47
  vippsConfirm?: VippsConfirm | null;
38
48
  /** Draw the «have an account? Log in» row. Default `true`. */
39
49
  trigger?: boolean;
50
+ } | {
51
+ presentation: 'gate';
52
+ /** Where a Vipps login should land: the wizard's resume URL. */
53
+ resumePath: string;
54
+ /** A good code; `guardian` is `null` when the read after it failed. */
55
+ onSignedIn: (guardian: BookingGuardian | null) => void;
56
+ /** A Vipps return asking for the e-mailed code: the gate opens on it. */
57
+ vippsConfirm?: VippsConfirm | null;
40
58
  } | {
41
59
  presentation: 'inline';
42
60
  /** Already through `createReturnPath(config)`; `null` is the portal. */
@@ -1,4 +1,5 @@
1
1
  'use client';
2
+ import { labelText } from "../core/labels.mjs";
2
3
  import { readStartLogin } from "./actions.mjs";
3
4
  import { useBookingKit } from "./Provider.mjs";
4
5
  import { screenLabels } from "./screen-labels.mjs";
@@ -67,8 +68,21 @@ function LoginSheet$1(props) {
67
68
  otpClassNames: classNames.otp,
68
69
  vippsClassNames: classNames.vipps
69
70
  };
71
+ const vippsLabel = labelText(labels["login.continueVipps"]);
72
+ if (props.presentation === "gate") return /* @__PURE__ */ jsx(LoginPanel, {
73
+ ...screen,
74
+ emailCollapsed: true,
75
+ vippsLabel,
76
+ vippsNext: props.resumePath,
77
+ classNames: classNames.loginPanel,
78
+ onSignedIn: props.onSignedIn
79
+ });
70
80
  if (props.presentation === "inline") return /* @__PURE__ */ jsx(InlineLogin, {
71
- screen,
81
+ screen: {
82
+ ...screen,
83
+ emailCollapsed: true,
84
+ vippsLabel
85
+ },
72
86
  classNames: classNames.loginPanel,
73
87
  target: props.returnPath ?? null,
74
88
  fallback: config.paths.portal ?? "/"
@@ -1 +1 @@
1
- {"version":3,"file":"LoginSheet.mjs","names":["LoginSheet","SheetScreen"],"sources":["../../src/react/LoginSheet.tsx"],"sourcesContent":["import {\n LoginPanel,\n type LoginPanelProps,\n type LoginStartResult,\n type LoginVerifyResult,\n LoginSheet as SheetScreen,\n type VippsConfirm,\n} from '@medalsocial/meda/booking';\nimport { useRouter } from 'next/navigation';\nimport type { BookingGuardian } from '../core/types';\nimport { type PortalActions, readStartLogin } from './actions';\nimport { type BookingOverrides, useBookingKit } from './Provider';\nimport { screenLabels } from './screen-labels';\n\nexport type { VippsConfirm };\n\n/**\n * The site's one login — Vipps, or a six-digit code by e-mail — in the two\n * places a visitor takes it, over meda's `LoginSheet` / `LoginPanel`.\n *\n * - `presentation=\"sheet\"` is the booking wizard's: an offer the wizard never\n * waits on, that never navigates. A good code hands the visitor back\n * through `onSignedIn` with the `guardian` the verify route answers with.\n * - `presentation=\"inline\"` is the login page's. A good code ends in ONE\n * `router.replace` to `returnPath` or the portal — the destination is a\n * server page that reads the fresh cookie on the way in, and Back should not\n * land on a login whose job is done.\n *\n * THE CODE IS CHECKED BY `fetch`, NOT BY A SERVER ACTION\n * (`POST <portalApi>/login/verify`): in Next 16 an action that sets a cookie\n * makes the client re-fetch the route it was called from — the whole booking\n * page behind the sheet, or a second render racing the `replace`. Asking for\n * the code stays the app's action (`actions.startLogin`): it sets no cookie.\n *\n * Vipps is offered when the site has it (`config.portal.methods`) and the app\n * passed its start action.\n */\nexport type LoginSheetProps = BookingOverrides & {\n /** The app's server actions (Decision 8). */\n actions: Pick<PortalActions, 'startLogin' | 'startVipps'>;\n} & (\n | {\n presentation: 'sheet';\n /** Where a Vipps login should land: the wizard's resume URL. */\n resumePath: string;\n /** A good code; `guardian` is `null` when the read after it failed. */\n onSignedIn: (guardian: BookingGuardian | null) => void;\n /** A Vipps return asking for the e-mailed code: the sheet opens on it. */\n vippsConfirm?: VippsConfirm | null;\n /** Draw the «have an account? Log in» row. Default `true`. */\n trigger?: boolean;\n }\n | {\n presentation: 'inline';\n /** Already through `createReturnPath(config)`; `null` is the portal. */\n returnPath?: string | null;\n vippsConfirm?: VippsConfirm | null;\n }\n );\n\n/**\n * Ask a verify route, and say what its answer means for the form. The\n * route's own reasons are the form's; anything else (a 403, a body that is\n * not the route's, the network failing) is `unreachable`.\n */\nasync function askVerify(\n path: string,\n payload: Record<string, string>\n): Promise<LoginVerifyResult> {\n let body: unknown;\n try {\n const response = await fetch(path, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n cache: 'no-store',\n credentials: 'same-origin',\n });\n body = await response.json();\n } catch {\n return { ok: false, reason: 'unreachable' };\n }\n const answer = body as {\n ok?: unknown;\n guardian?: BookingGuardian | null;\n reason?: unknown;\n } | null;\n if (answer?.ok === true) return { ok: true, guardian: answer.guardian ?? null };\n const reason = answer?.reason;\n return reason === 'invalid' || reason === 'throttled' || reason === 'conflict'\n ? { ok: false, reason }\n : { ok: false, reason: 'unreachable' };\n}\n\nexport function LoginSheet(props: LoginSheetProps) {\n const { kit, classNames } = useBookingKit(props);\n const { config, labels } = kit;\n const { actions } = props;\n const api = config.paths.portalApi;\n const screen = {\n labels: screenLabels(labels),\n // «Sent» for every address; only a failed action is not (so the form is\n // never an oracle for who books here, and never claims a code it did not send).\n onStartLogin: async (email: string): Promise<LoginStartResult> => {\n try {\n return readStartLogin(await actions.startLogin({ email }));\n } catch {\n return { ok: false, reason: 'unreachable' };\n }\n },\n // The Vipps confirm code is checked by the code alone: the pending link is\n // in an httpOnly cookie the route reads.\n onVerify: (code: string, context: { mode: 'email' | 'vipps'; email: string }) =>\n context.mode === 'vipps'\n ? askVerify(`${api}/vipps/link/verify`, { code })\n : askVerify(`${api}/login/verify`, { email: context.email, code }),\n onVipps: config.portal.methods.includes('vipps') ? actions.startVipps : undefined,\n vippsConfirm: props.vippsConfirm ?? null,\n otpClassNames: classNames.otp,\n vippsClassNames: classNames.vipps,\n };\n if (props.presentation === 'inline') {\n return (\n <InlineLogin\n screen={screen}\n classNames={classNames.loginPanel}\n target={props.returnPath ?? null}\n fallback={config.paths.portal ?? '/'}\n />\n );\n }\n return (\n <SheetScreen\n {...screen}\n resumePath={props.resumePath}\n onSignedIn={props.onSignedIn}\n trigger={props.trigger}\n classNames={classNames.loginSheet}\n panelClassNames={classNames.loginPanel}\n />\n );\n}\n\n/** Its own component so only the page login reads the router. */\nfunction InlineLogin({\n screen,\n classNames,\n target,\n fallback,\n}: {\n screen: Omit<LoginPanelProps, 'onSignedIn'>;\n classNames: LoginPanelProps['classNames'];\n target: string | null;\n fallback: string;\n}) {\n const router = useRouter();\n return (\n <LoginPanel\n {...screen}\n classNames={classNames}\n vippsNext={target}\n onSignedIn={() => router.replace(target ?? fallback)}\n />\n );\n}\n"],"mappings":";;;;;;;;;;;;;AAiEA,eAAe,UACb,MACA,SAC4B;CAC5B,IAAI;CACJ,IAAI;EAQF,OAAO,OAAM,MAPU,MAAM,MAAM;GACjC,QAAQ;GACR,SAAS,EAAE,gBAAgB,mBAAmB;GAC9C,MAAM,KAAK,UAAU,OAAO;GAC5B,OAAO;GACP,aAAa;EACf,CAAC,EAAA,CACqB,KAAK;CAC7B,QAAQ;EACN,OAAO;GAAE,IAAI;GAAO,QAAQ;EAAc;CAC5C;CACA,MAAM,SAAS;CAKf,IAAI,QAAQ,OAAO,MAAM,OAAO;EAAE,IAAI;EAAM,UAAU,OAAO,YAAY;CAAK;CAC9E,MAAM,SAAS,QAAQ;CACvB,OAAO,WAAW,aAAa,WAAW,eAAe,WAAW,aAChE;EAAE,IAAI;EAAO;CAAO,IACpB;EAAE,IAAI;EAAO,QAAQ;CAAc;AACzC;AAEA,SAAgBA,aAAW,OAAwB;CACjD,MAAM,EAAE,KAAK,eAAe,cAAc,KAAK;CAC/C,MAAM,EAAE,QAAQ,WAAW;CAC3B,MAAM,EAAE,YAAY;CACpB,MAAM,MAAM,OAAO,MAAM;CACzB,MAAM,SAAS;EACb,QAAQ,aAAa,MAAM;EAG3B,cAAc,OAAO,UAA6C;GAChE,IAAI;IACF,OAAO,eAAe,MAAM,QAAQ,WAAW,EAAE,MAAM,CAAC,CAAC;GAC3D,QAAQ;IACN,OAAO;KAAE,IAAI;KAAO,QAAQ;IAAc;GAC5C;EACF;EAGA,WAAW,MAAc,YACvB,QAAQ,SAAS,UACb,UAAU,GAAG,IAAI,qBAAqB,EAAE,KAAK,CAAC,IAC9C,UAAU,GAAG,IAAI,gBAAgB;GAAE,OAAO,QAAQ;GAAO;EAAK,CAAC;EACrE,SAAS,OAAO,OAAO,QAAQ,SAAS,OAAO,IAAI,QAAQ,aAAa,KAAA;EACxE,cAAc,MAAM,gBAAgB;EACpC,eAAe,WAAW;EAC1B,iBAAiB,WAAW;CAC9B;CACA,IAAI,MAAM,iBAAiB,UACzB,OACE,oBAAC,aAAD;EACU;EACR,YAAY,WAAW;EACvB,QAAQ,MAAM,cAAc;EAC5B,UAAU,OAAO,MAAM,UAAU;CAClC,CAAA;CAGL,OACE,oBAACC,YAAD;EACE,GAAI;EACJ,YAAY,MAAM;EAClB,YAAY,MAAM;EAClB,SAAS,MAAM;EACf,YAAY,WAAW;EACvB,iBAAiB,WAAW;CAC7B,CAAA;AAEL;;AAGA,SAAS,YAAY,EACnB,QACA,YACA,QACA,YAMC;CACD,MAAM,SAAS,UAAU;CACzB,OACE,oBAAC,YAAD;EACE,GAAI;EACQ;EACZ,WAAW;EACX,kBAAkB,OAAO,QAAQ,UAAU,QAAQ;CACpD,CAAA;AAEL"}
1
+ {"version":3,"file":"LoginSheet.mjs","names":["LoginSheet","SheetScreen"],"sources":["../../src/react/LoginSheet.tsx"],"sourcesContent":["import {\n LoginPanel,\n type LoginPanelProps,\n type LoginStartResult,\n type LoginVerifyResult,\n LoginSheet as SheetScreen,\n type VippsConfirm,\n} from '@medalsocial/meda/booking';\nimport { useRouter } from 'next/navigation';\nimport { labelText } from '../core/labels';\nimport type { BookingGuardian } from '../core/types';\nimport { type PortalActions, readStartLogin } from './actions';\nimport { type BookingOverrides, useBookingKit } from './Provider';\nimport { screenLabels } from './screen-labels';\n\nexport type { VippsConfirm };\n\n/**\n * The site's one login — Vipps, or a six-digit code by e-mail — in the three\n * places a visitor takes it, over meda's `LoginSheet` / `LoginPanel`.\n *\n * - `presentation=\"sheet\"` is the booking wizard's: an offer the wizard never\n * waits on, that never navigates. A good code hands the visitor back\n * through `onSignedIn` with the `guardian` the verify route answers with.\n * - `presentation=\"inline\"` is the login page's. A good code ends in ONE\n * `router.replace` to `returnPath` or the portal — the destination is a\n * server page that reads the fresh cookie on the way in, and Back should not\n * land on a login whose job is done.\n * - `presentation=\"gate\"` is the wizard's «Bekreft» under `account.required`:\n * the panel in place of the form, never navigating. Vipps lands on the\n * wizard's resume URL; a good code hands the parent back through\n * `onSignedIn`, like the sheet.\n *\n * Where Vipps is offered, the page and the gate draw e-mail as a second button\n * under it («Fortsett med Vipps» / «Fortsett med e-post») that opens the form.\n * The sheet keeps its form open under Vipps: meda's `LoginSheet` does not take\n * `emailCollapsed` (and under `account.required` the wizard draws no sheet but\n * a Vipps confirm, which opens on the code).\n *\n * THE CODE IS CHECKED BY `fetch`, NOT BY A SERVER ACTION\n * (`POST <portalApi>/login/verify`): in Next 16 an action that sets a cookie\n * makes the client re-fetch the route it was called from — the whole booking\n * page behind the sheet, or a second render racing the `replace`. Asking for\n * the code stays the app's action (`actions.startLogin`): it sets no cookie.\n *\n * Vipps is offered when the site has it (`config.portal.methods`) and the app\n * passed its start action.\n */\nexport type LoginSheetProps = BookingOverrides & {\n /** The app's server actions (Decision 8). */\n actions: Pick<PortalActions, 'startLogin' | 'startVipps'>;\n} & (\n | {\n presentation: 'sheet';\n /** Where a Vipps login should land: the wizard's resume URL. */\n resumePath: string;\n /** A good code; `guardian` is `null` when the read after it failed. */\n onSignedIn: (guardian: BookingGuardian | null) => void;\n /** A Vipps return asking for the e-mailed code: the sheet opens on it. */\n vippsConfirm?: VippsConfirm | null;\n /** Draw the «have an account? Log in» row. Default `true`. */\n trigger?: boolean;\n }\n | {\n presentation: 'gate';\n /** Where a Vipps login should land: the wizard's resume URL. */\n resumePath: string;\n /** A good code; `guardian` is `null` when the read after it failed. */\n onSignedIn: (guardian: BookingGuardian | null) => void;\n /** A Vipps return asking for the e-mailed code: the gate opens on it. */\n vippsConfirm?: VippsConfirm | null;\n }\n | {\n presentation: 'inline';\n /** Already through `createReturnPath(config)`; `null` is the portal. */\n returnPath?: string | null;\n vippsConfirm?: VippsConfirm | null;\n }\n );\n\n/**\n * Ask a verify route, and say what its answer means for the form. The\n * route's own reasons are the form's; anything else (a 403, a body that is\n * not the route's, the network failing) is `unreachable`.\n */\nasync function askVerify(\n path: string,\n payload: Record<string, string>\n): Promise<LoginVerifyResult> {\n let body: unknown;\n try {\n const response = await fetch(path, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n cache: 'no-store',\n credentials: 'same-origin',\n });\n body = await response.json();\n } catch {\n return { ok: false, reason: 'unreachable' };\n }\n const answer = body as {\n ok?: unknown;\n guardian?: BookingGuardian | null;\n reason?: unknown;\n } | null;\n if (answer?.ok === true) return { ok: true, guardian: answer.guardian ?? null };\n const reason = answer?.reason;\n return reason === 'invalid' || reason === 'throttled' || reason === 'conflict'\n ? { ok: false, reason }\n : { ok: false, reason: 'unreachable' };\n}\n\nexport function LoginSheet(props: LoginSheetProps) {\n const { kit, classNames } = useBookingKit(props);\n const { config, labels } = kit;\n const { actions } = props;\n const api = config.paths.portalApi;\n const screen = {\n labels: screenLabels(labels),\n // «Sent» for every address; only a failed action is not (so the form is\n // never an oracle for who books here, and never claims a code it did not send).\n onStartLogin: async (email: string): Promise<LoginStartResult> => {\n try {\n return readStartLogin(await actions.startLogin({ email }));\n } catch {\n return { ok: false, reason: 'unreachable' };\n }\n },\n // The Vipps confirm code is checked by the code alone: the pending link is\n // in an httpOnly cookie the route reads.\n onVerify: (code: string, context: { mode: 'email' | 'vipps'; email: string }) =>\n context.mode === 'vipps'\n ? askVerify(`${api}/vipps/link/verify`, { code })\n : askVerify(`${api}/login/verify`, { email: context.email, code }),\n onVipps: config.portal.methods.includes('vipps') ? actions.startVipps : undefined,\n vippsConfirm: props.vippsConfirm ?? null,\n otpClassNames: classNames.otp,\n vippsClassNames: classNames.vipps,\n };\n const vippsLabel = labelText(labels['login.continueVipps']);\n if (props.presentation === 'gate') {\n return (\n <LoginPanel\n {...screen}\n emailCollapsed\n vippsLabel={vippsLabel}\n vippsNext={props.resumePath}\n classNames={classNames.loginPanel}\n onSignedIn={props.onSignedIn}\n />\n );\n }\n if (props.presentation === 'inline') {\n return (\n <InlineLogin\n screen={{ ...screen, emailCollapsed: true, vippsLabel }}\n classNames={classNames.loginPanel}\n target={props.returnPath ?? null}\n fallback={config.paths.portal ?? '/'}\n />\n );\n }\n return (\n <SheetScreen\n {...screen}\n resumePath={props.resumePath}\n onSignedIn={props.onSignedIn}\n trigger={props.trigger}\n classNames={classNames.loginSheet}\n panelClassNames={classNames.loginPanel}\n />\n );\n}\n\n/** Its own component so only the page login reads the router. */\nfunction InlineLogin({\n screen,\n classNames,\n target,\n fallback,\n}: {\n screen: Omit<LoginPanelProps, 'onSignedIn'>;\n classNames: LoginPanelProps['classNames'];\n target: string | null;\n fallback: string;\n}) {\n const router = useRouter();\n return (\n <LoginPanel\n {...screen}\n classNames={classNames}\n vippsNext={target}\n onSignedIn={() => router.replace(target ?? fallback)}\n />\n );\n}\n"],"mappings":";;;;;;;;;;;;;;AAqFA,eAAe,UACb,MACA,SAC4B;CAC5B,IAAI;CACJ,IAAI;EAQF,OAAO,OAAM,MAPU,MAAM,MAAM;GACjC,QAAQ;GACR,SAAS,EAAE,gBAAgB,mBAAmB;GAC9C,MAAM,KAAK,UAAU,OAAO;GAC5B,OAAO;GACP,aAAa;EACf,CAAC,EAAA,CACqB,KAAK;CAC7B,QAAQ;EACN,OAAO;GAAE,IAAI;GAAO,QAAQ;EAAc;CAC5C;CACA,MAAM,SAAS;CAKf,IAAI,QAAQ,OAAO,MAAM,OAAO;EAAE,IAAI;EAAM,UAAU,OAAO,YAAY;CAAK;CAC9E,MAAM,SAAS,QAAQ;CACvB,OAAO,WAAW,aAAa,WAAW,eAAe,WAAW,aAChE;EAAE,IAAI;EAAO;CAAO,IACpB;EAAE,IAAI;EAAO,QAAQ;CAAc;AACzC;AAEA,SAAgBA,aAAW,OAAwB;CACjD,MAAM,EAAE,KAAK,eAAe,cAAc,KAAK;CAC/C,MAAM,EAAE,QAAQ,WAAW;CAC3B,MAAM,EAAE,YAAY;CACpB,MAAM,MAAM,OAAO,MAAM;CACzB,MAAM,SAAS;EACb,QAAQ,aAAa,MAAM;EAG3B,cAAc,OAAO,UAA6C;GAChE,IAAI;IACF,OAAO,eAAe,MAAM,QAAQ,WAAW,EAAE,MAAM,CAAC,CAAC;GAC3D,QAAQ;IACN,OAAO;KAAE,IAAI;KAAO,QAAQ;IAAc;GAC5C;EACF;EAGA,WAAW,MAAc,YACvB,QAAQ,SAAS,UACb,UAAU,GAAG,IAAI,qBAAqB,EAAE,KAAK,CAAC,IAC9C,UAAU,GAAG,IAAI,gBAAgB;GAAE,OAAO,QAAQ;GAAO;EAAK,CAAC;EACrE,SAAS,OAAO,OAAO,QAAQ,SAAS,OAAO,IAAI,QAAQ,aAAa,KAAA;EACxE,cAAc,MAAM,gBAAgB;EACpC,eAAe,WAAW;EAC1B,iBAAiB,WAAW;CAC9B;CACA,MAAM,aAAa,UAAU,OAAO,sBAAsB;CAC1D,IAAI,MAAM,iBAAiB,QACzB,OACE,oBAAC,YAAD;EACE,GAAI;EACJ,gBAAA;EACY;EACZ,WAAW,MAAM;EACjB,YAAY,WAAW;EACvB,YAAY,MAAM;CACnB,CAAA;CAGL,IAAI,MAAM,iBAAiB,UACzB,OACE,oBAAC,aAAD;EACE,QAAQ;GAAE,GAAG;GAAQ,gBAAgB;GAAM;EAAW;EACtD,YAAY,WAAW;EACvB,QAAQ,MAAM,cAAc;EAC5B,UAAU,OAAO,MAAM,UAAU;CAClC,CAAA;CAGL,OACE,oBAACC,YAAD;EACE,GAAI;EACJ,YAAY,MAAM;EAClB,YAAY,MAAM;EAClB,SAAS,MAAM;EACf,YAAY,WAAW;EACvB,iBAAiB,WAAW;CAC7B,CAAA;AAEL;;AAGA,SAAS,YAAY,EACnB,QACA,YACA,QACA,YAMC;CACD,MAAM,SAAS,UAAU;CACzB,OACE,oBAAC,YAAD;EACE,GAAI;EACQ;EACZ,WAAW;EACX,kBAAkB,OAAO,QAAQ,UAAU,QAAQ;CACpD,CAAA;AAEL"}
@@ -1,7 +1,7 @@
1
1
  import { BookingConfig } from "../core/config.mjs";
2
2
  import { BookingLabelsInput } from "./labels/index.mjs";
3
3
  import { BookingKit } from "./kit.mjs";
4
- import { AccountCardSlot, AddChildSheetSlot, BookingCardProps, BookingSkeletonSlot, ChildCardProps, ChildCardsSlot, ConfirmationSlot, DataControlsSlot, DayChipProps, DetailsScreenSlot, FamilyChipProps, FamilyEditorSlot, FamilyMemberCardProps, LoginPanelSlot, LoginSheetSlot, LogoutButtonSlot, ManageScreenSlot, OtpSlotsSlot, PartyLineProps, PersonCardProps, PortalShellSlot, PortalUnreachableSlot, ProfileFormSlot, RebookCardProps, RebookCardsSlot, ServiceCardProps, ServiceScreenSlot, SlotClassNames, StylistCardProps, StylistScreenSlot, SummaryBarSlot, TimeChipProps, TimeScreenSlot, UpcomingBookingsSlot, VippsButtonSlot, VippsLinkRowSlot, VisitHistorySlot, VisitRowProps, WhoScreenSlot } from "@medalsocial/meda/booking";
4
+ import { AccountCardSlot, AddChildSheetSlot, BookingCardProps, BookingRecapSlot, BookingSkeletonSlot, ChildCardProps, ChildCardsSlot, ConfirmationSlot, DataControlsSlot, DayChipProps, DetailsScreenSlot, FamilyChipProps, FamilyEditorSlot, FamilyMemberCardProps, LoginPanelSlot, LoginSheetSlot, LogoutButtonSlot, ManageScreenSlot, OtpSlotsSlot, PartyLineProps, PersonCardProps, PortalShellSlot, PortalUnreachableSlot, ProfileFormSlot, RebookCardProps, RebookCardsSlot, ServiceCardProps, ServiceScreenSlot, SlotClassNames, StylistCardProps, StylistScreenSlot, SummaryBarSlot, TimeChipProps, TimeScreenSlot, UpcomingBookingsSlot, VippsButtonSlot, VippsLinkRowSlot, VisitHistorySlot, VisitRowProps, WhoScreenSlot } from "@medalsocial/meda/booking";
5
5
  import { ComponentType, ReactNode } from "react";
6
6
  //#region src/react/Provider.d.ts
7
7
  /** Per-screen slot classes (see each meda screen's `…Slot` type for the slots). */
@@ -13,6 +13,7 @@ export interface BookingClassNames {
13
13
  stylist?: SlotClassNames<StylistScreenSlot>;
14
14
  time?: SlotClassNames<TimeScreenSlot>;
15
15
  details?: SlotClassNames<DetailsScreenSlot>;
16
+ recap?: SlotClassNames<BookingRecapSlot>;
16
17
  summary?: SlotClassNames<SummaryBarSlot>;
17
18
  confirmation?: SlotClassNames<ConfirmationSlot>;
18
19
  skeleton?: SlotClassNames<BookingSkeletonSlot>;