@medalsocial/meda 3.7.0 → 3.9.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.
@@ -18,7 +18,7 @@ import { UPCOMING_BOOKINGS_LABEL_KEYS } from './portal/upcoming-bookings.js';
18
18
  import { VIPPS_LINK_ROW_LABEL_KEYS } from './portal/vipps-link-row.js';
19
19
  import { VISIT_HISTORY_LABEL_KEYS } from './portal/visit-history.js';
20
20
  import { SERVICE_SCREEN_LABEL_KEYS, SERVICE_SCREEN_MULTI_LABEL_KEYS } from './service-screen.js';
21
- import { STYLIST_SCREEN_LABEL_KEYS } from './stylist-screen.js';
21
+ import { STYLIST_SCREEN_LABEL_KEYS, STYLIST_SCREEN_SEAT_LABEL_KEYS } from './stylist-screen.js';
22
22
  import { SUMMARY_BAR_HINT_LABEL_KEYS, SUMMARY_BAR_LABEL_KEYS } from './summary-bar.js';
23
23
  import { TIME_SCREEN_FINDER_LABEL_KEYS, TIME_SCREEN_LABEL_KEYS } from './time-screen.js';
24
24
  import { WHO_SCREEN_LABEL_KEYS, WHO_SCREEN_PARTY_LABEL_KEYS } from './who-screen.js';
@@ -30,13 +30,13 @@ 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" | "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")[];
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.
37
37
  * A pack may leave them out; a screen using the feature needs them.
38
38
  */
39
- export type BookingOptionalLabelKey = (typeof SERVICE_SCREEN_MULTI_LABEL_KEYS)[number] | (typeof BOOKING_RECAP_LABEL_KEYS)[number] | (typeof TIME_SCREEN_FINDER_LABEL_KEYS)[number] | (typeof SUMMARY_BAR_HINT_LABEL_KEYS)[number] | (typeof WHO_SCREEN_PARTY_LABEL_KEYS)[number];
39
+ export type BookingOptionalLabelKey = (typeof SERVICE_SCREEN_MULTI_LABEL_KEYS)[number] | (typeof BOOKING_RECAP_LABEL_KEYS)[number] | (typeof TIME_SCREEN_FINDER_LABEL_KEYS)[number] | (typeof STYLIST_SCREEN_SEAT_LABEL_KEYS)[number] | (typeof SUMMARY_BAR_HINT_LABEL_KEYS)[number] | (typeof WHO_SCREEN_PARTY_LABEL_KEYS)[number];
40
40
  /** Keys every complete pack defines. */
41
41
  export type BookingRequiredLabelKey = (typeof WHO_SCREEN_LABEL_KEYS)[number] | (typeof ADD_CHILD_SHEET_LABEL_KEYS)[number] | (typeof SERVICE_SCREEN_LABEL_KEYS)[number] | (typeof STYLIST_SCREEN_LABEL_KEYS)[number] | (typeof TIME_SCREEN_LABEL_KEYS)[number] | (typeof MANAGE_SCREEN_LABEL_KEYS)[number] | (typeof DETAILS_SCREEN_LABEL_KEYS)[number] | (typeof SUMMARY_BAR_LABEL_KEYS)[number] | (typeof CONFIRMATION_LABEL_KEYS)[number] | (typeof BOOKING_SKELETON_LABEL_KEYS)[number] | (typeof LOGIN_SHEET_LABEL_KEYS)[number] | (typeof PORTAL_SHELL_LABEL_KEYS)[number] | (typeof PORTAL_UNREACHABLE_LABEL_KEYS)[number] | (typeof UPCOMING_BOOKINGS_LABEL_KEYS)[number] | (typeof VISIT_HISTORY_LABEL_KEYS)[number] | (typeof REBOOK_CARDS_LABEL_KEYS)[number] | (typeof CHILD_CARDS_LABEL_KEYS)[number] | (typeof FAMILY_EDITOR_LABEL_KEYS)[number] | (typeof PROFILE_FORM_LABEL_KEYS)[number] | (typeof DATA_CONTROLS_LABEL_KEYS)[number] | (typeof LOGOUT_BUTTON_LABEL_KEYS)[number] | (typeof VIPPS_LINK_ROW_LABEL_KEYS)[number];
42
42
  /** A complete label pack for one locale. The opt-in keys may be absent. */
@@ -17,7 +17,7 @@ import { UPCOMING_BOOKINGS_LABEL_KEYS } from './portal/upcoming-bookings.js';
17
17
  import { VIPPS_LINK_ROW_LABEL_KEYS } from './portal/vipps-link-row.js';
18
18
  import { VISIT_HISTORY_LABEL_KEYS } from './portal/visit-history.js';
19
19
  import { SERVICE_SCREEN_LABEL_KEYS, SERVICE_SCREEN_MULTI_LABEL_KEYS } from './service-screen.js';
20
- import { STYLIST_SCREEN_LABEL_KEYS } from './stylist-screen.js';
20
+ import { STYLIST_SCREEN_LABEL_KEYS, STYLIST_SCREEN_SEAT_LABEL_KEYS } from './stylist-screen.js';
21
21
  import { SUMMARY_BAR_HINT_LABEL_KEYS, SUMMARY_BAR_LABEL_KEYS } from './summary-bar.js';
22
22
  import { TIME_SCREEN_FINDER_LABEL_KEYS, TIME_SCREEN_LABEL_KEYS } from './time-screen.js';
23
23
  import { WHO_SCREEN_LABEL_KEYS, WHO_SCREEN_PARTY_LABEL_KEYS } from './who-screen.js';
@@ -37,6 +37,7 @@ export const BOOKING_LABEL_KEYS = [
37
37
  ...SERVICE_SCREEN_LABEL_KEYS,
38
38
  ...SERVICE_SCREEN_MULTI_LABEL_KEYS,
39
39
  ...STYLIST_SCREEN_LABEL_KEYS,
40
+ ...STYLIST_SCREEN_SEAT_LABEL_KEYS,
40
41
  ...TIME_SCREEN_LABEL_KEYS,
41
42
  ...TIME_SCREEN_FINDER_LABEL_KEYS,
42
43
  ...MANAGE_SCREEN_LABEL_KEYS,
@@ -21,7 +21,12 @@ import type { BookingResourceDto, PartyMode } from './types.js';
21
21
  * available» included. Arrow keys move and choose, Tab enters and leaves once.
22
22
  */
23
23
  export declare const STYLIST_SCREEN_LABEL_KEYS: readonly ["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"];
24
- export type StylistScreenLabels = Record<(typeof STYLIST_SCREEN_LABEL_KEYS)[number], BookingLabel>;
24
+ /**
25
+ * Opt-in keys, for `party.seats` (a stylist per person when seen at once).
26
+ * Placeholders: `stylist.party.seat` `{who}`.
27
+ */
28
+ export declare const STYLIST_SCREEN_SEAT_LABEL_KEYS: readonly ["stylist.party.seatsHeading", "stylist.party.seat"];
29
+ export type StylistScreenLabels = Record<(typeof STYLIST_SCREEN_LABEL_KEYS)[number], BookingLabel> & Partial<Record<(typeof STYLIST_SCREEN_SEAT_LABEL_KEYS)[number], BookingLabel>>;
25
30
  /**
26
31
  * - `root` — the `<section>`
27
32
  * - `heading` — the `<h2>`
@@ -65,6 +70,8 @@ export interface StylistCardProps {
65
70
  selectedClassName?: string;
66
71
  /** Merged onto the next-opening line (the `nextAvailable` slot). */
67
72
  nextAvailableClassName?: string;
73
+ /** `StylistScreen`'s `row`: draw the phone's avatar tile at every width. */
74
+ row?: boolean;
68
75
  }
69
76
  export interface StylistScreenComponents {
70
77
  StylistCard?: ComponentType<StylistCardProps>;
@@ -116,6 +123,13 @@ export interface StylistScreenProps {
116
123
  * as «there is more», not as a layout bug.
117
124
  */
118
125
  edgeFade?: boolean;
126
+ /**
127
+ * Keep the phone's one row of avatars at every width instead of the column
128
+ * of cards from `md`, so a wide screen reaches the times without scrolling
129
+ * past every stylist. The chosen stylist's bio, which a tile has no room
130
+ * for, is shown under the row.
131
+ */
132
+ row?: boolean;
119
133
  /**
120
134
  * Present only for a family. One child gets no picker: «one after the
121
135
  * other» is not a choice when there is nobody to be after.
@@ -138,10 +152,26 @@ export interface StylistScreenProps {
138
152
  parallel: number;
139
153
  };
140
154
  onMode: (mode: PartyMode) => void;
155
+ /**
156
+ * Seen at once: one optional stylist per person, in basket order. Each
157
+ * offers «first available» and the stylists who can do that person's whole
158
+ * visit; the search seats the rest with anyone free at that minute. Needs
159
+ * the `stylist.party.seat*` labels; without `seats` the mode note stands
160
+ * alone, as before.
161
+ */
162
+ seats?: Array<{
163
+ /** Who sits there, e.g. «Barn 1», «Deg», a child's name. */
164
+ who: string;
165
+ /** Every service of this person's visit. */
166
+ serviceIds: string[];
167
+ /** Their stylist, `null` for first available. */
168
+ resourceId: string | null;
169
+ }>;
170
+ onSeat?: (index: number, resourceId: string | null) => void;
141
171
  };
142
172
  classNames?: SlotClassNames<StylistScreenSlot>;
143
173
  components?: StylistScreenComponents;
144
174
  }
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;
175
+ 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
176
  /** 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;
177
+ export declare function DefaultStylistCard({ ref, option, selected, tabIndex, onClick, labels, className, selectedClassName, nextAvailableClassName, row, }: StylistCardProps): import("react").JSX.Element;
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useId, useRef, } from 'react';
4
4
  import { cn } from '../lib/utils.js';
5
5
  import { renderLabel } from './internal/label-parts.js';
6
+ import { BOOKING_SELECT_CLASS } from './internal/ui.js';
6
7
  import { labelText } from './labels.js';
7
8
  import { LiveStatus } from './live-status.js';
8
9
  import { slotClass } from './slots.js';
@@ -42,6 +43,16 @@ export const STYLIST_SCREEN_LABEL_KEYS = [
42
43
  'stylist.party.parallelNote.three',
43
44
  'stylist.party.parallelNote.other',
44
45
  ];
46
+ /**
47
+ * Opt-in keys, for `party.seats` (a stylist per person when seen at once).
48
+ * Placeholders: `stylist.party.seat` `{who}`.
49
+ */
50
+ export const STYLIST_SCREEN_SEAT_LABEL_KEYS = [
51
+ /** The heading over the per-person choices, e.g. «Who cuts whom? (optional)». */
52
+ 'stylist.party.seatsHeading',
53
+ /** One person's label: `{who}`, e.g. «Stylist for {who}». */
54
+ 'stylist.party.seat',
55
+ ];
45
56
  /**
46
57
  * One height per breakpoint for every option, filled or not, so nothing on the
47
58
  * step — nor what is under it — moves when a list or a line lands: a 128 px
@@ -72,7 +83,7 @@ function bySize(labels, base, size, sizeWord) {
72
83
  const suffix = SIZE_WORDS[size] ?? 'other';
73
84
  return renderLabel(labels[`${base}.${suffix}`], { count: size, sizeWord: sizeWord ?? size });
74
85
  }
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, }) {
86
+ 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
87
  // `every`, not `some`: the step asks one question for the whole visit, so a
77
88
  // stylist who covers one of two services is not an answer — it is a refusal
78
89
  // the engine issues at submit, offered here as a choice.
@@ -127,10 +138,13 @@ export function StylistScreen({ labels, format, now, serviceIds, resources, sele
127
138
  minutes: party.minutes.sequential,
128
139
  }), classNames: classNames }), _jsx(ModeCard, { selected: party.mode === 'parallel', onClick: () => party.onMode('parallel'), title: bySize(labels, 'stylist.party.parallel', party.size, party.sizeWord), subtitle: renderLabel(labels['stylist.party.parallelMinutes'], {
129
140
  minutes: party.minutes.parallel,
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,
141
+ }), classNames: classNames })] })), party?.mode === 'parallel' ? (_jsxs("div", { className: "space-y-4", children: [_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) }), party.seats &&
142
+ party.onSeat &&
143
+ labels['stylist.party.seatsHeading'] &&
144
+ labels['stylist.party.seat'] && (_jsx(SeatChoices, { seats: party.seats, onSeat: party.onSeat, resources: resources, heading: labels['stylist.party.seatsHeading'], seatLabel: labels['stylist.party.seat'], anyone: labelText(labels['stylist.firstAvailable']), pending: labelText(labels['stylist.pendingName']), format: format }))] })) : (_jsx(StylistRadioGroup, { options: options, selectedResourceId: selectedResourceId,
131
145
  // The held option takes one placeholder's place, so the row is
132
146
  // 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 })] }));
147
+ 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
148
  }
135
149
  /**
136
150
  * The options as one radiogroup with a roving tab stop.
@@ -139,7 +153,7 @@ export function StylistScreen({ labels, format, now, serviceIds, resources, sele
139
153
  * stylist is cheap and reversible, and a group that needed Space after every
140
154
  * arrow would be a list of buttons in a radiogroup's clothes.
141
155
  */
142
- function StylistRadioGroup({ options, selectedResourceId, skeletons, busy, onPick, labels, classNames, edgeFade, Card, }) {
156
+ function StylistRadioGroup({ options, selectedResourceId, skeletons, busy, onPick, labels, classNames, edgeFade, row, Card, }) {
143
157
  const groupRef = useRef(null);
144
158
  const optionRefs = useRef([]);
145
159
  const selectedIndex = options.findIndex((option) => option.resourceId === selectedResourceId);
@@ -196,9 +210,9 @@ function StylistRadioGroup({ options, selectedResourceId, skeletons, busy, onPic
196
210
  // The last few pixels fade, so the cut-off card reads as «more this way».
197
211
  // The trailing room lets the last card scroll wholly clear of the fade.
198
212
  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) => {
213
+ 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
214
  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)))] }));
215
+ }, 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
216
  }
203
217
  /** The restored stylist's stand-in: a real, checked radio of the final size,
204
218
  * with its next-opening row pulsing like every card still waiting for one. */
@@ -228,8 +242,10 @@ function ModeCard({ selected, onClick, title, subtitle, classNames, }) {
228
242
  : '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
243
  }
230
244
  /** 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, }) {
245
+ export function DefaultStylistCard({ ref, option, selected, tabIndex, onClick, labels, className, selectedClassName, nextAvailableClassName, row = false, }) {
232
246
  const id = useId();
247
+ // From `md` the tile becomes a card, unless the screen keeps the row.
248
+ const md = (classes) => (row ? '' : classes);
233
249
  const nameId = `${id}-name`;
234
250
  const nextId = `${id}-next`;
235
251
  const bioId = `${id}-bio`;
@@ -238,27 +254,65 @@ export function DefaultStylistCard({ ref, option, selected, tabIndex, onClick, l
238
254
  // Ring and weight alongside the tint: the selected option has to be
239
255
  // obvious on a monochrome screen too. On a phone the ring is on the
240
256
  // 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" })) })))] })] }));
257
+ 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
258
+ ? cn('font-semibold', md('md:border-primary md:bg-primary/10 md:ring-2 md:ring-primary'))
259
+ : 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
260
+ ? cn('ring-2 ring-primary ring-offset-2 ring-offset-background', md('md:ring-0 md:ring-offset-0'))
261
+ : 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
262
  }
247
263
  /**
248
264
  * What fills the avatar circle: the stylist's photo, a small group of faces
249
265
  * for «first available» (`firstAvailableFaces`), or the badge.
250
266
  */
251
- function StylistAvatarContent({ photoUrl, faces, badge, }) {
267
+ function StylistAvatarContent({ photoUrl, faces, badge, row, }) {
252
268
  if (photoUrl) {
253
269
  return (_jsx("img", { src: photoUrl, alt: "", className: "size-full object-cover", width: 56, height: 56, loading: "lazy", decoding: "async" }));
254
270
  }
255
271
  if (faces && faces.length >= 2) {
256
272
  // 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))) }));
273
+ 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
274
  }
259
275
  return badge;
260
276
  }
261
277
  /** 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" })] })] }));
278
+ function SkeletonCard({ row }) {
279
+ const md = (classes) => (row ? '' : classes);
280
+ 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')) })] })] }));
281
+ }
282
+ /**
283
+ * Under a `row`: the chosen stylist's bio, which a tile has no room for —
284
+ * «tar oftest de minste», read where the choice is made. Nothing for «first
285
+ * available», or a stylist without one.
286
+ */
287
+ function ChosenBio({ options, selectedResourceId, }) {
288
+ const chosen = options.find((option) => option.resourceId === selectedResourceId);
289
+ if (!chosen || chosen.resourceId === null || !chosen.subtitle)
290
+ return null;
291
+ 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] }));
292
+ }
293
+ /**
294
+ * Seen at once: «who cuts whom», one native select per person. A select, not a
295
+ * second row of avatar tiles: a family of three would be three radiogroups of
296
+ * the same faces, and the question is optional — most leave it on «first
297
+ * available». Each lists only the stylists who can do that person's whole visit,
298
+ * in the salon's order; one already chosen for another person is left out, as
299
+ * one stylist cannot cut two people at once.
300
+ */
301
+ function SeatChoices({ seats, onSeat, resources, heading, seatLabel, anyone, pending, format, }) {
302
+ const id = useId();
303
+ const ordered = [...resources].sort((a, b) => a.sortOrder - b.sortOrder);
304
+ return (_jsxs("fieldset", { className: "space-y-3", children: [_jsx("legend", { className: "mb-1 font-sans text-base font-semibold", children: renderLabel(heading) }), seats.map((seat, index) => {
305
+ const takenElsewhere = new Set(seats.flatMap((other, at) => at !== index && other.resourceId !== null ? [other.resourceId] : []));
306
+ const options = ordered.filter((resource) => seat.serviceIds.every((serviceId) => resource.serviceIds.includes(serviceId)) &&
307
+ !takenElsewhere.has(resource.id));
308
+ const selectId = `${id}-seat-${index}`;
309
+ // A saved choice the list cannot show (still loading, gone, or no longer
310
+ // able to do the visit) stays the selected option, by name where known:
311
+ // shown as «first available» it would read as an answer never given.
312
+ const held = seat.resourceId !== null && !options.some((resource) => resource.id === seat.resourceId)
313
+ ? seat.resourceId
314
+ : null;
315
+ const heldResource = held === null ? undefined : resources.find((r) => r.id === held);
316
+ return (_jsxs("div", { className: "space-y-1", children: [_jsx("label", { htmlFor: selectId, className: "block text-sm font-medium", children: renderLabel(seatLabel, { who: seat.who }) }), _jsxs("select", { id: selectId, value: seat.resourceId ?? '', onChange: (event) => onSeat(index, event.target.value === '' ? null : event.target.value), className: BOOKING_SELECT_CLASS, children: [_jsx("option", { value: "", children: anyone }), held !== null && (_jsx("option", { value: held, children: heldResource ? format.stylistName(heldResource.name) : pending })), options.map((resource) => (_jsx("option", { value: resource.id, children: format.stylistName(resource.name) }, resource.id)))] })] }, selectId));
317
+ })] }));
264
318
  }
@@ -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.9.0",
4
4
  "description": "Shared Meda UI shell and runtime package.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {