@medalsocial/meda 3.2.0 → 3.3.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.
- package/dist/booking/confirmation.js +9 -9
- package/dist/booking/details-screen.js +6 -4
- package/dist/booking/index.d.ts +1 -0
- package/dist/booking/index.js +1 -0
- package/dist/booking/internal/label-parts.d.ts +20 -0
- package/dist/booking/internal/label-parts.js +39 -1
- package/dist/booking/login-panel.js +5 -5
- package/dist/booking/manage-screen.js +14 -11
- package/dist/booking/portal/age-confirm-card.js +3 -3
- package/dist/booking/portal/child-cards.js +5 -5
- package/dist/booking/portal/data-controls.js +2 -1
- package/dist/booking/portal/portal-unreachable.js +2 -2
- package/dist/booking/portal/upcoming-bookings.js +7 -11
- package/dist/booking/portal/visit-history.js +2 -2
- package/dist/booking/service-screen.d.ts +5 -2
- package/dist/booking/service-screen.js +7 -5
- package/dist/booking/stylist-screen.d.ts +13 -2
- package/dist/booking/stylist-screen.js +14 -11
- package/dist/booking/time-screen.js +10 -9
- package/dist/booking/who-screen.js +4 -4
- package/package.json +8 -8
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { joinLabelParts, renderLabel } from './internal/label-parts.js';
|
|
3
4
|
import { BookingButton } from './internal/ui.js';
|
|
4
5
|
import { fillLabel } from './labels.js';
|
|
5
6
|
import { slotClass } from './slots.js';
|
|
@@ -50,24 +51,21 @@ export function Confirmation({ lines: rawLines, startTs, totalOre, address = nul
|
|
|
50
51
|
}));
|
|
51
52
|
const linkClass = slotClass(classNames, 'link', 'font-semibold text-primary underline underline-offset-4');
|
|
52
53
|
const first = lines[0];
|
|
53
|
-
const singleCard = first ? (_jsx("p", { className: slotClass(classNames, 'card', 'rounded-lg border border-border bg-card px-5 py-4 font-medium'), children: [
|
|
54
|
+
const singleCard = first ? (_jsx("p", { className: slotClass(classNames, 'card', 'rounded-lg border border-border bg-card px-5 py-4 font-medium'), children: joinLabelParts([
|
|
54
55
|
first.item.bookedForName
|
|
55
|
-
?
|
|
56
|
+
? renderLabel(labels['confirmation.serviceFor'], {
|
|
56
57
|
service: first.item.service.name,
|
|
57
58
|
name: first.item.bookedForName,
|
|
58
59
|
})
|
|
59
60
|
: first.item.service.name,
|
|
60
61
|
first.stylistName,
|
|
61
|
-
|
|
62
|
+
renderLabel(labels['confirmation.when'], {
|
|
62
63
|
day: clock.dayLabel(startTs, now),
|
|
63
64
|
time: clock.formatTime(startTs),
|
|
64
65
|
}),
|
|
65
66
|
// This line's own price, which for one person is the whole total.
|
|
66
67
|
format.price(first.priceOre),
|
|
67
|
-
]
|
|
68
|
-
// Filtered, not conditionally joined: an absent stylist closes the gap.
|
|
69
|
-
.filter((part) => Boolean(part))
|
|
70
|
-
.join(SEPARATOR) })) : null;
|
|
68
|
+
], SEPARATOR) })) : null;
|
|
71
69
|
return (_jsxs("section", { "aria-labelledby": "booking-confirmed-heading", className: slotClass(classNames, 'root', 'space-y-6'), children: [_jsx("h2", { id: "booking-confirmed-heading", tabIndex: -1, className: slotClass(classNames, 'heading', 'font-sans text-2xl font-bold outline-none md:text-3xl'), children: labels['confirmation.heading'] }), lines.length > 1 ? (_jsx(PartyCard, { lines: lines, totalOre: totalOre, now: now, format: format, labels: labels, classNames: classNames, PartyLine: components?.PartyLine ?? DefaultPartyLine })) : (singleCard), address && (_jsx("p", { className: slotClass(classNames, 'address', 'text-sm text-muted-foreground'), children: address })), _jsxs("div", { className: "flex flex-wrap gap-4", children: [calendarHref && (_jsx("a", { href: calendarHref, download: calendarFileName, className: linkClass, children: labels['confirmation.calendar'] })), lines.length === 1 && first?.manageHref && (_jsx("a", { href: first.manageHref, className: linkClass, children: labels['confirmation.manage'] }))] }), portalHref && (_jsx("p", { className: "text-sm", children: _jsx("a", { href: portalHref, className: linkClass, children: labels['confirmation.portal'] }) })), onStartOver && (_jsx("div", { children: _jsx(BookingButton, { variant: "outline", className: slotClass(classNames, 'startOver'), onClick: onStartOver, children: labels['confirmation.startOver'] }) }))] }));
|
|
72
70
|
}
|
|
73
71
|
/**
|
|
@@ -77,7 +75,9 @@ export function Confirmation({ lines: rawLines, startTs, totalOre, address = nul
|
|
|
77
75
|
function PartyCard({ lines, totalOre, now, format, labels, classNames, PartyLine, }) {
|
|
78
76
|
const { clock } = format;
|
|
79
77
|
const firstStart = lines[0]?.startTs ?? 0;
|
|
80
|
-
return (_jsxs("div", { className: slotClass(classNames, 'partyCard', 'space-y-2 rounded-lg border border-border bg-card px-5 py-4'), children: [_jsx("p", { className: "font-semibold", children:
|
|
78
|
+
return (_jsxs("div", { className: slotClass(classNames, 'partyCard', 'space-y-2 rounded-lg border border-border bg-card px-5 py-4'), children: [_jsx("p", { className: "font-semibold", children: renderLabel(labels['confirmation.party.heading'], {
|
|
79
|
+
day: clock.dayLabel(firstStart, now),
|
|
80
|
+
}) }), _jsx("ul", { className: "space-y-1", children: lines.map((line, index) => {
|
|
81
81
|
const time = clock.formatTime(line.startTs);
|
|
82
82
|
const text = fillLabel(labels['confirmation.party.line'], {
|
|
83
83
|
time,
|
|
@@ -98,5 +98,5 @@ function PartyCard({ lines, totalOre, now, format, labels, classNames, PartyLine
|
|
|
98
98
|
who: line.item.bookedForName ?? line.item.service.name,
|
|
99
99
|
time,
|
|
100
100
|
}), linkClassName: slotClass(classNames, 'link', 'text-sm font-semibold text-primary underline underline-offset-4') }, index));
|
|
101
|
-
}) }), _jsx("p", { className: "font-semibold", children:
|
|
101
|
+
}) }), _jsx("p", { className: "font-semibold", children: renderLabel(labels['confirmation.party.total'], { total: format.price(totalOre) }) })] }));
|
|
102
102
|
}
|
|
@@ -5,6 +5,7 @@ import { Checkbox } from '../primitives/checkbox.js';
|
|
|
5
5
|
import { Field } from '../primitives/field.js';
|
|
6
6
|
import { Input } from '../primitives/input.js';
|
|
7
7
|
import { Textarea } from '../primitives/textarea.js';
|
|
8
|
+
import { renderLabel } from './internal/label-parts.js';
|
|
8
9
|
import { BOOKING_CHECKBOX_CLASS, BOOKING_INPUT_CLASS, BOOKING_TEXTAREA_CLASS, BookingButton, } from './internal/ui.js';
|
|
9
10
|
import { fillLabel } from './labels.js';
|
|
10
11
|
import { slotClass } from './slots.js';
|
|
@@ -285,11 +286,11 @@ export function DetailsScreen({ state, onChange, onSubmit, lines, totalOre, phon
|
|
|
285
286
|
}
|
|
286
287
|
const inputClass = (...extra) => slotClass(classNames, 'input', BOOKING_INPUT_CLASS, ...extra);
|
|
287
288
|
const fieldClass = slotClass(classNames, 'field');
|
|
288
|
-
return (_jsxs("section", { "aria-labelledby": "booking-details-heading", className: slotClass(classNames, 'root', 'space-y-6'), children: [_jsx("h2", { id: "booking-details-heading", tabIndex: -1, className: slotClass(classNames, 'heading', 'font-sans text-2xl font-bold outline-none md:text-3xl'), children: labels['details.heading'] }), _jsxs(Field, { className: fieldClass, children: [_jsx(Field.Label, { htmlFor: field('phone'), children: labels['details.phone.label'] }), _jsxs("div", { className: "flex items-stretch", children: [phonePrefix && (_jsx("span", { id: field('phone-prefix'), className: "flex h-12 items-center rounded-l-[4px] border border-r-0 border-input bg-muted px-4 text-base text-muted-foreground", children: phonePrefix })), _jsx(Input, { id: field('phone'), ref: phoneRef, type: "tel", inputMode: "numeric", autoComplete: "tel-national", className: inputClass(phonePrefix ? 'rounded-l-none' : ''), value: state.contact.phone, "aria-describedby": phonePrefix ? field('phone-prefix') : undefined, "aria-invalid": showPhoneError || undefined, onFocus: () => setFocused('phone'), onBlur: () => setFocused(null), onChange: (event) => setContact('phone', event.target.value) })] }), showPhoneError && _jsx(Field.Error, { children: labels['details.phone.error'] })] }), _jsxs(Field, { className: fieldClass, children: [_jsx(Field.Label, { htmlFor: field('name'), children: labels['details.name.label'] }), _jsx(Input, { id: field('name'), autoComplete: "name", className: inputClass(), value: state.contact.name, onChange: (event) => setContact('name', event.target.value) })] }), state.items.some((_, index) => isKnownChair(state, index)) && (_jsx("ul", { "aria-label": labels['details.known.label'], className: "space-y-1 text-sm", children: state.items.map((item, index) => isKnownChair(state, index) ? (_jsxs("li", { children: [_jsx("span", { className: "font-semibold", children: item.service.name }), ' ',
|
|
289
|
+
return (_jsxs("section", { "aria-labelledby": "booking-details-heading", className: slotClass(classNames, 'root', 'space-y-6'), children: [_jsx("h2", { id: "booking-details-heading", tabIndex: -1, className: slotClass(classNames, 'heading', 'font-sans text-2xl font-bold outline-none md:text-3xl'), children: labels['details.heading'] }), _jsxs(Field, { className: fieldClass, children: [_jsx(Field.Label, { htmlFor: field('phone'), children: labels['details.phone.label'] }), _jsxs("div", { className: "flex items-stretch", children: [phonePrefix && (_jsx("span", { id: field('phone-prefix'), className: "flex h-12 items-center rounded-l-[4px] border border-r-0 border-input bg-muted px-4 text-base text-muted-foreground", children: phonePrefix })), _jsx(Input, { id: field('phone'), ref: phoneRef, type: "tel", inputMode: "numeric", autoComplete: "tel-national", className: inputClass(phonePrefix ? 'rounded-l-none' : ''), value: state.contact.phone, "aria-describedby": phonePrefix ? field('phone-prefix') : undefined, "aria-invalid": showPhoneError || undefined, onFocus: () => setFocused('phone'), onBlur: () => setFocused(null), onChange: (event) => setContact('phone', event.target.value) })] }), showPhoneError && _jsx(Field.Error, { children: labels['details.phone.error'] })] }), _jsxs(Field, { className: fieldClass, children: [_jsx(Field.Label, { htmlFor: field('name'), children: labels['details.name.label'] }), _jsx(Input, { id: field('name'), autoComplete: "name", className: inputClass(), value: state.contact.name, onChange: (event) => setContact('name', event.target.value) })] }), state.items.some((_, index) => isKnownChair(state, index)) && (_jsx("ul", { "aria-label": labels['details.known.label'], className: "space-y-1 text-sm", children: state.items.map((item, index) => isKnownChair(state, index) ? (_jsxs("li", { children: [_jsx("span", { className: "font-semibold", children: item.service.name }), ' ', renderLabel(labels['details.known.for'], { who: knownWho(state, index, labels) })] }, index)) : null) })), state.items.map((item, index) =>
|
|
289
290
|
// A seat step 1 already named is not asked again: a second answer could disagree.
|
|
290
291
|
isKnownChair(state, index) ? null : (_jsx(ChildFields
|
|
291
292
|
// biome-ignore lint/suspicious/noArrayIndexKey: two lines can share a service; the index is the identity
|
|
292
|
-
, { legend: state.items.length > 1 ? item.service.name : 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: 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:
|
|
293
|
+
, { legend: state.items.length > 1 ? item.service.name : 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: 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 }) })] }));
|
|
293
294
|
}
|
|
294
295
|
/**
|
|
295
296
|
* The name and birth year for one line. Wrapped in a fieldset only when there
|
|
@@ -309,6 +310,7 @@ function ChildFields({ legend, nameId, yearId, value, family, labels, classNames
|
|
|
309
310
|
* visitor cannot fix by trying harder.
|
|
310
311
|
*/
|
|
311
312
|
function SubmitFailure({ error, phone, format, labels, className, }) {
|
|
312
|
-
const
|
|
313
|
-
|
|
313
|
+
const template = labels['details.call.link'];
|
|
314
|
+
const call = phone ? fillLabel(template, { phone }) : null;
|
|
315
|
+
return (_jsxs("p", { role: "alert", className: className, children: [labels[DETAILS_ERROR_LABEL_KEYS[error]], ' ', phone && call ? (_jsx("a", { href: format.telHref(phone), "aria-label": call, className: "font-semibold text-primary underline underline-offset-4", children: renderLabel(template, { phone }) })) : (labels['details.call.none'])] }));
|
|
314
316
|
}
|
package/dist/booking/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export * from './booking-skeleton.js';
|
|
|
3
3
|
export * from './confirmation.js';
|
|
4
4
|
export * from './details-screen.js';
|
|
5
5
|
export type { BookingClock, BookingDaypart, BookingFormat } from './format.js';
|
|
6
|
+
export { joinLabelParts, labelParts, renderLabel } from './internal/label-parts.js';
|
|
6
7
|
export * from './internal/ui.js';
|
|
7
8
|
export * from './label-keys.js';
|
|
8
9
|
export { fillLabel } from './labels.js';
|
package/dist/booking/index.js
CHANGED
|
@@ -10,6 +10,7 @@ export * from './add-child-sheet.js';
|
|
|
10
10
|
export * from './booking-skeleton.js';
|
|
11
11
|
export * from './confirmation.js';
|
|
12
12
|
export * from './details-screen.js';
|
|
13
|
+
export { joinLabelParts, labelParts, renderLabel } from './internal/label-parts.js';
|
|
13
14
|
export * from './internal/ui.js';
|
|
14
15
|
export * from './label-keys.js';
|
|
15
16
|
export { fillLabel } from './labels.js';
|
|
@@ -6,3 +6,23 @@ import { type ReactNode } from 'react';
|
|
|
6
6
|
* placeholders are left as written.
|
|
7
7
|
*/
|
|
8
8
|
export declare function labelParts(template: string, parts: Readonly<Record<string, ReactNode>>): ReactNode;
|
|
9
|
+
/**
|
|
10
|
+
* A filled label as element children: the template split around its
|
|
11
|
+
* `{placeholders}`, so React renders one text node per literal run and one
|
|
12
|
+
* per value — the same DOM as writing the sentence out in JSX
|
|
13
|
+
* (`Total {total} · paid at the salon`). Prefer it over `fillLabel` wherever
|
|
14
|
+
* a label is rendered as children; a browser lays text out per text node, so
|
|
15
|
+
* one joined string can land a line's glyphs a sub-pixel away from the same
|
|
16
|
+
* sentence composed in JSX.
|
|
17
|
+
*
|
|
18
|
+
* Text-only targets (`aria-label`, `title`, a file name) keep `fillLabel`.
|
|
19
|
+
* Empty literal runs and empty values render nothing, as in JSX; a label
|
|
20
|
+
* that fills to nothing at all is `null`.
|
|
21
|
+
*/
|
|
22
|
+
export declare function renderLabel(template: string, values: Readonly<Record<string, string | number>>): ReactNode;
|
|
23
|
+
/**
|
|
24
|
+
* `parts.filter(Boolean).join(separator)` as element children: every part
|
|
25
|
+
* and every separator its own text piece. Blank parts drop out, so an absent
|
|
26
|
+
* one closes the gap rather than leaving a doubled separator.
|
|
27
|
+
*/
|
|
28
|
+
export declare function joinLabelParts(parts: ReadonlyArray<ReactNode>, separator: string): ReactNode;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Fragment } from 'react';
|
|
3
3
|
/**
|
|
4
4
|
* `fillLabel` for templates whose placeholders are elements (a link, a
|
|
@@ -17,3 +17,41 @@ export function labelParts(template, parts) {
|
|
|
17
17
|
return piece;
|
|
18
18
|
});
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* A filled label as element children: the template split around its
|
|
22
|
+
* `{placeholders}`, so React renders one text node per literal run and one
|
|
23
|
+
* per value — the same DOM as writing the sentence out in JSX
|
|
24
|
+
* (`Total {total} · paid at the salon`). Prefer it over `fillLabel` wherever
|
|
25
|
+
* a label is rendered as children; a browser lays text out per text node, so
|
|
26
|
+
* one joined string can land a line's glyphs a sub-pixel away from the same
|
|
27
|
+
* sentence composed in JSX.
|
|
28
|
+
*
|
|
29
|
+
* Text-only targets (`aria-label`, `title`, a file name) keep `fillLabel`.
|
|
30
|
+
* Empty literal runs and empty values render nothing, as in JSX; a label
|
|
31
|
+
* that fills to nothing at all is `null`.
|
|
32
|
+
*/
|
|
33
|
+
export function renderLabel(template, values) {
|
|
34
|
+
const pieces = template.split(/(\{\w+\})/g);
|
|
35
|
+
const nodes = [];
|
|
36
|
+
for (const piece of pieces) {
|
|
37
|
+
const match = /^\{(\w+)\}$/.exec(piece);
|
|
38
|
+
const value = match && Object.hasOwn(values, match[1]) ? values[match[1]] : piece;
|
|
39
|
+
if (value !== '')
|
|
40
|
+
nodes.push(value);
|
|
41
|
+
}
|
|
42
|
+
return nodes.length > 0 ? nodes : null;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* `parts.filter(Boolean).join(separator)` as element children: every part
|
|
46
|
+
* and every separator its own text piece. Blank parts drop out, so an absent
|
|
47
|
+
* one closes the gap rather than leaving a doubled separator.
|
|
48
|
+
*/
|
|
49
|
+
export function joinLabelParts(parts, separator) {
|
|
50
|
+
return parts
|
|
51
|
+
.filter((part) => part !== null &&
|
|
52
|
+
part !== undefined &&
|
|
53
|
+
part !== false &&
|
|
54
|
+
part !== '' &&
|
|
55
|
+
!(Array.isArray(part) && part.length === 0))
|
|
56
|
+
.map((part, index) => (_jsxs(Fragment, { children: [index > 0 && separator, part] }, index)));
|
|
57
|
+
}
|
|
@@ -3,8 +3,8 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
3
3
|
import { useEffect, useId, useRef, useState, useTransition, } from 'react';
|
|
4
4
|
import { Field } from '../primitives/field.js';
|
|
5
5
|
import { Input } from '../primitives/input.js';
|
|
6
|
+
import { renderLabel } from './internal/label-parts.js';
|
|
6
7
|
import { BOOKING_INPUT_CLASS, BookingButton } from './internal/ui.js';
|
|
7
|
-
import { fillLabel } from './labels.js';
|
|
8
8
|
import { OTP_SLOTS_LABEL_KEYS, OtpSlots, } from './otp-slots.js';
|
|
9
9
|
import { slotClass } from './slots.js';
|
|
10
10
|
import { VIPPS_BUTTON_LABEL_KEYS, VippsButton, } from './vipps-button.js';
|
|
@@ -221,19 +221,19 @@ export function LoginPanel({ labels, onStartLogin, onVerify, onSignedIn, onVipps
|
|
|
221
221
|
// The countdown on the button changes every second; this region says only
|
|
222
222
|
// the two things worth hearing — how long, and that it is over.
|
|
223
223
|
const cooldownSentence = waitSeconds > 0
|
|
224
|
-
?
|
|
224
|
+
? renderLabel(labels['login.cooldownWait'], { seconds: Math.ceil(resendCooldownMs / 1000) })
|
|
225
225
|
: labels['login.cooldownReady'];
|
|
226
226
|
const cooldownRegion = (_jsx("p", { "aria-live": "polite", className: "sr-only", children: mode === 'code' ? cooldownSentence : null }));
|
|
227
227
|
const link = slotClass(classNames, 'link', 'font-semibold text-primary underline underline-offset-4 disabled:no-underline disabled:opacity-60');
|
|
228
228
|
return (_jsxs("div", { className: slotClass(classNames, 'root', 'space-y-6'), children: [heading(mode === 'start' ? labels['login.heading'] : labels['login.codeHeading']), mode === 'vipps' && (_jsx("p", { className: "text-muted-foreground", children: sentTo === ''
|
|
229
229
|
? labels['login.vippsSentToUnknown']
|
|
230
|
-
:
|
|
230
|
+
: 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" })] })] })), _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, { id: emailId, type: "email", autoComplete: "email", required: true, value: email, "aria-describedby": noticeText ? noticeId : undefined, "aria-invalid": isFieldError || 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,
|
|
231
231
|
// Read-only rather than disabled while a request is out, so
|
|
232
232
|
// the caret stays in the field instead of falling out of a
|
|
233
233
|
// dialog; a wrong code then only has to empty it.
|
|
234
234
|
readOnly: checking, invalid: isFieldError, describedBy: `${codeId}-help${noticeText ? ` ${noticeId}` : ''}`, labels: labels, classNames: otpClassNames, onChange: setCode, onComplete: verify }) }), _jsx(Field.Description, { id: `${codeId}-help`, children: mode === 'vipps'
|
|
235
235
|
? labels['login.codeHelpVipps']
|
|
236
|
-
:
|
|
237
|
-
?
|
|
236
|
+
: renderLabel(labels['login.codeHelp'], { email: sentTo }) })] }), _jsx(BookingButton, { type: "submit", size: "lg", className: slotClass(classNames, 'submit', 'w-full'), disabled: pending, children: labels['login.submitCode'] })] }), _jsxs("div", { className: slotClass(classNames, 'links', 'flex flex-wrap items-center gap-4 text-sm'), children: [mode !== 'vipps' && (_jsx("button", { type: "button", disabled: pending || waitSeconds > 0, onClick: () => requestCode(sentTo, true), className: link, children: waitSeconds > 0
|
|
237
|
+
? renderLabel(labels['login.resendIn'], { time: timer(waitSeconds) })
|
|
238
238
|
: labels['login.resend'] })), _jsx("button", { type: "button", disabled: pending, onClick: switchEmail, className: link, children: mode === 'vipps' ? labels['login.useEmailInstead'] : labels['login.switchEmail'] })] })] }))] }));
|
|
239
239
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useEffect, useState } from 'react';
|
|
4
4
|
import { cn } from '../lib/utils.js';
|
|
5
|
+
import { renderLabel } from './internal/label-parts.js';
|
|
5
6
|
import { BookingButton } from './internal/ui.js';
|
|
6
7
|
import { fillLabel } from './labels.js';
|
|
7
8
|
import { slotClass } from './slots.js';
|
|
@@ -271,14 +272,14 @@ export function ManageScreen({ labels, format, dayparts, booking, phone = null,
|
|
|
271
272
|
const panelClass = (base) => slotClass(classNames, 'panel', base);
|
|
272
273
|
return (_jsxs("section", { "aria-labelledby": "booking-manage-heading", className: slotClass(classNames, 'root', 'space-y-6'), children: [_jsx("h1", { id: "booking-manage-heading", className: slotClass(classNames, 'heading', 'font-sans text-2xl font-bold md:text-3xl'), children: labels['manage.heading'] }), error !== null && (_jsx("p", { role: "alert", className: slotClass(classNames, 'alert', 'rounded-lg border border-secondary/40 bg-secondary/10 px-5 py-3 text-sm'), children: errorSentence(labels, error, phone) })), view === 'cancelled' && (_jsxs("div", { className: panelClass('space-y-4 rounded-lg border border-border bg-card px-5 py-4'), children: [_jsx("p", { className: "font-medium", children: labels['manage.cancelled'] }), _jsx("a", { href: bookingHref, className: LINK_CLASS, children: labels['manage.cancelled.findNew'] })] })), view === 'moved' && (_jsxs("div", { className: panelClass('space-y-4 rounded-lg border border-border bg-card px-5 py-4'), children: [_jsx("p", { className: "font-medium", children: movedTo === null
|
|
273
274
|
? labels['manage.moved']
|
|
274
|
-
:
|
|
275
|
+
: renderLabel(labels['manage.movedTo'], {
|
|
275
276
|
dateTime: format.clock.dateTime(movedTo),
|
|
276
277
|
}) }), newManageHref ? (_jsx("a", { href: newManageHref, className: LINK_CLASS, children: labels['manage.moved.view'] })) : (_jsxs("p", { className: "text-sm text-muted-foreground", children: [labels['manage.moved.noLink'], ' ', _jsx(PhoneSentence, { format: format, phone: phone, children: callUs(labels, phone) }), ' ', labels['manage.moved.noLinkSuffix']] }))] })), view !== 'cancelled' && view !== 'moved' && (_jsxs(_Fragment, { children: [_jsxs("div", { className: slotClass(classNames, 'card', 'space-y-1 rounded-lg border border-border bg-card px-5 py-4'), children: [_jsx("p", { className: "font-semibold", children: booking.bookedForName
|
|
277
|
-
?
|
|
278
|
+
? renderLabel(labels['manage.card.named'], {
|
|
278
279
|
name: booking.bookedForName,
|
|
279
280
|
service: serviceLabel,
|
|
280
281
|
})
|
|
281
|
-
: serviceLabel }), _jsx("p", { className: "tabular-nums", children:
|
|
282
|
+
: serviceLabel }), _jsx("p", { className: "tabular-nums", children: renderLabel(labels[stylist ? 'manage.card.whenWithStylist' : 'manage.card.when'], {
|
|
282
283
|
dateTime: format.clock.dateTime(booking.startTs),
|
|
283
284
|
stylist: stylist ?? '',
|
|
284
285
|
price: format.price(booking.amountOre),
|
|
@@ -287,11 +288,11 @@ export function ManageScreen({ labels, format, dayparts, booking, phone = null,
|
|
|
287
288
|
setError(null);
|
|
288
289
|
setView('cancel');
|
|
289
290
|
} })) : (_jsx(ClosedBooking, { labels: labels, format: format, status: booking.status, bookingHref: bookingHref, phone: phone, className: panelClass('space-y-3 rounded-lg border border-border bg-card px-5 py-4') }))), view === 'party' && (_jsx(PartyChoice, { labels: labels, format: format, bookedForName: booking.bookedForName, phone: phone, onJustThisOne: goToTime, onBack: () => setView('overview'), className: panelClass('space-y-4 rounded-lg border border-primary/40 bg-primary/5 px-5 py-4') })), view === 'time' && (_jsxs("div", { className: "space-y-4", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: stylist
|
|
290
|
-
?
|
|
291
|
+
? renderLabel(labels['manage.reschedule.leadWithStylist'], {
|
|
291
292
|
service: serviceLabel,
|
|
292
293
|
stylist,
|
|
293
294
|
})
|
|
294
|
-
:
|
|
295
|
+
: renderLabel(labels['manage.reschedule.lead'], { service: serviceLabel }) }), slotsFailed && (_jsxs("p", { className: "rounded-lg border border-border bg-card px-5 py-4 text-sm", children: [labels['manage.reschedule.failed'], ' ', _jsx(PhoneSentence, { format: format, phone: phone, children: ringUs(labels, phone) }), labels['manage.reschedule.failedSuffix']] })), !slotsFailed && slots === null && (_jsx("p", { className: "text-sm text-muted-foreground", children: labels['manage.reschedule.loading'] })), !slotsFailed && slots !== null && (_jsx(TimeScreen, { labels: labels, format: format, dayparts: dayparts, slots: slots, days: reschedule.days, openDays: reschedule.openDays ?? null, currentSlotTs: booking.startTs, weekendNote: weekendNoteFor(pricing), phone: phone, now: nowProp, classNames: timeClassNames, components: components, onPick: (slot) => {
|
|
295
296
|
setPending(slot);
|
|
296
297
|
setError(null);
|
|
297
298
|
setView('move');
|
|
@@ -311,13 +312,15 @@ function Overview({ labels, format, booking, freeUntilTs, remaining, phone, icsH
|
|
|
311
312
|
// The largest window that has already closed — the number the sentence below
|
|
312
313
|
// quotes. Zero when both are open, and then it is not rendered.
|
|
313
314
|
const blockedHours = Math.max(booking.canCancel ? 0 : booking.cancelWindowHours, booking.canReschedule ? 0 : booking.rescheduleWindowHours);
|
|
314
|
-
return (_jsxs(_Fragment, { children: [booking.canCancel && booking.canReschedule && (_jsxs("p", { className: "text-sm text-muted-foreground", children: [
|
|
315
|
+
return (_jsxs(_Fragment, { children: [booking.canCancel && booking.canReschedule && (_jsxs("p", { className: "text-sm text-muted-foreground", children: [renderLabel(labels['manage.freeUntil'], {
|
|
316
|
+
dateTime: format.clock.dateTime(freeUntilTs),
|
|
317
|
+
}), remaining !== null && _jsxs("span", { children: [" (", remaining, ")"] })] })), _jsxs("div", { className: cn('flex flex-wrap items-center gap-4', actionsClassName), children: [booking.canReschedule ? (_jsx(BookingButton, { size: "lg", onClick: onChange, children: labels['manage.change'] })) : (_jsx(PhoneSentence, { format: format, phone: phone, children: renderLabel(labels['manage.callToChange'], { hours: booking.rescheduleWindowHours }) })), booking.canCancel ? (_jsx(BookingButton, { size: "lg", variant: "outline", onClick: onCancel, children: labels['manage.cancel'] })) : (_jsx(PhoneSentence, { format: format, phone: phone, children: renderLabel(labels['manage.callToCancel'], { hours: booking.cancelWindowHours }) })), icsHref !== null && (_jsx("a", { href: icsHref, download: icsFileName ?? true, className: LINK_CLASS, children: labels['manage.calendar'] }))] }), blockedHours > 0 && (_jsxs("p", { className: "rounded-lg border border-border bg-card px-5 py-4 text-sm", children: [renderLabel(labels['manage.blocked'], { hours: blockedHours }), ' ', phone ? (_jsxs(_Fragment, { children: [_jsx(PhoneSentence, { format: format, phone: phone, children: renderLabel(labels['manage.ringUsOn'], { phone }) }), labels['manage.blocked.suffix']] })) : (labels['manage.blocked.plain'])] }))] }));
|
|
315
318
|
}
|
|
316
319
|
/** A booking no longer open to either action, saying which kind it is. */
|
|
317
320
|
function ClosedBooking({ labels, format, status, bookingHref, phone, className, }) {
|
|
318
321
|
const sentence = labels[CLOSED_SENTENCE[status] ?? 'manage.closed.pending'];
|
|
319
322
|
return (_jsxs("div", { className: className, children: [_jsx("p", { className: "text-sm", children: sentence }), status === 'pending' ? (_jsx("p", { className: "text-sm", children: _jsx(PhoneSentence, { format: format, phone: phone, children: phone
|
|
320
|
-
?
|
|
323
|
+
? renderLabel(labels['manage.closed.pendingCall'], { phone })
|
|
321
324
|
: labels['manage.closed.pendingCallPlain'] }) })) : (_jsx("a", { href: bookingHref, className: LINK_CLASS, children: labels['manage.bookNew'] }))] }));
|
|
322
325
|
}
|
|
323
326
|
/**
|
|
@@ -328,7 +331,7 @@ function ClosedBooking({ labels, format, status, bookingHref, phone, className,
|
|
|
328
331
|
*/
|
|
329
332
|
function PartyChoice({ labels, format, bookedForName, phone, onJustThisOne, onBack, className, }) {
|
|
330
333
|
const who = bookedForName ?? labels['manage.party.unnamed'];
|
|
331
|
-
return (_jsxs("div", { className: className, children: [_jsx("h2", { className: "font-sans text-lg font-bold", children:
|
|
334
|
+
return (_jsxs("div", { className: className, children: [_jsx("h2", { className: "font-sans text-lg font-bold", children: renderLabel(labels['manage.party.heading'], { who }) }), _jsxs("div", { className: "flex flex-wrap items-center gap-4", children: [_jsx(BookingButton, { size: "lg", onClick: onJustThisOne, children: renderLabel(labels['manage.party.justThisOne'], { who }) }), _jsx(BookingButton, { size: "lg", variant: "outline", onClick: onBack, children: labels['manage.back'] })] }), _jsxs("p", { className: "text-sm text-muted-foreground", children: [labels['manage.party.note'], ' ', _jsx(PhoneSentence, { format: format, phone: phone, children: ringUs(labels, phone) }), labels['manage.party.noteSuffix']] })] }));
|
|
332
335
|
}
|
|
333
336
|
/**
|
|
334
337
|
* The single confirm for a move. The weekend note appears only when the new
|
|
@@ -340,13 +343,13 @@ function ConfirmMove({ labels, format, booking, pricing, stylist, pending, busy,
|
|
|
340
343
|
const nextOre = pricing ? pricing.priceAt(pending.startTs) : 0;
|
|
341
344
|
const nowOre = pricing ? pricing.priceAt(booking.startTs) : 0;
|
|
342
345
|
const dearer = pricing !== null && nextOre > nowOre;
|
|
343
|
-
const question =
|
|
346
|
+
const question = renderLabel(labels[stylist ? 'manage.move.questionWithStylist' : 'manage.move.question'], {
|
|
344
347
|
appointment: theirAppointment(labels, booking.bookedForName),
|
|
345
348
|
from: `${clock.date(booking.startTs)} ${clock.formatTime(booking.startTs)}`,
|
|
346
349
|
to: `${clock.date(pending.startTs)} ${clock.formatTime(pending.startTs)}`,
|
|
347
350
|
stylist: stylist ?? '',
|
|
348
351
|
});
|
|
349
|
-
return (_jsxs("div", { className: className, children: [_jsx("p", { className: "font-medium", children: question }), dearer && pricing && (_jsx("p", { className: "text-sm", children:
|
|
352
|
+
return (_jsxs("div", { className: className, children: [_jsx("p", { className: "font-medium", children: question }), dearer && pricing && (_jsx("p", { className: "text-sm", children: renderLabel(labels['manage.move.surcharge'], {
|
|
350
353
|
weekday: clock.weekday(pending.startTs),
|
|
351
354
|
price: format.price(nextOre),
|
|
352
355
|
pct: pricing.weekendSurchargePct,
|
|
@@ -354,7 +357,7 @@ function ConfirmMove({ labels, format, booking, pricing, stylist, pending, busy,
|
|
|
354
357
|
}
|
|
355
358
|
/** The single confirm for a cancellation, with the reason asked and never required. */
|
|
356
359
|
function ConfirmCancel({ labels, format, booking, reason, busy, onPickReason, onConfirm, onBack, className, }) {
|
|
357
|
-
return (_jsxs("div", { className: className, children: [_jsx("p", { className: "font-medium", children:
|
|
360
|
+
return (_jsxs("div", { className: className, children: [_jsx("p", { className: "font-medium", children: renderLabel(labels['manage.cancel.question'], {
|
|
358
361
|
appointment: theirAppointment(labels, booking.bookedForName),
|
|
359
362
|
dateTime: format.clock.dateTime(booking.startTs),
|
|
360
363
|
hours: booking.cancelWindowHours,
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useId, useState } from 'react';
|
|
4
4
|
import { Field } from '../../primitives/field.js';
|
|
5
|
+
import { renderLabel } from '../internal/label-parts.js';
|
|
5
6
|
import { BookingButton } from '../internal/ui.js';
|
|
6
|
-
import { fillLabel } from '../labels.js';
|
|
7
7
|
import { slotClass } from '../slots.js';
|
|
8
8
|
export const AGE_CONFIRM_CARD_LABEL_KEYS = [
|
|
9
9
|
'ageConfirm.heading',
|
|
@@ -35,8 +35,8 @@ export function AgeConfirmCard({ labels, name, birthYear, years, months, busy, e
|
|
|
35
35
|
const [month, setMonth] = useState('');
|
|
36
36
|
const headingId = `${id}-heading`;
|
|
37
37
|
const errorId = `${id}-error`;
|
|
38
|
-
return (_jsxs("section", { "aria-labelledby": headingId, "aria-busy": busy || undefined, className: slotClass(classNames, 'root', 'space-y-3 rounded-lg border border-secondary/40 bg-secondary/10 px-5 py-4'), children: [_jsxs("div", { className: "space-y-1", children: [_jsx("h3", { id: headingId, className: slotClass(classNames, 'heading', 'font-sans text-lg font-bold'), children:
|
|
38
|
+
return (_jsxs("section", { "aria-labelledby": headingId, "aria-busy": busy || undefined, className: slotClass(classNames, 'root', 'space-y-3 rounded-lg border border-secondary/40 bg-secondary/10 px-5 py-4'), children: [_jsxs("div", { className: "space-y-1", children: [_jsx("h3", { id: headingId, className: slotClass(classNames, 'heading', 'font-sans text-lg font-bold'), children: renderLabel(labels['ageConfirm.heading'], { name }) }), _jsx("p", { className: "text-sm text-muted-foreground", children: labels['ageConfirm.lead'] })] }), _jsxs("form", { onSubmit: (event) => {
|
|
39
39
|
event.preventDefault();
|
|
40
40
|
onConfirm({ birthYear: year, birthMonth: month });
|
|
41
|
-
}, "aria-describedby": error ? errorId : undefined, className: slotClass(classNames, 'form', 'space-y-3'), children: [_jsxs("fieldset", { disabled: busy, className: "grid min-w-0 grid-cols-2 gap-3", children: [_jsx("legend", { className: "sr-only", children:
|
|
41
|
+
}, "aria-describedby": error ? errorId : undefined, className: slotClass(classNames, 'form', 'space-y-3'), children: [_jsxs("fieldset", { disabled: busy, className: "grid min-w-0 grid-cols-2 gap-3", children: [_jsx("legend", { className: "sr-only", children: renderLabel(labels['ageConfirm.legend'], { name }) }), _jsxs(Field, { children: [_jsx(Field.Label, { htmlFor: `${id}-year`, children: labels['ageConfirm.birthYear'] }), _jsx("select", { id: `${id}-year`, value: year, onChange: (event) => setYear(event.target.value), className: SELECT_CLASS, children: years.map((option) => (_jsx("option", { value: String(option), children: option }, option))) })] }), _jsxs(Field, { children: [_jsx(Field.Label, { htmlFor: `${id}-month`, children: labels['ageConfirm.birthMonth'] }), _jsxs("select", { id: `${id}-month`, value: month, onChange: (event) => setMonth(event.target.value), className: SELECT_CLASS, children: [_jsx("option", { value: "", children: labels['ageConfirm.birthMonthUnknown'] }), months.map((label, index) => (_jsx("option", { value: String(index + 1), children: label }, label)))] })] })] }), _jsxs("div", { className: slotClass(classNames, 'actions', 'flex flex-wrap items-center gap-3'), children: [_jsx(BookingButton, { type: "submit", size: "sm", disabled: busy, children: labels['ageConfirm.confirm'] }), _jsx(BookingButton, { type: "button", variant: "ghost", size: "sm", disabled: busy, onClick: onDismiss, children: labels['ageConfirm.dismiss'] }), _jsx("span", { "aria-live": "polite", className: "min-h-5 text-sm", children: error && _jsx(Field.Error, { id: errorId, children: error }) })] })] })] }));
|
|
42
42
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { ArrowRight } from 'lucide-react';
|
|
4
4
|
import { cn } from '../../lib/utils.js';
|
|
5
|
-
import {
|
|
5
|
+
import { renderLabel } from '../internal/label-parts.js';
|
|
6
6
|
import { slotClass } from '../slots.js';
|
|
7
7
|
export const CHILD_CARDS_LABEL_KEYS = [
|
|
8
8
|
'childCards.empty',
|
|
@@ -20,11 +20,11 @@ const AVATAR_TINTS = ['bg-secondary/30', 'bg-primary/10', 'bg-accent'];
|
|
|
20
20
|
function lastVisitLine(child, labels, format) {
|
|
21
21
|
const age = format.ageLabel(child.ageRange);
|
|
22
22
|
if (child.lastVisitTs === null)
|
|
23
|
-
return
|
|
23
|
+
return renderLabel(labels['childCards.neverVisited'], { age });
|
|
24
24
|
const date = format.clock.date(child.lastVisitTs);
|
|
25
25
|
return child.serviceName
|
|
26
|
-
?
|
|
27
|
-
:
|
|
26
|
+
? renderLabel(labels['childCards.lastVisit'], { age, service: child.serviceName, date })
|
|
27
|
+
: renderLabel(labels['childCards.lastVisitNoService'], { age, date });
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
30
|
* «My children» — one card per child, and one tap to book them again. A plain
|
|
@@ -44,7 +44,7 @@ export function ChildCards({ kids, variant = 'full', hrefFor, onBook, empty, sty
|
|
|
44
44
|
export function DefaultChildCard({ child, index, variant, href, onBook, stylistNames, labels, format, classNames, }) {
|
|
45
45
|
return (_jsxs("li", { className: slotClass(classNames, 'card', 'flex gap-4 rounded-lg border border-border bg-card px-5 py-4', variant === 'full' ? 'flex-col' : 'items-center'), children: [_jsxs("div", { className: "flex min-w-0 items-center gap-3", children: [_jsx("span", { "aria-hidden": "true", className: slotClass(classNames, 'avatar', 'flex size-11 shrink-0 items-center justify-center rounded-full font-sans text-lg font-bold', AVATAR_TINTS[index % AVATAR_TINTS.length]), children: child.name.trim().charAt(0).toUpperCase() }), _jsxs("span", { className: "min-w-0", children: [_jsx("span", { className: slotClass(classNames, 'name', 'block truncate font-semibold'), children: child.name }), _jsx("span", { className: slotClass(classNames, 'detail', 'block truncate text-sm text-muted-foreground'), children: lastVisitLine(child, labels, format) })] })] }), variant === 'full' && (_jsx(ChildDetails, { child: child, stylistNames: stylistNames, labels: labels, format: format, className: classNames?.details })), _jsxs("a", { href: href, onClick: onBook ? () => onBook(child) : undefined, className: slotClass(classNames, 'link', 'font-semibold text-primary underline underline-offset-4', variant === 'full'
|
|
46
46
|
? 'inline-flex items-center gap-2 no-underline'
|
|
47
|
-
: 'ms-auto shrink-0 text-sm'), children: [
|
|
47
|
+
: 'ms-auto shrink-0 text-sm'), children: [renderLabel(labels['childCards.book'], { name: child.name }), variant === 'full' && _jsx(ArrowRight, { "aria-hidden": "true", className: "size-4" })] })] }));
|
|
48
48
|
}
|
|
49
49
|
/**
|
|
50
50
|
* The full card's two facts beyond the last visit: the usual stylist and the
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useId, useState, useTransition } from 'react';
|
|
4
4
|
import { Field } from '../../primitives/field.js';
|
|
5
5
|
import { Input } from '../../primitives/input.js';
|
|
6
|
+
import { renderLabel } from '../internal/label-parts.js';
|
|
6
7
|
import { BOOKING_INPUT_CLASS, BookingButton } from '../internal/ui.js';
|
|
7
8
|
import { fillLabel } from '../labels.js';
|
|
8
9
|
import { slotClass } from '../slots.js';
|
|
@@ -92,7 +93,7 @@ export function DataControls({ labels, onExport, onDelete, onSessionExpired, cla
|
|
|
92
93
|
setError(message);
|
|
93
94
|
});
|
|
94
95
|
}
|
|
95
|
-
return (_jsxs("section", { "aria-labelledby": "portal-data-heading", className: slotClass(classNames, 'root', 'space-y-4'), children: [_jsx("h2", { id: "portal-data-heading", className: slotClass(classNames, 'heading', 'font-sans text-xl font-bold md:text-2xl'), children: labels['dataControls.heading'] }), _jsxs("div", { className: slotClass(classNames, 'actions', 'flex flex-wrap items-center gap-3'), children: [_jsx(BookingButton, { variant: "outline", onClick: exportData, disabled: pending, children: labels['dataControls.export'] }), !confirming && (_jsx(BookingButton, { variant: "destructive", onClick: () => setConfirming(true), disabled: pending, children: labels['dataControls.delete'] }))] }), _jsx("div", { "aria-live": "polite", children: exportError && _jsx(Field.Error, { children: exportError }) }), confirming && (_jsxs("form", { onSubmit: deleteMe, "aria-labelledby": `${id}-delete-heading`, className: slotClass(classNames, 'confirm', 'space-y-4 rounded-lg border border-destructive/40 bg-destructive/10 px-5 py-4'), children: [_jsx("p", { id: `${id}-delete-heading`, className: "font-semibold", children: labels['dataControls.confirmHeading'] }), _jsx("p", { className: "text-sm", children: labels['dataControls.consequences'] }), _jsxs(Field, { children: [_jsx(Field.Label, { htmlFor: confirmId, children:
|
|
96
|
+
return (_jsxs("section", { "aria-labelledby": "portal-data-heading", className: slotClass(classNames, 'root', 'space-y-4'), children: [_jsx("h2", { id: "portal-data-heading", className: slotClass(classNames, 'heading', 'font-sans text-xl font-bold md:text-2xl'), children: labels['dataControls.heading'] }), _jsxs("div", { className: slotClass(classNames, 'actions', 'flex flex-wrap items-center gap-3'), children: [_jsx(BookingButton, { variant: "outline", onClick: exportData, disabled: pending, children: labels['dataControls.export'] }), !confirming && (_jsx(BookingButton, { variant: "destructive", onClick: () => setConfirming(true), disabled: pending, children: labels['dataControls.delete'] }))] }), _jsx("div", { "aria-live": "polite", children: exportError && _jsx(Field.Error, { children: exportError }) }), confirming && (_jsxs("form", { onSubmit: deleteMe, "aria-labelledby": `${id}-delete-heading`, className: slotClass(classNames, 'confirm', 'space-y-4 rounded-lg border border-destructive/40 bg-destructive/10 px-5 py-4'), children: [_jsx("p", { id: `${id}-delete-heading`, className: "font-semibold", children: labels['dataControls.confirmHeading'] }), _jsx("p", { className: "text-sm", children: labels['dataControls.consequences'] }), _jsxs(Field, { children: [_jsx(Field.Label, { htmlFor: confirmId, children: renderLabel(labels['dataControls.confirmLabel'], { word }) }), _jsx(Input, { id: confirmId, autoComplete: "off", autoCapitalize: "characters", value: typed, "aria-describedby": `${confirmId}-help${error ? ` ${errorId}` : ''}`, "aria-invalid": error ? true : undefined, onChange: (event) => {
|
|
96
97
|
setTyped(event.target.value);
|
|
97
98
|
setError(null);
|
|
98
99
|
}, className: BOOKING_INPUT_CLASS }), _jsx(Field.Description, { id: `${confirmId}-help`, children: labels['dataControls.confirmHelp'] })] }), _jsx("div", { "aria-live": "polite", children: error && _jsx(Field.Error, { id: errorId, children: error }) }), _jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [_jsx(BookingButton, { type: "submit", variant: "destructive", disabled: pending, children: labels['dataControls.deleteForever'] }), _jsx(BookingButton, { variant: "ghost", disabled: pending, onClick: () => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { renderLabel } from '../internal/label-parts.js';
|
|
3
3
|
import { slotClass } from '../slots.js';
|
|
4
4
|
export const PORTAL_UNREACHABLE_LABEL_KEYS = [
|
|
5
5
|
'portalUnreachable.heading',
|
|
@@ -15,5 +15,5 @@ export const PORTAL_UNREACHABLE_LABEL_KEYS = [
|
|
|
15
15
|
*/
|
|
16
16
|
export function PortalUnreachable({ labels, format, phone, retryHref, logout, headingId = 'portal-unreachable-heading', classNames, }) {
|
|
17
17
|
const link = slotClass(classNames, 'link', 'font-semibold text-primary underline underline-offset-4');
|
|
18
|
-
return (_jsxs("section", { "aria-labelledby": headingId, className: slotClass(classNames, 'root', 'space-y-6'), children: [_jsx("h1", { id: headingId, className: slotClass(classNames, 'heading', 'font-sans text-2xl font-bold md:text-3xl'), children: labels['portalUnreachable.heading'] }), _jsx("p", { className: "text-muted-foreground", children: labels['portalUnreachable.body'] }), _jsxs("div", { className: slotClass(classNames, 'actions', 'flex flex-wrap items-center gap-4'), children: [_jsx("a", { href: retryHref, className: link, children: labels['portalUnreachable.retry'] }), phone && (_jsx("a", { href: format.telHref(phone), className: link, children:
|
|
18
|
+
return (_jsxs("section", { "aria-labelledby": headingId, className: slotClass(classNames, 'root', 'space-y-6'), children: [_jsx("h1", { id: headingId, className: slotClass(classNames, 'heading', 'font-sans text-2xl font-bold md:text-3xl'), children: labels['portalUnreachable.heading'] }), _jsx("p", { className: "text-muted-foreground", children: labels['portalUnreachable.body'] }), _jsxs("div", { className: slotClass(classNames, 'actions', 'flex flex-wrap items-center gap-4'), children: [_jsx("a", { href: retryHref, className: link, children: labels['portalUnreachable.retry'] }), phone && (_jsx("a", { href: format.telHref(phone), className: link, children: renderLabel(labels['portalUnreachable.call'], { phone }) })), logout] })] }));
|
|
19
19
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { cn } from '../../lib/utils.js';
|
|
4
|
-
import { labelParts } from '../internal/label-parts.js';
|
|
4
|
+
import { joinLabelParts, labelParts, renderLabel } from '../internal/label-parts.js';
|
|
5
5
|
import { bookingButtonClass } from '../internal/ui.js';
|
|
6
6
|
import { fillLabel } from '../labels.js';
|
|
7
7
|
import { slotClass } from '../slots.js';
|
|
@@ -36,22 +36,18 @@ function howFarOff(startTs, now, labels, format) {
|
|
|
36
36
|
}
|
|
37
37
|
/** What is booked and who for. */
|
|
38
38
|
function whatLine(booking, labels) {
|
|
39
|
-
return [booking.serviceName ?? labels['upcoming.serviceFallback'], booking.bookedForName]
|
|
40
|
-
.filter(Boolean)
|
|
41
|
-
.join(SEPARATOR);
|
|
39
|
+
return joinLabelParts([booking.serviceName ?? labels['upcoming.serviceFallback'], booking.bookedForName], SEPARATOR);
|
|
42
40
|
}
|
|
43
41
|
/** When, with whom, and what it costs. */
|
|
44
42
|
function whenLine(booking, now, labels, format) {
|
|
45
|
-
return [
|
|
46
|
-
|
|
43
|
+
return joinLabelParts([
|
|
44
|
+
renderLabel(labels['upcoming.when'], {
|
|
47
45
|
day: format.clock.dayLabel(booking.startTs, now),
|
|
48
46
|
time: format.clock.formatTime(booking.startTs),
|
|
49
47
|
}),
|
|
50
48
|
booking.resourceName ?? labels['upcoming.anyStylist'],
|
|
51
49
|
booking.amountOre === null ? null : format.price(booking.amountOre),
|
|
52
|
-
]
|
|
53
|
-
.filter(Boolean)
|
|
54
|
-
.join(SEPARATOR);
|
|
50
|
+
], SEPARATOR);
|
|
55
51
|
}
|
|
56
52
|
/**
|
|
57
53
|
* «Upcoming appointments» — one card per appointment still ahead.
|
|
@@ -72,7 +68,7 @@ export function UpcomingBookings({ bookings, phone, labels, format, bookingHref,
|
|
|
72
68
|
export function DefaultBookingCard({ booking, phone, now, hero, labels, format, classNames, }) {
|
|
73
69
|
return (_jsxs("li", { className: cn('space-y-2 px-5 py-4', hero
|
|
74
70
|
? slotClass(classNames, 'cardHero', 'rounded-2xl bg-primary p-6 text-primary-foreground md:p-8')
|
|
75
|
-
: slotClass(classNames, 'card', 'rounded-lg border border-border bg-card')), children: [hero && (_jsxs("p", { className: slotClass(classNames, 'badge', 'inline-flex items-center gap-2 rounded-full bg-primary-foreground/15 px-3 py-1 text-xs font-bold'), children: [_jsx("span", { "aria-hidden": "true", className: "size-2 rounded-full bg-current" }),
|
|
71
|
+
: slotClass(classNames, 'card', 'rounded-lg border border-border bg-card')), children: [hero && (_jsxs("p", { className: slotClass(classNames, 'badge', 'inline-flex items-center gap-2 rounded-full bg-primary-foreground/15 px-3 py-1 text-xs font-bold'), children: [_jsx("span", { "aria-hidden": "true", className: "size-2 rounded-full bg-current" }), renderLabel(labels['upcoming.next'], {
|
|
76
72
|
when: howFarOff(booking.startTs, now, labels, format),
|
|
77
73
|
})] })), _jsx("p", { className: cn('font-semibold', hero && 'font-sans text-2xl md:text-3xl'), children: whatLine(booking, labels) }), _jsx("p", { className: cn('tabular-nums', hero && slotClass(classNames, 'heroMuted', 'text-primary-foreground/85')), children: whenLine(booking, now, labels, format) }), booking.managePath ? (_jsx("a", { href: booking.managePath, className: slotClass(classNames, 'manage', 'text-sm font-semibold underline underline-offset-4', hero
|
|
78
74
|
? 'inline-block rounded-lg bg-background px-4 py-2 text-foreground no-underline'
|
|
@@ -80,7 +76,7 @@ export function DefaultBookingCard({ booking, phone, now, hero, labels, format,
|
|
|
80
76
|
? slotClass(classNames, 'heroMuted', 'text-primary-foreground/85')
|
|
81
77
|
: 'text-muted-foreground'), children: phone
|
|
82
78
|
? labelParts(labels['upcoming.noManageWithPhone'], {
|
|
83
|
-
call: (_jsx("a", { href: format.telHref(phone), className: cn('font-semibold underline underline-offset-4', hero ? 'text-primary-foreground' : 'text-primary'), children:
|
|
79
|
+
call: (_jsx("a", { href: format.telHref(phone), className: cn('font-semibold underline underline-offset-4', hero ? 'text-primary-foreground' : 'text-primary'), children: renderLabel(labels['upcoming.call'], { phone }) })),
|
|
84
80
|
})
|
|
85
81
|
: // No number on file: the sentence without a link, because a `tel:`
|
|
86
82
|
// that dials nothing fails in the hand of someone already stuck.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useState } from 'react';
|
|
4
4
|
import { cn } from '../../lib/utils.js';
|
|
5
|
-
import {
|
|
5
|
+
import { renderLabel } from '../internal/label-parts.js';
|
|
6
6
|
import { slotClass } from '../slots.js';
|
|
7
7
|
export const VISIT_HISTORY_LABEL_KEYS = [
|
|
8
8
|
'history.heading',
|
|
@@ -60,7 +60,7 @@ export function VisitHistory({ past, labels, format, years: yearsProp, headingId
|
|
|
60
60
|
return (_jsx("button", { type: "button", "aria-pressed": pressed, onClick: () => setSelected(year.year), className: cn(slotClass(classNames, 'yearChip', 'rounded-full border px-4 py-1.5 text-sm font-semibold transition-colors', pressed
|
|
61
61
|
? 'border-foreground bg-foreground text-background'
|
|
62
62
|
: 'border-border hover:bg-muted'), pressed && classNames?.yearChipSelected), children: year.year }, year.year));
|
|
63
|
-
}) })), _jsx("ul", { className: slotClass(classNames, 'list', 'divide-y divide-border border-y border-border'), children: shown.visits.map((visit) => (_jsx(Row, { visit: visit, labels: labels, format: format, classNames: classNames }, visit.bookingId))) }), _jsxs("p", { className: slotClass(classNames, 'total', 'flex flex-wrap items-baseline justify-between gap-3 rounded-lg bg-muted px-5 py-4'), children: [_jsx("span", { className: "font-semibold", children:
|
|
63
|
+
}) })), _jsx("ul", { className: slotClass(classNames, 'list', 'divide-y divide-border border-y border-border'), children: shown.visits.map((visit) => (_jsx(Row, { visit: visit, labels: labels, format: format, classNames: classNames }, visit.bookingId))) }), _jsxs("p", { className: slotClass(classNames, 'total', 'flex flex-wrap items-baseline justify-between gap-3 rounded-lg bg-muted px-5 py-4'), children: [_jsx("span", { className: "font-semibold", children: renderLabel(labels[shown.visits.length === 1 ? 'history.count.one' : 'history.count.other'], { count: shown.visits.length, year: shown.year }) }), _jsx("span", { className: "font-sans text-lg font-bold tabular-nums", children: shown.totalOre === null
|
|
64
64
|
? labels['history.unknownAmount']
|
|
65
65
|
: format.price(shown.totalOre) })] })] }))] }));
|
|
66
66
|
}
|
|
@@ -17,8 +17,9 @@ export type ServiceScreenLabels = Record<(typeof SERVICE_SCREEN_LABEL_KEYS)[numb
|
|
|
17
17
|
* - `pill` / `pillSelected` — a category pill / the marked one
|
|
18
18
|
* - `groupHeading` — a category's (or a party member's) `<h3>`
|
|
19
19
|
* - `card` / `cardSelected` — a service card / a pressed one (passed to `ServiceCard`)
|
|
20
|
+
* - `price` — a card's price (passed to `ServiceCard`, and the «same as last time» card's)
|
|
20
21
|
*/
|
|
21
|
-
export type ServiceScreenSlot = 'root' | 'heading' | 'pill' | 'pillSelected' | 'groupHeading' | 'card' | 'cardSelected';
|
|
22
|
+
export type ServiceScreenSlot = 'root' | 'heading' | 'pill' | 'pillSelected' | 'groupHeading' | 'card' | 'cardSelected' | 'price';
|
|
22
23
|
/** One service group, in display order. */
|
|
23
24
|
export interface ServiceScreenCategory {
|
|
24
25
|
key: string;
|
|
@@ -56,6 +57,8 @@ export interface ServiceCardProps {
|
|
|
56
57
|
labels: ServiceScreenLabels;
|
|
57
58
|
className?: string;
|
|
58
59
|
selectedClassName?: string;
|
|
60
|
+
/** Merged onto the price (the `price` slot). */
|
|
61
|
+
priceClassName?: string;
|
|
59
62
|
}
|
|
60
63
|
export interface ServiceScreenComponents {
|
|
61
64
|
ServiceCard?: ComponentType<ServiceCardProps>;
|
|
@@ -134,4 +137,4 @@ export declare function ServiceScreen(props: ServiceScreenProps): import("react"
|
|
|
134
137
|
* online, and a tap target that accepts the tap and does nothing is the dead
|
|
135
138
|
* end the phone number replaces. The card stays so the catalogue is whole.
|
|
136
139
|
*/
|
|
137
|
-
export declare function DefaultServiceCard({ service, kind, selected, onPick, phone, format, labels, className, selectedClassName, }: ServiceCardProps): import("react").JSX.Element;
|
|
140
|
+
export declare function DefaultServiceCard({ service, kind, selected, onPick, phone, format, labels, className, selectedClassName, priceClassName, }: ServiceCardProps): import("react").JSX.Element;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useState } from 'react';
|
|
4
4
|
import { cn } from '../lib/utils.js';
|
|
5
|
+
import { renderLabel } from './internal/label-parts.js';
|
|
5
6
|
import { BookingButton } from './internal/ui.js';
|
|
6
7
|
import { fillLabel } from './labels.js';
|
|
7
8
|
import { slotClass } from './slots.js';
|
|
@@ -57,6 +58,7 @@ function cardProps(shared) {
|
|
|
57
58
|
labels: shared.labels,
|
|
58
59
|
className: shared.classNames?.card,
|
|
59
60
|
selectedClassName: shared.classNames?.cardSelected,
|
|
61
|
+
priceClassName: shared.classNames?.price,
|
|
60
62
|
};
|
|
61
63
|
}
|
|
62
64
|
export function ServiceScreen(props) {
|
|
@@ -187,7 +189,7 @@ function PartyServiceScreen({ services, categories, categoryOf, childCategory, s
|
|
|
187
189
|
*/
|
|
188
190
|
function NothingInThisParty({ person, labels, onRemove, }) {
|
|
189
191
|
const values = { label: person.label };
|
|
190
|
-
return (_jsxs("div", { className: "space-y-2 rounded-lg border border-dashed border-border px-5 py-4 text-sm", children: [_jsx("p", { children:
|
|
192
|
+
return (_jsxs("div", { className: "space-y-2 rounded-lg border border-dashed border-border px-5 py-4 text-sm", children: [_jsx("p", { children: renderLabel(labels[person.adult ? 'service.party.adultAlone' : 'service.party.nothingFits'], values) }), onRemove && (_jsx("button", { type: "button", onClick: onRemove, className: "font-semibold text-primary underline underline-offset-4", children: renderLabel(labels['service.party.remove'], values) }))] }));
|
|
191
193
|
}
|
|
192
194
|
/**
|
|
193
195
|
* One person with a «same as last time»: the suggestion as the answer, and the
|
|
@@ -206,7 +208,7 @@ function SameAsLast({ suggestion, selected, onPick, shared, }) {
|
|
|
206
208
|
const { labels, format, classNames } = shared;
|
|
207
209
|
return (_jsxs("div", { className: "space-y-1", children: [_jsxs("button", { type: "button", "aria-pressed": selected, onClick: onPick, className: cn('flex min-h-14 w-full items-baseline justify-between gap-4 rounded-lg border px-5 py-4 text-left transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary', selected
|
|
208
210
|
? 'border-primary bg-primary/5 ring-2 ring-primary'
|
|
209
|
-
: 'border-primary/60 bg-card hover:border-primary', classNames?.card, selected && classNames?.cardSelected), children: [_jsxs("span", { children: [_jsx("span", { className: "block text-xs font-bold tracking-wide text-primary uppercase", children: labels['service.sameAsLast'] }), _jsx("span", { className: "font-medium", children: service.name })] }), _jsxs("span", { className: "flex items-baseline gap-4 text-right", children: [_jsx("span", { className: "text-sm text-muted-foreground tabular-nums", children:
|
|
211
|
+
: 'border-primary/60 bg-card hover:border-primary', classNames?.card, selected && classNames?.cardSelected), children: [_jsxs("span", { children: [_jsx("span", { className: "block text-xs font-bold tracking-wide text-primary uppercase", children: labels['service.sameAsLast'] }), _jsx("span", { className: "font-medium", children: service.name })] }), _jsxs("span", { className: "flex items-baseline gap-4 text-right", children: [_jsx("span", { className: "text-sm text-muted-foreground tabular-nums", children: renderLabel(labels['service.duration'], { minutes: service.durationMinutes }) }), _jsx("span", { className: slotClass(classNames, 'price', 'whitespace-nowrap font-semibold text-foreground tabular-nums'), children: format.price(service.priceOre) })] })] }), note && _jsx("p", { className: "text-sm text-muted-foreground", children: note })] }));
|
|
210
212
|
}
|
|
211
213
|
/**
|
|
212
214
|
* The default service card.
|
|
@@ -218,9 +220,9 @@ function SameAsLast({ suggestion, selected, onPick, shared, }) {
|
|
|
218
220
|
* online, and a tap target that accepts the tap and does nothing is the dead
|
|
219
221
|
* end the phone number replaces. The card stays so the catalogue is whole.
|
|
220
222
|
*/
|
|
221
|
-
export function DefaultServiceCard({ service, kind, selected, onPick, phone, format, labels, className, selectedClassName, }) {
|
|
222
|
-
const duration =
|
|
223
|
-
const price = (_jsx("span", { className:
|
|
223
|
+
export function DefaultServiceCard({ service, kind, selected, onPick, phone, format, labels, className, selectedClassName, priceClassName, }) {
|
|
224
|
+
const duration = renderLabel(labels['service.duration'], { minutes: service.durationMinutes });
|
|
225
|
+
const price = (_jsx("span", { className: cn('whitespace-nowrap font-semibold text-foreground tabular-nums', priceClassName), children: format.price(service.priceOre) }));
|
|
224
226
|
if (kind === 'phone') {
|
|
225
227
|
return (_jsxs("div", { className: cn('flex min-h-14 w-full items-baseline justify-between gap-4 rounded-lg border border-dashed border-border px-5 py-4', className), children: [_jsx("span", { className: "font-medium", children: service.name }), _jsxs("span", { className: "flex items-baseline gap-4 text-right", children: [_jsx("span", { className: "text-sm text-muted-foreground tabular-nums", children: duration }), _jsxs("span", { className: "text-sm", children: [labels['service.phoneOnly'], phone ? (_jsxs(_Fragment, { children: [' – ', _jsx("a", { href: format.telHref(phone), "aria-label": fillLabel(labels['service.phoneOnlyLinkLabel'], { phone }), className: "font-semibold text-primary underline underline-offset-4", children: phone })] })) : null] })] })] }));
|
|
226
228
|
}
|
|
@@ -27,8 +27,9 @@ export type StylistScreenLabels = Record<(typeof STYLIST_SCREEN_LABEL_KEYS)[numb
|
|
|
27
27
|
* - `group` — the radiogroup (the sideways row / the column)
|
|
28
28
|
* - `card` / `cardSelected` — a stylist option / the checked one (passed to `StylistCard`)
|
|
29
29
|
* - `modeCard` / `modeCardSelected` — a family's mode card / the pressed one
|
|
30
|
+
* - `nextAvailable` — a stylist option's next-opening line (passed to `StylistCard`)
|
|
30
31
|
*/
|
|
31
|
-
export type StylistScreenSlot = 'root' | 'heading' | 'group' | 'card' | 'cardSelected' | 'modeCard' | 'modeCardSelected';
|
|
32
|
+
export type StylistScreenSlot = 'root' | 'heading' | 'group' | 'card' | 'cardSelected' | 'modeCard' | 'modeCardSelected' | 'nextAvailable';
|
|
32
33
|
/** One option of the radiogroup as the card draws it. */
|
|
33
34
|
export interface StylistOption {
|
|
34
35
|
/** `null` is «first available». */
|
|
@@ -55,6 +56,8 @@ export interface StylistCardProps {
|
|
|
55
56
|
labels: StylistScreenLabels;
|
|
56
57
|
className?: string;
|
|
57
58
|
selectedClassName?: string;
|
|
59
|
+
/** Merged onto the next-opening line (the `nextAvailable` slot). */
|
|
60
|
+
nextAvailableClassName?: string;
|
|
58
61
|
}
|
|
59
62
|
export interface StylistScreenComponents {
|
|
60
63
|
StylistCard?: ComponentType<StylistCardProps>;
|
|
@@ -102,6 +105,14 @@ export interface StylistScreenProps {
|
|
|
102
105
|
mode: PartyMode;
|
|
103
106
|
/** How many people, for the option labels. */
|
|
104
107
|
size: number;
|
|
108
|
+
/**
|
|
109
|
+
* The size as the copy spells it (e.g. «two»), filled into `{sizeWord}`
|
|
110
|
+
* in `stylist.party.parallel.*` and `stylist.party.parallelNote.*`.
|
|
111
|
+
* Default: the number. Lets a pack keep the word as its own text piece
|
|
112
|
+
* (`'We find {sizeWord} stylists …'`) rather than writing it into the
|
|
113
|
+
* sentence.
|
|
114
|
+
*/
|
|
115
|
+
sizeWord?: string;
|
|
105
116
|
/** How long the visit is in each mode. */
|
|
106
117
|
minutes: {
|
|
107
118
|
sequential: number;
|
|
@@ -114,4 +125,4 @@ export interface StylistScreenProps {
|
|
|
114
125
|
}
|
|
115
126
|
export declare function StylistScreen({ labels, format, now, serviceIds, resources, selectedResourceId, nextAvailableTs, nextAvailableLoading, loading, skeletonCount, pendingName, notice, onPick, party, classNames, components, }: StylistScreenProps): import("react").JSX.Element;
|
|
116
127
|
/** The default stylist option: avatar (photo or badge), name(s), bio, next opening. */
|
|
117
|
-
export declare function DefaultStylistCard({ ref, option, selected, tabIndex, onClick, labels, className, selectedClassName, }: StylistCardProps): import("react").JSX.Element;
|
|
128
|
+
export declare function DefaultStylistCard({ ref, option, selected, tabIndex, onClick, labels, className, selectedClassName, nextAvailableClassName, }: StylistCardProps): import("react").JSX.Element;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useEffect, useId, useRef } from 'react';
|
|
3
|
+
import { useEffect, useId, useRef, } from 'react';
|
|
4
4
|
import { cn } from '../lib/utils.js';
|
|
5
|
-
import {
|
|
5
|
+
import { renderLabel } from './internal/label-parts.js';
|
|
6
6
|
import { LiveStatus } from './live-status.js';
|
|
7
7
|
import { slotClass } from './slots.js';
|
|
8
8
|
/**
|
|
@@ -55,10 +55,13 @@ function firstName(displayName) {
|
|
|
55
55
|
}
|
|
56
56
|
/** Sizes the label packs write out in words; any other size reads `.other`. */
|
|
57
57
|
const SIZE_WORDS = { 2: 'two', 3: 'three' };
|
|
58
|
-
/**
|
|
59
|
-
|
|
58
|
+
/**
|
|
59
|
+
* The size-specific label: `.two`, `.three`, else `.other`, filled with
|
|
60
|
+
* `{count}` (the number) and `{sizeWord}` (`party.sizeWord`, else the number).
|
|
61
|
+
*/
|
|
62
|
+
function bySize(labels, base, size, sizeWord) {
|
|
60
63
|
const suffix = SIZE_WORDS[size] ?? 'other';
|
|
61
|
-
return
|
|
64
|
+
return renderLabel(labels[`${base}.${suffix}`], { count: size, sizeWord: sizeWord ?? size });
|
|
62
65
|
}
|
|
63
66
|
export function StylistScreen({ labels, format, now, serviceIds, resources, selectedResourceId = null, nextAvailableTs, nextAvailableLoading = false, loading = false, skeletonCount = DEFAULT_SKELETON_COUNT, pendingName = null, notice = null, onPick, party, classNames, components, }) {
|
|
64
67
|
// `every`, not `some`: the step asks one question for the whole visit, so a
|
|
@@ -103,11 +106,11 @@ export function StylistScreen({ labels, format, now, serviceIds, resources, sele
|
|
|
103
106
|
};
|
|
104
107
|
})),
|
|
105
108
|
];
|
|
106
|
-
return (_jsxs("section", { "aria-labelledby": HEADING_ID, className: slotClass(classNames, 'root', 'space-y-6'), children: [_jsx("h2", { id: HEADING_ID, tabIndex: -1, className: slotClass(classNames, 'heading', 'font-sans text-2xl font-bold outline-none md:text-3xl'), children: labels['stylist.heading'] }), party && (_jsxs("fieldset", { className: "flex flex-col gap-2 sm:flex-row", children: [_jsx("legend", { className: "mb-2 font-sans text-lg font-bold", children: labels['stylist.party.legend'] }), _jsx(ModeCard, { selected: party.mode === 'sequential', onClick: () => party.onMode('sequential'), title: labels['stylist.party.sequential'], subtitle:
|
|
109
|
+
return (_jsxs("section", { "aria-labelledby": HEADING_ID, className: slotClass(classNames, 'root', 'space-y-6'), children: [_jsx("h2", { id: HEADING_ID, tabIndex: -1, className: slotClass(classNames, 'heading', 'font-sans text-2xl font-bold outline-none md:text-3xl'), children: labels['stylist.heading'] }), party && (_jsxs("fieldset", { className: "flex flex-col gap-2 sm:flex-row", children: [_jsx("legend", { className: "mb-2 font-sans text-lg font-bold", children: labels['stylist.party.legend'] }), _jsx(ModeCard, { selected: party.mode === 'sequential', onClick: () => party.onMode('sequential'), title: labels['stylist.party.sequential'], subtitle: renderLabel(labels['stylist.party.sequentialMinutes'], {
|
|
107
110
|
minutes: party.minutes.sequential,
|
|
108
|
-
}), classNames: classNames }), _jsx(ModeCard, { selected: party.mode === 'parallel', onClick: () => party.onMode('parallel'), title: bySize(labels, 'stylist.party.parallel', party.size), subtitle:
|
|
111
|
+
}), classNames: classNames }), _jsx(ModeCard, { selected: party.mode === 'parallel', onClick: () => party.onMode('parallel'), title: bySize(labels, 'stylist.party.parallel', party.size, party.sizeWord), subtitle: renderLabel(labels['stylist.party.parallelMinutes'], {
|
|
109
112
|
minutes: party.minutes.parallel,
|
|
110
|
-
}), 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) })) : (_jsx(StylistRadioGroup, { options: options, selectedResourceId: selectedResourceId,
|
|
113
|
+
}), 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,
|
|
111
114
|
// The held option takes one placeholder's place, so the row is
|
|
112
115
|
// exactly as long as it was and nothing after it moves.
|
|
113
116
|
skeletons: loading ? Math.max(0, skeletonCount - (options.length - 1)) : 0, busy: loading, onPick: onPick, labels: labels, classNames: classNames, Card: components?.StylistCard ?? DefaultStylistCard })), _jsx(LiveStatus, { text: loading ? labels['stylist.loading'] : notice })] }));
|
|
@@ -174,7 +177,7 @@ function StylistRadioGroup({ options, selectedResourceId, skeletons, busy, onPic
|
|
|
174
177
|
// box would otherwise clip. `md`: the column of cards.
|
|
175
178
|
className: slotClass(classNames, 'group', 'relative -mx-1 flex h-36 snap-x snap-mandatory scroll-px-1 gap-1 overflow-x-auto overscroll-x-contain px-1 py-2', '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) => {
|
|
176
179
|
optionRefs.current[index] = node;
|
|
177
|
-
}, option: option, selected: option.resourceId === selectedResourceId, tabIndex: index === tabStop ? 0 : -1, onClick: () => onPick(option.resourceId), labels: labels, className: classNames?.card, selectedClassName: classNames?.cardSelected }, option.resourceId ?? 'first-available'))), Array.from({ length: skeletons }, (_, index) => (_jsx(SkeletonCard, {}, index)))] }));
|
|
180
|
+
}, 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)))] }));
|
|
178
181
|
}
|
|
179
182
|
/** The restored stylist's stand-in: a real, checked radio of the final size,
|
|
180
183
|
* with its next-opening row pulsing like every card still waiting for one. */
|
|
@@ -204,7 +207,7 @@ function ModeCard({ selected, onClick, title, subtitle, classNames, }) {
|
|
|
204
207
|
: '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 })] }));
|
|
205
208
|
}
|
|
206
209
|
/** The default stylist option: avatar (photo or badge), name(s), bio, next opening. */
|
|
207
|
-
export function DefaultStylistCard({ ref, option, selected, tabIndex, onClick, labels, className, selectedClassName, }) {
|
|
210
|
+
export function DefaultStylistCard({ ref, option, selected, tabIndex, onClick, labels, className, selectedClassName, nextAvailableClassName, }) {
|
|
208
211
|
const id = useId();
|
|
209
212
|
const nameId = `${id}-name`;
|
|
210
213
|
const nextId = `${id}-next`;
|
|
@@ -218,7 +221,7 @@ export function DefaultStylistCard({ ref, option, selected, tabIndex, onClick, l
|
|
|
218
221
|
? 'font-semibold md:border-primary md:bg-primary/10 md:ring-2 md:ring-primary'
|
|
219
222
|
: '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
|
|
220
223
|
? 'ring-2 ring-primary ring-offset-2 ring-offset-background md:ring-0 md:ring-offset-0'
|
|
221
|
-
: 'ring-1 ring-border md:ring-0'), children: photoUrl ? (_jsx("img", { src: photoUrl, alt: "", className: "size-full object-cover", width: 56, height: 56, loading: "lazy", decoding: "async" })) : (badge) }), _jsxs("span", { className: "flex w-full min-w-0 flex-col md:w-auto", children: [_jsx("span", { "aria-hidden": "true", className: "h-5 truncate text-sm font-medium md:hidden", children: option.shortName }), _jsx("span", { id: nameId, className: "hidden truncate font-medium md:block", children: name }), subtitle && (_jsx("span", { id: bioId, className: "hidden truncate text-sm text-muted-foreground md:block", children: subtitle })), availability ? (_jsxs("span", { id: nextId, "data-testid": "next-available-row", className:
|
|
224
|
+
: 'ring-1 ring-border md:ring-0'), children: photoUrl ? (_jsx("img", { src: photoUrl, alt: "", className: "size-full object-cover", width: 56, height: 56, loading: "lazy", decoding: "async" })) : (badge) }), _jsxs("span", { className: "flex w-full min-w-0 flex-col md:w-auto", children: [_jsx("span", { "aria-hidden": "true", className: "h-5 truncate text-sm font-medium md:hidden", children: option.shortName }), _jsx("span", { id: nameId, className: "hidden truncate font-medium md:block", children: name }), subtitle && (_jsx("span", { id: bioId, className: "hidden truncate text-sm text-muted-foreground md:block", children: subtitle })), availability ? (_jsxs("span", { id: nextId, "data-testid": "next-available-row", className: cn('line-clamp-2 h-8 text-xs leading-4 text-foreground tabular-nums md:line-clamp-1 md:h-5 md:text-sm md:leading-5', nextAvailableClassName), children: [_jsx("span", { className: "sr-only md:not-sr-only", children: labels['stylist.nextAvailable'] }), ' ', availability] })) : (reserveAvailability && (_jsx("span", { "data-testid": "next-available-row", "aria-hidden": "true", className: "flex h-8 justify-center pt-1 md:block md:h-5 md:py-1", children: availabilityLoading && (_jsx("span", { className: "block h-3 w-16 animate-pulse rounded bg-muted md:w-36" })) })))] })] }));
|
|
222
225
|
}
|
|
223
226
|
/** A stylist option before there is a stylist: the same box, the same circle, at both widths. */
|
|
224
227
|
function SkeletonCard() {
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
3
3
|
import { ChevronDown, ChevronLeft, ChevronRight, ChevronUp, X } from 'lucide-react';
|
|
4
4
|
import { useEffect, useState } from 'react';
|
|
5
5
|
import { cn } from '../lib/utils.js';
|
|
6
|
+
import { renderLabel } from './internal/label-parts.js';
|
|
6
7
|
import { BookingButton, bookingButtonClass } from './internal/ui.js';
|
|
7
8
|
import { fillLabel } from './labels.js';
|
|
8
9
|
import { LiveStatus } from './live-status.js';
|
|
@@ -155,8 +156,8 @@ function offeredSlots(slots, party) {
|
|
|
155
156
|
function weekendSentence(labels, format, note) {
|
|
156
157
|
const price = format.price(note.priceOre);
|
|
157
158
|
return note.pct === null
|
|
158
|
-
?
|
|
159
|
-
:
|
|
159
|
+
? renderLabel(labels['time.weekend.mixed'], { price })
|
|
160
|
+
: renderLabel(labels['time.weekend.pct'], { pct: note.pct, price });
|
|
160
161
|
}
|
|
161
162
|
// Month grid — pure calendar arithmetic over the business-local date, so a
|
|
162
163
|
// viewer in another zone is never shown a grid built around the wrong day.
|
|
@@ -317,7 +318,7 @@ function MonthCalendar({ labels, clock, open, onToggle, monthTs, onMonth, strip,
|
|
|
317
318
|
const { year, month, day: anchorDay } = ymd(clock, monthTs);
|
|
318
319
|
const firstOfMonth = clock.dayStart(monthTs, 1 - anchorDay);
|
|
319
320
|
const monthName = clock.monthName(month);
|
|
320
|
-
const heading =
|
|
321
|
+
const heading = renderLabel(labels['time.month.heading'], {
|
|
321
322
|
month: `${monthName.charAt(0).toUpperCase()}${monthName.slice(1)}`,
|
|
322
323
|
year,
|
|
323
324
|
});
|
|
@@ -380,7 +381,7 @@ function DaypartSections({ labels, clock, dayparts, daySlots, nearest, currentSl
|
|
|
380
381
|
function ParallelInstead({ labels, clock, slot, day, now, size, onAccept, className, }) {
|
|
381
382
|
const time = clock.formatTime(slot.startTs);
|
|
382
383
|
const offer = size === 2 ? labels['time.parallel.offer.two'] : labels['time.parallel.offer.other'];
|
|
383
|
-
return (_jsxs("div", { className: cn('space-y-3 rounded-lg border border-primary/40 bg-primary/5 px-5 py-4', className), children: [_jsx("p", { className: "text-sm", children:
|
|
384
|
+
return (_jsxs("div", { className: cn('space-y-3 rounded-lg border border-primary/40 bg-primary/5 px-5 py-4', className), children: [_jsx("p", { className: "text-sm", children: renderLabel(offer, { day: clock.dayLabel(day, now), time }) }), _jsx(BookingButton, { size: "lg", onClick: onAccept, className: "tabular-nums", children: renderLabel(labels['time.parallel.accept'], { time }) })] }));
|
|
384
385
|
}
|
|
385
386
|
/**
|
|
386
387
|
* A day with nothing left, in four different situations:
|
|
@@ -395,17 +396,17 @@ function NothingFreeCard({ labels, format, day, now, phone, standing, stylistNam
|
|
|
395
396
|
const dayLabel = format.clock.dayLabel(day, now);
|
|
396
397
|
const cardClass = cn('rounded-lg border border-border bg-card px-5 py-4 text-sm', className);
|
|
397
398
|
if (standing === 'closed' || standing === 'over') {
|
|
398
|
-
return (_jsx("p", { className: cardClass, children:
|
|
399
|
+
return (_jsx("p", { className: cardClass, children: renderLabel(labels[standing === 'closed' ? 'time.empty.closed' : 'time.empty.over'], {
|
|
399
400
|
day: dayLabel,
|
|
400
401
|
}) }));
|
|
401
402
|
}
|
|
402
|
-
let lead =
|
|
403
|
+
let lead = renderLabel(labels['time.empty.unknown'], { day: dayLabel });
|
|
403
404
|
if (standing === 'open') {
|
|
404
405
|
lead = stylistName
|
|
405
|
-
?
|
|
406
|
-
:
|
|
406
|
+
? renderLabel(labels['time.empty.stylist'], { stylist: stylistName, day: dayLabel })
|
|
407
|
+
: renderLabel(labels['time.empty.full'], { day: dayLabel });
|
|
407
408
|
}
|
|
408
|
-
return (_jsxs("p", { className: cardClass, children: [lead, ' ', phone ? (_jsxs(_Fragment, { children: [_jsx("a", { href: format.telHref(phone), "aria-label": fillLabel(labels['time.empty.callLinkAria'], { phone }), className: "font-semibold text-primary underline underline-offset-4", children:
|
|
409
|
+
return (_jsxs("p", { className: cardClass, children: [lead, ' ', phone ? (_jsxs(_Fragment, { children: [_jsx("a", { href: format.telHref(phone), "aria-label": fillLabel(labels['time.empty.callLinkAria'], { phone }), className: "font-semibold text-primary underline underline-offset-4", children: renderLabel(labels['time.empty.callLink'], { phone }) }), labels['time.empty.callSuffix']] })) : (labels['time.empty.callPlain'])] }));
|
|
409
410
|
}
|
|
410
411
|
/**
|
|
411
412
|
* Today's remaining gaps sit inside the business's lead time, so they are never
|
|
@@ -4,7 +4,7 @@ import { Check } from 'lucide-react';
|
|
|
4
4
|
import { useRef } from 'react';
|
|
5
5
|
import { cn } from '../lib/utils.js';
|
|
6
6
|
import { ADD_CHILD_SHEET_LABEL_KEYS, AddChildSheet, } from './add-child-sheet.js';
|
|
7
|
-
import {
|
|
7
|
+
import { renderLabel } from './internal/label-parts.js';
|
|
8
8
|
import { slotClass } from './slots.js';
|
|
9
9
|
/**
|
|
10
10
|
* The first step — who is the visit for?
|
|
@@ -67,7 +67,7 @@ function choiceLabel(choice, labels) {
|
|
|
67
67
|
return labels['who.guest.adult'];
|
|
68
68
|
}
|
|
69
69
|
const count = choice.people.length;
|
|
70
|
-
return
|
|
70
|
+
return renderLabel(labels[count === 1 ? 'who.guest.children.one' : 'who.guest.children.other'], {
|
|
71
71
|
count,
|
|
72
72
|
});
|
|
73
73
|
}
|
|
@@ -104,10 +104,10 @@ export function WhoScreen({ labels, format, people, family, guestChoices, addedC
|
|
|
104
104
|
return;
|
|
105
105
|
onChoose([...seated, person], false);
|
|
106
106
|
}
|
|
107
|
-
return (_jsxs("section", { "aria-labelledby": HEADING_ID, className: slotClass(classNames, 'root', 'space-y-6'), children: [heading, _jsxs("fieldset", { className: "space-y-4", children: [_jsx("legend", { className: "mb-2 text-sm text-muted-foreground", children:
|
|
107
|
+
return (_jsxs("section", { "aria-labelledby": HEADING_ID, className: slotClass(classNames, 'root', 'space-y-6'), children: [heading, _jsxs("fieldset", { className: "space-y-4", children: [_jsx("legend", { className: "mb-2 text-sm text-muted-foreground", children: renderLabel(labels['who.family.legend'], { max: maxPeople }) }), _jsxs("ul", { className: "space-y-2", children: [family.map((entry, index) => {
|
|
108
108
|
const selected = chosen.has(entry.person.key);
|
|
109
109
|
return (_jsx("li", { children: _jsx(PersonCard, { name: entry.person.name ?? '', line: entry.line, index: index, selected: selected, disabled: !selected && full, describedBy: !selected && full ? LIMIT_ID : undefined, onToggle: () => toggle(entry.person), ...cardClasses }) }, entry.person.key));
|
|
110
|
-
}), _jsx("li", { children: sheet(full, true) })] }), _jsxs(CheckRow, { checked: selfChecked, disabled: !selfChecked && full, describedBy: !selfChecked && full ? LIMIT_ID : undefined, onToggle: () => toggle(self), className: cn('h-14 justify-between px-5 text-sm', classNames?.card), selectedClassName: classNames?.cardSelected, children: [_jsx("span", { className: "font-medium", children: labels['who.family.self'] }), _jsx(CheckCircle, { checked: selfChecked })] }), _jsx("p", { id: LIMIT_ID, "aria-live": "polite", className: slotClass(classNames, 'limit', 'min-h-5 text-sm text-muted-foreground'), children: full ?
|
|
110
|
+
}), _jsx("li", { children: sheet(full, true) })] }), _jsxs(CheckRow, { checked: selfChecked, disabled: !selfChecked && full, describedBy: !selfChecked && full ? LIMIT_ID : undefined, onToggle: () => toggle(self), className: cn('h-14 justify-between px-5 text-sm', classNames?.card), selectedClassName: classNames?.cardSelected, children: [_jsx("span", { className: "font-medium", children: labels['who.family.self'] }), _jsx(CheckCircle, { checked: selfChecked })] }), _jsx("p", { id: LIMIT_ID, "aria-live": "polite", className: slotClass(classNames, 'limit', 'min-h-5 text-sm text-muted-foreground'), children: full ? renderLabel(labels['who.family.limit'], { max: maxPeople }) : '' })] })] }));
|
|
111
111
|
}
|
|
112
112
|
/**
|
|
113
113
|
* The guest chips as a radio group. Roving focus: one chip is in the tab
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@medalsocial/meda",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"description": "Shared Meda UI shell and runtime package.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -201,13 +201,13 @@
|
|
|
201
201
|
}
|
|
202
202
|
},
|
|
203
203
|
"dependencies": {
|
|
204
|
-
"@base-ui/react": "1.
|
|
205
|
-
"class-variance-authority": "0.7.1",
|
|
206
|
-
"clsx": "2.1.1",
|
|
207
|
-
"cmdk": "1.1.1",
|
|
208
|
-
"react-resizable-panels": "4.13.1",
|
|
209
|
-
"tailwind-merge": "3.
|
|
210
|
-
"vaul": "1.1.2"
|
|
204
|
+
"@base-ui/react": "^1.7.0",
|
|
205
|
+
"class-variance-authority": "^0.7.1",
|
|
206
|
+
"clsx": "^2.1.1",
|
|
207
|
+
"cmdk": "^1.1.1",
|
|
208
|
+
"react-resizable-panels": "^4.13.1",
|
|
209
|
+
"tailwind-merge": "^3.6.0",
|
|
210
|
+
"vaul": "^1.1.2"
|
|
211
211
|
},
|
|
212
212
|
"devDependencies": {
|
|
213
213
|
"@biomejs/biome": "2.5.14",
|