@medalsocial/meda 3.11.0 → 3.13.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.
@@ -83,10 +83,36 @@ export interface ConfirmationProps {
83
83
  /** The visit's total in minor units. */
84
84
  totalOre: number;
85
85
  address?: string | null;
86
- /** The calendar file (e.g. a `data:text/calendar` URL). `null` draws no link. */
86
+ /**
87
+ * The calendar link: a calendar file the site serves (`text/calendar`, which
88
+ * iOS and macOS open straight into «Add to Calendar»), a calendar's own
89
+ * add-event page (Google Calendar), or a `data:text/calendar` URL. `null`
90
+ * draws no link.
91
+ */
87
92
  calendarHref?: string | null;
88
- /** `download` filename for the calendar link. Required for a `data:` URL to work. */
93
+ /**
94
+ * `download` filename for the calendar link. Only a `data:` or `blob:` URL
95
+ * gets it: browsers refuse to navigate to one, so the attribute is what makes
96
+ * it do anything. On a real URL it would force a download, and an iPhone
97
+ * would save the file to Files instead of opening Calendar.
98
+ */
89
99
  calendarFileName?: string;
100
+ /**
101
+ * The calendar link is another site's page (Google Calendar's add-event
102
+ * form): it opens in a new tab. A calendar file — relative or absolute —
103
+ * opens in place, which is what lets iOS show «Add to Calendar».
104
+ */
105
+ calendarNewTab?: boolean;
106
+ /**
107
+ * A second way into a calendar, drawn as a small link: Google Calendar beside
108
+ * a calendar file, or the file beside Google Calendar. `newTab` as for
109
+ * `calendarNewTab`. `null` draws none.
110
+ */
111
+ calendarAlt?: {
112
+ href: string;
113
+ label: string;
114
+ newTab?: boolean;
115
+ } | null;
90
116
  /** Where all of the visitor's bookings are listed. `null` draws no link. */
91
117
  portalHref?: string | null;
92
118
  /** A way back to an empty wizard. Absent, the button is not drawn. */
@@ -109,4 +135,4 @@ export interface ConfirmationProps {
109
135
  classNames?: SlotClassNames<ConfirmationSlot>;
110
136
  components?: ConfirmationComponents;
111
137
  }
112
- export declare function Confirmation({ lines: rawLines, startTs, totalOre, address, calendarHref, calendarFileName, portalHref, onStartOver, now, layout, directionsHref, freeChangeUntilTs, format, labels, classNames, components, }: ConfirmationProps): import("react").JSX.Element;
138
+ export declare function Confirmation({ lines: rawLines, startTs, totalOre, address, calendarHref, calendarFileName, calendarAlt, calendarNewTab, portalHref, onStartOver, now, layout, directionsHref, freeChangeUntilTs, format, labels, classNames, components, }: ConfirmationProps): import("react").JSX.Element;
@@ -68,10 +68,22 @@ export function DefaultPartyLine({ line, text, manageLabel, manageAriaLabel, lin
68
68
  return (_jsxs("li", { className: "flex flex-wrap items-baseline gap-x-3", children: [_jsx("span", { className: "tabular-nums", children: text }), line.manageHref && (_jsx("a", { href: line.manageHref, "aria-label": manageAriaLabel, className: linkClassName ?? 'text-sm font-semibold text-primary underline underline-offset-4', children: manageLabel }))] }));
69
69
  }
70
70
  const SEPARATOR = ' · ';
71
- export function Confirmation({ lines: rawLines, startTs, totalOre, address = null, calendarHref = null, calendarFileName = 'booking.ics', portalHref = null, onStartOver, now = Date.now(), layout = 'card', directionsHref = null, freeChangeUntilTs = null, format, labels, classNames, components, }) {
71
+ /** `download` only where it is needed (see `calendarFileName`). */
72
+ function downloadFor(href, fileName) {
73
+ return /^(data|blob):/i.test(href) ? fileName : undefined;
74
+ }
75
+ /** A calendar link: the file in place, another calendar's page in a new tab. */
76
+ function calendarLinkProps(href, fileName, newTab = false) {
77
+ return {
78
+ href,
79
+ download: downloadFor(href, fileName),
80
+ ...(newTab ? { target: '_blank', rel: 'noopener' } : {}),
81
+ };
82
+ }
83
+ export function Confirmation({ lines: rawLines, startTs, totalOre, address = null, calendarHref = null, calendarFileName = 'booking.ics', calendarAlt = null, calendarNewTab = false, portalHref = null, onStartOver, now = Date.now(), layout = 'card', directionsHref = null, freeChangeUntilTs = null, format, labels, classNames, components, }) {
72
84
  const { clock } = format;
73
85
  if (layout === 'hero') {
74
- return (_jsx(HeroConfirmation, { lines: rawLines, startTs: startTs, totalOre: totalOre, address: address, calendarHref: calendarHref, calendarFileName: calendarFileName, portalHref: portalHref, onStartOver: onStartOver, now: now, directionsHref: directionsHref, freeChangeUntilTs: freeChangeUntilTs, format: format, labels: labels, classNames: classNames, components: components }));
86
+ return (_jsx(HeroConfirmation, { lines: rawLines, startTs: startTs, totalOre: totalOre, address: address, calendarHref: calendarHref, calendarFileName: calendarFileName, calendarAlt: calendarAlt, calendarNewTab: calendarNewTab, portalHref: portalHref, onStartOver: onStartOver, now: now, directionsHref: directionsHref, freeChangeUntilTs: freeChangeUntilTs, format: format, labels: labels, classNames: classNames, components: components }));
75
87
  }
76
88
  // Cleaned here as well as upstream: a restored confirmation carries whatever
77
89
  // name was resolved when it was stored.
@@ -96,7 +108,7 @@ export function Confirmation({ lines: rawLines, startTs, totalOre, address = nul
96
108
  // This line's own price, which for one person is the whole total.
97
109
  format.price(first.priceOre),
98
110
  ], SEPARATOR) })) : null;
99
- return (_jsxs("section", { "aria-labelledby": "booking-confirmed-heading", className: slotClass(classNames, 'root', 'space-y-6'), children: [_jsx("h2", { id: "booking-confirmed-heading", tabIndex: -1, className: slotClass(classNames, 'heading', 'font-sans text-2xl font-bold outline-none md:text-3xl'), children: labels['confirmation.heading'] }), lines.length > 1 ? (_jsx(PartyCard, { lines: lines, totalOre: totalOre, now: now, format: format, labels: labels, classNames: classNames, PartyLine: components?.PartyLine ?? DefaultPartyLine })) : (singleCard), address && (_jsx("p", { className: slotClass(classNames, 'address', 'text-sm text-muted-foreground'), children: address })), _jsxs("div", { className: "flex flex-wrap gap-4", children: [calendarHref && (_jsx("a", { href: calendarHref, download: calendarFileName, className: linkClass, children: labels['confirmation.calendar'] })), lines.length === 1 && first?.manageHref && (_jsx("a", { href: first.manageHref, className: linkClass, children: labels['confirmation.manage'] }))] }), portalHref && (_jsx("p", { className: "text-sm", children: _jsx("a", { href: portalHref, className: linkClass, children: labels['confirmation.portal'] }) })), onStartOver && (_jsx("div", { children: _jsx(BookingButton, { variant: "outline", className: slotClass(classNames, 'startOver'), onClick: onStartOver, children: labels['confirmation.startOver'] }) }))] }));
111
+ return (_jsxs("section", { "aria-labelledby": "booking-confirmed-heading", className: slotClass(classNames, 'root', 'space-y-6'), children: [_jsx("h2", { id: "booking-confirmed-heading", tabIndex: -1, className: slotClass(classNames, 'heading', 'font-sans text-2xl font-bold outline-none md:text-3xl'), children: labels['confirmation.heading'] }), lines.length > 1 ? (_jsx(PartyCard, { lines: lines, totalOre: totalOre, now: now, format: format, labels: labels, classNames: classNames, PartyLine: components?.PartyLine ?? DefaultPartyLine })) : (singleCard), address && (_jsx("p", { className: slotClass(classNames, 'address', 'text-sm text-muted-foreground'), children: address })), _jsxs("div", { className: "flex flex-wrap gap-4", children: [calendarHref && (_jsx("a", { ...calendarLinkProps(calendarHref, calendarFileName, calendarNewTab), className: linkClass, children: labels['confirmation.calendar'] })), calendarAlt && (_jsx("a", { ...calendarLinkProps(calendarAlt.href, calendarFileName, calendarAlt.newTab), className: linkClass, children: calendarAlt.label })), lines.length === 1 && first?.manageHref && (_jsx("a", { href: first.manageHref, className: linkClass, children: labels['confirmation.manage'] }))] }), portalHref && (_jsx("p", { className: "text-sm", children: _jsx("a", { href: portalHref, className: linkClass, children: labels['confirmation.portal'] }) })), onStartOver && (_jsx("div", { children: _jsx(BookingButton, { variant: "outline", className: slotClass(classNames, 'startOver'), onClick: onStartOver, children: labels['confirmation.startOver'] }) }))] }));
100
112
  }
101
113
  /**
102
114
  * The family card: one line per person in the order they sit down, each with
@@ -139,7 +151,7 @@ function visitMinutes(item) {
139
151
  * booked, and the two things a parent does next — put it in the calendar and
140
152
  * find the way — as buttons rather than links.
141
153
  */
142
- function HeroConfirmation({ lines: rawLines, startTs, totalOre, address, calendarHref, calendarFileName, portalHref, onStartOver, now, directionsHref, freeChangeUntilTs, format, labels, classNames, components, }) {
154
+ function HeroConfirmation({ lines: rawLines, startTs, totalOre, address, calendarHref, calendarFileName, calendarAlt, calendarNewTab, portalHref, onStartOver, now, directionsHref, freeChangeUntilTs, format, labels, classNames, components, }) {
143
155
  const { clock } = format;
144
156
  const lines = rawLines.map((line) => ({
145
157
  ...line,
@@ -201,11 +213,11 @@ function HeroConfirmation({ lines: rawLines, startTs, totalOre, address, calenda
201
213
  ? fillLabel(label('confirmation.leafTime'), {
202
214
  time: clock.formatTime(startTs),
203
215
  })
204
- : clock.formatTime(startTs) })] }), _jsx("h2", { id: "booking-confirmed-heading", tabIndex: -1, className: slotClass(classNames, 'heading', 'font-sans text-2xl font-bold outline-none md:text-3xl'), children: labels['confirmation.heading'] }), _jsx("p", { className: "sr-only", children: when }), label('confirmation.sent') && (_jsx("p", { className: "text-sm text-muted-foreground", children: renderLabel(label('confirmation.sent')) }))] }), plainLine, lines.length > 1 && (_jsx(PartyCard, { lines: lines, totalOre: totalOre, now: now, format: format, labels: labels, classNames: classNames, PartyLine: components?.PartyLine ?? DefaultPartyLine })), rows.length > 0 && (_jsx("dl", { className: slotClass(classNames, 'card', 'divide-y divide-border rounded-lg border border-border bg-card px-5'), children: rows.map((row) => (_jsxs("div", { className: "flex items-center gap-4 py-3 text-sm", children: [_jsx("dt", { className: "w-16 shrink-0 text-muted-foreground", children: renderLabel(row.name) }), _jsx("dd", { className: "min-w-0 font-medium", children: row.value })] }, labelText(row.name)))) })), (calendarHref || (directionsHref && label('confirmation.directions'))) && (_jsxs("div", { className: "grid grid-cols-2 gap-2", children: [calendarHref && (_jsxs("a", { href: calendarHref, download: calendarFileName, className: bookingButtonClass({ size: 'lg', className: 'gap-2' }), children: [_jsx(CalendarPlus, { "aria-hidden": "true", className: "size-4" }), labels['confirmation.calendar']] })), directionsHref && label('confirmation.directions') && (_jsxs("a", { href: directionsHref, target: "_blank", rel: "noopener", className: bookingButtonClass({
216
+ : clock.formatTime(startTs) })] }), _jsx("h2", { id: "booking-confirmed-heading", tabIndex: -1, className: slotClass(classNames, 'heading', 'font-sans text-2xl font-bold outline-none md:text-3xl'), children: labels['confirmation.heading'] }), _jsx("p", { className: "sr-only", children: when }), label('confirmation.sent') && (_jsx("p", { className: "text-sm text-muted-foreground", children: renderLabel(label('confirmation.sent')) }))] }), plainLine, lines.length > 1 && (_jsx(PartyCard, { lines: lines, totalOre: totalOre, now: now, format: format, labels: labels, classNames: classNames, PartyLine: components?.PartyLine ?? DefaultPartyLine })), rows.length > 0 && (_jsx("dl", { className: slotClass(classNames, 'card', 'divide-y divide-border rounded-lg border border-border bg-card px-5'), children: rows.map((row) => (_jsxs("div", { className: "flex items-center gap-4 py-3 text-sm", children: [_jsx("dt", { className: "w-16 shrink-0 text-muted-foreground", children: renderLabel(row.name) }), _jsx("dd", { className: "min-w-0 font-medium", children: row.value })] }, labelText(row.name)))) })), (calendarHref || (directionsHref && label('confirmation.directions'))) && (_jsxs("div", { className: "grid grid-cols-2 gap-2", children: [calendarHref && (_jsxs("a", { ...calendarLinkProps(calendarHref, calendarFileName, calendarNewTab), className: bookingButtonClass({ size: 'lg', className: 'gap-2' }), children: [_jsx(CalendarPlus, { "aria-hidden": "true", className: "size-4" }), labels['confirmation.calendar']] })), directionsHref && label('confirmation.directions') && (_jsxs("a", { href: directionsHref, target: "_blank", rel: "noopener", className: bookingButtonClass({
205
217
  variant: 'outline',
206
218
  size: 'lg',
207
219
  className: cn('gap-2', !calendarHref && 'col-span-2'),
208
- }), children: [_jsx(MapPin, { "aria-hidden": "true", className: "size-4" }), renderLabel(label('confirmation.directions'))] }))] })), label('confirmation.tip.heading') && label('confirmation.tip.body') && (_jsxs("div", { className: "rounded-lg bg-muted px-5 py-4 text-sm", children: [_jsx("p", { className: "font-semibold", children: renderLabel(label('confirmation.tip.heading')) }), _jsx("p", { className: "text-muted-foreground", children: renderLabel(label('confirmation.tip.body')) })] })), lines.length === 1 && first?.manageHref && (_jsxs("p", { className: "text-center text-sm", children: [_jsx("a", { href: first.manageHref, className: linkClass, children: labels['confirmation.manage'] }), manageUntil && freeChangeUntilTs !== null && freeChangeUntilTs !== undefined && (_jsxs("span", { className: "text-muted-foreground", children: [SEPARATOR, fillLabel(manageUntil, {
220
+ }), children: [_jsx(MapPin, { "aria-hidden": "true", className: "size-4" }), renderLabel(label('confirmation.directions'))] }))] })), calendarAlt && (_jsx("p", { className: "-mt-2 text-center text-sm", children: _jsx("a", { ...calendarLinkProps(calendarAlt.href, calendarFileName, calendarAlt.newTab), className: linkClass, children: calendarAlt.label }) })), label('confirmation.tip.heading') && label('confirmation.tip.body') && (_jsxs("div", { className: "rounded-lg bg-muted px-5 py-4 text-sm", children: [_jsx("p", { className: "font-semibold", children: renderLabel(label('confirmation.tip.heading')) }), _jsx("p", { className: "text-muted-foreground", children: renderLabel(label('confirmation.tip.body')) })] })), lines.length === 1 && first?.manageHref && (_jsxs("p", { className: "text-center text-sm", children: [_jsx("a", { href: first.manageHref, className: linkClass, children: labels['confirmation.manage'] }), manageUntil && freeChangeUntilTs !== null && freeChangeUntilTs !== undefined && (_jsxs("span", { className: "text-muted-foreground", children: [SEPARATOR, fillLabel(manageUntil, {
209
221
  day: clock.dayLabel(freeChangeUntilTs, now),
210
222
  time: clock.formatTime(freeChangeUntilTs),
211
223
  })] }))] })), portalHref && (_jsx("p", { className: "text-center text-sm", children: _jsx("a", { href: portalHref, className: linkClass, children: labels['confirmation.portal'] }) })), onStartOver && (_jsx("div", { className: "flex justify-center", children: _jsx(BookingButton, { variant: "outline", className: slotClass(classNames, 'startOver'), onClick: onStartOver, children: labels['confirmation.startOver'] }) }))] }));
@@ -31,7 +31,7 @@ import { WHO_SCREEN_LABEL_KEYS, WHO_SCREEN_PARTY_LABEL_KEYS } from './who-screen
31
31
  * (Screens that embed another screen already include its keys, e.g. the
32
32
  * login sheet carries the login panel's, OTP slots' and Vipps button's.)
33
33
  */
34
- export declare const BOOKING_LABEL_KEYS: readonly ("addChild.trigger" | "addChild.title" | "addChild.description.saved" | "addChild.description.local" | "addChild.name" | "addChild.birthYear" | "addChild.birthYearPlaceholder" | "addChild.birthMonth" | "addChild.birthMonthUnknown" | "addChild.notes" | "addChild.missing" | "addChild.submit" | "addChild.close" | "panel.heading" | "panel.who" | "panel.service" | "panel.time" | "panel.stylist" | "panel.notChosen" | "panel.firstAvailable" | "panel.alsoFree" | "panel.swap" | "panel.swapTo" | "panel.total" | "panel.continue" | "recap.label" | "recap.time" | "recap.line" | "recap.lineFor" | "recap.anyStylist" | "recap.total" | "recap.edit" | "recap.editLabel" | "recap.alsoFree" | "recap.swapTo" | "bookingSkeleton.opening" | "confirmation.heading" | "confirmation.serviceFor" | "confirmation.when" | "confirmation.party.heading" | "confirmation.party.line" | "confirmation.party.stylist" | "confirmation.party.total" | "confirmation.calendar" | "confirmation.manage" | "confirmation.manageFor" | "confirmation.portal" | "confirmation.startOver" | "confirmation.sent" | "confirmation.leafTime" | "confirmation.detail.service" | "confirmation.detail.stylist" | "confirmation.detail.where" | "confirmation.duration" | "confirmation.directions" | "confirmation.tip.heading" | "confirmation.tip.body" | "confirmation.manageUntil" | "details.heading" | "details.phone.label" | "details.phone.prefix" | "details.phone.error" | "details.name.label" | "details.known.label" | "details.known.for" | "details.known.self" | "details.known.adult" | "details.known.child" | "details.child.name" | "details.child.year" | "details.child.yearPlaceholder" | "details.email.label" | "details.email.help" | "details.email.lockedHelp" | "details.email.error" | "details.notes.label" | "details.notes.placeholder" | "details.terms.text" | "details.terms.link" | "details.marketing.text" | "details.error.maxParty" | "details.error.slotTaken" | "details.error.conflict" | "details.error.invalidInput" | "details.error.unconfigured" | "details.error.upstreamError" | "details.error.inProgress" | "details.call.link" | "details.call.none" | "details.submit" | "otp.label" | "vipps.button" | "vipps.unavailable" | "vipps.throttled" | "login.heading" | "login.codeHeading" | "login.or" | "login.continueEmail" | "login.emailIntro" | "login.emailLabel" | "login.sendCode" | "login.codeLabel" | "login.submitCode" | "login.codeHelp" | "login.codeHelpVipps" | "login.vippsSentTo" | "login.vippsSentToUnknown" | "login.cooldownWait" | "login.cooldownReady" | "login.resendIn" | "login.resend" | "login.switchEmail" | "login.useEmailInstead" | "login.notice.sent" | "login.notice.resent" | "login.notice.unreachable" | "login.notice.badEmail" | "login.notice.invalid" | "login.notice.throttled" | "login.notice.vippsInvalid" | "login.notice.vippsConflict" | "loginSheet.prompt" | "loginSheet.trigger" | "loginSheet.intro" | "loginSheet.vipps" | "loginSheet.close" | "time.heading" | "time.dayStrip.legend" | "time.nearest" | "time.current" | "time.nextFree" | "time.shortNotice" | "time.weekend.pct" | "time.weekend.mixed" | "time.month.show" | "time.month.hide" | "time.month.previous" | "time.month.next" | "time.month.heading" | "time.month.note" | "time.party.sequential" | "time.party.parallel" | "time.party.seat" | "time.party.seatSeparator" | "time.parallel.offer.two" | "time.parallel.offer.other" | "time.parallel.accept" | "time.empty.closed" | "time.empty.over" | "time.empty.full" | "time.empty.stylist" | "time.empty.unknown" | "time.empty.callLink" | "time.empty.callLinkAria" | "time.empty.callSuffix" | "time.empty.callPlain" | "time.loading" | "time.skeleton.dayPlaceholder" | "time.taken.message" | "time.taken.close" | "time.soonest.heading" | "time.soonest.pick" | "time.soonest.with" | "time.dayChip.free" | "time.dayChip.freeOne" | "time.dayChip.full" | "time.week.none" | "time.week.more" | "manage.heading" | "manage.serviceFallback" | "manage.card.named" | "manage.card.when" | "manage.card.whenWithStylist" | "manage.appointment.named" | "manage.appointment.namedSibilant" | "manage.appointment.unnamed" | "manage.ringUs" | "manage.ringUsOn" | "manage.callUs" | "manage.callUsOn" | "manage.error.windowPassed" | "manage.error.slotTaken" | "manage.error.notFound" | "manage.error.conflict" | "manage.error.invalidInput" | "manage.error.unavailable" | "manage.freeUntil" | "manage.remaining.in" | "manage.remaining.daysAndHours" | "manage.remaining.underMinute" | "manage.remaining.day.one" | "manage.remaining.day.other" | "manage.remaining.hour.one" | "manage.remaining.hour.other" | "manage.remaining.minute.one" | "manage.remaining.minute.other" | "manage.change" | "manage.cancel" | "manage.callToChange" | "manage.callToCancel" | "manage.blocked" | "manage.blocked.suffix" | "manage.blocked.plain" | "manage.calendar" | "manage.closed.cancelled" | "manage.closed.completed" | "manage.closed.noShow" | "manage.closed.pending" | "manage.closed.pendingCall" | "manage.closed.pendingCallPlain" | "manage.bookNew" | "manage.party.heading" | "manage.party.justThisOne" | "manage.party.unnamed" | "manage.party.note" | "manage.party.noteSuffix" | "manage.back" | "manage.reschedule.lead" | "manage.reschedule.leadWithStylist" | "manage.reschedule.failed" | "manage.reschedule.failedSuffix" | "manage.reschedule.loading" | "manage.move.question" | "manage.move.questionWithStylist" | "manage.move.surcharge" | "manage.move.confirm" | "manage.move.pickAnother" | "manage.cancel.question" | "manage.cancel.reasonLegend" | "manage.cancel.reason.illness" | "manage.cancel.reason.noLongerSuits" | "manage.cancel.reason.other" | "manage.cancel.confirm" | "manage.cancel.keep" | "manage.cancelled" | "manage.cancelled.findNew" | "manage.moved" | "manage.movedTo" | "manage.moved.view" | "manage.moved.noLink" | "manage.moved.noLinkSuffix" | "manage.portal" | "childCards.empty" | "childCards.neverVisited" | "childCards.lastVisit" | "childCards.lastVisitNoService" | "childCards.book" | "childCards.stylist" | "childCards.next" | "childCards.none" | "dataControls.heading" | "dataControls.export" | "dataControls.delete" | "dataControls.confirmHeading" | "dataControls.consequences" | "dataControls.confirmLabel" | "dataControls.confirmHelp" | "dataControls.wrongWord" | "dataControls.deleteForever" | "dataControls.cancel" | "dataControls.unreachable" | "dataControls.confirmWord" | "ageConfirm.heading" | "ageConfirm.lead" | "ageConfirm.legend" | "ageConfirm.birthYear" | "ageConfirm.birthMonth" | "ageConfirm.birthMonthUnknown" | "ageConfirm.confirm" | "ageConfirm.dismiss" | "familyEditor.heading" | "familyEditor.lead" | "familyEditor.empty" | "familyEditor.legend" | "familyEditor.add" | "familyEditor.name" | "familyEditor.birthYear" | "familyEditor.birthYearPlaceholder" | "familyEditor.birthMonth" | "familyEditor.birthMonthUnknown" | "familyEditor.stylist" | "familyEditor.stylistNone" | "familyEditor.stylistFormer" | "familyEditor.notes" | "familyEditor.notesPlaceholder" | "familyEditor.newRow" | "familyEditor.save" | "familyEditor.remove" | "familyEditor.removeNamed" | "familyEditor.removeRow" | "familyEditor.saved" | "familyEditor.savedNoDetails" | "familyEditor.partial" | "familyEditor.removed" | "familyEditor.unreachable" | "familyEditor.ageConfirmed" | "familyEditor.ageSaved" | "logout.button" | "logout.unreachable" | "portalShell.nav" | "portalShell.navBar" | "portalUnreachable.heading" | "portalUnreachable.body" | "portalUnreachable.retry" | "portalUnreachable.call" | "profileForm.heading" | "profileForm.firstName" | "profileForm.lastName" | "profileForm.phone" | "profileForm.email" | "profileForm.emailHelp" | "profileForm.save" | "profileForm.saved" | "profileForm.restored" | "profileForm.marketing" | "profileForm.unreachable" | "rebook.heading" | "rebook.empty" | "rebook.serviceFallback" | "rebook.anyStylist" | "rebook.cta" | "upcoming.heading" | "upcoming.empty" | "upcoming.book" | "upcoming.next" | "upcoming.today" | "upcoming.tomorrow" | "upcoming.inDays" | "upcoming.when" | "upcoming.serviceFallback" | "upcoming.anyStylist" | "upcoming.manage" | "upcoming.noManage" | "upcoming.noManageWithPhone" | "upcoming.call" | "vippsLink.heading" | "vippsLink.linked" | "vippsLink.pitch" | "vippsLink.button" | "vippsLink.success" | "vippsLink.conflict" | "vippsLink.failed" | "vippsLink.unavailable" | "vippsLink.throttled" | "history.heading" | "history.empty" | "history.serviceFallback" | "history.count.one" | "history.count.other" | "history.unknownAmount" | "service.multiHint" | "service.total" | "service.chooseFirst" | "service.continue" | "service.party.nothingBookable" | "service.tabDone" | "service.heading" | "service.categoriesLegend" | "service.sameAsLast" | "service.chooseOther" | "service.duration" | "service.phoneOnly" | "service.phoneOnlyLinkLabel" | "service.ageDivider.named" | "service.ageDivider.unnamed" | "service.party.listLabel" | "service.party.adultAlone" | "service.party.nothingFits" | "service.party.remove" | "service.search" | "service.searchLabel" | "service.searchEmpty" | "stylist.heading" | "stylist.firstAvailable" | "stylist.firstAvailableSubtitle" | "stylist.firstAvailableBadge" | "stylist.pendingName" | "stylist.nextAvailable" | "stylist.loading" | "stylist.party.legend" | "stylist.party.sequential" | "stylist.party.sequentialMinutes" | "stylist.party.parallel.two" | "stylist.party.parallel.three" | "stylist.party.parallel.other" | "stylist.party.parallelMinutes" | "stylist.party.parallelNote.two" | "stylist.party.parallelNote.three" | "stylist.party.parallelNote.other" | "stylist.party.seatsHeading" | "stylist.party.seat" | "summary.placeholder.who" | "summary.placeholder.service" | "summary.next" | "summary.hint.who" | "summary.hint.service" | "summary.hint.when" | "who.heading" | "who.guest.groupLabel" | "who.guest.children.one" | "who.guest.children.other" | "who.guest.adult" | "who.guest.addedList" | "who.family.legend" | "who.family.self" | "who.family.limit" | "who.party.children" | "who.party.childrenNote" | "who.party.adult" | "who.party.adultNote" | "who.party.fewer" | "who.party.more" | "who.party.count" | "who.party.adultFewer" | "who.party.adultMore" | "who.party.adultCount" | "who.party.lead" | "who.party.togetherHint")[];
34
+ export declare const BOOKING_LABEL_KEYS: readonly ("addChild.trigger" | "addChild.title" | "addChild.description.saved" | "addChild.description.local" | "addChild.name" | "addChild.birthYear" | "addChild.birthYearPlaceholder" | "addChild.birthMonth" | "addChild.birthMonthUnknown" | "addChild.notes" | "addChild.missing" | "addChild.submit" | "addChild.close" | "panel.heading" | "panel.who" | "panel.service" | "panel.time" | "panel.stylist" | "panel.notChosen" | "panel.firstAvailable" | "panel.alsoFree" | "panel.swap" | "panel.swapTo" | "panel.total" | "panel.continue" | "recap.label" | "recap.time" | "recap.line" | "recap.lineFor" | "recap.anyStylist" | "recap.total" | "recap.edit" | "recap.editLabel" | "recap.alsoFree" | "recap.swapTo" | "bookingSkeleton.opening" | "confirmation.heading" | "confirmation.serviceFor" | "confirmation.when" | "confirmation.party.heading" | "confirmation.party.line" | "confirmation.party.stylist" | "confirmation.party.total" | "confirmation.calendar" | "confirmation.manage" | "confirmation.manageFor" | "confirmation.portal" | "confirmation.startOver" | "confirmation.sent" | "confirmation.leafTime" | "confirmation.detail.service" | "confirmation.detail.stylist" | "confirmation.detail.where" | "confirmation.duration" | "confirmation.directions" | "confirmation.tip.heading" | "confirmation.tip.body" | "confirmation.manageUntil" | "details.heading" | "details.phone.label" | "details.phone.prefix" | "details.phone.error" | "details.name.label" | "details.known.label" | "details.known.for" | "details.known.self" | "details.known.adult" | "details.known.child" | "details.child.name" | "details.child.year" | "details.child.yearPlaceholder" | "details.email.label" | "details.email.help" | "details.email.lockedHelp" | "details.email.error" | "details.notes.label" | "details.notes.placeholder" | "details.terms.text" | "details.terms.link" | "details.marketing.text" | "details.error.maxParty" | "details.error.slotTaken" | "details.error.conflict" | "details.error.invalidInput" | "details.error.unconfigured" | "details.error.upstreamError" | "details.error.inProgress" | "details.call.link" | "details.call.none" | "details.submit" | "otp.label" | "vipps.button" | "vipps.unavailable" | "vipps.throttled" | "login.heading" | "login.codeHeading" | "login.or" | "login.continueEmail" | "login.emailIntro" | "login.emailLabel" | "login.sendCode" | "login.codeLabel" | "login.submitCode" | "login.codeHelp" | "login.codeHelpVipps" | "login.vippsSentTo" | "login.vippsSentToUnknown" | "login.cooldownWait" | "login.cooldownReady" | "login.resendIn" | "login.resend" | "login.switchEmail" | "login.useEmailInstead" | "login.notice.sent" | "login.notice.resent" | "login.notice.unreachable" | "login.notice.badEmail" | "login.notice.invalid" | "login.notice.throttled" | "login.notice.vippsInvalid" | "login.notice.vippsConflict" | "loginSheet.prompt" | "loginSheet.trigger" | "loginSheet.intro" | "loginSheet.vipps" | "loginSheet.close" | "time.heading" | "time.dayStrip.legend" | "time.nearest" | "time.current" | "time.nextFree" | "time.shortNotice" | "time.weekend.pct" | "time.weekend.mixed" | "time.month.show" | "time.month.hide" | "time.month.previous" | "time.month.next" | "time.month.heading" | "time.month.note" | "time.party.sequential" | "time.party.parallel" | "time.party.seat" | "time.party.seatSeparator" | "time.parallel.offer.two" | "time.parallel.offer.other" | "time.parallel.accept" | "time.empty.closed" | "time.empty.over" | "time.empty.full" | "time.empty.stylist" | "time.empty.unknown" | "time.empty.callLink" | "time.empty.callLinkAria" | "time.empty.callSuffix" | "time.empty.callPlain" | "time.loading" | "time.skeleton.dayPlaceholder" | "time.taken.message" | "time.taken.close" | "time.soonest.heading" | "time.soonest.pick" | "time.soonest.with" | "time.dayChip.free" | "time.dayChip.freeOne" | "time.dayChip.full" | "time.week.none" | "time.week.more" | "manage.heading" | "manage.serviceFallback" | "manage.card.named" | "manage.card.when" | "manage.card.whenWithStylist" | "manage.appointment.named" | "manage.appointment.namedSibilant" | "manage.appointment.unnamed" | "manage.ringUs" | "manage.ringUsOn" | "manage.callUs" | "manage.callUsOn" | "manage.error.windowPassed" | "manage.error.slotTaken" | "manage.error.notFound" | "manage.error.conflict" | "manage.error.invalidInput" | "manage.error.unavailable" | "manage.freeUntil" | "manage.remaining.in" | "manage.remaining.daysAndHours" | "manage.remaining.underMinute" | "manage.remaining.day.one" | "manage.remaining.day.other" | "manage.remaining.hour.one" | "manage.remaining.hour.other" | "manage.remaining.minute.one" | "manage.remaining.minute.other" | "manage.change" | "manage.cancel" | "manage.callToChange" | "manage.callToCancel" | "manage.blocked" | "manage.blocked.suffix" | "manage.blocked.plain" | "manage.calendar" | "manage.closed.cancelled" | "manage.closed.completed" | "manage.closed.noShow" | "manage.closed.pending" | "manage.closed.pendingCall" | "manage.closed.pendingCallPlain" | "manage.bookNew" | "manage.party.heading" | "manage.party.justThisOne" | "manage.party.unnamed" | "manage.party.note" | "manage.party.noteSuffix" | "manage.back" | "manage.reschedule.lead" | "manage.reschedule.leadWithStylist" | "manage.reschedule.failed" | "manage.reschedule.failedSuffix" | "manage.reschedule.loading" | "manage.move.question" | "manage.move.questionWithStylist" | "manage.move.surcharge" | "manage.move.confirm" | "manage.move.pickAnother" | "manage.cancel.question" | "manage.cancel.reasonLegend" | "manage.cancel.reason.illness" | "manage.cancel.reason.noLongerSuits" | "manage.cancel.reason.other" | "manage.cancel.confirm" | "manage.cancel.keep" | "manage.cancelled" | "manage.cancelled.findNew" | "manage.moved" | "manage.movedTo" | "manage.moved.view" | "manage.moved.noLink" | "manage.moved.noLinkSuffix" | "manage.portal" | "childCards.empty" | "childCards.neverVisited" | "childCards.lastVisit" | "childCards.lastVisitNoService" | "childCards.book" | "childCards.stylist" | "childCards.next" | "childCards.none" | "dataControls.heading" | "dataControls.export" | "dataControls.delete" | "dataControls.confirmHeading" | "dataControls.consequences" | "dataControls.confirmLabel" | "dataControls.confirmHelp" | "dataControls.wrongWord" | "dataControls.deleteForever" | "dataControls.cancel" | "dataControls.unreachable" | "dataControls.confirmWord" | "ageConfirm.heading" | "ageConfirm.lead" | "ageConfirm.legend" | "ageConfirm.birthYear" | "ageConfirm.birthMonth" | "ageConfirm.birthMonthUnknown" | "ageConfirm.confirm" | "ageConfirm.dismiss" | "familyEditor.heading" | "familyEditor.lead" | "familyEditor.empty" | "familyEditor.legend" | "familyEditor.add" | "familyEditor.name" | "familyEditor.birthYear" | "familyEditor.birthYearPlaceholder" | "familyEditor.birthMonth" | "familyEditor.birthMonthUnknown" | "familyEditor.stylist" | "familyEditor.stylistNone" | "familyEditor.stylistFormer" | "familyEditor.notes" | "familyEditor.notesPlaceholder" | "familyEditor.newRow" | "familyEditor.save" | "familyEditor.remove" | "familyEditor.removeNamed" | "familyEditor.removeRow" | "familyEditor.saved" | "familyEditor.savedNoDetails" | "familyEditor.partial" | "familyEditor.removed" | "familyEditor.unreachable" | "familyEditor.ageConfirmed" | "familyEditor.ageSaved" | "logout.button" | "logout.unreachable" | "portalShell.nav" | "portalShell.navBar" | "portalUnreachable.heading" | "portalUnreachable.body" | "portalUnreachable.retry" | "portalUnreachable.call" | "profileForm.heading" | "profileForm.firstName" | "profileForm.lastName" | "profileForm.phone" | "profileForm.email" | "profileForm.emailHelp" | "profileForm.save" | "profileForm.saved" | "profileForm.restored" | "profileForm.marketing" | "profileForm.unreachable" | "rebook.heading" | "rebook.empty" | "rebook.serviceFallback" | "rebook.anyStylist" | "rebook.cta" | "upcoming.heading" | "upcoming.empty" | "upcoming.book" | "upcoming.next" | "upcoming.today" | "upcoming.tomorrow" | "upcoming.inDays" | "upcoming.when" | "upcoming.serviceFallback" | "upcoming.anyStylist" | "upcoming.manage" | "upcoming.noManage" | "upcoming.noManageWithPhone" | "upcoming.call" | "vippsLink.heading" | "vippsLink.linked" | "vippsLink.pitch" | "vippsLink.button" | "vippsLink.success" | "vippsLink.conflict" | "vippsLink.failed" | "vippsLink.unavailable" | "vippsLink.throttled" | "history.heading" | "history.empty" | "history.serviceFallback" | "history.count.one" | "history.count.other" | "history.unknownAmount" | "service.multiHint" | "service.total" | "service.chooseFirst" | "service.continue" | "service.party.nothingBookable" | "service.tabDone" | "service.heading" | "service.categoriesLegend" | "service.sameAsLast" | "service.chooseOther" | "service.duration" | "service.phoneOnly" | "service.phoneOnlyLinkLabel" | "service.ageDivider.named" | "service.ageDivider.unnamed" | "service.party.listLabel" | "service.party.adultAlone" | "service.party.nothingFits" | "service.party.remove" | "service.search" | "service.searchLabel" | "service.searchEmpty" | "stylist.heading" | "stylist.firstAvailable" | "stylist.firstAvailableSubtitle" | "stylist.firstAvailableBadge" | "stylist.pendingName" | "stylist.nextAvailable" | "stylist.loading" | "stylist.party.legend" | "stylist.party.sequential" | "stylist.party.sequentialMinutes" | "stylist.party.parallel.two" | "stylist.party.parallel.three" | "stylist.party.parallel.other" | "stylist.party.parallelMinutes" | "stylist.party.parallelNote.two" | "stylist.party.parallelNote.three" | "stylist.party.parallelNote.other" | "stylist.party.seatsHeading" | "stylist.party.seat" | "summary.placeholder.who" | "summary.placeholder.service" | "summary.next" | "summary.hint.who" | "summary.hint.service" | "summary.hint.when" | "who.heading" | "who.guest.groupLabel" | "who.guest.children.one" | "who.guest.children.other" | "who.guest.adult" | "who.guest.addedList" | "who.family.legend" | "who.family.self" | "who.family.limit" | "who.party.children" | "who.party.childrenNote" | "who.party.adult" | "who.party.adultNote" | "who.party.fewer" | "who.party.more" | "who.party.count" | "who.party.adultFewer" | "who.party.adultMore" | "who.party.adultCount" | "who.party.lead" | "who.party.togetherHint" | "who.family.adults")[];
35
35
  export type BookingLabelKey = BookingRequiredLabelKey | BookingOptionalLabelKey;
36
36
  /**
37
37
  * Keys added after a screen first shipped, for a feature a caller opts into.
@@ -41,7 +41,7 @@ export declare const WHO_SCREEN_LABEL_KEYS: readonly ["who.heading", "who.guest.
41
41
  * stepper buttons' accessible names; `who.party.count` `{count}` is read out
42
42
  * when the number changes.
43
43
  */
44
- export declare const WHO_SCREEN_PARTY_LABEL_KEYS: readonly ["who.party.children", "who.party.childrenNote", "who.party.adult", "who.party.adultNote", "who.party.fewer", "who.party.more", "who.party.count", "who.party.adultFewer", "who.party.adultMore", "who.party.adultCount", "who.party.lead", "who.party.togetherHint"];
44
+ export declare const WHO_SCREEN_PARTY_LABEL_KEYS: readonly ["who.party.children", "who.party.childrenNote", "who.party.adult", "who.party.adultNote", "who.party.fewer", "who.party.more", "who.party.count", "who.party.adultFewer", "who.party.adultMore", "who.party.adultCount", "who.party.lead", "who.party.togetherHint", "who.family.adults"];
45
45
  export type WhoScreenLabels = Record<(typeof WHO_SCREEN_LABEL_KEYS)[number], BookingLabel> & Partial<Record<(typeof WHO_SCREEN_PARTY_LABEL_KEYS)[number], BookingLabel>> & AddChildSheetLabels;
46
46
  /**
47
47
  * - `root` — the `<section>`
@@ -143,6 +143,11 @@ export interface WhoScreenProps {
143
143
  * rather than one check row: two parents, or a parent and a grandparent,
144
144
  * in one booking. Seat 1 must be `adult`. Needs the `who.party.adultFewer`,
145
145
  * `adultMore` and `adultCount` labels; without them the check row stays.
146
+ *
147
+ * The one part of `guestParty` a logged-in parent's list reads too: with
148
+ * `who.family.adults` as well, its «myself» row is the same stepper, seat 1
149
+ * being `selfKey` and seats 2, 3 these — so a second grown-up survives a
150
+ * login. `isGuestSeat` must then not claim seats 2 and up.
146
151
  */
147
152
  adults?: (seat: number) => WizardPerson & {
148
153
  adult: true;
@@ -82,6 +82,18 @@ export const WHO_SCREEN_PARTY_LABEL_KEYS = [
82
82
  'who.party.lead',
83
83
  /** Under the steppers while there are children and no grown-up. */
84
84
  'who.party.togetherHint',
85
+ /**
86
+ * A logged-in parent's grown-ups row, e.g. «Grown-ups», when
87
+ * `guestParty.adults` makes it a stepper: «myself» is seat 1, the rest are
88
+ * the guest's seats 2, 3. Without it the «myself» check row stays.
89
+ */
90
+ 'who.family.adults',
91
+ ];
92
+ /** The labels a «Voksen» stepper cannot draw without. */
93
+ const ADULT_STEPPER_LABEL_KEYS = [
94
+ 'who.party.adultFewer',
95
+ 'who.party.adultMore',
96
+ 'who.party.adultCount',
85
97
  ];
86
98
  /** The `who.party.*` keys `guestParty` cannot draw without; the two notes are extras. */
87
99
  const PARTY_REQUIRED_LABEL_KEYS = [
@@ -138,6 +150,27 @@ export function WhoScreen({ labels, format, people, family, guestChoices, addedC
138
150
  const full = seated.length >= maxPeople;
139
151
  const self = { key: selfKey, adult: true };
140
152
  const selfChecked = chosen.has(selfKey);
153
+ // «Voksne» as a number under a family too: «myself» first, then the guest's
154
+ // seats 2, 3 — a second grown-up has no profile card to tick.
155
+ const moreAdults = guestParty?.adults !== undefined &&
156
+ labels['who.family.adults'] !== undefined &&
157
+ ADULT_STEPPER_LABEL_KEYS.every((key) => labels[key] !== undefined)
158
+ ? guestParty.adults
159
+ : undefined;
160
+ const otherAdultKeys = new Set(moreAdults ? Array.from({ length: maxPeople - 1 }, (_, index) => moreAdults(index + 2).key) : []);
161
+ const isGrownUp = (person) => person.key === selfKey || otherAdultKeys.has(person.key);
162
+ const grownUps = seated.filter(isGrownUp).length;
163
+ const setGrownUps = (count) => {
164
+ if (!moreAdults)
165
+ return;
166
+ const seats = Array.from({ length: count }, (_, index) => index === 0 ? self : moreAdults(index + 1));
167
+ onChoose([...seated.filter((person) => !isGrownUp(person)), ...seats], false);
168
+ };
169
+ const partyText = (key, values = {}) => {
170
+ const label = labels[key];
171
+ return label ? fillLabel(label, values) : '';
172
+ };
173
+ const adultNote = labels['who.party.adultNote'];
141
174
  function toggle(person) {
142
175
  if (chosen.has(person.key)) {
143
176
  onChoose(seated.filter((other) => other.key !== person.key), false);
@@ -150,7 +183,7 @@ export function WhoScreen({ labels, format, people, family, guestChoices, addedC
150
183
  return (_jsxs("section", { "aria-labelledby": HEADING_ID, className: slotClass(classNames, 'root', 'space-y-6'), children: [heading, _jsxs("fieldset", { className: "space-y-4", children: [_jsx("legend", { className: "mb-2 text-sm text-muted-foreground", children: renderLabel(labels['who.family.legend'], { max: maxPeople }) }), _jsxs("ul", { className: "space-y-2", children: [family.map((entry, index) => {
151
184
  const selected = chosen.has(entry.person.key);
152
185
  return (_jsx("li", { children: _jsx(PersonCard, { name: entry.person.name ?? '', line: entry.line, index: index, selected: selected, disabled: !selected && full, describedBy: !selected && full ? LIMIT_ID : undefined, onToggle: () => toggle(entry.person), ...cardClasses }) }, entry.person.key));
153
- }), _jsx("li", { children: sheet(full, true) })] }), _jsxs(CheckRow, { checked: selfChecked, disabled: !selfChecked && full, describedBy: !selfChecked && full ? LIMIT_ID : undefined, onToggle: () => toggle(self), className: cn('h-14 justify-between px-5 text-sm', classNames?.card), selectedClassName: classNames?.cardSelected, children: [_jsx("span", { className: "font-medium", children: labels['who.family.self'] }), _jsx(CheckCircle, { checked: selfChecked })] }), _jsx("p", { id: LIMIT_ID, "aria-live": "polite", className: slotClass(classNames, 'limit', 'min-h-5 text-sm text-muted-foreground'), children: full ? renderLabel(labels['who.family.limit'], { max: maxPeople }) : '' })] })] }));
186
+ }), _jsx("li", { children: sheet(full, true) })] }), moreAdults ? (_jsx("div", { className: cn('rounded-lg border border-border bg-card', classNames?.card), children: _jsx(StepperRow, { title: partyText('who.family.adults'), note: adultNote ? renderLabel(adultNote) : null, value: grownUps, countLabel: partyText('who.party.adultCount', { count: grownUps }), fewerLabel: partyText('who.party.adultFewer'), moreLabel: partyText('who.party.adultMore'), canFewer: grownUps > 0, canMore: !full, limitId: full ? LIMIT_ID : undefined, onFewer: () => setGrownUps(grownUps - 1), onMore: () => setGrownUps(grownUps + 1) }) })) : (_jsxs(CheckRow, { checked: selfChecked, disabled: !selfChecked && full, describedBy: !selfChecked && full ? LIMIT_ID : undefined, onToggle: () => toggle(self), className: cn('h-14 justify-between px-5 text-sm', classNames?.card), selectedClassName: classNames?.cardSelected, children: [_jsx("span", { className: "font-medium", children: labels['who.family.self'] }), _jsx(CheckCircle, { checked: selfChecked })] })), _jsx("p", { id: LIMIT_ID, "aria-live": "polite", className: slotClass(classNames, 'limit', 'min-h-5 text-sm text-muted-foreground'), children: full ? renderLabel(labels['who.family.limit'], { max: maxPeople }) : '' })] })] }));
154
187
  }
155
188
  /**
156
189
  * A guest's party: how many children (a stepper) and whether the grown-up
@@ -161,9 +194,7 @@ export function WhoScreen({ labels, format, people, family, guestChoices, addedC
161
194
  */
162
195
  function GuestParty({ party, people, maxPeople, labels, classNames, onChoose, }) {
163
196
  const adultStepper = party.adults !== undefined &&
164
- labels['who.party.adultFewer'] !== undefined &&
165
- labels['who.party.adultMore'] !== undefined &&
166
- labels['who.party.adultCount'] !== undefined;
197
+ ADULT_STEPPER_LABEL_KEYS.every((key) => labels[key] !== undefined);
167
198
  const adultSeat = (seat) => seat === 1 || !party.adults ? party.adult : party.adults(seat);
168
199
  const adultKeys = new Set(Array.from({ length: maxPeople }, (_, index) => adultSeat(index + 1).key));
169
200
  const isAdultSeat = (person) => adultKeys.has(person.key);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@medalsocial/meda",
3
- "version": "3.11.0",
3
+ "version": "3.13.0",
4
4
  "description": "Shared Meda UI shell and runtime package.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {