@medalsocial/meda 3.7.0 → 3.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -30,7 +30,7 @@ import { WHO_SCREEN_LABEL_KEYS, WHO_SCREEN_PARTY_LABEL_KEYS } from './who-screen
|
|
|
30
30
|
* (Screens that embed another screen already include its keys, e.g. the
|
|
31
31
|
* login sheet carries the login panel's, OTP slots' and Vipps button's.)
|
|
32
32
|
*/
|
|
33
|
-
export declare const BOOKING_LABEL_KEYS: readonly ("addChild.trigger" | "addChild.title" | "addChild.description.saved" | "addChild.description.local" | "addChild.name" | "addChild.birthYear" | "addChild.birthYearPlaceholder" | "addChild.birthMonth" | "addChild.birthMonthUnknown" | "addChild.notes" | "addChild.missing" | "addChild.submit" | "addChild.close" | "recap.label" | "recap.time" | "recap.line" | "recap.lineFor" | "recap.anyStylist" | "recap.total" | "recap.edit" | "recap.editLabel" | "recap.alsoFree" | "recap.swapTo" | "bookingSkeleton.opening" | "confirmation.heading" | "confirmation.serviceFor" | "confirmation.when" | "confirmation.party.heading" | "confirmation.party.line" | "confirmation.party.stylist" | "confirmation.party.total" | "confirmation.calendar" | "confirmation.manage" | "confirmation.manageFor" | "confirmation.portal" | "confirmation.startOver" | "details.heading" | "details.phone.label" | "details.phone.prefix" | "details.phone.error" | "details.name.label" | "details.known.label" | "details.known.for" | "details.known.self" | "details.known.adult" | "details.known.child" | "details.child.name" | "details.child.year" | "details.child.yearPlaceholder" | "details.email.label" | "details.email.help" | "details.email.lockedHelp" | "details.email.error" | "details.notes.label" | "details.notes.placeholder" | "details.terms.text" | "details.terms.link" | "details.marketing.text" | "details.error.maxParty" | "details.error.slotTaken" | "details.error.conflict" | "details.error.invalidInput" | "details.error.unconfigured" | "details.error.upstreamError" | "details.error.inProgress" | "details.call.link" | "details.call.none" | "details.submit" | "otp.label" | "vipps.button" | "vipps.unavailable" | "vipps.throttled" | "login.heading" | "login.codeHeading" | "login.or" | "login.continueEmail" | "login.emailIntro" | "login.emailLabel" | "login.sendCode" | "login.codeLabel" | "login.submitCode" | "login.codeHelp" | "login.codeHelpVipps" | "login.vippsSentTo" | "login.vippsSentToUnknown" | "login.cooldownWait" | "login.cooldownReady" | "login.resendIn" | "login.resend" | "login.switchEmail" | "login.useEmailInstead" | "login.notice.sent" | "login.notice.resent" | "login.notice.unreachable" | "login.notice.badEmail" | "login.notice.invalid" | "login.notice.throttled" | "login.notice.vippsInvalid" | "login.notice.vippsConflict" | "loginSheet.prompt" | "loginSheet.trigger" | "loginSheet.intro" | "loginSheet.vipps" | "loginSheet.close" | "time.heading" | "time.dayStrip.legend" | "time.nearest" | "time.current" | "time.nextFree" | "time.shortNotice" | "time.weekend.pct" | "time.weekend.mixed" | "time.month.show" | "time.month.hide" | "time.month.previous" | "time.month.next" | "time.month.heading" | "time.month.note" | "time.party.sequential" | "time.party.parallel" | "time.party.seat" | "time.party.seatSeparator" | "time.parallel.offer.two" | "time.parallel.offer.other" | "time.parallel.accept" | "time.empty.closed" | "time.empty.over" | "time.empty.full" | "time.empty.stylist" | "time.empty.unknown" | "time.empty.callLink" | "time.empty.callLinkAria" | "time.empty.callSuffix" | "time.empty.callPlain" | "time.loading" | "time.skeleton.dayPlaceholder" | "time.taken.message" | "time.taken.close" | "time.soonest.heading" | "time.soonest.pick" | "time.soonest.with" | "time.dayChip.free" | "time.dayChip.freeOne" | "time.dayChip.full" | "manage.heading" | "manage.serviceFallback" | "manage.card.named" | "manage.card.when" | "manage.card.whenWithStylist" | "manage.appointment.named" | "manage.appointment.namedSibilant" | "manage.appointment.unnamed" | "manage.ringUs" | "manage.ringUsOn" | "manage.callUs" | "manage.callUsOn" | "manage.error.windowPassed" | "manage.error.slotTaken" | "manage.error.notFound" | "manage.error.conflict" | "manage.error.invalidInput" | "manage.error.unavailable" | "manage.freeUntil" | "manage.remaining.in" | "manage.remaining.daysAndHours" | "manage.remaining.underMinute" | "manage.remaining.day.one" | "manage.remaining.day.other" | "manage.remaining.hour.one" | "manage.remaining.hour.other" | "manage.remaining.minute.one" | "manage.remaining.minute.other" | "manage.change" | "manage.cancel" | "manage.callToChange" | "manage.callToCancel" | "manage.blocked" | "manage.blocked.suffix" | "manage.blocked.plain" | "manage.calendar" | "manage.closed.cancelled" | "manage.closed.completed" | "manage.closed.noShow" | "manage.closed.pending" | "manage.closed.pendingCall" | "manage.closed.pendingCallPlain" | "manage.bookNew" | "manage.party.heading" | "manage.party.justThisOne" | "manage.party.unnamed" | "manage.party.note" | "manage.party.noteSuffix" | "manage.back" | "manage.reschedule.lead" | "manage.reschedule.leadWithStylist" | "manage.reschedule.failed" | "manage.reschedule.failedSuffix" | "manage.reschedule.loading" | "manage.move.question" | "manage.move.questionWithStylist" | "manage.move.surcharge" | "manage.move.confirm" | "manage.move.pickAnother" | "manage.cancel.question" | "manage.cancel.reasonLegend" | "manage.cancel.reason.illness" | "manage.cancel.reason.noLongerSuits" | "manage.cancel.reason.other" | "manage.cancel.confirm" | "manage.cancel.keep" | "manage.cancelled" | "manage.cancelled.findNew" | "manage.moved" | "manage.movedTo" | "manage.moved.view" | "manage.moved.noLink" | "manage.moved.noLinkSuffix" | "manage.portal" | "childCards.empty" | "childCards.neverVisited" | "childCards.lastVisit" | "childCards.lastVisitNoService" | "childCards.book" | "childCards.stylist" | "childCards.next" | "childCards.none" | "dataControls.heading" | "dataControls.export" | "dataControls.delete" | "dataControls.confirmHeading" | "dataControls.consequences" | "dataControls.confirmLabel" | "dataControls.confirmHelp" | "dataControls.wrongWord" | "dataControls.deleteForever" | "dataControls.cancel" | "dataControls.unreachable" | "dataControls.confirmWord" | "ageConfirm.heading" | "ageConfirm.lead" | "ageConfirm.legend" | "ageConfirm.birthYear" | "ageConfirm.birthMonth" | "ageConfirm.birthMonthUnknown" | "ageConfirm.confirm" | "ageConfirm.dismiss" | "familyEditor.heading" | "familyEditor.lead" | "familyEditor.empty" | "familyEditor.legend" | "familyEditor.add" | "familyEditor.name" | "familyEditor.birthYear" | "familyEditor.birthYearPlaceholder" | "familyEditor.birthMonth" | "familyEditor.birthMonthUnknown" | "familyEditor.stylist" | "familyEditor.stylistNone" | "familyEditor.stylistFormer" | "familyEditor.notes" | "familyEditor.notesPlaceholder" | "familyEditor.newRow" | "familyEditor.save" | "familyEditor.remove" | "familyEditor.removeNamed" | "familyEditor.removeRow" | "familyEditor.saved" | "familyEditor.savedNoDetails" | "familyEditor.partial" | "familyEditor.removed" | "familyEditor.unreachable" | "familyEditor.ageConfirmed" | "familyEditor.ageSaved" | "logout.button" | "logout.unreachable" | "portalShell.nav" | "portalShell.navBar" | "portalUnreachable.heading" | "portalUnreachable.body" | "portalUnreachable.retry" | "portalUnreachable.call" | "profileForm.heading" | "profileForm.firstName" | "profileForm.lastName" | "profileForm.phone" | "profileForm.email" | "profileForm.emailHelp" | "profileForm.save" | "profileForm.saved" | "profileForm.restored" | "profileForm.marketing" | "profileForm.unreachable" | "rebook.heading" | "rebook.empty" | "rebook.serviceFallback" | "rebook.anyStylist" | "rebook.cta" | "upcoming.heading" | "upcoming.empty" | "upcoming.book" | "upcoming.next" | "upcoming.today" | "upcoming.tomorrow" | "upcoming.inDays" | "upcoming.when" | "upcoming.serviceFallback" | "upcoming.anyStylist" | "upcoming.manage" | "upcoming.noManage" | "upcoming.noManageWithPhone" | "upcoming.call" | "vippsLink.heading" | "vippsLink.linked" | "vippsLink.pitch" | "vippsLink.button" | "vippsLink.success" | "vippsLink.conflict" | "vippsLink.failed" | "vippsLink.unavailable" | "vippsLink.throttled" | "history.heading" | "history.empty" | "history.serviceFallback" | "history.count.one" | "history.count.other" | "history.unknownAmount" | "service.multiHint" | "service.total" | "service.chooseFirst" | "service.continue" | "service.party.nothingBookable" | "service.tabDone" | "service.heading" | "service.categoriesLegend" | "service.sameAsLast" | "service.chooseOther" | "service.duration" | "service.phoneOnly" | "service.phoneOnlyLinkLabel" | "service.ageDivider.named" | "service.ageDivider.unnamed" | "service.party.listLabel" | "service.party.adultAlone" | "service.party.nothingFits" | "service.party.remove" | "stylist.heading" | "stylist.firstAvailable" | "stylist.firstAvailableSubtitle" | "stylist.firstAvailableBadge" | "stylist.pendingName" | "stylist.nextAvailable" | "stylist.loading" | "stylist.party.legend" | "stylist.party.sequential" | "stylist.party.sequentialMinutes" | "stylist.party.parallel.two" | "stylist.party.parallel.three" | "stylist.party.parallel.other" | "stylist.party.parallelMinutes" | "stylist.party.parallelNote.two" | "stylist.party.parallelNote.three" | "stylist.party.parallelNote.other" | "summary.placeholder.who" | "summary.placeholder.service" | "summary.next" | "summary.hint.who" | "summary.hint.service" | "summary.hint.when" | "who.heading" | "who.guest.groupLabel" | "who.guest.children.one" | "who.guest.children.other" | "who.guest.adult" | "who.guest.addedList" | "who.family.legend" | "who.family.self" | "who.family.limit" | "who.party.children" | "who.party.childrenNote" | "who.party.adult" | "who.party.adultNote" | "who.party.fewer" | "who.party.more" | "who.party.count")[];
|
|
33
|
+
export declare const BOOKING_LABEL_KEYS: readonly ("addChild.trigger" | "addChild.title" | "addChild.description.saved" | "addChild.description.local" | "addChild.name" | "addChild.birthYear" | "addChild.birthYearPlaceholder" | "addChild.birthMonth" | "addChild.birthMonthUnknown" | "addChild.notes" | "addChild.missing" | "addChild.submit" | "addChild.close" | "recap.label" | "recap.time" | "recap.line" | "recap.lineFor" | "recap.anyStylist" | "recap.total" | "recap.edit" | "recap.editLabel" | "recap.alsoFree" | "recap.swapTo" | "bookingSkeleton.opening" | "confirmation.heading" | "confirmation.serviceFor" | "confirmation.when" | "confirmation.party.heading" | "confirmation.party.line" | "confirmation.party.stylist" | "confirmation.party.total" | "confirmation.calendar" | "confirmation.manage" | "confirmation.manageFor" | "confirmation.portal" | "confirmation.startOver" | "details.heading" | "details.phone.label" | "details.phone.prefix" | "details.phone.error" | "details.name.label" | "details.known.label" | "details.known.for" | "details.known.self" | "details.known.adult" | "details.known.child" | "details.child.name" | "details.child.year" | "details.child.yearPlaceholder" | "details.email.label" | "details.email.help" | "details.email.lockedHelp" | "details.email.error" | "details.notes.label" | "details.notes.placeholder" | "details.terms.text" | "details.terms.link" | "details.marketing.text" | "details.error.maxParty" | "details.error.slotTaken" | "details.error.conflict" | "details.error.invalidInput" | "details.error.unconfigured" | "details.error.upstreamError" | "details.error.inProgress" | "details.call.link" | "details.call.none" | "details.submit" | "otp.label" | "vipps.button" | "vipps.unavailable" | "vipps.throttled" | "login.heading" | "login.codeHeading" | "login.or" | "login.continueEmail" | "login.emailIntro" | "login.emailLabel" | "login.sendCode" | "login.codeLabel" | "login.submitCode" | "login.codeHelp" | "login.codeHelpVipps" | "login.vippsSentTo" | "login.vippsSentToUnknown" | "login.cooldownWait" | "login.cooldownReady" | "login.resendIn" | "login.resend" | "login.switchEmail" | "login.useEmailInstead" | "login.notice.sent" | "login.notice.resent" | "login.notice.unreachable" | "login.notice.badEmail" | "login.notice.invalid" | "login.notice.throttled" | "login.notice.vippsInvalid" | "login.notice.vippsConflict" | "loginSheet.prompt" | "loginSheet.trigger" | "loginSheet.intro" | "loginSheet.vipps" | "loginSheet.close" | "time.heading" | "time.dayStrip.legend" | "time.nearest" | "time.current" | "time.nextFree" | "time.shortNotice" | "time.weekend.pct" | "time.weekend.mixed" | "time.month.show" | "time.month.hide" | "time.month.previous" | "time.month.next" | "time.month.heading" | "time.month.note" | "time.party.sequential" | "time.party.parallel" | "time.party.seat" | "time.party.seatSeparator" | "time.parallel.offer.two" | "time.parallel.offer.other" | "time.parallel.accept" | "time.empty.closed" | "time.empty.over" | "time.empty.full" | "time.empty.stylist" | "time.empty.unknown" | "time.empty.callLink" | "time.empty.callLinkAria" | "time.empty.callSuffix" | "time.empty.callPlain" | "time.loading" | "time.skeleton.dayPlaceholder" | "time.taken.message" | "time.taken.close" | "time.soonest.heading" | "time.soonest.pick" | "time.soonest.with" | "time.dayChip.free" | "time.dayChip.freeOne" | "time.dayChip.full" | "time.week.none" | "time.week.more" | "manage.heading" | "manage.serviceFallback" | "manage.card.named" | "manage.card.when" | "manage.card.whenWithStylist" | "manage.appointment.named" | "manage.appointment.namedSibilant" | "manage.appointment.unnamed" | "manage.ringUs" | "manage.ringUsOn" | "manage.callUs" | "manage.callUsOn" | "manage.error.windowPassed" | "manage.error.slotTaken" | "manage.error.notFound" | "manage.error.conflict" | "manage.error.invalidInput" | "manage.error.unavailable" | "manage.freeUntil" | "manage.remaining.in" | "manage.remaining.daysAndHours" | "manage.remaining.underMinute" | "manage.remaining.day.one" | "manage.remaining.day.other" | "manage.remaining.hour.one" | "manage.remaining.hour.other" | "manage.remaining.minute.one" | "manage.remaining.minute.other" | "manage.change" | "manage.cancel" | "manage.callToChange" | "manage.callToCancel" | "manage.blocked" | "manage.blocked.suffix" | "manage.blocked.plain" | "manage.calendar" | "manage.closed.cancelled" | "manage.closed.completed" | "manage.closed.noShow" | "manage.closed.pending" | "manage.closed.pendingCall" | "manage.closed.pendingCallPlain" | "manage.bookNew" | "manage.party.heading" | "manage.party.justThisOne" | "manage.party.unnamed" | "manage.party.note" | "manage.party.noteSuffix" | "manage.back" | "manage.reschedule.lead" | "manage.reschedule.leadWithStylist" | "manage.reschedule.failed" | "manage.reschedule.failedSuffix" | "manage.reschedule.loading" | "manage.move.question" | "manage.move.questionWithStylist" | "manage.move.surcharge" | "manage.move.confirm" | "manage.move.pickAnother" | "manage.cancel.question" | "manage.cancel.reasonLegend" | "manage.cancel.reason.illness" | "manage.cancel.reason.noLongerSuits" | "manage.cancel.reason.other" | "manage.cancel.confirm" | "manage.cancel.keep" | "manage.cancelled" | "manage.cancelled.findNew" | "manage.moved" | "manage.movedTo" | "manage.moved.view" | "manage.moved.noLink" | "manage.moved.noLinkSuffix" | "manage.portal" | "childCards.empty" | "childCards.neverVisited" | "childCards.lastVisit" | "childCards.lastVisitNoService" | "childCards.book" | "childCards.stylist" | "childCards.next" | "childCards.none" | "dataControls.heading" | "dataControls.export" | "dataControls.delete" | "dataControls.confirmHeading" | "dataControls.consequences" | "dataControls.confirmLabel" | "dataControls.confirmHelp" | "dataControls.wrongWord" | "dataControls.deleteForever" | "dataControls.cancel" | "dataControls.unreachable" | "dataControls.confirmWord" | "ageConfirm.heading" | "ageConfirm.lead" | "ageConfirm.legend" | "ageConfirm.birthYear" | "ageConfirm.birthMonth" | "ageConfirm.birthMonthUnknown" | "ageConfirm.confirm" | "ageConfirm.dismiss" | "familyEditor.heading" | "familyEditor.lead" | "familyEditor.empty" | "familyEditor.legend" | "familyEditor.add" | "familyEditor.name" | "familyEditor.birthYear" | "familyEditor.birthYearPlaceholder" | "familyEditor.birthMonth" | "familyEditor.birthMonthUnknown" | "familyEditor.stylist" | "familyEditor.stylistNone" | "familyEditor.stylistFormer" | "familyEditor.notes" | "familyEditor.notesPlaceholder" | "familyEditor.newRow" | "familyEditor.save" | "familyEditor.remove" | "familyEditor.removeNamed" | "familyEditor.removeRow" | "familyEditor.saved" | "familyEditor.savedNoDetails" | "familyEditor.partial" | "familyEditor.removed" | "familyEditor.unreachable" | "familyEditor.ageConfirmed" | "familyEditor.ageSaved" | "logout.button" | "logout.unreachable" | "portalShell.nav" | "portalShell.navBar" | "portalUnreachable.heading" | "portalUnreachable.body" | "portalUnreachable.retry" | "portalUnreachable.call" | "profileForm.heading" | "profileForm.firstName" | "profileForm.lastName" | "profileForm.phone" | "profileForm.email" | "profileForm.emailHelp" | "profileForm.save" | "profileForm.saved" | "profileForm.restored" | "profileForm.marketing" | "profileForm.unreachable" | "rebook.heading" | "rebook.empty" | "rebook.serviceFallback" | "rebook.anyStylist" | "rebook.cta" | "upcoming.heading" | "upcoming.empty" | "upcoming.book" | "upcoming.next" | "upcoming.today" | "upcoming.tomorrow" | "upcoming.inDays" | "upcoming.when" | "upcoming.serviceFallback" | "upcoming.anyStylist" | "upcoming.manage" | "upcoming.noManage" | "upcoming.noManageWithPhone" | "upcoming.call" | "vippsLink.heading" | "vippsLink.linked" | "vippsLink.pitch" | "vippsLink.button" | "vippsLink.success" | "vippsLink.conflict" | "vippsLink.failed" | "vippsLink.unavailable" | "vippsLink.throttled" | "history.heading" | "history.empty" | "history.serviceFallback" | "history.count.one" | "history.count.other" | "history.unknownAmount" | "service.multiHint" | "service.total" | "service.chooseFirst" | "service.continue" | "service.party.nothingBookable" | "service.tabDone" | "service.heading" | "service.categoriesLegend" | "service.sameAsLast" | "service.chooseOther" | "service.duration" | "service.phoneOnly" | "service.phoneOnlyLinkLabel" | "service.ageDivider.named" | "service.ageDivider.unnamed" | "service.party.listLabel" | "service.party.adultAlone" | "service.party.nothingFits" | "service.party.remove" | "stylist.heading" | "stylist.firstAvailable" | "stylist.firstAvailableSubtitle" | "stylist.firstAvailableBadge" | "stylist.pendingName" | "stylist.nextAvailable" | "stylist.loading" | "stylist.party.legend" | "stylist.party.sequential" | "stylist.party.sequentialMinutes" | "stylist.party.parallel.two" | "stylist.party.parallel.three" | "stylist.party.parallel.other" | "stylist.party.parallelMinutes" | "stylist.party.parallelNote.two" | "stylist.party.parallelNote.three" | "stylist.party.parallelNote.other" | "summary.placeholder.who" | "summary.placeholder.service" | "summary.next" | "summary.hint.who" | "summary.hint.service" | "summary.hint.when" | "who.heading" | "who.guest.groupLabel" | "who.guest.children.one" | "who.guest.children.other" | "who.guest.adult" | "who.guest.addedList" | "who.family.legend" | "who.family.self" | "who.family.limit" | "who.party.children" | "who.party.childrenNote" | "who.party.adult" | "who.party.adultNote" | "who.party.fewer" | "who.party.more" | "who.party.count")[];
|
|
34
34
|
export type BookingLabelKey = BookingRequiredLabelKey | BookingOptionalLabelKey;
|
|
35
35
|
/**
|
|
36
36
|
* Keys added after a screen first shipped, for a feature a caller opts into.
|
|
@@ -65,6 +65,8 @@ export interface StylistCardProps {
|
|
|
65
65
|
selectedClassName?: string;
|
|
66
66
|
/** Merged onto the next-opening line (the `nextAvailable` slot). */
|
|
67
67
|
nextAvailableClassName?: string;
|
|
68
|
+
/** `StylistScreen`'s `row`: draw the phone's avatar tile at every width. */
|
|
69
|
+
row?: boolean;
|
|
68
70
|
}
|
|
69
71
|
export interface StylistScreenComponents {
|
|
70
72
|
StylistCard?: ComponentType<StylistCardProps>;
|
|
@@ -116,6 +118,13 @@ export interface StylistScreenProps {
|
|
|
116
118
|
* as «there is more», not as a layout bug.
|
|
117
119
|
*/
|
|
118
120
|
edgeFade?: boolean;
|
|
121
|
+
/**
|
|
122
|
+
* Keep the phone's one row of avatars at every width instead of the column
|
|
123
|
+
* of cards from `md`, so a wide screen reaches the times without scrolling
|
|
124
|
+
* past every stylist. The chosen stylist's bio, which a tile has no room
|
|
125
|
+
* for, is shown under the row.
|
|
126
|
+
*/
|
|
127
|
+
row?: boolean;
|
|
119
128
|
/**
|
|
120
129
|
* Present only for a family. One child gets no picker: «one after the
|
|
121
130
|
* other» is not a choice when there is nobody to be after.
|
|
@@ -142,6 +151,6 @@ export interface StylistScreenProps {
|
|
|
142
151
|
classNames?: SlotClassNames<StylistScreenSlot>;
|
|
143
152
|
components?: StylistScreenComponents;
|
|
144
153
|
}
|
|
145
|
-
export declare function StylistScreen({ labels, format, now, serviceIds, resources, selectedResourceId, nextAvailableTs, nextAvailableLoading, loading, skeletonCount, pendingName, notice, onPick, firstAvailableFaces, edgeFade, party, classNames, components, }: StylistScreenProps): import("react").JSX.Element;
|
|
154
|
+
export declare function StylistScreen({ labels, format, now, serviceIds, resources, selectedResourceId, nextAvailableTs, nextAvailableLoading, loading, skeletonCount, pendingName, notice, onPick, firstAvailableFaces, edgeFade, row, party, classNames, components, }: StylistScreenProps): import("react").JSX.Element;
|
|
146
155
|
/** The default stylist option: avatar (photo or badge), name(s), bio, next opening. */
|
|
147
|
-
export declare function DefaultStylistCard({ ref, option, selected, tabIndex, onClick, labels, className, selectedClassName, nextAvailableClassName, }: StylistCardProps): import("react").JSX.Element;
|
|
156
|
+
export declare function DefaultStylistCard({ ref, option, selected, tabIndex, onClick, labels, className, selectedClassName, nextAvailableClassName, row, }: StylistCardProps): import("react").JSX.Element;
|
|
@@ -72,7 +72,7 @@ function bySize(labels, base, size, sizeWord) {
|
|
|
72
72
|
const suffix = SIZE_WORDS[size] ?? 'other';
|
|
73
73
|
return renderLabel(labels[`${base}.${suffix}`], { count: size, sizeWord: sizeWord ?? size });
|
|
74
74
|
}
|
|
75
|
-
export function StylistScreen({ labels, format, now, serviceIds, resources, selectedResourceId = null, nextAvailableTs, nextAvailableLoading = false, loading = false, skeletonCount = DEFAULT_SKELETON_COUNT, pendingName = null, notice = null, onPick, firstAvailableFaces = false, edgeFade = false, party, classNames, components, }) {
|
|
75
|
+
export function StylistScreen({ labels, format, now, serviceIds, resources, selectedResourceId = null, nextAvailableTs, nextAvailableLoading = false, loading = false, skeletonCount = DEFAULT_SKELETON_COUNT, pendingName = null, notice = null, onPick, firstAvailableFaces = false, edgeFade = false, row = false, party, classNames, components, }) {
|
|
76
76
|
// `every`, not `some`: the step asks one question for the whole visit, so a
|
|
77
77
|
// stylist who covers one of two services is not an answer — it is a refusal
|
|
78
78
|
// the engine issues at submit, offered here as a choice.
|
|
@@ -130,7 +130,7 @@ export function StylistScreen({ labels, format, now, serviceIds, resources, sele
|
|
|
130
130
|
}), classNames: classNames })] })), party?.mode === 'parallel' ? (_jsx("p", { className: "rounded-lg border border-border bg-card px-5 py-4 text-sm", children: bySize(labels, 'stylist.party.parallelNote', party.size, party.sizeWord) })) : (_jsx(StylistRadioGroup, { options: options, selectedResourceId: selectedResourceId,
|
|
131
131
|
// The held option takes one placeholder's place, so the row is
|
|
132
132
|
// exactly as long as it was and nothing after it moves.
|
|
133
|
-
skeletons: loading ? Math.max(0, skeletonCount - (options.length - 1)) : 0, busy: loading, onPick: onPick, labels: labels, classNames: classNames, edgeFade: edgeFade, Card: components?.StylistCard ?? DefaultStylistCard })), _jsx(LiveStatus, { text: loading ? labelText(labels['stylist.loading']) : notice })] }));
|
|
133
|
+
skeletons: loading ? Math.max(0, skeletonCount - (options.length - 1)) : 0, busy: loading, onPick: onPick, labels: labels, classNames: classNames, edgeFade: edgeFade, row: row, Card: components?.StylistCard ?? DefaultStylistCard })), row && party?.mode !== 'parallel' && (_jsx(ChosenBio, { options: options, selectedResourceId: selectedResourceId })), _jsx(LiveStatus, { text: loading ? labelText(labels['stylist.loading']) : notice })] }));
|
|
134
134
|
}
|
|
135
135
|
/**
|
|
136
136
|
* The options as one radiogroup with a roving tab stop.
|
|
@@ -139,7 +139,7 @@ export function StylistScreen({ labels, format, now, serviceIds, resources, sele
|
|
|
139
139
|
* stylist is cheap and reversible, and a group that needed Space after every
|
|
140
140
|
* arrow would be a list of buttons in a radiogroup's clothes.
|
|
141
141
|
*/
|
|
142
|
-
function StylistRadioGroup({ options, selectedResourceId, skeletons, busy, onPick, labels, classNames, edgeFade, Card, }) {
|
|
142
|
+
function StylistRadioGroup({ options, selectedResourceId, skeletons, busy, onPick, labels, classNames, edgeFade, row, Card, }) {
|
|
143
143
|
const groupRef = useRef(null);
|
|
144
144
|
const optionRefs = useRef([]);
|
|
145
145
|
const selectedIndex = options.findIndex((option) => option.resourceId === selectedResourceId);
|
|
@@ -196,9 +196,9 @@ function StylistRadioGroup({ options, selectedResourceId, skeletons, busy, onPic
|
|
|
196
196
|
// The last few pixels fade, so the cut-off card reads as «more this way».
|
|
197
197
|
// The trailing room lets the last card scroll wholly clear of the fade.
|
|
198
198
|
edgeFade &&
|
|
199
|
-
'pr-10 [mask-image:linear-gradient(to_right,#000_calc(100%-2.5rem),transparent)] md:[mask-image:none]', 'md:mx-0 md:h-auto md:snap-none md:flex-col md:gap-2 md:overflow-visible md:p-0'), children: [options.map((option, index) => (_jsx(Card, { ref: (node) => {
|
|
199
|
+
cn('pr-10 [mask-image:linear-gradient(to_right,#000_calc(100%-2.5rem),transparent)]', !row && 'md:[mask-image:none]'), !row && 'md:mx-0 md:h-auto md:snap-none md:flex-col md:gap-2 md:overflow-visible md:p-0'), children: [options.map((option, index) => (_jsx(Card, { ref: (node) => {
|
|
200
200
|
optionRefs.current[index] = node;
|
|
201
|
-
}, option: option, selected: option.resourceId === selectedResourceId, tabIndex: index === tabStop ? 0 : -1, onClick: () => onPick(option.resourceId), labels: labels, className: classNames?.card, selectedClassName: classNames?.cardSelected, nextAvailableClassName: classNames?.nextAvailable }, option.resourceId ?? 'first-available'))), Array.from({ length: skeletons }, (_, index) => (_jsx(SkeletonCard, {}, index)))] }));
|
|
201
|
+
}, option: option, selected: option.resourceId === selectedResourceId, tabIndex: index === tabStop ? 0 : -1, onClick: () => onPick(option.resourceId), labels: labels, className: classNames?.card, selectedClassName: classNames?.cardSelected, nextAvailableClassName: classNames?.nextAvailable, row: row }, option.resourceId ?? 'first-available'))), Array.from({ length: skeletons }, (_, index) => (_jsx(SkeletonCard, { row: row }, index)))] }));
|
|
202
202
|
}
|
|
203
203
|
/** The restored stylist's stand-in: a real, checked radio of the final size,
|
|
204
204
|
* with its next-opening row pulsing like every card still waiting for one. */
|
|
@@ -228,8 +228,10 @@ function ModeCard({ selected, onClick, title, subtitle, classNames, }) {
|
|
|
228
228
|
: 'border-border bg-card hover:border-primary', classNames?.modeCard, selected && classNames?.modeCardSelected), children: [_jsx("span", { className: "font-medium", children: title }), _jsx("span", { className: "text-sm font-normal text-muted-foreground tabular-nums", children: subtitle })] }));
|
|
229
229
|
}
|
|
230
230
|
/** The default stylist option: avatar (photo or badge), name(s), bio, next opening. */
|
|
231
|
-
export function DefaultStylistCard({ ref, option, selected, tabIndex, onClick, labels, className, selectedClassName, nextAvailableClassName, }) {
|
|
231
|
+
export function DefaultStylistCard({ ref, option, selected, tabIndex, onClick, labels, className, selectedClassName, nextAvailableClassName, row = false, }) {
|
|
232
232
|
const id = useId();
|
|
233
|
+
// From `md` the tile becomes a card, unless the screen keeps the row.
|
|
234
|
+
const md = (classes) => (row ? '' : classes);
|
|
233
235
|
const nameId = `${id}-name`;
|
|
234
236
|
const nextId = `${id}-next`;
|
|
235
237
|
const bioId = `${id}-bio`;
|
|
@@ -238,27 +240,39 @@ export function DefaultStylistCard({ ref, option, selected, tabIndex, onClick, l
|
|
|
238
240
|
// Ring and weight alongside the tint: the selected option has to be
|
|
239
241
|
// obvious on a monochrome screen too. On a phone the ring is on the
|
|
240
242
|
// avatar; from `md` it is on the card.
|
|
241
|
-
className: cn('group flex w-24 shrink-0 snap-start flex-col items-center gap-1 rounded-lg px-1 pt-1 text-center transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary', 'md:w-full md:shrink md:flex-row md:gap-4 md:border md:px-5 md:py-4 md:text-left', OPTION_HEIGHT, selected
|
|
242
|
-
? 'font-semibold md:border-primary md:bg-primary/10 md:ring-2 md:ring-primary'
|
|
243
|
-
: 'md:border-border md:bg-card md:hover:border-primary', className, selected && selectedClassName), children: [_jsx("span", { "aria-hidden": "true", "data-testid": "stylist-avatar", className: cn('flex size-14 shrink-0 items-center justify-center overflow-hidden rounded-full bg-secondary/30 font-semibold md:size-12', selected
|
|
244
|
-
? 'ring-2 ring-primary ring-offset-2 ring-offset-background md:ring-0 md:ring-offset-0'
|
|
245
|
-
: 'ring-1 ring-border md:ring-0'), children: _jsx(StylistAvatarContent, { photoUrl: photoUrl, faces: faces, badge: badge }) }), _jsxs("span", { className:
|
|
243
|
+
className: cn('group flex w-24 shrink-0 snap-start flex-col items-center gap-1 rounded-lg px-1 pt-1 text-center transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary', md('md:w-full md:shrink md:flex-row md:gap-4 md:border md:px-5 md:py-4 md:text-left'), row ? 'h-32' : OPTION_HEIGHT, selected
|
|
244
|
+
? cn('font-semibold', md('md:border-primary md:bg-primary/10 md:ring-2 md:ring-primary'))
|
|
245
|
+
: md('md:border-border md:bg-card md:hover:border-primary'), className, selected && selectedClassName), children: [_jsx("span", { "aria-hidden": "true", "data-testid": "stylist-avatar", className: cn('flex size-14 shrink-0 items-center justify-center overflow-hidden rounded-full bg-secondary/30 font-semibold', md('md:size-12'), selected
|
|
246
|
+
? cn('ring-2 ring-primary ring-offset-2 ring-offset-background', md('md:ring-0 md:ring-offset-0'))
|
|
247
|
+
: cn('ring-1 ring-border', md('md:ring-0'))), children: _jsx(StylistAvatarContent, { photoUrl: photoUrl, faces: faces, badge: badge, row: row }) }), _jsxs("span", { className: cn('flex w-full min-w-0 flex-col', md('md:w-auto')), children: [_jsx("span", { "aria-hidden": "true", className: cn('h-5 truncate text-sm font-medium', md('md:hidden')), children: option.shortName }), _jsx("span", { id: nameId, className: cn('hidden truncate font-medium', md('md:block')), children: name }), subtitle && (_jsx("span", { id: bioId, className: cn('hidden truncate text-sm text-muted-foreground', md('md:block')), children: subtitle })), availability ? (_jsxs("span", { id: nextId, "data-testid": "next-available-row", className: cn('line-clamp-2 h-8 text-xs leading-4 text-foreground tabular-nums', md('md:line-clamp-1 md:h-5 md:text-sm md:leading-5'), nextAvailableClassName), children: [_jsx("span", { className: cn('sr-only', md('md:not-sr-only')), children: labels['stylist.nextAvailable'] }), ' ', availability] })) : (reserveAvailability && (_jsx("span", { "data-testid": "next-available-row", "aria-hidden": "true", className: cn('flex h-8 justify-center pt-1', md('md:block md:h-5 md:py-1')), children: availabilityLoading && (_jsx("span", { className: cn('block h-3 w-16 animate-pulse rounded bg-muted', md('md:w-36')) })) })))] })] }));
|
|
246
248
|
}
|
|
247
249
|
/**
|
|
248
250
|
* What fills the avatar circle: the stylist's photo, a small group of faces
|
|
249
251
|
* for «first available» (`firstAvailableFaces`), or the badge.
|
|
250
252
|
*/
|
|
251
|
-
function StylistAvatarContent({ photoUrl, faces, badge, }) {
|
|
253
|
+
function StylistAvatarContent({ photoUrl, faces, badge, row, }) {
|
|
252
254
|
if (photoUrl) {
|
|
253
255
|
return (_jsx("img", { src: photoUrl, alt: "", className: "size-full object-cover", width: 56, height: 56, loading: "lazy", decoding: "async" }));
|
|
254
256
|
}
|
|
255
257
|
if (faces && faces.length >= 2) {
|
|
256
258
|
// A small group, for «any of these»: the faces of who it can be.
|
|
257
|
-
return (_jsx("span", { "data-testid": "stylist-faces", className: "relative size-full", children: faces.slice(0, FACE_POSITIONS.length).map((src, index) => (_jsx("img", { src: src, alt: "", width: 30, height: 30, decoding: "async", className: cn('absolute size-[30px] rounded-full object-cover ring-2 ring-background md:size-[26px]', FACE_POSITIONS[index]) }, src))) }));
|
|
259
|
+
return (_jsx("span", { "data-testid": "stylist-faces", className: "relative size-full", children: faces.slice(0, FACE_POSITIONS.length).map((src, index) => (_jsx("img", { src: src, alt: "", width: 30, height: 30, decoding: "async", className: cn('absolute size-[30px] rounded-full object-cover ring-2 ring-background', !row && 'md:size-[26px]', FACE_POSITIONS[index]) }, src))) }));
|
|
258
260
|
}
|
|
259
261
|
return badge;
|
|
260
262
|
}
|
|
261
263
|
/** A stylist option before there is a stylist: the same box, the same circle, at both widths. */
|
|
262
|
-
function SkeletonCard() {
|
|
263
|
-
|
|
264
|
+
function SkeletonCard({ row }) {
|
|
265
|
+
const md = (classes) => (row ? '' : classes);
|
|
266
|
+
return (_jsxs("div", { "aria-hidden": "true", "data-testid": "stylist-skeleton", className: cn('flex w-24 shrink-0 flex-col items-center gap-1 px-1 pt-1', md('md:w-full md:flex-row md:gap-4 md:rounded-lg md:border md:border-border md:bg-card md:px-5 md:py-4'), row ? 'h-32' : OPTION_HEIGHT), children: [_jsx("span", { className: cn('size-14 shrink-0 animate-pulse rounded-full bg-muted', md('md:size-12')) }), _jsxs("span", { className: cn('flex w-full min-w-0 flex-col items-center gap-2', md('md:w-auto md:flex-1 md:items-start')), children: [_jsx("span", { className: cn('h-4 w-14 animate-pulse rounded bg-muted', md('md:w-28')) }), _jsx("span", { className: cn('h-3 w-16 animate-pulse rounded bg-muted', md('md:w-40')) })] })] }));
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Under a `row`: the chosen stylist's bio, which a tile has no room for —
|
|
270
|
+
* «tar oftest de minste», read where the choice is made. Nothing for «first
|
|
271
|
+
* available», or a stylist without one.
|
|
272
|
+
*/
|
|
273
|
+
function ChosenBio({ options, selectedResourceId, }) {
|
|
274
|
+
const chosen = options.find((option) => option.resourceId === selectedResourceId);
|
|
275
|
+
if (!chosen || chosen.resourceId === null || !chosen.subtitle)
|
|
276
|
+
return null;
|
|
277
|
+
return (_jsxs("p", { "data-testid": "stylist-bio", className: "-mt-3 text-sm text-muted-foreground", children: [_jsx("span", { className: "font-medium text-foreground", children: chosen.name }), " \u00B7 ", chosen.subtitle] }));
|
|
264
278
|
}
|
|
@@ -28,11 +28,12 @@ export declare const TIME_SCREEN_LABEL_KEYS: readonly ["time.heading", "time.day
|
|
|
28
28
|
* - `time.empty.callLink|callLinkAria` `{phone}`
|
|
29
29
|
*/
|
|
30
30
|
/**
|
|
31
|
-
* Opt-in keys, for the `soonest` row and `
|
|
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
|
}
|