@medalsocial/meda 3.7.0 → 3.8.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.
@@ -30,7 +30,7 @@ import { WHO_SCREEN_LABEL_KEYS, WHO_SCREEN_PARTY_LABEL_KEYS } from './who-screen
30
30
  * (Screens that embed another screen already include its keys, e.g. the
31
31
  * login sheet carries the login panel's, OTP slots' and Vipps button's.)
32
32
  */
33
- 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" | "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" | "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" | "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" | "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" | "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")[];
33
+ 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" | "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" | "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" | "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" | "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")[];
34
34
  export type BookingLabelKey = BookingRequiredLabelKey | BookingOptionalLabelKey;
35
35
  /**
36
36
  * Keys added after a screen first shipped, for a feature a caller opts into.
@@ -65,6 +65,8 @@ export interface StylistCardProps {
65
65
  selectedClassName?: string;
66
66
  /** Merged onto the next-opening line (the `nextAvailable` slot). */
67
67
  nextAvailableClassName?: string;
68
+ /** `StylistScreen`'s `row`: draw the phone's avatar tile at every width. */
69
+ row?: boolean;
68
70
  }
69
71
  export interface StylistScreenComponents {
70
72
  StylistCard?: ComponentType<StylistCardProps>;
@@ -116,6 +118,13 @@ export interface StylistScreenProps {
116
118
  * as «there is more», not as a layout bug.
117
119
  */
118
120
  edgeFade?: boolean;
121
+ /**
122
+ * Keep the phone's one row of avatars at every width instead of the column
123
+ * of cards from `md`, so a wide screen reaches the times without scrolling
124
+ * past every stylist. The chosen stylist's bio, which a tile has no room
125
+ * for, is shown under the row.
126
+ */
127
+ row?: boolean;
119
128
  /**
120
129
  * Present only for a family. One child gets no picker: «one after the
121
130
  * other» is not a choice when there is nobody to be after.
@@ -142,6 +151,6 @@ export interface StylistScreenProps {
142
151
  classNames?: SlotClassNames<StylistScreenSlot>;
143
152
  components?: StylistScreenComponents;
144
153
  }
145
- export declare function StylistScreen({ labels, format, now, serviceIds, resources, selectedResourceId, nextAvailableTs, nextAvailableLoading, loading, skeletonCount, pendingName, notice, onPick, firstAvailableFaces, edgeFade, party, classNames, components, }: StylistScreenProps): import("react").JSX.Element;
154
+ export declare function StylistScreen({ labels, format, now, serviceIds, resources, selectedResourceId, nextAvailableTs, nextAvailableLoading, loading, skeletonCount, pendingName, notice, onPick, firstAvailableFaces, edgeFade, row, party, classNames, components, }: StylistScreenProps): import("react").JSX.Element;
146
155
  /** The default stylist option: avatar (photo or badge), name(s), bio, next opening. */
147
- export declare function DefaultStylistCard({ ref, option, selected, tabIndex, onClick, labels, className, selectedClassName, nextAvailableClassName, }: StylistCardProps): import("react").JSX.Element;
156
+ export declare function DefaultStylistCard({ ref, option, selected, tabIndex, onClick, labels, className, selectedClassName, nextAvailableClassName, row, }: StylistCardProps): import("react").JSX.Element;
@@ -72,7 +72,7 @@ function bySize(labels, base, size, sizeWord) {
72
72
  const suffix = SIZE_WORDS[size] ?? 'other';
73
73
  return renderLabel(labels[`${base}.${suffix}`], { count: size, sizeWord: sizeWord ?? size });
74
74
  }
75
- export function StylistScreen({ labels, format, now, serviceIds, resources, selectedResourceId = null, nextAvailableTs, nextAvailableLoading = false, loading = false, skeletonCount = DEFAULT_SKELETON_COUNT, pendingName = null, notice = null, onPick, firstAvailableFaces = false, edgeFade = false, party, classNames, components, }) {
75
+ export function StylistScreen({ labels, format, now, serviceIds, resources, selectedResourceId = null, nextAvailableTs, nextAvailableLoading = false, loading = false, skeletonCount = DEFAULT_SKELETON_COUNT, pendingName = null, notice = null, onPick, firstAvailableFaces = false, edgeFade = false, row = false, party, classNames, components, }) {
76
76
  // `every`, not `some`: the step asks one question for the whole visit, so a
77
77
  // stylist who covers one of two services is not an answer — it is a refusal
78
78
  // the engine issues at submit, offered here as a choice.
@@ -130,7 +130,7 @@ export function StylistScreen({ labels, format, now, serviceIds, resources, sele
130
130
  }), classNames: classNames })] })), party?.mode === 'parallel' ? (_jsx("p", { className: "rounded-lg border border-border bg-card px-5 py-4 text-sm", children: bySize(labels, 'stylist.party.parallelNote', party.size, party.sizeWord) })) : (_jsx(StylistRadioGroup, { options: options, selectedResourceId: selectedResourceId,
131
131
  // The held option takes one placeholder's place, so the row is
132
132
  // exactly as long as it was and nothing after it moves.
133
- skeletons: loading ? Math.max(0, skeletonCount - (options.length - 1)) : 0, busy: loading, onPick: onPick, labels: labels, classNames: classNames, edgeFade: edgeFade, Card: components?.StylistCard ?? DefaultStylistCard })), _jsx(LiveStatus, { text: loading ? labelText(labels['stylist.loading']) : notice })] }));
133
+ skeletons: loading ? Math.max(0, skeletonCount - (options.length - 1)) : 0, busy: loading, onPick: onPick, labels: labels, classNames: classNames, edgeFade: edgeFade, row: row, Card: components?.StylistCard ?? DefaultStylistCard })), row && party?.mode !== 'parallel' && (_jsx(ChosenBio, { options: options, selectedResourceId: selectedResourceId })), _jsx(LiveStatus, { text: loading ? labelText(labels['stylist.loading']) : notice })] }));
134
134
  }
135
135
  /**
136
136
  * The options as one radiogroup with a roving tab stop.
@@ -139,7 +139,7 @@ export function StylistScreen({ labels, format, now, serviceIds, resources, sele
139
139
  * stylist is cheap and reversible, and a group that needed Space after every
140
140
  * arrow would be a list of buttons in a radiogroup's clothes.
141
141
  */
142
- function StylistRadioGroup({ options, selectedResourceId, skeletons, busy, onPick, labels, classNames, edgeFade, Card, }) {
142
+ function StylistRadioGroup({ options, selectedResourceId, skeletons, busy, onPick, labels, classNames, edgeFade, row, Card, }) {
143
143
  const groupRef = useRef(null);
144
144
  const optionRefs = useRef([]);
145
145
  const selectedIndex = options.findIndex((option) => option.resourceId === selectedResourceId);
@@ -196,9 +196,9 @@ function StylistRadioGroup({ options, selectedResourceId, skeletons, busy, onPic
196
196
  // The last few pixels fade, so the cut-off card reads as «more this way».
197
197
  // The trailing room lets the last card scroll wholly clear of the fade.
198
198
  edgeFade &&
199
- 'pr-10 [mask-image:linear-gradient(to_right,#000_calc(100%-2.5rem),transparent)] md:[mask-image:none]', 'md:mx-0 md:h-auto md:snap-none md:flex-col md:gap-2 md:overflow-visible md:p-0'), children: [options.map((option, index) => (_jsx(Card, { ref: (node) => {
199
+ cn('pr-10 [mask-image:linear-gradient(to_right,#000_calc(100%-2.5rem),transparent)]', !row && 'md:[mask-image:none]'), !row && 'md:mx-0 md:h-auto md:snap-none md:flex-col md:gap-2 md:overflow-visible md:p-0'), children: [options.map((option, index) => (_jsx(Card, { ref: (node) => {
200
200
  optionRefs.current[index] = node;
201
- }, option: option, selected: option.resourceId === selectedResourceId, tabIndex: index === tabStop ? 0 : -1, onClick: () => onPick(option.resourceId), labels: labels, className: classNames?.card, selectedClassName: classNames?.cardSelected, nextAvailableClassName: classNames?.nextAvailable }, option.resourceId ?? 'first-available'))), Array.from({ length: skeletons }, (_, index) => (_jsx(SkeletonCard, {}, index)))] }));
201
+ }, option: option, selected: option.resourceId === selectedResourceId, tabIndex: index === tabStop ? 0 : -1, onClick: () => onPick(option.resourceId), labels: labels, className: classNames?.card, selectedClassName: classNames?.cardSelected, nextAvailableClassName: classNames?.nextAvailable, row: row }, option.resourceId ?? 'first-available'))), Array.from({ length: skeletons }, (_, index) => (_jsx(SkeletonCard, { row: row }, index)))] }));
202
202
  }
203
203
  /** The restored stylist's stand-in: a real, checked radio of the final size,
204
204
  * with its next-opening row pulsing like every card still waiting for one. */
@@ -228,8 +228,10 @@ function ModeCard({ selected, onClick, title, subtitle, classNames, }) {
228
228
  : 'border-border bg-card hover:border-primary', classNames?.modeCard, selected && classNames?.modeCardSelected), children: [_jsx("span", { className: "font-medium", children: title }), _jsx("span", { className: "text-sm font-normal text-muted-foreground tabular-nums", children: subtitle })] }));
229
229
  }
230
230
  /** The default stylist option: avatar (photo or badge), name(s), bio, next opening. */
