@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
|
-
|
|
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) })
|
|
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:
|
|
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
|
-
|
|
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 `
|
|
32
|
-
* `time.soonest.pick` `{day}` `{time}`;
|
|
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 `
|
|
83
|
-
* `time.soonest.pick` `{day}` `{time}`;
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
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
|
}
|