@medalsocial/meda 3.5.0 → 3.6.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.
|
@@ -13,7 +13,7 @@ import type { BookingFamilyMember, WizardAction, WizardError, WizardState } from
|
|
|
13
13
|
* booking — the keystrokes behind the per-child fields, which field has focus,
|
|
14
14
|
* whether submit has been pressed, and a fallback submission nonce.
|
|
15
15
|
*/
|
|
16
|
-
export declare const DETAILS_SCREEN_LABEL_KEYS: readonly ["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.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"];
|
|
16
|
+
export declare const DETAILS_SCREEN_LABEL_KEYS: readonly ["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"];
|
|
17
17
|
export type DetailsScreenLabelKey = (typeof DETAILS_SCREEN_LABEL_KEYS)[number];
|
|
18
18
|
export type DetailsScreenLabels = Record<DetailsScreenLabelKey, BookingLabel>;
|
|
19
19
|
/**
|
|
@@ -110,6 +110,8 @@ export interface DetailsScreenProps {
|
|
|
110
110
|
guardianPhone?: string | null;
|
|
111
111
|
/** When set, a link to the full terms is drawn after the terms sentence. */
|
|
112
112
|
termsHref?: string | null;
|
|
113
|
+
/** The e-mail is the address the guardian logged in with: shown, not editable. */
|
|
114
|
+
emailReadOnly?: boolean;
|
|
113
115
|
/** Draw the (optional, unticked) marketing opt-in. */
|
|
114
116
|
marketingConsent?: boolean;
|
|
115
117
|
format: BookingFormat;
|
|
@@ -119,4 +121,4 @@ export interface DetailsScreenProps {
|
|
|
119
121
|
}
|
|
120
122
|
/** Blank is fine (the field is optional). Anything else has to be reachable. */
|
|
121
123
|
export declare function looksLikeEmail(value: string): boolean;
|
|
122
|
-
export declare function DetailsScreen({ state, onChange, onSubmit, lines, totalOre, phoneLooksValid, normalisePhone, submitting, phone, submissionNonce: nonceProp, family, guardianPhone, termsHref, marketingConsent, format, labels, classNames, components, }: DetailsScreenProps): import("react").JSX.Element;
|
|
124
|
+
export declare function DetailsScreen({ state, onChange, onSubmit, lines, totalOre, phoneLooksValid, normalisePhone, submitting, phone, submissionNonce: nonceProp, family, guardianPhone, termsHref, emailReadOnly, marketingConsent, format, labels, classNames, components, }: DetailsScreenProps): import("react").JSX.Element;
|
|
@@ -42,6 +42,7 @@ export const DETAILS_SCREEN_LABEL_KEYS = [
|
|
|
42
42
|
'details.child.yearPlaceholder',
|
|
43
43
|
'details.email.label',
|
|
44
44
|
'details.email.help',
|
|
45
|
+
'details.email.lockedHelp',
|
|
45
46
|
'details.email.error',
|
|
46
47
|
'details.notes.label',
|
|
47
48
|
'details.notes.placeholder',
|
|
@@ -171,7 +172,7 @@ function isKnownChair(state, index) {
|
|
|
171
172
|
// ---------------------------------------------------------------------------
|
|
172
173
|
// Screen
|
|
173
174
|
// ---------------------------------------------------------------------------
|
|
174
|
-
export function DetailsScreen({ state, onChange, onSubmit, lines, totalOre, phoneLooksValid, normalisePhone, submitting = false, phone = null, submissionNonce: nonceProp, family, guardianPhone = null, termsHref = null, marketingConsent = false, format, labels, classNames, components, }) {
|
|
175
|
+
export function DetailsScreen({ state, onChange, onSubmit, lines, totalOre, phoneLooksValid, normalisePhone, submitting = false, phone = null, submissionNonce: nonceProp, family, guardianPhone = null, termsHref = null, emailReadOnly = false, marketingConsent = false, format, labels, classNames, components, }) {
|
|
175
176
|
const ids = useId();
|
|
176
177
|
const field = (name) => `${ids}-${name}`;
|
|
177
178
|
const phoneRef = useRef(null);
|
|
@@ -197,6 +198,9 @@ export function DetailsScreen({ state, onChange, onSubmit, lines, totalOre, phon
|
|
|
197
198
|
const phoneTyped = state.contact.phone.trim().length > 0;
|
|
198
199
|
const phoneValid = phoneLooksValid(state.contact.phone);
|
|
199
200
|
const emailValid = looksLikeEmail(state.contact.email);
|
|
201
|
+
// Only lock an address that is present and well-formed; a blank or malformed
|
|
202
|
+
// one stays editable so the parent is never stuck with an unfixable field.
|
|
203
|
+
const emailLocked = emailReadOnly && state.contact.email.trim().length > 0 && emailValid;
|
|
200
204
|
const showPhoneError = !phoneValid && (attempted || (phoneTyped && focused !== 'phone'));
|
|
201
205
|
const showEmailError = !emailValid && (attempted || focused !== 'email');
|
|
202
206
|
const showTermsError = attempted && !state.consentTerms;
|
|
@@ -295,7 +299,7 @@ export function DetailsScreen({ state, onChange, onSubmit, lines, totalOre, phon
|
|
|
295
299
|
// A seat step 1 already named is not asked again: a second answer could disagree.
|
|
296
300
|
isKnownChair(state, index) ? null : (_jsx(ChildFields
|
|
297
301
|
// biome-ignore lint/suspicious/noArrayIndexKey: two lines can share a service; the index is the identity
|
|
298
|
-
, { legend: state.items.length > 1 ? visitName(item) : null, nameId: field(`child-name-${index}`), yearId: field(`child-year-${index}`), value: childAt(index), family: family, labels: labels, classNames: classNames, FamilyChip: FamilyChip, onChange: (patch, personId) => setChild(index, patch, personId) }, index))), _jsxs(Field, { className: fieldClass, children: [_jsx(Field.Label, { htmlFor: field('email'), children: labels['details.email.label'] }), _jsx(Input, { id: field('email'), ref: emailRef, type: "email", autoComplete: "email", className: inputClass(), value: state.contact.email, "aria-describedby": field('email-help'), "aria-invalid": showEmailError || undefined, onFocus: () => setFocused('email'), onBlur: () => setFocused(null), onChange: (event) => setContact('email', event.target.value) }), _jsx(Field.Description, { id: field('email-help'), children: labels['details.email.help'] }), showEmailError && _jsx(Field.Error, { children: labels['details.email.error'] })] }), _jsxs(Field, { className: fieldClass, children: [_jsx(Field.Label, { htmlFor: field('notes'), children: labels['details.notes.label'] }), _jsx(Textarea, { id: field('notes'), rows: 3, className: slotClass(classNames, 'input', BOOKING_TEXTAREA_CLASS), placeholder: labelText(labels['details.notes.placeholder']), value: state.notes, onChange: (event) => onChange({ type: 'setNotes', value: event.target.value }) })] }), _jsxs(Field, { orientation: "horizontal", className: slotClass(classNames, 'consent'), children: [_jsx(Checkbox, { id: field('terms'), ref: termsRef, className: BOOKING_CHECKBOX_CLASS, checked: state.consentTerms, "aria-invalid": showTermsError || undefined, onCheckedChange: (accepted) => onChange({ type: 'setConsent', which: 'terms', accepted }) }), _jsx(Field.Label, { htmlFor: field('terms'), className: "cursor-pointer font-normal", children: labels['details.terms.text'] }), termsHref && (_jsx("a", { href: termsHref, className: "col-start-2 text-sm font-semibold text-primary underline underline-offset-4", children: labels['details.terms.link'] }))] }), marketingConsent && (_jsxs(Field, { orientation: "horizontal", className: slotClass(classNames, 'consent'), children: [_jsx(Checkbox, { id: field('marketing'), className: BOOKING_CHECKBOX_CLASS, checked: state.consentMarketing, onCheckedChange: (accepted) => onChange({ type: 'setConsent', which: 'marketing', accepted }) }), _jsx(Field.Label, { htmlFor: field('marketing'), className: "cursor-pointer font-normal", children: labels['details.marketing.text'] })] })), state.error && (_jsx(SubmitFailure, { error: state.error, phone: phone, format: format, labels: labels, className: slotClass(classNames, 'error', 'rounded-lg border border-destructive/40 bg-destructive/10 px-5 py-3 text-sm') })), _jsx(BookingButton, { size: "lg", className: slotClass(classNames, 'submit', 'w-full'), disabled: submitting, "aria-busy": submitting || undefined, onClick: handleSubmit, children: renderLabel(labels['details.submit'], { price }) })] }));
|
|
302
|
+
, { legend: state.items.length > 1 ? visitName(item) : null, nameId: field(`child-name-${index}`), yearId: field(`child-year-${index}`), value: childAt(index), family: family, labels: labels, classNames: classNames, FamilyChip: FamilyChip, onChange: (patch, personId) => setChild(index, patch, personId) }, index))), _jsxs(Field, { className: fieldClass, children: [_jsx(Field.Label, { htmlFor: field('email'), children: labels['details.email.label'] }), _jsx(Input, { id: field('email'), ref: emailRef, type: "email", autoComplete: "email", className: inputClass(), value: state.contact.email, readOnly: emailLocked || undefined, "aria-readonly": emailLocked || undefined, "aria-describedby": field('email-help'), "aria-invalid": showEmailError || undefined, onFocus: () => setFocused('email'), onBlur: () => setFocused(null), onChange: (event) => setContact('email', event.target.value) }), _jsx(Field.Description, { id: field('email-help'), children: emailLocked ? labels['details.email.lockedHelp'] : labels['details.email.help'] }), showEmailError && _jsx(Field.Error, { children: labels['details.email.error'] })] }), _jsxs(Field, { className: fieldClass, children: [_jsx(Field.Label, { htmlFor: field('notes'), children: labels['details.notes.label'] }), _jsx(Textarea, { id: field('notes'), rows: 3, className: slotClass(classNames, 'input', BOOKING_TEXTAREA_CLASS), placeholder: labelText(labels['details.notes.placeholder']), value: state.notes, onChange: (event) => onChange({ type: 'setNotes', value: event.target.value }) })] }), _jsxs(Field, { orientation: "horizontal", className: slotClass(classNames, 'consent'), children: [_jsx(Checkbox, { id: field('terms'), ref: termsRef, className: BOOKING_CHECKBOX_CLASS, checked: state.consentTerms, "aria-invalid": showTermsError || undefined, onCheckedChange: (accepted) => onChange({ type: 'setConsent', which: 'terms', accepted }) }), _jsx(Field.Label, { htmlFor: field('terms'), className: "cursor-pointer font-normal", children: labels['details.terms.text'] }), termsHref && (_jsx("a", { href: termsHref, className: "col-start-2 text-sm font-semibold text-primary underline underline-offset-4", children: labels['details.terms.link'] }))] }), marketingConsent && (_jsxs(Field, { orientation: "horizontal", className: slotClass(classNames, 'consent'), children: [_jsx(Checkbox, { id: field('marketing'), className: BOOKING_CHECKBOX_CLASS, checked: state.consentMarketing, onCheckedChange: (accepted) => onChange({ type: 'setConsent', which: 'marketing', accepted }) }), _jsx(Field.Label, { htmlFor: field('marketing'), className: "cursor-pointer font-normal", children: labels['details.marketing.text'] })] })), state.error && (_jsx(SubmitFailure, { error: state.error, phone: phone, format: format, labels: labels, className: slotClass(classNames, 'error', 'rounded-lg border border-destructive/40 bg-destructive/10 px-5 py-3 text-sm') })), _jsx(BookingButton, { size: "lg", className: slotClass(classNames, 'submit', 'w-full'), disabled: submitting, "aria-busy": submitting || undefined, onClick: handleSubmit, children: renderLabel(labels['details.submit'], { price }) })] }));
|
|
299
303
|
}
|
|
300
304
|
/**
|
|
301
305
|
* The name and birth year for one line. Wrapped in a fieldset only when there
|
|
@@ -29,7 +29,7 @@ import { WHO_SCREEN_LABEL_KEYS } from './who-screen.js';
|
|
|
29
29
|
* (Screens that embed another screen already include its keys, e.g. the
|
|
30
30
|
* login sheet carries the login panel's, OTP slots' and Vipps button's.)
|
|
31
31
|
*/
|
|
32
|
-
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" | "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.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.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" | "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" | "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")[];
|
|
32
|
+
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" | "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" | "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" | "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")[];
|
|
33
33
|
export type BookingLabelKey = BookingRequiredLabelKey | BookingOptionalLabelKey;
|
|
34
34
|
/**
|
|
35
35
|
* Keys added after a screen first shipped, for a feature a caller opts into.
|
|
@@ -4,8 +4,8 @@ import { type OtpSlotsLabels, type OtpSlotsSlot } from './otp-slots.js';
|
|
|
4
4
|
import { type SlotClassNames } from './slots.js';
|
|
5
5
|
import type { BookingGuardian } from './types.js';
|
|
6
6
|
import { type VippsButtonLabels, type VippsButtonSlot, type VippsStartAction } from './vipps-button.js';
|
|
7
|
-
export declare const LOGIN_PANEL_OWN_LABEL_KEYS: readonly ["login.heading", "login.codeHeading", "login.or", "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"];
|
|
8
|
-
export declare const LOGIN_PANEL_LABEL_KEYS: readonly ["login.heading", "login.codeHeading", "login.or", "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", "otp.label", "vipps.button", "vipps.unavailable", "vipps.throttled"];
|
|
7
|
+
export declare const LOGIN_PANEL_OWN_LABEL_KEYS: readonly ["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"];
|
|
8
|
+
export declare const LOGIN_PANEL_LABEL_KEYS: readonly ["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", "otp.label", "vipps.button", "vipps.unavailable", "vipps.throttled"];
|
|
9
9
|
/**
|
|
10
10
|
* Placeholders: `login.codeHelp` `{email}`, `login.vippsSentTo` `{to}`,
|
|
11
11
|
* `login.cooldownWait` `{seconds}`, `login.resendIn` `{time}` («0:30»).
|
|
@@ -20,13 +20,13 @@ export type LoginPanelLabels = Record<(typeof LOGIN_PANEL_OWN_LABEL_KEYS)[number
|
|
|
20
20
|
* - `divider`: the «or» rule between Vipps and e-mail.
|
|
21
21
|
* - `links`: the row under the code form; `link`: each of its buttons.
|
|
22
22
|
*/
|
|
23
|
-
export type LoginPanelSlot = 'root' | 'notice' | 'form' | 'input' | 'submit' | 'divider' | 'links' | 'link';
|
|
23
|
+
export type LoginPanelSlot = 'root' | 'notice' | 'form' | 'input' | 'submit' | 'divider' | 'emailButton' | 'links' | 'link';
|
|
24
24
|
/** What asking for a code answered. `invalidEmail`: the address was refused. */
|
|
25
25
|
export type LoginStartResult = {
|
|
26
26
|
ok: true;
|
|
27
27
|
} | {
|
|
28
28
|
ok: false;
|
|
29
|
-
reason: 'unreachable' | 'invalidEmail';
|
|
29
|
+
reason: 'unreachable' | 'invalidEmail' | 'throttled';
|
|
30
30
|
};
|
|
31
31
|
/** What checking a code answered. `conflict`: the Vipps account belongs to another profile. */
|
|
32
32
|
export type LoginVerifyResult = {
|
|
@@ -53,6 +53,8 @@ export interface LoginPanelMemory {
|
|
|
53
53
|
sentTo: string;
|
|
54
54
|
/** When «send a new code» opens again (epoch ms); `0` before any code went out. */
|
|
55
55
|
resendAt: number;
|
|
56
|
+
/** `emailCollapsed` only: the e-mail form has been opened. */
|
|
57
|
+
emailOpen?: boolean;
|
|
56
58
|
}
|
|
57
59
|
export declare const FRESH_LOGIN_MEMORY: LoginPanelMemory;
|
|
58
60
|
/** Where a Vipps return that asks for the code starts: on the code, for `to`. */
|
|
@@ -75,6 +77,8 @@ export interface LoginPanelProps {
|
|
|
75
77
|
onVipps?: VippsStartAction;
|
|
76
78
|
/** Where a Vipps login should land. */
|
|
77
79
|
vippsNext?: string | null;
|
|
80
|
+
/** Show e-mail as a second button under Vipps; the form opens on tap. Ignored without `onVipps`. */
|
|
81
|
+
emailCollapsed?: boolean;
|
|
78
82
|
/** Replaces the Vipps button's label. */
|
|
79
83
|
vippsLabel?: string;
|
|
80
84
|
/** Start on the Vipps confirm code screen (uncontrolled memory only). */
|
|
@@ -106,4 +110,4 @@ export interface LoginPanelProps {
|
|
|
106
110
|
* waits between asks, so an impatient tap does not spend the rate-limit
|
|
107
111
|
* allowance on a code that is still valid.
|
|
108
112
|
*/
|
|
109
|
-
export declare function LoginPanel({ labels, onStartLogin, onVerify, onSignedIn, onVipps, vippsNext, vippsLabel, vippsConfirm, memory: memoryProp, onMemoryChange, heading, intro, resendCooldownMs, classNames, otpClassNames, vippsClassNames, }: LoginPanelProps): import("react").JSX.Element;
|
|
113
|
+
export declare function LoginPanel({ labels, onStartLogin, onVerify, onSignedIn, onVipps, vippsNext, vippsLabel, emailCollapsed, vippsConfirm, memory: memoryProp, onMemoryChange, heading, intro, resendCooldownMs, classNames, otpClassNames, vippsClassNames, }: LoginPanelProps): import("react").JSX.Element;
|
|
@@ -15,6 +15,7 @@ export const LOGIN_PANEL_OWN_LABEL_KEYS = [
|
|
|
15
15
|
'login.heading',
|
|
16
16
|
'login.codeHeading',
|
|
17
17
|
'login.or',
|
|
18
|
+
'login.continueEmail',
|
|
18
19
|
'login.emailIntro',
|
|
19
20
|
'login.emailLabel',
|
|
20
21
|
'login.sendCode',
|
|
@@ -56,6 +57,12 @@ export function loginMemoryFor(vippsConfirm) {
|
|
|
56
57
|
? { mode: 'vipps', email: '', sentTo: vippsConfirm.to ?? '', resendAt: 0 }
|
|
57
58
|
: FRESH_LOGIN_MEMORY;
|
|
58
59
|
}
|
|
60
|
+
/** Which notice each refused send shows. */
|
|
61
|
+
const START_NOTICE = {
|
|
62
|
+
invalidEmail: 'badEmail',
|
|
63
|
+
throttled: 'throttled',
|
|
64
|
+
unreachable: 'unreachable',
|
|
65
|
+
};
|
|
59
66
|
/** The notices that describe a mistake in the field they sit under. */
|
|
60
67
|
const FIELD_ERRORS = new Set([
|
|
61
68
|
'badEmail',
|
|
@@ -92,11 +99,11 @@ function verifyNotice(reason, vipps) {
|
|
|
92
99
|
* waits between asks, so an impatient tap does not spend the rate-limit
|
|
93
100
|
* allowance on a code that is still valid.
|
|
94
101
|
*/
|
|
95
|
-
export function LoginPanel({ labels, onStartLogin, onVerify, onSignedIn, onVipps, vippsNext = null, vippsLabel, vippsConfirm = null, memory: memoryProp, onMemoryChange, heading = () => null, intro, resendCooldownMs = 30_000, classNames, otpClassNames, vippsClassNames, }) {
|
|
102
|
+
export function LoginPanel({ labels, onStartLogin, onVerify, onSignedIn, onVipps, vippsNext = null, vippsLabel, emailCollapsed = false, vippsConfirm = null, memory: memoryProp, onMemoryChange, heading = () => null, intro, resendCooldownMs = 30_000, classNames, otpClassNames, vippsClassNames, }) {
|
|
96
103
|
const id = useId();
|
|
97
104
|
const [heldHere, setHeldHere] = useState(() => loginMemoryFor(vippsConfirm));
|
|
98
105
|
const controlled = memoryProp !== undefined && onMemoryChange !== undefined;
|
|
99
|
-
const { mode, email, sentTo, resendAt } = controlled ? memoryProp : heldHere;
|
|
106
|
+
const { mode, email, sentTo, resendAt, emailOpen = false } = controlled ? memoryProp : heldHere;
|
|
100
107
|
const setMemory = controlled ? onMemoryChange : setHeldHere;
|
|
101
108
|
const remember = (patch) => setMemory((previous) => ({ ...previous, ...patch }));
|
|
102
109
|
const setEmail = (next) => remember({ email: next });
|
|
@@ -112,6 +119,10 @@ export function LoginPanel({ labels, onStartLogin, onVerify, onSignedIn, onVipps
|
|
|
112
119
|
const verifying = useRef(false);
|
|
113
120
|
const [checking, setChecking] = useState(false);
|
|
114
121
|
const codeRef = useRef(null);
|
|
122
|
+
const emailRef = useRef(null);
|
|
123
|
+
const collapsed = emailCollapsed && !!onVipps && !emailOpen;
|
|
124
|
+
/** Focus only after a tap opened the form, not when memory restores it. */
|
|
125
|
+
const focusEmail = useRef(false);
|
|
115
126
|
const emailId = `${id}-email`;
|
|
116
127
|
const codeId = `${id}-code`;
|
|
117
128
|
const noticeId = `${id}-notice`;
|
|
@@ -136,6 +147,12 @@ export function LoginPanel({ labels, onStartLogin, onVerify, onSignedIn, onVipps
|
|
|
136
147
|
setFocusCode(false);
|
|
137
148
|
codeRef.current?.querySelector('input')?.focus();
|
|
138
149
|
}, [focusCode, pending, mode]);
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
if (!focusEmail.current || collapsed)
|
|
152
|
+
return;
|
|
153
|
+
focusEmail.current = false;
|
|
154
|
+
emailRef.current?.focus();
|
|
155
|
+
}, [collapsed]);
|
|
139
156
|
function requestCode(address, resend) {
|
|
140
157
|
startTransition(async () => {
|
|
141
158
|
let result;
|
|
@@ -146,7 +163,7 @@ export function LoginPanel({ labels, onStartLogin, onVerify, onSignedIn, onVipps
|
|
|
146
163
|
result = { ok: false, reason: 'unreachable' };
|
|
147
164
|
}
|
|
148
165
|
if (!result.ok) {
|
|
149
|
-
setNotice(result.reason
|
|
166
|
+
setNotice(START_NOTICE[result.reason]);
|
|
150
167
|
return;
|
|
151
168
|
}
|
|
152
169
|
const sentAt = Date.now();
|
|
@@ -206,7 +223,8 @@ export function LoginPanel({ labels, onStartLogin, onVerify, onSignedIn, onVipps
|
|
|
206
223
|
verify(code);
|
|
207
224
|
}
|
|
208
225
|
function switchEmail() {
|
|
209
|
-
|
|
226
|
+
// They already chose e-mail: back to the open form, not the two buttons.
|
|
227
|
+
remember({ mode: 'start', emailOpen: true });
|
|
210
228
|
setCode('');
|
|
211
229
|
setNotice(null);
|
|
212
230
|
}
|
|
@@ -228,7 +246,10 @@ export function LoginPanel({ labels, onStartLogin, onVerify, onSignedIn, onVipps
|
|
|
228
246
|
const link = slotClass(classNames, 'link', 'font-semibold text-primary underline underline-offset-4 disabled:no-underline disabled:opacity-60');
|
|
229
247
|
return (_jsxs("div", { className: slotClass(classNames, 'root', 'space-y-6'), children: [heading(renderLabel(labels[mode === 'start' ? 'login.heading' : 'login.codeHeading'])), mode === 'vipps' && (_jsx("p", { className: "text-muted-foreground", children: sentTo === ''
|
|
230
248
|
? labels['login.vippsSentToUnknown']
|
|
231
|
-
: renderLabel(labels['login.vippsSentTo'], { to: sentTo }) })), liveRegion, cooldownRegion, mode === 'start' ? (_jsxs(_Fragment, { children: [intro, onVipps && (_jsxs(_Fragment, { children: [_jsx(VippsButton, { onVipps: onVipps, next: vippsNext, label: vippsLabel, labels: labels, classNames: vippsClassNames }), _jsxs("div", { className: slotClass(classNames, 'divider', 'flex items-center gap-4 text-sm text-muted-foreground'), "aria-hidden": "true", children: [_jsx("span", { className: "h-px flex-1 bg-border" }), labels['login.or'], _jsx("span", { className: "h-px flex-1 bg-border" })] })
|
|
249
|
+
: renderLabel(labels['login.vippsSentTo'], { to: sentTo }) })), liveRegion, cooldownRegion, mode === 'start' ? (_jsxs(_Fragment, { children: [!collapsed && intro, onVipps && (_jsxs(_Fragment, { children: [_jsx(VippsButton, { onVipps: onVipps, next: vippsNext, label: vippsLabel, labels: labels, classNames: vippsClassNames }), !emailCollapsed && (_jsxs("div", { className: slotClass(classNames, 'divider', 'flex items-center gap-4 text-sm text-muted-foreground'), "aria-hidden": "true", children: [_jsx("span", { className: "h-px flex-1 bg-border" }), labels['login.or'], _jsx("span", { className: "h-px flex-1 bg-border" })] }))] })), collapsed ? (_jsx(BookingButton, { type: "button", variant: "outline", size: "lg", className: slotClass(classNames, 'emailButton', 'w-full'), onClick: () => {
|
|
250
|
+
focusEmail.current = true;
|
|
251
|
+
remember({ emailOpen: true });
|
|
252
|
+
}, children: labels['login.continueEmail'] })) : (_jsxs(_Fragment, { children: [_jsx("p", { className: "text-muted-foreground", children: labels['login.emailIntro'] }), _jsxs("form", { onSubmit: submitEmail, className: slotClass(classNames, 'form', 'space-y-6'), children: [_jsxs(Field, { children: [_jsx(Field.Label, { htmlFor: emailId, children: labels['login.emailLabel'] }), _jsx(Input, { ref: emailRef, id: emailId, type: "email", autoComplete: "email", required: true, value: email, "aria-describedby": noticeText ? noticeId : undefined, "aria-invalid": (isFieldError && notice !== 'throttled') || undefined, onChange: (event) => setEmail(event.target.value), className: slotClass(classNames, 'input', BOOKING_INPUT_CLASS) })] }), _jsx(BookingButton, { type: "submit", size: "lg", className: slotClass(classNames, 'submit', 'w-full'), disabled: pending, children: labels['login.sendCode'] })] })] }))] })) : (_jsxs(_Fragment, { children: [_jsxs("form", { onSubmit: submitCode, className: slotClass(classNames, 'form', 'space-y-6'), children: [_jsxs(Field, { children: [_jsx(Field.Label, { htmlFor: codeId, children: labels['login.codeLabel'] }), _jsx("div", { ref: codeRef, children: _jsx(OtpSlots, { id: codeId, value: code,
|
|
232
253
|
// Read-only rather than disabled while a request is out, so
|
|
233
254
|
// the caret stays in the field instead of falling out of a
|
|
234
255
|
// dialog; a wrong code then only has to empty it.
|
|
@@ -3,7 +3,7 @@ import { type LoginPanelLabels, type LoginPanelProps } from './login-panel.js';
|
|
|
3
3
|
import { type SlotClassNames } from './slots.js';
|
|
4
4
|
import type { BookingGuardian } from './types.js';
|
|
5
5
|
export declare const LOGIN_SHEET_OWN_LABEL_KEYS: readonly ["loginSheet.prompt", "loginSheet.trigger", "loginSheet.intro", "loginSheet.vipps", "loginSheet.close"];
|
|
6
|
-
export declare const LOGIN_SHEET_LABEL_KEYS: readonly ["loginSheet.prompt", "loginSheet.trigger", "loginSheet.intro", "loginSheet.vipps", "loginSheet.close", "login.heading", "login.codeHeading", "login.or", "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", "otp.label", "vipps.button", "vipps.unavailable", "vipps.throttled"];
|
|
6
|
+
export declare const LOGIN_SHEET_LABEL_KEYS: readonly ["loginSheet.prompt", "loginSheet.trigger", "loginSheet.intro", "loginSheet.vipps", "loginSheet.close", "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", "otp.label", "vipps.button", "vipps.unavailable", "vipps.throttled"];
|
|
7
7
|
/** `loginSheet.prompt` holds a `{trigger}` placeholder where the trigger button goes. */
|
|
8
8
|
export type LoginSheetLabels = Record<(typeof LOGIN_SHEET_OWN_LABEL_KEYS)[number], BookingLabel> & LoginPanelLabels;
|
|
9
9
|
/**
|