231
- export function DefaultStylistCard({ ref, option, selected, tabIndex, onClick, labels, className, selectedClassName, nextAvailableClassName, }) {
231
+ export function DefaultStylistCard({ ref, option, selected, tabIndex, onClick, labels, className, selectedClassName, nextAvailableClassName, row = false, }) {
232
232
  const id = useId();
233
+ // From `md` the tile becomes a card, unless the screen keeps the row.
234
+ const md = (classes) => (row ? '' : classes);
233
235
  const nameId = `${id}-name`;
234
236
  const nextId = `${id}-next`;
235
237
  const bioId = `${id}-bio`;
@@ -238,27 +240,39 @@ export function DefaultStylistCard({ ref, option, selected, tabIndex, onClick, l
238
240
  // Ring and weight alongside the tint: the selected option has to be
239
241
  // obvious on a monochrome screen too. On a phone the ring is on the
240
242
  // avatar; from `md` it is on the card.
241
- className: cn('group flex w-24 shrink-0 snap-start flex-col items-center gap-1 rounded-lg px-1 pt-1 text-center transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary', 'md:w-full md:shrink md:flex-row md:gap-4 md:border md:px-5 md:py-4 md:text-left', OPTION_HEIGHT, selected
242
- ? 'font-semibold md:border-primary md:bg-primary/10 md:ring-2 md:ring-primary'
243
- : 'md:border-border md:bg-card md:hover:border-primary', className, selected && selectedClassName), children: [_jsx("span", { "aria-hidden": "true", "data-testid": "stylist-avatar", className: cn('flex size-14 shrink-0 items-center justify-center overflow-hidden rounded-full bg-secondary/30 font-semibold md:size-12', selected
244
- ? 'ring-2 ring-primary ring-offset-2 ring-offset-background md:ring-0 md:ring-offset-0'
245
- : 'ring-1 ring-border md:ring-0'), children: _jsx(StylistAvatarContent, { photoUrl: photoUrl, faces: faces, badge: badge }) }), _jsxs("span", { className: "flex w-full min-w-0 flex-col md:w-auto", children: [_jsx("span", { "aria-hidden": "true", className: "h-5 truncate text-sm font-medium md:hidden", children: option.shortName }), _jsx("span", { id: nameId, className: "hidden truncate font-medium md:block", children: name }), subtitle && (_jsx("span", { id: bioId, className: "hidden truncate text-sm text-muted-foreground md:block", children: subtitle })), availability ? (_jsxs("span", { id: nextId, "data-testid": "next-available-row", className: cn('line-clamp-2 h-8 text-xs leading-4 text-foreground tabular-nums md:line-clamp-1 md:h-5 md:text-sm md:leading-5', nextAvailableClassName), children: [_jsx("span", { className: "sr-only md:not-sr-only", children: labels['stylist.nextAvailable'] }), ' ', availability] })) : (reserveAvailability && (_jsx("span", { "data-testid": "next-available-row", "aria-hidden": "true", className: "flex h-8 justify-center pt-1 md:block md:h-5 md:py-1", children: availabilityLoading && (_jsx("span", { className: "block h-3 w-16 animate-pulse rounded bg-muted md:w-36" })) })))] })] }));
243
+ className: cn('group flex w-24 shrink-0 snap-start flex-col items-center gap-1 rounded-lg px-1 pt-1 text-center transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary', md('md:w-full md:shrink md:flex-row md:gap-4 md:border md:px-5 md:py-4 md:text-left'), row ? 'h-32' : OPTION_HEIGHT, selected
244
+ ? cn('font-semibold', md('md:border-primary md:bg-primary/10 md:ring-2 md:ring-primary'))
245
+ : md('md:border-border md:bg-card md:hover:border-primary'), className, selected && selectedClassName), children: [_jsx("span", { "aria-hidden": "true", "data-testid": "stylist-avatar", className: cn('flex size-14 shrink-0 items-center justify-center overflow-hidden rounded-full bg-secondary/30 font-semibold', md('md:size-12'), selected
246
+ ? cn('ring-2 ring-primary ring-offset-2 ring-offset-background', md('md:ring-0 md:ring-offset-0'))
247
+ : cn('ring-1 ring-border', md('md:ring-0'))), children: _jsx(StylistAvatarContent, { photoUrl: photoUrl, faces: faces, badge: badge, row: row }) }), _jsxs("span", { className: cn('flex w-full min-w-0 flex-col', md('md:w-auto')), children: [_jsx("span", { "aria-hidden": "true", className: cn('h-5 truncate text-sm font-medium', md('md:hidden')), children: option.shortName }), _jsx("span", { id: nameId, className: cn('hidden truncate font-medium', md('md:block')), children: name }), subtitle && (_jsx("span", { id: bioId, className: cn('hidden truncate text-sm text-muted-foreground', md('md:block')), children: subtitle })), availability ? (_jsxs("span", { id: nextId, "data-testid": "next-available-row", className: cn('line-clamp-2 h-8 text-xs leading-4 text-foreground tabular-nums', md('md:line-clamp-1 md:h-5 md:text-sm md:leading-5'), nextAvailableClassName), children: [_jsx("span", { className: cn('sr-only', md('md:not-sr-only')), children: labels['stylist.nextAvailable'] }), ' ', availability] })) : (reserveAvailability && (_jsx("span", { "data-testid": "next-available-row", "aria-hidden": "true", className: cn('flex h-8 justify-center pt-1', md('md:block md:h-5 md:py-1')), children: availabilityLoading && (_jsx("span", { className: cn('block h-3 w-16 animate-pulse rounded bg-muted', md('md:w-36')) })) })))] })] }));
246
248
  }
247
249
  /**
248
250
  * What fills the avatar circle: the stylist's photo, a small group of faces
249
251
  * for «first available» (`firstAvailableFaces`), or the badge.
250
252
  */
251
- function StylistAvatarContent({ photoUrl, faces, badge, }) {
253
+ function StylistAvatarContent({ photoUrl, faces, badge, row, }) {
252
254
  if (photoUrl) {
253
255
  return (_jsx("img", { src: photoUrl, alt: "", className: "size-full object-cover", width: 56, height: 56, loading: "lazy", decoding: "async" }));
254
256
  }
255
257
  if (faces && faces.length >= 2) {
256
258
  // A small group, for «any of these»: the faces of who it can be.
257
- return (_jsx("span", { "data-testid": "stylist-faces", className: "relative size-full", children: faces.slice(0, FACE_POSITIONS.length).map((src, index) => (_jsx("img", { src: src, alt: "", width: 30, height: 30, decoding: "async", className: cn('absolute size-[30px] rounded-full object-cover ring-2 ring-background md:size-[26px]', FACE_POSITIONS[index]) }, src))) }));
259
+ return (_jsx("span", { "data-testid": "stylist-faces", className: "relative size-full", children: faces.slice(0, FACE_POSITIONS.length).map((src, index) => (_jsx("img", { src: src, alt: "", width: 30, height: 30, decoding: "async", className: cn('absolute size-[30px] rounded-full object-cover ring-2 ring-background', !row && 'md:size-[26px]', FACE_POSITIONS[index]) }, src))) }));
258
260
  }
259
261
  return badge;
260
262
  }
261
263
  /** A stylist option before there is a stylist: the same box, the same circle, at both widths. */
262
- function SkeletonCard() {
263
- return (_jsxs("div", { "aria-hidden": "true", "data-testid": "stylist-skeleton", className: cn('flex w-24 shrink-0 flex-col items-center gap-1 px-1 pt-1', 'md:w-full md:flex-row md:gap-4 md:rounded-lg md:border md:border-border md:bg-card md:px-5 md:py-4', OPTION_HEIGHT), children: [_jsx("span", { className: "size-14 shrink-0 animate-pulse rounded-full bg-muted md:size-12" }), _jsxs("span", { className: "flex w-full min-w-0 flex-col items-center gap-2 md:w-auto md:flex-1 md:items-start", children: [_jsx("span", { className: "h-4 w-14 animate-pulse rounded bg-muted md:w-28" }), _jsx("span", { className: "h-3 w-16 animate-pulse rounded bg-muted md:w-40" })] })] }));
264
+ function SkeletonCard({ row }) {
265
+ const md = (classes) => (row ? '' : classes);
266
+ return (_jsxs("div", { "aria-hidden": "true", "data-testid": "stylist-skeleton", className: cn('flex w-24 shrink-0 flex-col items-center gap-1 px-1 pt-1', md('md:w-full md:flex-row md:gap-4 md:rounded-lg md:border md:border-border md:bg-card md:px-5 md:py-4'), row ? 'h-32' : OPTION_HEIGHT), children: [_jsx("span", { className: cn('size-14 shrink-0 animate-pulse rounded-full bg-muted', md('md:size-12')) }), _jsxs("span", { className: cn('flex w-full min-w-0 flex-col items-center gap-2', md('md:w-auto md:flex-1 md:items-start')), children: [_jsx("span", { className: cn('h-4 w-14 animate-pulse rounded bg-muted', md('md:w-28')) }), _jsx("span", { className: cn('h-3 w-16 animate-pulse rounded bg-muted', md('md:w-40')) })] })] }));
267
+ }
268
+ /**
269
+ * Under a `row`: the chosen stylist's bio, which a tile has no room for —
270
+ * «tar oftest de minste», read where the choice is made. Nothing for «first
271
+ * available», or a stylist without one.
272
+ */
273
+ function ChosenBio({ options, selectedResourceId, }) {
274
+ const chosen = options.find((option) => option.resourceId === selectedResourceId);
275
+ if (!chosen || chosen.resourceId === null || !chosen.subtitle)
276
+ return null;
277
+ return (_jsxs("p", { "data-testid": "stylist-bio", className: "-mt-3 text-sm text-muted-foreground", children: [_jsx("span", { className: "font-medium text-foreground", children: chosen.name }), " \u00B7 ", chosen.subtitle] }));
264
278
  }
@@ -28,11 +28,12 @@ export declare const TIME_SCREEN_LABEL_KEYS: readonly ["time.heading", "time.day
28
28
  * - `time.empty.callLink|callLinkAria` `{phone}`
29
29
  */
30
30
  /**
31
- * Opt-in keys, for the `soonest` row and `dayFullness`. Placeholders:
32
- * `time.soonest.pick` `{day}` `{time}`; `time.soonest.with` `{stylist}`;
33
- * `time.dayChip.free` `{count}`.
31
+ * Opt-in keys, for the `soonest` row, `dayFullness` and the `adaptive` week
32
+ * grid. Placeholders: `time.soonest.pick` `{day}` `{time}`;
33
+ * `time.soonest.with` `{stylist}`; `time.dayChip.free` `{count}`;
34
+ * `time.week.more` `{count}`.
34
35
  */
35
- export declare const TIME_SCREEN_FINDER_LABEL_KEYS: readonly ["time.soonest.heading", "time.soonest.pick", "time.soonest.with", "time.dayChip.free", "time.dayChip.freeOne", "time.dayChip.full"];
36
+ export declare const TIME_SCREEN_FINDER_LABEL_KEYS: readonly ["time.soonest.heading", "time.soonest.pick", "time.soonest.with", "time.dayChip.free", "time.dayChip.freeOne", "time.dayChip.full", "time.week.none", "time.week.more"];
36
37
  export type TimeScreenLabels = Record<(typeof TIME_SCREEN_LABEL_KEYS)[number], BookingLabel> & Partial<Record<(typeof TIME_SCREEN_FINDER_LABEL_KEYS)[number], BookingLabel>>;
37
38
  /**
38
39
  * `classNames` slots:
@@ -182,6 +183,15 @@ export interface TimeScreenProps {
182
183
  dayFullness?: boolean;
183
184
  /** «Now», for today / tomorrow and whether today is over. Defaults to `Date.now()`. */
184
185
  now?: number;
186
+ /**
187
+ * `stack` (default): the days above the chosen day's times at every width.
188
+ * `adaptive`: the same on a phone, with the days, the month and the weekend
189
+ * note kept together; from `md` the days are a column beside the chosen
190
+ * day's times; from `lg` the whole week is a grid of times, seven columns,
191
+ * nothing to tap first. The grid needs the `time.week.*` labels and a strip
192
+ * of at most seven days (with `monthView`, the week on screen).
193
+ */
194
+ layout?: 'stack' | 'adaptive';
185
195
  classNames?: SlotClassNames<TimeScreenSlot>;
186
196
  components?: TimeScreenComponents;
187
197
  }
@@ -194,7 +204,7 @@ export interface TimeScreenProps {
194
204
  * - `open` — open, with a start still ahead.
195
205
  */
196
206
  export type DayStanding = 'unknown' | 'closed' | 'over' | 'open';
197
- export declare function TimeScreen({ labels, format, dayparts, slots, days, openDays, stylistName, takenSlotTs, phone, weekendNote, currentSlotTs, monthView, onPick, party, soonest, dayFullness, now: nowProp, classNames, components, }: TimeScreenProps): import("react").JSX.Element;
207
+ export declare function TimeScreen({ labels, format, dayparts, slots, days, openDays, stylistName, takenSlotTs, phone, weekendNote, currentSlotTs, monthView, onPick, party, soonest, dayFullness, now: nowProp, layout, classNames, components, }: TimeScreenProps): import("react").JSX.Element;
198
208
  export interface TakenToastProps {
199
209
  labels: Pick<TimeScreenLabels, 'time.taken.message' | 'time.taken.close'>;
200
210
  /** The instant that was just lost; each new one is a fresh toast. */
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { ChevronDown, ChevronLeft, ChevronRight, ChevronUp, X } from 'lucide-react';
4
- import { useEffect, useState } from 'react';
4
+ import { useEffect, useId, useRef, useState } from 'react';
5
5
  import { cn } from '../lib/utils.js';
6
6
  import { renderLabel } from './internal/label-parts.js';
7
7
  import { BookingButton, bookingButtonClass } from './internal/ui.js';
@@ -79,9 +79,10 @@ export const TIME_SCREEN_LABEL_KEYS = [
79
79
  * - `time.empty.callLink|callLinkAria` `{phone}`
80
80
  */
81
81
  /**
82
- * Opt-in keys, for the `soonest` row and `dayFullness`. Placeholders:
83
- * `time.soonest.pick` `{day}` `{time}`; `time.soonest.with` `{stylist}`;
84
- * `time.dayChip.free` `{count}`.
82
+ * Opt-in keys, for the `soonest` row, `dayFullness` and the `adaptive` week
83
+ * grid. Placeholders: `time.soonest.pick` `{day}` `{time}`;
84
+ * `time.soonest.with` `{stylist}`; `time.dayChip.free` `{count}`;
85
+ * `time.week.more` `{count}`.
85
86
  */
86
87
  export const TIME_SCREEN_FINDER_LABEL_KEYS = [
87
88
  /** Heading of the soonest-times row, e.g. «Free soon». */
@@ -96,6 +97,10 @@ export const TIME_SCREEN_FINDER_LABEL_KEYS = [
96
97
  'time.dayChip.freeOne',
97
98
  /** A day chip with none, shown and read, e.g. «full». */
98
99
  'time.dayChip.full',
100
+ /** A week-grid day with nothing free, e.g. «Nothing free». */
101
+ 'time.week.none',
102
+ /** A week-grid day's button for the rest of its times: `{count}`, e.g. «{count} more». */
103
+ 'time.week.more',
99
104
  ];
100
105
  /** How full a day looks, as filled marks out of three: 1–3 free, 4–7, 8 and more. */
101
106
  export function dayFreeMarks(free) {
@@ -258,7 +263,7 @@ function monthGrid(clock, ts) {
258
263
  // ---------------------------------------------------------------------------
259
264
  // TimeScreen
260
265
  // ---------------------------------------------------------------------------
261
- export function TimeScreen({ labels, format, dayparts, slots, days, openDays = null, stylistName = null, takenSlotTs = null, phone = null, weekendNote, currentSlotTs = null, monthView = false, onPick, party, soonest, dayFullness = false, now: nowProp, classNames, components, }) {
266
+ export function TimeScreen({ labels, format, dayparts, slots, days, openDays = null, stylistName = null, takenSlotTs = null, phone = null, weekendNote, currentSlotTs = null, monthView = false, onPick, party, soonest, dayFullness = false, now: nowProp, layout = 'stack', classNames, components, }) {
262
267
  const now = nowProp ?? Date.now();
263
268
  const { clock } = format;
264
269
  const DayChip = components?.DayChip ?? DefaultDayChip;
@@ -338,19 +343,105 @@ export function TimeScreen({ labels, format, dayparts, slots, days, openDays = n
338
343
  const alternative = party !== undefined && party.mode === 'sequential'
339
344
  ? (party.alternativeFor?.(selectedDay) ?? null)
340
345
  : null;
341
- return (_jsxs("section", { "aria-labelledby": "booking-time-heading", className: slotClass(classNames, 'root', 'space-y-6'), children: [_jsx(TimeHeading, { labels: labels, className: classNames?.heading }), soonestSlots.length > 0 && (_jsx(SoonestRow, { labels: labels, format: format, slots: soonestSlots, now: now, resolveStylist: soonest?.resolveStylist, weekendNote: weekendNote, onPick: onPick, className: classNames?.soonest })), strip.length > 1 && (_jsxs("fieldset", { className: slotClass(classNames, 'dayStrip', 'flex flex-wrap gap-2'), children: [_jsx("legend", { className: "sr-only", children: labels['time.dayStrip.legend'] }), strip.map((day) => {
342
- const selected = clock.dayKey(day) === selectedKey;
343
- return (_jsx(DayChip, { dayTs: day, label: clock.dayChip(day, now), selected: selected, onSelect: () => setTappedDayKey(clock.dayKey(day)), ...(dayFullness
344
- ? dayFreeProps(labels, freeByDay.get(clock.dayKey(day)) ?? 0,
345
- // «Full» only when it is true of the business: an open
346
- // day, asked for everyone — not hours that could not be
347
- // read, nor a day one named stylist does not work, nor a
348
- // family's day (the other mode may still seat them).
349
- !party &&
350
- stylistName === null &&
351
- dayStanding(clock, day, openDays, now) === 'open')
352
- : {}), className: cn('rounded-full', selected ? 'border-primary bg-primary/10 font-bold ring-2 ring-primary' : '', classNames?.dayChip, selected ? classNames?.dayChipSelected : undefined) }, clock.dayKey(day)));
353
- })] })), monthView && (_jsx(MonthCalendar, { labels: labels, clock: clock, open: showMonth, onToggle: () => setShowMonth((shown) => !shown), monthTs: monthTs ?? selectedDay, onMonth: setMonthTs, strip: offeredDays, selectedDay: selectedDay, now: now, onPickDay: (day) => setTappedDayKey(clock.dayKey(day)), classNames: classNames })), surcharge !== null && (_jsx("p", { "data-testid": "surcharge-row", className: slotClass(classNames, 'surcharge', 'min-h-5 text-sm text-muted-foreground'), children: clock.isWeekend(selectedDay) ? weekendSentence(labels, format, surcharge) : null })), dayBookable.length === 0 && alternative && party && (_jsx(ParallelInstead, { labels: labels, clock: clock, slot: alternative, day: selectedDay, now: now, size: party.items.length, onAccept: () => party.onPick(alternative), className: classNames?.parallel })), dayBookable.length === 0 && !(alternative && party) && (_jsx(NothingFreeCard, { labels: labels, format: format, day: selectedDay, now: now, phone: phone, standing: dayStanding(clock, selectedDay, openDays, now), stylistName: stylistName, className: classNames?.empty })), dayBookable.length > 0 && (_jsx(DaypartSections, { labels: labels, clock: clock, dayparts: dayparts, daySlots: daySlots, nearest: nearest, currentSlotTs: currentSlotTs, party: party, partyByStart: partyByStart, onPick: onPick, classNames: classNames, TimeChip: components?.TimeChip ?? DefaultTimeChip })), elsewhere && (_jsx(BookingButton, { variant: "link", onClick: () => setTappedDayKey(clock.dayKey(elsewhere.startTs)), className: classNames?.nextFree, children: labels['time.nextFree'] })), isToday && dayBookable.length > 0 && (_jsx(ShortNoticeLine, { labels: labels, format: format, phone: phone, className: classNames?.shortNotice }))] }));
346
+ const adaptive = layout === 'adaptive';
347
+ // The week grid shows the strip whole, so it needs one week of it, and its words.
348
+ const weekNone = labels['time.week.none'];
349
+ const weekMore = labels['time.week.more'];
350
+ const week = adaptive &&
351
+ strip.length > 1 &&
352
+ strip.length <= DAY_STRIP_LIMIT &&
353
+ weekNone !== undefined &&
354
+ weekMore !== undefined
355
+ ? { none: weekNone, more: weekMore }
356
+ : null;
357
+ const TimeChip = components?.TimeChip ?? DefaultTimeChip;
358
+ const dayStrip = strip.length > 1 && (_jsxs("fieldset", { className: slotClass(classNames, 'dayStrip', cn('flex flex-wrap gap-2', adaptive && 'md:flex-col md:flex-nowrap')), children: [_jsx("legend", { className: "sr-only", children: labels['time.dayStrip.legend'] }), strip.map((day) => {
359
+ const selected = clock.dayKey(day) === selectedKey;
360
+ return (_jsx(DayChip, { dayTs: day, label: clock.dayChip(day, now), selected: selected, onSelect: () => setTappedDayKey(clock.dayKey(day)), ...(dayFullness
361
+ ? dayFreeProps(labels, freeByDay.get(clock.dayKey(day)) ?? 0,
362
+ // «Full» only when it is true of the business: an open
363
+ // day, asked for everyone — not hours that could not be
364
+ // read, nor a day one named stylist does not work, nor a
365
+ // family's day (the other mode may still seat them).
366
+ !party &&
367
+ stylistName === null &&
368
+ dayStanding(clock, day, openDays, now) === 'open')
369
+ : {}), className: cn('rounded-full',
370
+ // A column of days: full-width rows, not pills.
371
+ adaptive && 'md:w-full md:rounded-lg', selected ? 'border-primary bg-primary/10 font-bold ring-2 ring-primary' : '', classNames?.dayChip, selected ? classNames?.dayChipSelected : undefined) }, clock.dayKey(day)));
372
+ })] }));
373
+ const month = monthView && (_jsx(MonthCalendar, { labels: labels, clock: clock, open: showMonth, onToggle: () => setShowMonth((shown) => !shown), monthTs: monthTs ?? selectedDay, onMonth: setMonthTs, strip: offeredDays, selectedDay: selectedDay, now: now, onPickDay: (day) => setTappedDayKey(clock.dayKey(day)), classNames: classNames }));
374
+ const surchargeRow = surcharge !== null && (_jsx("p", { "data-testid": "surcharge-row", className: slotClass(classNames, 'surcharge', 'min-h-5 text-sm text-muted-foreground'), children: clock.isWeekend(selectedDay) ? weekendSentence(labels, format, surcharge) : null }));
375
+ const dayTimes = (_jsxs(_Fragment, { children: [dayBookable.length === 0 && alternative && party && (_jsx(ParallelInstead, { labels: labels, clock: clock, slot: alternative, day: selectedDay, now: now, size: party.items.length, onAccept: () => party.onPick(alternative), className: classNames?.parallel })), dayBookable.length === 0 && !(alternative && party) && (_jsx(NothingFreeCard, { labels: labels, format: format, day: selectedDay, now: now, phone: phone, standing: dayStanding(clock, selectedDay, openDays, now), stylistName: stylistName, className: classNames?.empty })), dayBookable.length > 0 && (_jsx(DaypartSections, { labels: labels, clock: clock, dayparts: dayparts, daySlots: daySlots, nearest: nearest, currentSlotTs: currentSlotTs, party: party, partyByStart: partyByStart, onPick: onPick, classNames: classNames, TimeChip: TimeChip })), elsewhere && (_jsx(BookingButton, { variant: "link", onClick: () => setTappedDayKey(clock.dayKey(elsewhere.startTs)), className: classNames?.nextFree, children: labels['time.nextFree'] })), isToday && dayBookable.length > 0 && (_jsx(ShortNoticeLine, { labels: labels, format: format, phone: phone, className: classNames?.shortNotice }))] }));
376
+ return (_jsxs("section", { "aria-labelledby": "booking-time-heading", className: slotClass(classNames, 'root', 'space-y-6'), children: [_jsx(TimeHeading, { labels: labels, className: classNames?.heading }), soonestSlots.length > 0 && (_jsx(SoonestRow, { labels: labels, format: format, slots: soonestSlots, now: now, resolveStylist: soonest?.resolveStylist, weekendNote: weekendNote, onPick: onPick, className: classNames?.soonest })), adaptive ? (_jsxs(_Fragment, { children: [month, _jsxs("div", { "data-testid": "time-columns", className: cn('space-y-6 md:grid md:grid-cols-[minmax(0,13rem)_minmax(0,1fr)] md:items-start md:gap-8 md:space-y-0', week && 'lg:hidden'), children: [_jsxs("div", { className: "space-y-2", children: [dayStrip, surchargeRow] }), _jsx("div", { className: "min-w-0 space-y-6", children: dayTimes })] }), week && (_jsx(WeekGrid, { labels: labels, words: week, format: format, days: strip, offered: offered, currentSlotTs: currentSlotTs, nearest: nearest, party: party, partyByStart: partyByStart, onPick: onPick, freeByDay: dayFullness ? freeByDay : null, openDays: openDays, stylistName: stylistName, weekendNote: weekendNote, phone: phone, now: now, classNames: classNames, TimeChip: TimeChip, empty: _jsx(NothingFreeCard, { labels: labels, format: format, day: selectedDay, now: now, phone: phone, standing: dayStanding(clock, selectedDay, openDays, now), stylistName: stylistName, className: classNames?.empty }), next: elsewhere &&
377
+ !strip.some((day) => clock.dayKey(day) === clock.dayKey(elsewhere.startTs)) && (_jsx(BookingButton, { variant: "link", onClick: () => setTappedDayKey(clock.dayKey(elsewhere.startTs)), className: classNames?.nextFree, children: labels['time.nextFree'] })), className: "hidden lg:block" }))] })) : (_jsxs(_Fragment, { children: [dayStrip, month, surchargeRow, dayTimes] }))] }));
378
+ }
379
+ /** How many times a week-grid day shows before «{count} more». */
380
+ const WEEK_CAP = 8;
381
+ /**
382
+ * `layout="adaptive"` from `lg`: the week as seven columns of times, so a
383
+ * wide screen books in one tap. Each day heads its column with how full it is;
384
+ * a long day folds after `WEEK_CAP`; an empty one says so, or offers a
385
+ * family's simultaneous time. The chips are the strip's own `TimeChip`s.
386
+ */
387
+ function WeekGrid({ labels, words, format, days, offered, currentSlotTs, nearest, party, partyByStart, onPick, freeByDay, openDays, stylistName, weekendNote, phone, now, classNames, TimeChip, empty, next, className, }) {
388
+ const { clock } = format;
389
+ const id = useId();
390
+ const [unfolded, setUnfolded] = useState(null);
391
+ // Unfolding unmounts the button that held focus: hand it to the first time
392
+ // it revealed, so a keyboard user carries on where they were.
393
+ const lists = useRef(new Map());
394
+ useEffect(() => {
395
+ if (unfolded === null)
396
+ return;
397
+ lists.current.get(unfolded)?.querySelectorAll('button')[WEEK_CAP]?.focus();
398
+ }, [unfolded]);
399
+ let todayOpen = false;
400
+ let anyBookable = false;
401
+ const columns = days.map((day, column) => {
402
+ const key = clock.dayKey(day);
403
+ const daySlots = offered
404
+ .filter((slot) => clock.dayKey(slot.startTs) === key)
405
+ .sort((a, b) => a.startTs - b.startTs);
406
+ const bookable = daySlots.filter((slot) => slot.startTs !== currentSlotTs).length;
407
+ if (bookable > 0 && clock.daysBetween(now, day) === 0)
408
+ todayOpen = true;
409
+ if (bookable > 0)
410
+ anyBookable = true;
411
+ // Each weekend day says its own surcharge: the price is the basket priced
412
+ // on that date, and Saturday's need not be Sunday's.
413
+ const note = clock.isWeekend(day) ? (weekendNote?.(day) ?? null) : null;
414
+ const shown = unfolded === key ? daySlots : daySlots.slice(0, WEEK_CAP);
415
+ const more = daySlots.length - shown.length;
416
+ const headId = `${id}-day-${column}`;
417
+ const alternative = bookable === 0 && party?.mode === 'sequential' ? (party.alternativeFor?.(day) ?? null) : null;
418
+ if (alternative)
419
+ anyBookable = true;
420
+ const free = freeByDay === null
421
+ ? null
422
+ : dayFreeProps(labels, freeByDay.get(key) ?? 0, !party && stylistName === null && dayStanding(clock, day, openDays, now) === 'open').freeLabel;
423
+ return (_jsxs("li", { className: "min-w-0 space-y-2", children: [_jsxs("p", { id: headId, className: "flex flex-col items-center gap-0.5 rounded-lg bg-muted/50 px-1 py-2 text-center", children: [_jsx("span", { className: "max-w-full truncate text-sm font-semibold", children: clock.dayChip(day, now) }), free ? (_jsx("span", { className: "max-w-full truncate text-xs text-muted-foreground", children: free })) : null] }), note !== null && (_jsx("p", { "data-testid": "week-surcharge", className: slotClass(classNames, 'surcharge', 'px-1 text-center text-xs leading-tight text-muted-foreground'), children: weekendSentence(labels, format, note) })), shown.length > 0 && (_jsx("ul", { ref: (node) => {
424
+ lists.current.set(key, node);
425
+ }, "aria-labelledby": headId, className: "flex flex-col gap-1.5", children: shown.map((slot) => {
426
+ const partySlot = partyByStart.get(slot.startTs);
427
+ const current = slot.startTs === currentSlotTs;
428
+ const isNearest = !current && nearest.has(slot.startTs);
429
+ return (_jsx("li", { children: _jsx(TimeChip, { startTs: slot.startTs, label: chipLabel(labels, clock, slot, current, partySlot, party), nearest: isNearest, nearestLabel: labelText(labels['time.nearest']), current: current, onPick: () => (partySlot && party ? party.onPick(partySlot) : onPick(slot)), className: cn('h-auto min-h-11 w-full min-w-0 whitespace-normal px-1 tabular-nums', isNearest ? 'border-primary ring-2 ring-primary' : '', classNames?.chip, isNearest ? classNames?.chipNearest : undefined, current ? classNames?.chipCurrent : undefined) }) }, slot.startTs));
430
+ }) })), more > 0 && (_jsx(BookingButton, { variant: "link", size: "sm", onClick: () => setUnfolded(key), className: "w-full", children: renderLabel(words.more, { count: more }) })), bookable === 0 &&
431
+ (alternative && party ? (_jsx(BookingButton, { size: "sm", onClick: () => party.onPick(alternative), className: "h-auto min-h-11 w-full whitespace-normal tabular-nums", children: renderLabel(labels['time.parallel.accept'], {
432
+ time: clock.formatTime(alternative.startTs),
433
+ }) })) : (_jsx("p", { className: "px-1 text-center text-xs text-muted-foreground", children: renderLabel(words.none) })))] }, key));
434
+ });
435
+ return (_jsxs("div", { "data-testid": "time-week", className: cn('space-y-4', className), children: [_jsx("ol", { className: "grid grid-cols-7 gap-2", children: columns }), !anyBookable && empty, next, todayOpen && (_jsx(ShortNoticeLine, { labels: labels, format: format, phone: phone, className: classNames?.shortNotice }))] }));
436
+ }
437
+ /** A time chip's words: the hour, the held hour marked, or a family's whole visit. */
438
+ function chipLabel(labels, clock, slot, current, partySlot, party) {
439
+ const time = clock.formatTime(slot.startTs);
440
+ if (current)
441
+ return fillLabel(labels['time.current'], { time });
442
+ if (partySlot && party)
443
+ return partyChipLabel(labels, clock, partySlot, party);
444
+ return time;
354
445
  }
355
446
  /** The weekend note as one plain sentence, for a card that books without the notice on screen. */
356
447
  function weekendSentenceText(labels, format, note) {
@@ -503,14 +594,7 @@ function DaypartSections({ labels, clock, dayparts, daySlots, nearest, currentSl
503
594
  const partySlot = partyByStart.get(slot.startTs);
504
595
  const current = slot.startTs === currentSlotTs;
505
596
  const isNearest = !current && nearest.has(slot.startTs);
506
- let label = clock.formatTime(slot.startTs);
507
- if (current) {
508
- label = fillLabel(labels['time.current'], { time: label });
509
- }
510
- else if (partySlot && party) {
511
- label = partyChipLabel(labels, clock, partySlot, party);
512
- }
513
- return (_jsx(TimeChip, { startTs: slot.startTs, label: label, nearest: isNearest, nearestLabel: labelText(labels['time.nearest']), current: current, onPick: () => (partySlot && party ? party.onPick(partySlot) : onPick(slot)), className: cn('min-w-20 tabular-nums', isNearest ? 'border-primary ring-2 ring-primary' : '', classNames?.chip, isNearest ? classNames?.chipNearest : undefined, current ? classNames?.chipCurrent : undefined) }, slot.startTs));
597
+ return (_jsx(TimeChip, { startTs: slot.startTs, label: chipLabel(labels, clock, slot, current, partySlot, party), nearest: isNearest, nearestLabel: labelText(labels['time.nearest']), current: current, onPick: () => (partySlot && party ? party.onPick(partySlot) : onPick(slot)), className: cn('min-w-20 tabular-nums', isNearest ? 'border-primary ring-2 ring-primary' : '', classNames?.chip, isNearest ? classNames?.chipNearest : undefined, current ? classNames?.chipCurrent : undefined) }, slot.startTs));
514
598
  }) })] }, daypart.key));
515
599
  });
516
600
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@medalsocial/meda",
3
- "version": "3.7.0",
3
+ "version": "3.8.0",
4
4
  "description": "Shared Meda UI shell and runtime package.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {