@nickkupriyanov/slotline-react 0.1.1 → 0.2.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/README.md +39 -21
- package/dist/index.cjs +544 -642
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +76 -77
- package/dist/index.d.ts +76 -77
- package/dist/index.js +378 -634
- package/dist/index.js.map +1 -1
- package/dist/motion.cjs +544 -642
- package/dist/motion.cjs.map +1 -1
- package/dist/motion.d.cts +4 -3
- package/dist/motion.d.ts +4 -3
- package/dist/motion.js +378 -634
- package/dist/motion.js.map +1 -1
- package/dist/slotline.css +18 -11
- package/package.json +2 -2
package/dist/motion.js
CHANGED
|
@@ -1,86 +1,42 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { motion, AnimatePresence } from 'motion/react';
|
|
3
|
-
import { createContext, useRef, useState,
|
|
4
|
-
import {
|
|
5
|
-
|
|
3
|
+
import { createContext, useRef, useState, useEffect, useContext, useCallback, useSyncExternalStore, useMemo, useId } from 'react';
|
|
4
|
+
import { observeAnimatedHeight, observeMediaQuery, REDUCED_MOTION_QUERY, trapFocus, observeVisibilityChange, DARK_SCHEME_QUERY, canMatchMedia, matchesMediaQuery } from '@nickkupriyanov/slotline-core/dom';
|
|
5
|
+
import { defaultMessages, cx, SLOT_CLASS, format, createBookingController, resolveMessages, monthKeyOf, weekdayIndex, addDays, resolveTimeZone, detectLocale } from '@nickkupriyanov/slotline-core';
|
|
6
|
+
export { BOOKING_ERROR_CODES, BookingError, DEFAULT_BOOKING_CONTEXT, DEFAULT_BOOKING_FIELDS, DEFAULT_CONTEXT_KEY, DEFAULT_DURATION_OPTIONS, DEFAULT_TIME_FORMAT, FALLBACK_LOCALE, FALLBACK_TIME_ZONE, FLOW_STEP_ORDER, SLOT_CLASS, addDays, addMonths, bookingReducer, buildMonthGrid, clampDayKey, compareDayKeys, contextKeyOf, countSlots, createBookingController, createBookingState, createEmptyValues, createIdempotencyKey, createMockAdapter, createTimeAnchor, cx, dayKeyOf, dayKeyOfInstant, defaultMessages, deriveBookingView, detectLocale, endOf, format, formatFullDay, formatMonthYear, formatTime, formatTimeZoneName, formatUtcOffset, guaranteesNotCreated, instantOf, instantOfZonedTime, isAbortError, isBookingError, isBookingErrorCode, isDayKey, isRepeatedLocalTime, isRetrySafe, midnightAfter, monthKeyOf, resolveBookingConfig, resolveBookingFields, resolveMessages, resolveTimeZone, retryWindowClosesAt, soonestBoundary, systemClock, timeAnchorKey, toBookingError, todayKey, unmappedFieldNames, userSteps, validateBookingValues, visibleFieldErrors, wallKeyOf, zoneOffsetMs } from '@nickkupriyanov/slotline-core';
|
|
6
7
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
7
8
|
|
|
8
|
-
function subscribe(query, listener) {
|
|
9
|
-
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return () => {
|
|
10
|
-
};
|
|
11
|
-
const list = window.matchMedia(query);
|
|
12
|
-
const handler = (event) => listener(event.matches);
|
|
13
|
-
listener(list.matches);
|
|
14
|
-
list.addEventListener("change", handler);
|
|
15
|
-
return () => list.removeEventListener("change", handler);
|
|
16
|
-
}
|
|
17
9
|
function useMediaQuery(query) {
|
|
18
10
|
const [matches, setMatches] = useState(false);
|
|
19
|
-
useEffect(() =>
|
|
20
|
-
let active = true;
|
|
21
|
-
const unsubscribe = subscribe(query, (next) => {
|
|
22
|
-
if (active) setMatches(next);
|
|
23
|
-
});
|
|
24
|
-
return () => {
|
|
25
|
-
active = false;
|
|
26
|
-
unsubscribe();
|
|
27
|
-
};
|
|
28
|
-
}, [query]);
|
|
11
|
+
useEffect(() => observeMediaQuery(query, setMatches), [query]);
|
|
29
12
|
return matches;
|
|
30
13
|
}
|
|
31
14
|
function useReducedMotion() {
|
|
32
|
-
return useMediaQuery(
|
|
15
|
+
return useMediaQuery(REDUCED_MOTION_QUERY);
|
|
33
16
|
}
|
|
34
17
|
|
|
35
18
|
// src/hooks/useAnimatedHeight.ts
|
|
36
19
|
function useAnimatedHeight(durationMs = 320) {
|
|
37
20
|
const contentRef = useRef(null);
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
21
|
+
const [state, setState] = useState({
|
|
22
|
+
height: void 0,
|
|
23
|
+
animating: false
|
|
24
|
+
});
|
|
42
25
|
const reducedMotion = useReducedMotion();
|
|
43
|
-
const measure = useCallback(() => {
|
|
44
|
-
const element = contentRef.current;
|
|
45
|
-
if (!element) return;
|
|
46
|
-
const next = element.getBoundingClientRect().height;
|
|
47
|
-
if (!(next > 0)) return;
|
|
48
|
-
const previous = heightRef.current;
|
|
49
|
-
if (previous !== void 0 && Math.abs(previous - next) >= 1) {
|
|
50
|
-
setAnimating(true);
|
|
51
|
-
if (timerRef.current) clearTimeout(timerRef.current);
|
|
52
|
-
timerRef.current = setTimeout(() => setAnimating(false), durationMs);
|
|
53
|
-
}
|
|
54
|
-
heightRef.current = next;
|
|
55
|
-
setHeight((current) => current === next ? current : next);
|
|
56
|
-
}, [durationMs]);
|
|
57
26
|
useEffect(() => {
|
|
58
|
-
if (reducedMotion) return;
|
|
59
|
-
measure();
|
|
60
27
|
const element = contentRef.current;
|
|
61
|
-
if (!element ||
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
const observer = new ResizeObserver(() => measure());
|
|
66
|
-
observer.observe(element);
|
|
67
|
-
return () => observer.disconnect();
|
|
68
|
-
}, [measure, reducedMotion]);
|
|
69
|
-
useEffect(
|
|
70
|
-
() => () => {
|
|
71
|
-
if (timerRef.current) clearTimeout(timerRef.current);
|
|
72
|
-
},
|
|
73
|
-
[]
|
|
74
|
-
);
|
|
28
|
+
if (!element || reducedMotion) return;
|
|
29
|
+
return observeAnimatedHeight(element, { durationMs, onChange: setState });
|
|
30
|
+
}, [durationMs, reducedMotion]);
|
|
75
31
|
if (reducedMotion) {
|
|
76
32
|
return { contentRef, style: void 0, animating: false };
|
|
77
33
|
}
|
|
78
|
-
const style = height === void 0 ? void 0 : {
|
|
79
|
-
height,
|
|
80
|
-
overflow: animating ? "hidden" : "visible",
|
|
34
|
+
const style = state.height === void 0 ? void 0 : {
|
|
35
|
+
height: state.height,
|
|
36
|
+
overflow: state.animating ? "hidden" : "visible",
|
|
81
37
|
transition: `height ${durationMs}ms var(--sl-ease-soft)`
|
|
82
38
|
};
|
|
83
|
-
return { contentRef, style, animating };
|
|
39
|
+
return { contentRef, style, animating: state.animating };
|
|
84
40
|
}
|
|
85
41
|
var spring = { type: "spring", stiffness: 420, damping: 34, mass: 0.7 };
|
|
86
42
|
var MotionButton = motion.button;
|
|
@@ -168,7 +124,11 @@ var useFlow = useSlotlineContext;
|
|
|
168
124
|
var UIContext = createContext({
|
|
169
125
|
animations: cssAnimations,
|
|
170
126
|
autoFocus: false,
|
|
171
|
-
trapFocus: false
|
|
127
|
+
trapFocus: false,
|
|
128
|
+
canDismiss: false,
|
|
129
|
+
messages: defaultMessages.en,
|
|
130
|
+
locale: "en",
|
|
131
|
+
theme: "dark"
|
|
172
132
|
});
|
|
173
133
|
function useUI() {
|
|
174
134
|
return useContext(UIContext);
|
|
@@ -183,57 +143,37 @@ function useClassNames() {
|
|
|
183
143
|
[classNames]
|
|
184
144
|
);
|
|
185
145
|
}
|
|
186
|
-
var FOCUSABLE = [
|
|
187
|
-
"a[href]",
|
|
188
|
-
"button:not([disabled])",
|
|
189
|
-
'input:not([disabled]):not([type="hidden"])',
|
|
190
|
-
"select:not([disabled])",
|
|
191
|
-
"textarea:not([disabled])",
|
|
192
|
-
'[tabindex]:not([tabindex="-1"])'
|
|
193
|
-
].join(",");
|
|
194
|
-
function focusableWithin(container) {
|
|
195
|
-
return Array.from(container.querySelectorAll(FOCUSABLE)).filter(
|
|
196
|
-
(element) => element.offsetParent !== null || element === document.activeElement || element.tabIndex >= 0
|
|
197
|
-
);
|
|
198
|
-
}
|
|
199
146
|
function useFocusTrap(containerRef, { active, onEscape }) {
|
|
200
147
|
useEffect(() => {
|
|
201
|
-
if (!active) return;
|
|
202
148
|
const container = containerRef.current;
|
|
203
|
-
if (!container) return;
|
|
204
|
-
|
|
205
|
-
if (event.key === "Escape" && onEscape) {
|
|
206
|
-
event.stopPropagation();
|
|
207
|
-
onEscape();
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
if (event.key !== "Tab") return;
|
|
211
|
-
const focusable = focusableWithin(container);
|
|
212
|
-
if (focusable.length === 0) {
|
|
213
|
-
event.preventDefault();
|
|
214
|
-
container.focus();
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
const first = focusable[0];
|
|
218
|
-
const last = focusable[focusable.length - 1];
|
|
219
|
-
const activeElement = document.activeElement;
|
|
220
|
-
if (!activeElement || !container.contains(activeElement)) {
|
|
221
|
-
event.preventDefault();
|
|
222
|
-
(event.shiftKey ? last : first).focus();
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
if (!event.shiftKey && activeElement === last) {
|
|
226
|
-
event.preventDefault();
|
|
227
|
-
first.focus();
|
|
228
|
-
} else if (event.shiftKey && activeElement === first) {
|
|
229
|
-
event.preventDefault();
|
|
230
|
-
last.focus();
|
|
231
|
-
}
|
|
232
|
-
};
|
|
233
|
-
container.addEventListener("keydown", onKeyDown);
|
|
234
|
-
return () => container.removeEventListener("keydown", onKeyDown);
|
|
149
|
+
if (!active || !container) return;
|
|
150
|
+
return trapFocus(container, onEscape ? { onEscape } : {});
|
|
235
151
|
}, [active, containerRef, onEscape]);
|
|
236
152
|
}
|
|
153
|
+
function AttemptStep() {
|
|
154
|
+
const { snapshot, actions } = useFlow();
|
|
155
|
+
const { messages, onCheckStatus } = useUI();
|
|
156
|
+
const cls = useClassNames();
|
|
157
|
+
const attempt = snapshot.view.attempt;
|
|
158
|
+
if (!attempt) return null;
|
|
159
|
+
return /* @__PURE__ */ jsxs("div", { className: cls("empty"), "data-attempt": attempt.status, children: [
|
|
160
|
+
/* @__PURE__ */ jsx("p", { className: "sl-empty-title", children: attempt.title }),
|
|
161
|
+
/* @__PURE__ */ jsx("p", { className: "sl-empty-body", children: attempt.description }),
|
|
162
|
+
attempt.instructions ? /* @__PURE__ */ jsx("p", { className: "sl-empty-body", children: attempt.instructions }) : null,
|
|
163
|
+
/* @__PURE__ */ jsx("div", { className: cls("summary"), "aria-label": attempt.summaryLabel, children: /* @__PURE__ */ jsxs("div", { className: "sl-summary-body", children: [
|
|
164
|
+
/* @__PURE__ */ jsx("p", { className: "sl-summary-date", children: attempt.summary.dateLabel }),
|
|
165
|
+
/* @__PURE__ */ jsx("p", { className: "sl-summary-meta", children: /* @__PURE__ */ jsxs("span", { children: [
|
|
166
|
+
attempt.summary.timeLabel,
|
|
167
|
+
" \xB7 ",
|
|
168
|
+
attempt.summary.durationLabel
|
|
169
|
+
] }) })
|
|
170
|
+
] }) }),
|
|
171
|
+
/* @__PURE__ */ jsx("p", { className: "sl-empty-body", children: attempt.retryDescription }),
|
|
172
|
+
attempt.retryAllowed ? /* @__PURE__ */ jsx("button", { type: "button", className: cls("submit"), onClick: actions.retry, children: attempt.retryLabel }) : null,
|
|
173
|
+
attempt.canCheckStatus && onCheckStatus ? /* @__PURE__ */ jsx("button", { type: "button", className: cls("secondaryButton"), onClick: () => onCheckStatus(), children: attempt.checkStatusLabel }) : null,
|
|
174
|
+
/* @__PURE__ */ jsx("span", { className: "sl-visually-hidden", children: messages.attemptSummaryLabel })
|
|
175
|
+
] });
|
|
176
|
+
}
|
|
237
177
|
function ChevronLeftIcon(props) {
|
|
238
178
|
return /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", width: "18", height: "18", "aria-hidden": "true", focusable: "false", ...props, children: /* @__PURE__ */ jsx(
|
|
239
179
|
"path",
|
|
@@ -352,55 +292,44 @@ function chunk(values, size) {
|
|
|
352
292
|
return rows;
|
|
353
293
|
}
|
|
354
294
|
function CalendarStep() {
|
|
355
|
-
const {
|
|
356
|
-
|
|
357
|
-
messages,
|
|
358
|
-
locale,
|
|
359
|
-
timeZone,
|
|
360
|
-
timeZoneName,
|
|
361
|
-
monthLabel,
|
|
362
|
-
monthGrid,
|
|
363
|
-
weekdayHeaders,
|
|
364
|
-
weekdayLongNames,
|
|
365
|
-
weekStartsOn,
|
|
366
|
-
actions,
|
|
367
|
-
availabilityOf,
|
|
368
|
-
isDayBookable
|
|
369
|
-
} = useFlow();
|
|
295
|
+
const { snapshot, actions } = useFlow();
|
|
296
|
+
const { messages } = useUI();
|
|
370
297
|
const cls = useClassNames();
|
|
298
|
+
const view = snapshot.view;
|
|
299
|
+
const { month, days } = view;
|
|
300
|
+
const selectedDate = view.selection.date;
|
|
371
301
|
const gridRef = useRef(null);
|
|
372
302
|
const pendingFocusRef = useRef(false);
|
|
373
303
|
const pendingMonthFocusRef = useRef(null);
|
|
374
304
|
const [focusKey, setFocusKey] = useState(null);
|
|
375
|
-
const
|
|
376
|
-
const rows = useMemo(() => chunk(monthGrid, 7), [monthGrid]);
|
|
305
|
+
const rows = useMemo(() => chunk(month.grid, 7), [month.grid]);
|
|
377
306
|
const firstBookable = useMemo(
|
|
378
|
-
() =>
|
|
379
|
-
[
|
|
307
|
+
() => month.grid.find((day) => monthKeyOf(day) === month.key && days[day]?.bookable),
|
|
308
|
+
[month.grid, month.key, days]
|
|
380
309
|
);
|
|
381
310
|
const focusTarget = useMemo(() => {
|
|
382
|
-
if (focusKey &&
|
|
383
|
-
if (
|
|
384
|
-
return firstBookable ??
|
|
385
|
-
}, [focusKey,
|
|
311
|
+
if (focusKey && month.grid.includes(focusKey)) return focusKey;
|
|
312
|
+
if (selectedDate && month.grid.includes(selectedDate)) return selectedDate;
|
|
313
|
+
return firstBookable ?? month.grid[0] ?? `${month.key}-01`;
|
|
314
|
+
}, [focusKey, month.grid, selectedDate, firstBookable, month.key]);
|
|
386
315
|
useEffect(() => {
|
|
387
316
|
if (!pendingFocusRef.current || !focusKey) return;
|
|
388
317
|
pendingFocusRef.current = false;
|
|
389
318
|
const node = gridRef.current?.querySelector(`[data-day="${focusKey}"]`);
|
|
390
319
|
node?.focus();
|
|
391
|
-
}, [focusKey,
|
|
320
|
+
}, [focusKey, month.grid]);
|
|
392
321
|
useEffect(() => {
|
|
393
322
|
const dayOfMonth = pendingMonthFocusRef.current;
|
|
394
323
|
if (!dayOfMonth) return;
|
|
395
324
|
pendingMonthFocusRef.current = null;
|
|
396
|
-
const sameDay =
|
|
397
|
-
(candidate) => monthKeyOf(candidate) ===
|
|
325
|
+
const sameDay = month.grid.find(
|
|
326
|
+
(candidate) => monthKeyOf(candidate) === month.key && candidate.slice(8, 10) === dayOfMonth
|
|
398
327
|
);
|
|
399
|
-
const target = sameDay ??
|
|
328
|
+
const target = sameDay ?? month.grid.find((candidate) => monthKeyOf(candidate) === month.key);
|
|
400
329
|
if (!target) return;
|
|
401
330
|
pendingFocusRef.current = true;
|
|
402
331
|
setFocusKey(target);
|
|
403
|
-
}, [
|
|
332
|
+
}, [month.key, month.grid]);
|
|
404
333
|
const moveFocus = useCallback((nextKey) => {
|
|
405
334
|
pendingFocusRef.current = true;
|
|
406
335
|
setFocusKey(nextKey);
|
|
@@ -409,7 +338,7 @@ function CalendarStep() {
|
|
|
409
338
|
(event) => {
|
|
410
339
|
const current = event.target.dataset?.day;
|
|
411
340
|
if (!current) return;
|
|
412
|
-
const offsetFromWeekStart = (weekdayIndex(current) - weekStartsOn + 7) % 7;
|
|
341
|
+
const offsetFromWeekStart = (weekdayIndex(current) - month.weekStartsOn + 7) % 7;
|
|
413
342
|
let next;
|
|
414
343
|
switch (event.key) {
|
|
415
344
|
case "ArrowLeft":
|
|
@@ -444,18 +373,18 @@ function CalendarStep() {
|
|
|
444
373
|
return;
|
|
445
374
|
}
|
|
446
375
|
event.preventDefault();
|
|
447
|
-
const last =
|
|
448
|
-
const first =
|
|
376
|
+
const last = month.grid[month.grid.length - 1];
|
|
377
|
+
const first = month.grid[0];
|
|
449
378
|
if (last && first && (next < first || next > last)) {
|
|
450
379
|
actions.goToMonth(monthKeyOf(next));
|
|
451
380
|
}
|
|
452
381
|
moveFocus(next);
|
|
453
382
|
},
|
|
454
|
-
[actions,
|
|
383
|
+
[actions, month.grid, month.key, month.weekStartsOn]
|
|
455
384
|
);
|
|
456
|
-
return /* @__PURE__ */ jsxs("div", { className: cls("calendar"), "data-status":
|
|
385
|
+
return /* @__PURE__ */ jsxs("div", { className: cls("calendar"), "data-status": month.status, children: [
|
|
457
386
|
/* @__PURE__ */ jsxs("div", { className: cls("calendarHeader"), children: [
|
|
458
|
-
/* @__PURE__ */ jsx("h3", { className: cls("monthLabel"), children:
|
|
387
|
+
/* @__PURE__ */ jsx("h3", { className: cls("monthLabel"), children: month.label }),
|
|
459
388
|
/* @__PURE__ */ jsxs("div", { className: "sl-calendar-nav", children: [
|
|
460
389
|
/* @__PURE__ */ jsx(
|
|
461
390
|
"button",
|
|
@@ -479,14 +408,14 @@ function CalendarStep() {
|
|
|
479
408
|
)
|
|
480
409
|
] })
|
|
481
410
|
] }),
|
|
482
|
-
/* @__PURE__ */ jsx("div", { className: cls("weekdays"), children: weekdayHeaders.map((label, index) => /* @__PURE__ */ jsxs("span", { className: cls("weekday"), children: [
|
|
411
|
+
/* @__PURE__ */ jsx("div", { className: cls("weekdays"), children: month.weekdayHeaders.map((label, index) => /* @__PURE__ */ jsxs("span", { className: cls("weekday"), children: [
|
|
483
412
|
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: label }),
|
|
484
|
-
/* @__PURE__ */ jsx("span", { className: "sl-visually-hidden", children: weekdayLongNames[index] })
|
|
485
|
-
] }, weekdayLongNames[index] ?? index)) }),
|
|
486
|
-
|
|
487
|
-
/* @__PURE__ */ jsx("span", { children:
|
|
413
|
+
/* @__PURE__ */ jsx("span", { className: "sl-visually-hidden", children: month.weekdayLongNames[index] })
|
|
414
|
+
] }, month.weekdayLongNames[index] ?? index)) }),
|
|
415
|
+
/* @__PURE__ */ jsxs("div", { className: cls("error"), role: "alert", children: [
|
|
416
|
+
/* @__PURE__ */ jsx("span", { children: month.error }),
|
|
488
417
|
/* @__PURE__ */ jsx("button", { type: "button", className: "sl-link-button", onClick: actions.reloadAvailability, children: messages.retry })
|
|
489
|
-
] })
|
|
418
|
+
] }),
|
|
490
419
|
/* @__PURE__ */ jsx(
|
|
491
420
|
"div",
|
|
492
421
|
{
|
|
@@ -494,17 +423,11 @@ function CalendarStep() {
|
|
|
494
423
|
className: cls("grid"),
|
|
495
424
|
role: "grid",
|
|
496
425
|
"aria-label": messages.calendarLabel,
|
|
497
|
-
"aria-busy":
|
|
426
|
+
"aria-busy": month.status === "loading",
|
|
498
427
|
onKeyDown,
|
|
499
428
|
children: rows.map((row, rowIndex) => /* @__PURE__ */ jsx("div", { className: "sl-grid-row", role: "row", children: row.map((day, cellIndex) => {
|
|
500
|
-
const
|
|
501
|
-
const bookable =
|
|
502
|
-
const count = countSlots(availabilityOf(day));
|
|
503
|
-
const selected = state.selectedDate === day;
|
|
504
|
-
const label = [
|
|
505
|
-
formatFullDay(day, locale),
|
|
506
|
-
count !== void 0 && count > 0 ? format(messages.slotsCount, { count }) : messages.dayUnavailable
|
|
507
|
-
].join(", ");
|
|
429
|
+
const cell = days[day];
|
|
430
|
+
const bookable = cell?.bookable ?? false;
|
|
508
431
|
return /* @__PURE__ */ jsxs(
|
|
509
432
|
"button",
|
|
510
433
|
{
|
|
@@ -512,14 +435,14 @@ function CalendarStep() {
|
|
|
512
435
|
role: "gridcell",
|
|
513
436
|
"data-day": day,
|
|
514
437
|
tabIndex: day === focusTarget ? 0 : -1,
|
|
515
|
-
"aria-selected":
|
|
438
|
+
"aria-selected": cell?.isSelected || void 0,
|
|
516
439
|
"aria-disabled": !bookable || void 0,
|
|
517
|
-
"aria-current":
|
|
518
|
-
"aria-label": label,
|
|
440
|
+
"aria-current": cell?.isToday ? "date" : void 0,
|
|
441
|
+
"aria-label": cell?.label,
|
|
519
442
|
className: cls(
|
|
520
443
|
"day",
|
|
521
|
-
|
|
522
|
-
|
|
444
|
+
cell?.isOutside && "is-outside",
|
|
445
|
+
cell?.isSelected && "is-selected",
|
|
523
446
|
bookable && "is-bookable"
|
|
524
447
|
),
|
|
525
448
|
style: { "--sl-index": rowIndex * 7 + cellIndex },
|
|
@@ -543,90 +466,127 @@ function CalendarStep() {
|
|
|
543
466
|
);
|
|
544
467
|
}) }, row[0]))
|
|
545
468
|
},
|
|
546
|
-
|
|
469
|
+
month.key
|
|
547
470
|
),
|
|
548
|
-
/* @__PURE__ */ jsx("p", { className: "sl-timezone", children:
|
|
471
|
+
/* @__PURE__ */ jsx("p", { className: "sl-timezone", children: view.time.zoneHint })
|
|
549
472
|
] });
|
|
550
473
|
}
|
|
551
474
|
function ConfirmedStep() {
|
|
552
|
-
const {
|
|
475
|
+
const { snapshot, actions } = useFlow();
|
|
553
476
|
const cls = useClassNames();
|
|
554
|
-
const
|
|
477
|
+
const confirmation = snapshot.view.confirmation;
|
|
478
|
+
if (!confirmation) return null;
|
|
555
479
|
return /* @__PURE__ */ jsxs("div", { className: cls("confirmation"), children: [
|
|
556
480
|
/* @__PURE__ */ jsx("span", { className: cls("confirmationIcon"), "aria-hidden": "true", children: /* @__PURE__ */ jsx(CheckIcon, {}) }),
|
|
557
|
-
/* @__PURE__ */ jsx("h3", { className: cls("confirmationTitle"), children:
|
|
481
|
+
/* @__PURE__ */ jsx("h3", { className: cls("confirmationTitle"), children: confirmation.title }),
|
|
558
482
|
/* @__PURE__ */ jsxs("div", { className: cls("confirmationText"), children: [
|
|
559
|
-
/* @__PURE__ */ jsx("p", { children:
|
|
560
|
-
|
|
561
|
-
time: selectedTimeLabel,
|
|
562
|
-
duration: durationLabel
|
|
563
|
-
}) }),
|
|
564
|
-
/* @__PURE__ */ jsx("p", { children: format(messages.inviteSent, { email: inviteeEmail }) })
|
|
483
|
+
/* @__PURE__ */ jsx("p", { children: confirmation.summary }),
|
|
484
|
+
confirmation.invitation ? /* @__PURE__ */ jsx("p", { children: confirmation.invitation }) : null
|
|
565
485
|
] }),
|
|
566
|
-
/* @__PURE__ */ jsx("button", { type: "button", className: cls("secondaryButton"), onClick: actions.reset, children:
|
|
486
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: cls("secondaryButton"), onClick: actions.reset, children: confirmation.bookAnotherLabel })
|
|
567
487
|
] });
|
|
568
488
|
}
|
|
569
|
-
function
|
|
570
|
-
name
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
489
|
+
function autoCompleteOf(name) {
|
|
490
|
+
if (name === "name") return "name";
|
|
491
|
+
if (name === "email") return "email";
|
|
492
|
+
if (name === "tel" || name === "phone") return "tel";
|
|
493
|
+
return void 0;
|
|
494
|
+
}
|
|
495
|
+
function inputTypeOf(field) {
|
|
496
|
+
if (field.type === "email") return "email";
|
|
497
|
+
if (field.type === "tel") return "tel";
|
|
498
|
+
if (field.type === "number") return "number";
|
|
499
|
+
return "text";
|
|
500
|
+
}
|
|
501
|
+
function textOf(value) {
|
|
502
|
+
if (value === void 0 || value === null || typeof value === "boolean") return "";
|
|
503
|
+
return String(value);
|
|
504
|
+
}
|
|
505
|
+
function Field({ field, value, error, disabled, autoFocus, onChange, onBlur }) {
|
|
580
506
|
const cls = useClassNames();
|
|
581
|
-
const
|
|
507
|
+
const generatedId = useId();
|
|
508
|
+
const errorId = `${generatedId}-error`;
|
|
509
|
+
const hintId = `${generatedId}-hint`;
|
|
510
|
+
const shared = {
|
|
511
|
+
id: generatedId,
|
|
512
|
+
name: field.name,
|
|
513
|
+
"aria-label": field.label,
|
|
514
|
+
"aria-invalid": error ? true : void 0,
|
|
515
|
+
"aria-describedby": error ? errorId : field.hint ? hintId : void 0,
|
|
516
|
+
disabled
|
|
517
|
+
};
|
|
582
518
|
return /* @__PURE__ */ jsxs("div", { className: cls("field"), "data-invalid": error ? "true" : void 0, children: [
|
|
583
|
-
/* @__PURE__ */ jsx(
|
|
519
|
+
field.type === "textarea" ? /* @__PURE__ */ jsx(
|
|
520
|
+
"textarea",
|
|
521
|
+
{
|
|
522
|
+
...shared,
|
|
523
|
+
className: cls("input"),
|
|
524
|
+
placeholder: field.placeholder ?? field.label,
|
|
525
|
+
value: textOf(value),
|
|
526
|
+
onChange: (event) => onChange(event.target.value),
|
|
527
|
+
onBlur
|
|
528
|
+
}
|
|
529
|
+
) : field.type === "select" ? /* @__PURE__ */ jsxs(
|
|
530
|
+
"select",
|
|
531
|
+
{
|
|
532
|
+
...shared,
|
|
533
|
+
className: cls("input"),
|
|
534
|
+
value: textOf(value),
|
|
535
|
+
onChange: (event) => onChange(event.target.value),
|
|
536
|
+
onBlur,
|
|
537
|
+
children: [
|
|
538
|
+
/* @__PURE__ */ jsx("option", { value: "", children: field.placeholder ?? "" }),
|
|
539
|
+
(field.options ?? []).map((option) => /* @__PURE__ */ jsx("option", { value: option.value, children: option.label }, option.value))
|
|
540
|
+
]
|
|
541
|
+
}
|
|
542
|
+
) : field.type === "checkbox" ? /* @__PURE__ */ jsxs("label", { className: "sl-checkbox", children: [
|
|
543
|
+
/* @__PURE__ */ jsx(
|
|
544
|
+
"input",
|
|
545
|
+
{
|
|
546
|
+
...shared,
|
|
547
|
+
type: "checkbox",
|
|
548
|
+
checked: value === true,
|
|
549
|
+
onChange: (event) => onChange(event.target.checked),
|
|
550
|
+
onBlur
|
|
551
|
+
}
|
|
552
|
+
),
|
|
553
|
+
/* @__PURE__ */ jsx("span", { children: field.label })
|
|
554
|
+
] }) : /* @__PURE__ */ jsx(
|
|
584
555
|
"input",
|
|
585
556
|
{
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
type,
|
|
589
|
-
|
|
590
|
-
autoComplete: name === "email" ? "email" : "name",
|
|
557
|
+
...shared,
|
|
558
|
+
type: inputTypeOf(field),
|
|
559
|
+
inputMode: field.type === "number" ? "numeric" : field.type === "email" ? "email" : void 0,
|
|
560
|
+
autoComplete: autoCompleteOf(field.name),
|
|
591
561
|
className: cls("input"),
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
"aria-describedby": error ? errorId : void 0,
|
|
595
|
-
placeholder: label,
|
|
596
|
-
value,
|
|
597
|
-
disabled,
|
|
562
|
+
placeholder: field.placeholder ?? field.label,
|
|
563
|
+
value: textOf(value),
|
|
598
564
|
autoFocus,
|
|
599
565
|
onChange: (event) => onChange(event.target.value),
|
|
600
566
|
onBlur
|
|
601
567
|
}
|
|
602
568
|
),
|
|
569
|
+
field.hint && !error ? /* @__PURE__ */ jsx("p", { className: "sl-field-hint", id: hintId, children: field.hint }) : null,
|
|
603
570
|
error ? /* @__PURE__ */ jsx("p", { className: "sl-field-error", id: errorId, role: "alert", children: error }) : null
|
|
604
571
|
] });
|
|
605
572
|
}
|
|
606
573
|
function DetailsStep() {
|
|
607
|
-
const {
|
|
608
|
-
|
|
609
|
-
messages,
|
|
610
|
-
selectedDateLabel,
|
|
611
|
-
selectedTimeLabel,
|
|
612
|
-
durationLabel,
|
|
613
|
-
actions,
|
|
614
|
-
shownFieldErrors,
|
|
615
|
-
detailsValid
|
|
616
|
-
} = useFlow();
|
|
574
|
+
const { snapshot, actions } = useFlow();
|
|
575
|
+
const { messages } = useUI();
|
|
617
576
|
const cls = useClassNames();
|
|
618
|
-
const
|
|
577
|
+
const view = snapshot.view;
|
|
578
|
+
const form = view.form;
|
|
619
579
|
return /* @__PURE__ */ jsxs("div", { className: "sl-details-step", children: [
|
|
620
580
|
/* @__PURE__ */ jsxs("div", { className: cls("summary"), children: [
|
|
621
581
|
/* @__PURE__ */ jsx("span", { className: "sl-summary-icon", "aria-hidden": "true", children: /* @__PURE__ */ jsx(CalendarIcon, {}) }),
|
|
622
582
|
/* @__PURE__ */ jsxs("div", { className: "sl-summary-body", children: [
|
|
623
|
-
/* @__PURE__ */ jsx("p", { className: "sl-summary-date", children:
|
|
583
|
+
/* @__PURE__ */ jsx("p", { className: "sl-summary-date", children: view.time.dateLabel }),
|
|
624
584
|
/* @__PURE__ */ jsxs("p", { className: "sl-summary-meta", children: [
|
|
625
585
|
/* @__PURE__ */ jsx(ClockIcon, {}),
|
|
626
586
|
/* @__PURE__ */ jsxs("span", { children: [
|
|
627
|
-
|
|
587
|
+
view.time.label,
|
|
628
588
|
" \xB7 ",
|
|
629
|
-
|
|
589
|
+
view.duration.label
|
|
630
590
|
] })
|
|
631
591
|
] })
|
|
632
592
|
] })
|
|
@@ -641,54 +601,34 @@ function DetailsStep() {
|
|
|
641
601
|
void actions.submit();
|
|
642
602
|
},
|
|
643
603
|
children: [
|
|
644
|
-
/* @__PURE__ */ jsx(
|
|
604
|
+
form.fields.map((field, index) => /* @__PURE__ */ jsx(
|
|
645
605
|
Field,
|
|
646
606
|
{
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
),
|
|
657
|
-
/* @__PURE__ */ jsx(
|
|
658
|
-
|
|
659
|
-
{
|
|
660
|
-
|
|
661
|
-
label: messages.emailLabel,
|
|
662
|
-
type: "email",
|
|
663
|
-
value: state.details.email,
|
|
664
|
-
error: shownFieldErrors.email,
|
|
665
|
-
disabled: submitting,
|
|
666
|
-
onChange: (value) => actions.setDetails({ email: value }),
|
|
667
|
-
onBlur: () => actions.touchField("email")
|
|
668
|
-
}
|
|
669
|
-
),
|
|
670
|
-
state.submitError ? /* @__PURE__ */ jsxs("div", { className: cls("error"), role: "alert", children: [
|
|
671
|
-
/* @__PURE__ */ jsx("span", { children: state.submitError }),
|
|
672
|
-
/* @__PURE__ */ jsx(
|
|
673
|
-
"button",
|
|
674
|
-
{
|
|
675
|
-
type: "button",
|
|
676
|
-
className: "sl-link-button",
|
|
677
|
-
onClick: () => void actions.submit(),
|
|
678
|
-
children: messages.retry
|
|
679
|
-
}
|
|
680
|
-
)
|
|
607
|
+
field,
|
|
608
|
+
value: form.values[field.name],
|
|
609
|
+
error: form.visibleErrors[field.name],
|
|
610
|
+
disabled: form.submitting,
|
|
611
|
+
autoFocus: index === 0,
|
|
612
|
+
onChange: (value) => actions.setValues({ [field.name]: value }),
|
|
613
|
+
onBlur: () => actions.touchField(field.name)
|
|
614
|
+
},
|
|
615
|
+
field.name
|
|
616
|
+
)),
|
|
617
|
+
form.formError ? /* @__PURE__ */ jsx("div", { className: cls("error"), role: "alert", children: /* @__PURE__ */ jsx("span", { children: form.formError }) }) : null,
|
|
618
|
+
form.submitError ? /* @__PURE__ */ jsxs("div", { className: cls("error"), role: "alert", children: [
|
|
619
|
+
/* @__PURE__ */ jsx("span", { children: form.submitError }),
|
|
620
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: "sl-link-button", onClick: () => void actions.submit(), children: messages.retry })
|
|
681
621
|
] }) : null,
|
|
682
622
|
/* @__PURE__ */ jsxs(
|
|
683
623
|
"button",
|
|
684
624
|
{
|
|
685
625
|
type: "submit",
|
|
686
626
|
className: cls("submit"),
|
|
687
|
-
disabled: submitting
|
|
688
|
-
"data-loading": submitting ? "true" : void 0,
|
|
627
|
+
disabled: form.submitting,
|
|
628
|
+
"data-loading": form.submitting ? "true" : void 0,
|
|
689
629
|
children: [
|
|
690
|
-
/* @__PURE__ */ jsx("span", { children:
|
|
691
|
-
submitting ? /* @__PURE__ */ jsx(SpinnerIcon, { className: "sl-spinner" }) : /* @__PURE__ */ jsx(ArrowRightIcon, { className: "sl-submit-arrow" })
|
|
630
|
+
/* @__PURE__ */ jsx("span", { children: form.submitLabel }),
|
|
631
|
+
form.submitting ? /* @__PURE__ */ jsx(SpinnerIcon, { className: "sl-spinner" }) : /* @__PURE__ */ jsx(ArrowRightIcon, { className: "sl-submit-arrow" })
|
|
692
632
|
]
|
|
693
633
|
}
|
|
694
634
|
)
|
|
@@ -698,12 +638,15 @@ function DetailsStep() {
|
|
|
698
638
|
] });
|
|
699
639
|
}
|
|
700
640
|
function DurationFooter() {
|
|
701
|
-
const {
|
|
641
|
+
const { snapshot, actions } = useFlow();
|
|
642
|
+
const { messages } = useUI();
|
|
702
643
|
const cls = useClassNames();
|
|
644
|
+
const duration = snapshot.view.duration;
|
|
645
|
+
if (!duration.showSelector) return null;
|
|
703
646
|
return /* @__PURE__ */ jsxs("footer", { className: cls("footer"), children: [
|
|
704
647
|
/* @__PURE__ */ jsx("span", { className: "sl-duration-label", "aria-hidden": "true", children: messages.durationLabel }),
|
|
705
|
-
/* @__PURE__ */ jsx("div", { className: "sl-duration-group", role: "radiogroup", "aria-label": messages.durationGroupLabel, children:
|
|
706
|
-
const active = minutes ===
|
|
648
|
+
/* @__PURE__ */ jsx("div", { className: "sl-duration-group", role: "radiogroup", "aria-label": messages.durationGroupLabel, children: duration.options.map((minutes) => {
|
|
649
|
+
const active = minutes === duration.current;
|
|
707
650
|
const label = format(messages.duration, { count: minutes });
|
|
708
651
|
return /* @__PURE__ */ jsx(
|
|
709
652
|
"button",
|
|
@@ -723,26 +666,23 @@ function DurationFooter() {
|
|
|
723
666
|
] });
|
|
724
667
|
}
|
|
725
668
|
function Progress() {
|
|
726
|
-
const {
|
|
669
|
+
const { snapshot } = useFlow();
|
|
727
670
|
const cls = useClassNames();
|
|
728
|
-
const
|
|
671
|
+
const { progress } = snapshot.view;
|
|
729
672
|
return /* @__PURE__ */ jsx(
|
|
730
673
|
"div",
|
|
731
674
|
{
|
|
732
675
|
className: cls("progress"),
|
|
733
676
|
role: "progressbar",
|
|
734
677
|
"aria-valuemin": 1,
|
|
735
|
-
"aria-valuemax":
|
|
736
|
-
"aria-valuenow":
|
|
737
|
-
"aria-valuetext":
|
|
738
|
-
|
|
739
|
-
total: FLOW_STEPS.length
|
|
740
|
-
}),
|
|
741
|
-
children: FLOW_STEPS.map((step, position) => /* @__PURE__ */ jsx(
|
|
678
|
+
"aria-valuemax": progress.total,
|
|
679
|
+
"aria-valuenow": progress.current,
|
|
680
|
+
"aria-valuetext": progress.label,
|
|
681
|
+
children: progress.steps.map((step, position) => /* @__PURE__ */ jsx(
|
|
742
682
|
"span",
|
|
743
683
|
{
|
|
744
684
|
className: "sl-progress-pill",
|
|
745
|
-
"data-state": position
|
|
685
|
+
"data-state": position < progress.current ? "done" : "todo",
|
|
746
686
|
"aria-hidden": "true"
|
|
747
687
|
},
|
|
748
688
|
step
|
|
@@ -752,17 +692,19 @@ function Progress() {
|
|
|
752
692
|
}
|
|
753
693
|
var SKELETON_COUNT = 9;
|
|
754
694
|
function SlotsStep() {
|
|
755
|
-
const {
|
|
695
|
+
const { snapshot, actions } = useFlow();
|
|
696
|
+
const { messages } = useUI();
|
|
756
697
|
const cls = useClassNames();
|
|
757
|
-
const
|
|
758
|
-
const
|
|
698
|
+
const Animations = useUI().animations;
|
|
699
|
+
const view = snapshot.view;
|
|
700
|
+
const { slots, time } = view;
|
|
759
701
|
return /* @__PURE__ */ jsxs("div", { className: "sl-slots-step", children: [
|
|
760
702
|
/* @__PURE__ */ jsxs("div", { className: cls("slotsHeader"), children: [
|
|
761
703
|
/* @__PURE__ */ jsx("span", { className: "sl-slots-icon", "aria-hidden": "true", children: /* @__PURE__ */ jsx(CalendarIcon, {}) }),
|
|
762
|
-
/* @__PURE__ */ jsx("h3", { className: "sl-slots-title", children:
|
|
763
|
-
|
|
704
|
+
/* @__PURE__ */ jsx("h3", { className: "sl-slots-title", children: time.dateLabel }),
|
|
705
|
+
slots.countLabel ? /* @__PURE__ */ jsx("span", { className: cls("slotsCount"), children: slots.countLabel }) : null
|
|
764
706
|
] }),
|
|
765
|
-
loading ? /* @__PURE__ */ jsx("div", { className: cls("slots"), "aria-hidden": "true", children: Array.from({ length: SKELETON_COUNT }, (_, index) => /* @__PURE__ */ jsx(
|
|
707
|
+
slots.loading ? /* @__PURE__ */ jsx("div", { className: cls("slots"), "aria-hidden": "true", children: Array.from({ length: SKELETON_COUNT }, (_, index) => /* @__PURE__ */ jsx(
|
|
766
708
|
"div",
|
|
767
709
|
{
|
|
768
710
|
className: cls("skeleton"),
|
|
@@ -770,38 +712,38 @@ function SlotsStep() {
|
|
|
770
712
|
},
|
|
771
713
|
index
|
|
772
714
|
)) }) : null,
|
|
773
|
-
|
|
774
|
-
/* @__PURE__ */ jsx("span", { children:
|
|
715
|
+
slots.status === "error" ? /* @__PURE__ */ jsxs("div", { className: cls("error"), role: "alert", children: [
|
|
716
|
+
/* @__PURE__ */ jsx("span", { children: slots.error }),
|
|
775
717
|
/* @__PURE__ */ jsx("button", { type: "button", className: "sl-link-button", onClick: actions.reloadSlots, children: messages.retry })
|
|
776
718
|
] }) : null,
|
|
777
|
-
|
|
719
|
+
slots.empty ? /* @__PURE__ */ jsxs("div", { className: cls("empty"), children: [
|
|
778
720
|
/* @__PURE__ */ jsx("p", { className: "sl-empty-title", children: messages.noSlotsTitle }),
|
|
779
721
|
/* @__PURE__ */ jsx("p", { className: "sl-empty-body", children: messages.noSlotsBody }),
|
|
780
722
|
/* @__PURE__ */ jsx("button", { type: "button", className: cls("secondaryButton"), onClick: actions.back, children: messages.pickAnotherDay })
|
|
781
723
|
] }) : null,
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
);
|
|
795
|
-
}) }) : null
|
|
724
|
+
slots.status === "ready" && slots.count > 0 ? /* @__PURE__ */ jsx("div", { className: cls("slots"), role: "group", "aria-label": slots.label, children: slots.items.map((slot, index) => /* @__PURE__ */ jsx(
|
|
725
|
+
Animations.Item,
|
|
726
|
+
{
|
|
727
|
+
index,
|
|
728
|
+
className: cls("slot", slot.selected && "is-selected"),
|
|
729
|
+
"aria-current": slot.selected ? "true" : void 0,
|
|
730
|
+
"aria-label": slot.utcOffsetLabel ? `${slot.label} ${slot.utcOffsetLabel}` : void 0,
|
|
731
|
+
onClick: () => actions.selectSlot(slot),
|
|
732
|
+
children: slot.label
|
|
733
|
+
},
|
|
734
|
+
slot.id
|
|
735
|
+
)) }) : null
|
|
796
736
|
] });
|
|
797
737
|
}
|
|
798
738
|
function StepBody() {
|
|
799
|
-
const {
|
|
800
|
-
switch (
|
|
739
|
+
const { snapshot } = useFlow();
|
|
740
|
+
switch (snapshot.view.step) {
|
|
801
741
|
case "time":
|
|
802
742
|
return /* @__PURE__ */ jsx(SlotsStep, {});
|
|
803
743
|
case "details":
|
|
804
744
|
return /* @__PURE__ */ jsx(DetailsStep, {});
|
|
745
|
+
case "attempt":
|
|
746
|
+
return /* @__PURE__ */ jsx(AttemptStep, {});
|
|
805
747
|
case "confirmed":
|
|
806
748
|
return /* @__PURE__ */ jsx(ConfirmedStep, {});
|
|
807
749
|
case "date":
|
|
@@ -810,15 +752,15 @@ function StepBody() {
|
|
|
810
752
|
}
|
|
811
753
|
}
|
|
812
754
|
function SlotlineView() {
|
|
813
|
-
const
|
|
814
|
-
const { state, messages, title, durationLabel, actions, canGoBack } = flow;
|
|
815
|
-
const submitting = state.submitStatus === "submitting";
|
|
755
|
+
const { snapshot, actions } = useFlow();
|
|
816
756
|
const ui = useUI();
|
|
817
757
|
const cls = useClassNames();
|
|
818
758
|
const Animations = ui.animations;
|
|
759
|
+
const view = snapshot.view;
|
|
819
760
|
const cardRef = useRef(null);
|
|
820
761
|
const panelRef = useRef(null);
|
|
821
762
|
const mountedRef = useRef(false);
|
|
763
|
+
const canGoBack = view.actions.canGoBack || (view.step === "date" || view.step === "confirmed") && ui.canDismiss;
|
|
822
764
|
useFocusTrap(cardRef, {
|
|
823
765
|
active: ui.trapFocus,
|
|
824
766
|
...canGoBack ? { onEscape: actions.back } : {}
|
|
@@ -831,22 +773,14 @@ function SlotlineView() {
|
|
|
831
773
|
}
|
|
832
774
|
const active = typeof document === "undefined" ? null : document.activeElement;
|
|
833
775
|
if (!active || active === document.body) panelRef.current?.focus();
|
|
834
|
-
}, [
|
|
835
|
-
const announcement = state.step === "confirmed" ? `${messages.bookedTitle}. ${format(messages.bookedSummary, {
|
|
836
|
-
date: flow.selectedDateLabel,
|
|
837
|
-
time: flow.selectedTimeLabel,
|
|
838
|
-
duration: durationLabel
|
|
839
|
-
})}` : format(messages.progressLabel, {
|
|
840
|
-
step: stepIndex(state.step) + 1,
|
|
841
|
-
total: FLOW_STEPS.length
|
|
842
|
-
});
|
|
776
|
+
}, [view.step, ui.autoFocus]);
|
|
843
777
|
return /* @__PURE__ */ jsx(
|
|
844
778
|
"div",
|
|
845
779
|
{
|
|
846
780
|
className: cls("root", ui.className),
|
|
847
|
-
"data-sl-theme":
|
|
848
|
-
"data-sl-step":
|
|
849
|
-
lang:
|
|
781
|
+
"data-sl-theme": ui.theme,
|
|
782
|
+
"data-sl-step": view.step,
|
|
783
|
+
lang: ui.locale,
|
|
850
784
|
children: /* @__PURE__ */ jsxs("div", { className: cls("card"), ref: cardRef, children: [
|
|
851
785
|
/* @__PURE__ */ jsxs("header", { className: cls("header"), children: [
|
|
852
786
|
canGoBack ? /* @__PURE__ */ jsx(
|
|
@@ -855,373 +789,183 @@ function SlotlineView() {
|
|
|
855
789
|
type: "button",
|
|
856
790
|
className: "sl-icon-button",
|
|
857
791
|
onClick: actions.back,
|
|
858
|
-
"aria-label": messages.back,
|
|
859
|
-
disabled: submitting,
|
|
792
|
+
"aria-label": ui.messages.back,
|
|
793
|
+
disabled: view.form.submitting,
|
|
860
794
|
children: /* @__PURE__ */ jsx(ChevronLeftIcon, {})
|
|
861
795
|
}
|
|
862
796
|
) : null,
|
|
863
797
|
/* @__PURE__ */ jsxs("div", { className: "sl-header-main", children: [
|
|
864
|
-
/* @__PURE__ */ jsx("h2", { className: cls("title"), children: title }),
|
|
865
|
-
/* @__PURE__ */ jsx("span", { className: cls("badge"), children:
|
|
798
|
+
/* @__PURE__ */ jsx("h2", { className: cls("title"), children: view.title }),
|
|
799
|
+
/* @__PURE__ */ jsx("span", { className: cls("badge"), children: view.duration.label })
|
|
866
800
|
] }),
|
|
867
801
|
/* @__PURE__ */ jsx(Progress, {})
|
|
868
802
|
] }),
|
|
869
|
-
/* @__PURE__ */ jsx("div", { className: cls("panel"), ref: panelRef, tabIndex: -1, children: /* @__PURE__ */ jsx(Animations.Height, { children: /* @__PURE__ */ jsx(Animations.StepHost, { stepKey:
|
|
870
|
-
|
|
871
|
-
/* @__PURE__ */ jsx("div", { className: "sl-visually-hidden", role: "status", "aria-live": "polite", children:
|
|
803
|
+
/* @__PURE__ */ jsx("div", { className: cls("panel"), ref: panelRef, tabIndex: -1, children: /* @__PURE__ */ jsx(Animations.Height, { children: /* @__PURE__ */ jsx(Animations.StepHost, { stepKey: view.step, direction: view.direction, children: /* @__PURE__ */ jsx(StepBody, {}) }) }) }),
|
|
804
|
+
view.step === "date" ? /* @__PURE__ */ jsx(DurationFooter, {}) : null,
|
|
805
|
+
/* @__PURE__ */ jsx("div", { className: "sl-visually-hidden", role: "status", "aria-live": "polite", children: view.live.message })
|
|
872
806
|
] })
|
|
873
807
|
}
|
|
874
808
|
);
|
|
875
809
|
}
|
|
876
|
-
function
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
onChange
|
|
880
|
-
}) {
|
|
881
|
-
const [internal, setInternal] = useState(defaultValue);
|
|
882
|
-
const isControlled = value !== void 0;
|
|
883
|
-
const current = isControlled ? value : internal;
|
|
884
|
-
const onChangeRef = useRef(onChange);
|
|
885
|
-
onChangeRef.current = onChange;
|
|
886
|
-
const setValue = useCallback(
|
|
887
|
-
(next) => {
|
|
888
|
-
if (!isControlled) setInternal(next);
|
|
889
|
-
onChangeRef.current?.(next);
|
|
890
|
-
},
|
|
891
|
-
[isControlled]
|
|
892
|
-
);
|
|
893
|
-
return [current, setValue];
|
|
810
|
+
function hostTheme() {
|
|
811
|
+
if (!canMatchMedia()) return "dark";
|
|
812
|
+
return matchesMediaQuery(DARK_SCHEME_QUERY) ? "dark" : "light";
|
|
894
813
|
}
|
|
895
814
|
function useResolvedTheme(theme) {
|
|
896
|
-
const [
|
|
897
|
-
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return "dark";
|
|
898
|
-
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
899
|
-
});
|
|
815
|
+
const [resolved, setResolved] = useState(hostTheme);
|
|
900
816
|
useEffect(() => {
|
|
901
817
|
if (theme !== "system") return;
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
return () => list.removeEventListener("change", handler);
|
|
818
|
+
return observeMediaQuery(
|
|
819
|
+
DARK_SCHEME_QUERY,
|
|
820
|
+
(matches) => setResolved(matches ? "dark" : "light")
|
|
821
|
+
);
|
|
907
822
|
}, [theme]);
|
|
908
|
-
|
|
909
|
-
return theme;
|
|
823
|
+
return theme === "system" ? resolved : theme;
|
|
910
824
|
}
|
|
911
825
|
|
|
912
826
|
// src/useBookingFlow.ts
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
const error = toError(value);
|
|
918
|
-
return error.message && error.message !== "undefined" ? error.message : fallback;
|
|
827
|
+
var cachedBrowserDefaults = null;
|
|
828
|
+
function readBrowserDefaults() {
|
|
829
|
+
cachedBrowserDefaults ??= { locale: detectLocale(), timeZone: resolveTimeZone() };
|
|
830
|
+
return cachedBrowserDefaults;
|
|
919
831
|
}
|
|
920
|
-
function
|
|
921
|
-
const
|
|
922
|
-
adapter,
|
|
923
|
-
durations = [15, 30, 60],
|
|
924
|
-
duration: durationProp,
|
|
925
|
-
defaultDuration,
|
|
926
|
-
onDurationChange,
|
|
927
|
-
timeZone: timeZoneProp,
|
|
928
|
-
locale: localeProp,
|
|
929
|
-
messages: messagesProp,
|
|
930
|
-
theme = "dark",
|
|
931
|
-
minDate: minDateProp,
|
|
932
|
-
maxDate: maxDateProp,
|
|
933
|
-
weekStartsOn = 0,
|
|
934
|
-
onBooked,
|
|
935
|
-
onError,
|
|
936
|
-
onDismiss
|
|
937
|
-
} = props;
|
|
938
|
-
const locale = localeProp ?? detectLocale();
|
|
939
|
-
const timeZone = timeZoneProp ?? resolveTimeZone();
|
|
940
|
-
const resolvedTheme = useResolvedTheme(theme);
|
|
941
|
-
const messages = useMemo(() => resolveMessages(locale, messagesProp), [locale, messagesProp]);
|
|
942
|
-
const durationOptions = useMemo(
|
|
943
|
-
() => durations.length > 0 ? durations : [15, 30, 60],
|
|
944
|
-
[durations]
|
|
945
|
-
);
|
|
946
|
-
const [duration, setDurationValue] = useControllableState({
|
|
947
|
-
value: durationProp,
|
|
948
|
-
defaultValue: defaultDuration ?? durationOptions[0] ?? 15,
|
|
949
|
-
onChange: onDurationChange
|
|
950
|
-
});
|
|
951
|
-
const minDate = useMemo(() => normalizeDateInput(minDateProp, timeZone), [minDateProp, timeZone]);
|
|
952
|
-
const maxDate = useMemo(() => normalizeDateInput(maxDateProp, timeZone), [maxDateProp, timeZone]);
|
|
953
|
-
const today = todayKey(timeZone);
|
|
954
|
-
const earliestDate = minDate ?? today;
|
|
955
|
-
const initialMonth = monthKeyOf(clampDayKey(earliestDate, minDate, maxDate));
|
|
956
|
-
const [state, dispatch] = useReducer(
|
|
957
|
-
flowReducer,
|
|
958
|
-
void 0,
|
|
959
|
-
() => createFlowState({ duration, month: initialMonth })
|
|
960
|
-
);
|
|
961
|
-
const adapterRef = useRef(adapter);
|
|
962
|
-
adapterRef.current = adapter;
|
|
963
|
-
const stateRef = useRef(state);
|
|
964
|
-
stateRef.current = state;
|
|
965
|
-
const messagesRef = useRef(messages);
|
|
966
|
-
messagesRef.current = messages;
|
|
967
|
-
const onErrorRef = useRef(onError);
|
|
968
|
-
onErrorRef.current = onError;
|
|
969
|
-
const onBookedRef = useRef(onBooked);
|
|
970
|
-
onBookedRef.current = onBooked;
|
|
971
|
-
const submitLockRef = useRef(false);
|
|
972
|
-
const daysTokenRef = useRef(0);
|
|
973
|
-
const slotsTokenRef = useRef(0);
|
|
974
|
-
const [availabilityNonce, setAvailabilityNonce] = useReducer((n) => n + 1, 0);
|
|
975
|
-
const [slotsNonce, setSlotsNonce] = useReducer((n) => n + 1, 0);
|
|
976
|
-
useEffect(() => {
|
|
977
|
-
dispatch({ type: "setDuration", duration });
|
|
978
|
-
}, [duration]);
|
|
979
|
-
useEffect(() => {
|
|
980
|
-
const controller = new AbortController();
|
|
981
|
-
const token = daysTokenRef.current += 1;
|
|
982
|
-
const from = firstDayOfMonth(state.month);
|
|
983
|
-
const to = lastDayOfMonth(state.month);
|
|
984
|
-
dispatch({ type: "daysLoading", token });
|
|
985
|
-
void (async () => {
|
|
986
|
-
try {
|
|
987
|
-
const days = await adapterRef.current.getAvailability({
|
|
988
|
-
from,
|
|
989
|
-
to,
|
|
990
|
-
duration,
|
|
991
|
-
timeZone,
|
|
992
|
-
signal: controller.signal
|
|
993
|
-
});
|
|
994
|
-
if (controller.signal.aborted) return;
|
|
995
|
-
dispatch({ type: "daysLoaded", token, days: Array.isArray(days) ? days : [] });
|
|
996
|
-
} catch (error) {
|
|
997
|
-
if (controller.signal.aborted) return;
|
|
998
|
-
dispatch({
|
|
999
|
-
type: "daysFailed",
|
|
1000
|
-
token,
|
|
1001
|
-
error: messageOf(error, messagesRef.current.genericError)
|
|
1002
|
-
});
|
|
1003
|
-
onErrorRef.current?.(toError(error));
|
|
1004
|
-
}
|
|
1005
|
-
})();
|
|
1006
|
-
return () => controller.abort();
|
|
1007
|
-
}, [state.month, duration, timeZone, availabilityNonce]);
|
|
1008
|
-
const daysRef = useRef(state.days);
|
|
1009
|
-
daysRef.current = state.days;
|
|
832
|
+
function useBrowserDefaults() {
|
|
833
|
+
const [defaults, setDefaults] = useState(null);
|
|
1010
834
|
useEffect(() => {
|
|
1011
|
-
|
|
1012
|
-
if (!date) return;
|
|
1013
|
-
const token = slotsTokenRef.current += 1;
|
|
1014
|
-
dispatch({ type: "slotsLoading", token });
|
|
1015
|
-
const inline = daysRef.current[date]?.slots;
|
|
1016
|
-
if (inline && inline.length > 0) {
|
|
1017
|
-
dispatch({ type: "slotsLoaded", token, slots: inline });
|
|
1018
|
-
return;
|
|
1019
|
-
}
|
|
1020
|
-
const controller = new AbortController();
|
|
1021
|
-
void (async () => {
|
|
1022
|
-
try {
|
|
1023
|
-
const slots = await adapterRef.current.getSlots({
|
|
1024
|
-
date,
|
|
1025
|
-
duration,
|
|
1026
|
-
timeZone,
|
|
1027
|
-
signal: controller.signal
|
|
1028
|
-
});
|
|
1029
|
-
if (controller.signal.aborted) return;
|
|
1030
|
-
dispatch({ type: "slotsLoaded", token, slots: Array.isArray(slots) ? slots : [] });
|
|
1031
|
-
} catch (error) {
|
|
1032
|
-
if (controller.signal.aborted) return;
|
|
1033
|
-
dispatch({
|
|
1034
|
-
type: "slotsFailed",
|
|
1035
|
-
token,
|
|
1036
|
-
error: messageOf(error, messagesRef.current.genericError)
|
|
1037
|
-
});
|
|
1038
|
-
onErrorRef.current?.(toError(error));
|
|
1039
|
-
}
|
|
1040
|
-
})();
|
|
1041
|
-
return () => controller.abort();
|
|
1042
|
-
}, [state.selectedDate, state.selectionNonce, duration, timeZone, slotsNonce]);
|
|
1043
|
-
const selectDate = useCallback((dayKey) => {
|
|
1044
|
-
dispatch({ type: "selectDate", date: dayKey });
|
|
1045
|
-
}, []);
|
|
1046
|
-
const selectSlot = useCallback((slot) => {
|
|
1047
|
-
dispatch({ type: "selectSlot", slot });
|
|
1048
|
-
}, []);
|
|
1049
|
-
const setDuration = useCallback(
|
|
1050
|
-
(minutes) => {
|
|
1051
|
-
setDurationValue(minutes);
|
|
1052
|
-
},
|
|
1053
|
-
[setDurationValue]
|
|
1054
|
-
);
|
|
1055
|
-
const setDetails = useCallback(
|
|
1056
|
-
(details) => {
|
|
1057
|
-
dispatch({ type: "setDetails", details });
|
|
1058
|
-
},
|
|
1059
|
-
[]
|
|
1060
|
-
);
|
|
1061
|
-
const goToMonth = useCallback((monthKey) => {
|
|
1062
|
-
dispatch({ type: "setMonth", month: monthKey });
|
|
1063
|
-
}, []);
|
|
1064
|
-
const touchField = useCallback((field) => {
|
|
1065
|
-
dispatch({ type: "touch", field });
|
|
1066
|
-
}, []);
|
|
1067
|
-
const reset = useCallback(() => {
|
|
1068
|
-
dispatch({ type: "reset" });
|
|
1069
|
-
}, []);
|
|
1070
|
-
const back = useCallback(() => {
|
|
1071
|
-
const current = stateRef.current;
|
|
1072
|
-
if (current.submitStatus === "submitting") return;
|
|
1073
|
-
if (current.step === "date" || current.step === "confirmed") {
|
|
1074
|
-
onDismiss?.();
|
|
1075
|
-
return;
|
|
1076
|
-
}
|
|
1077
|
-
dispatch({ type: "back" });
|
|
1078
|
-
}, [onDismiss]);
|
|
1079
|
-
const reloadAvailability = useCallback(() => {
|
|
1080
|
-
setAvailabilityNonce();
|
|
835
|
+
setDefaults(readBrowserDefaults());
|
|
1081
836
|
}, []);
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
const submit = useCallback(async () => {
|
|
1086
|
-
if (submitLockRef.current) return;
|
|
1087
|
-
const snapshot = stateRef.current;
|
|
1088
|
-
const currentMessages = messagesRef.current;
|
|
1089
|
-
dispatch({ type: "touch", field: "name" });
|
|
1090
|
-
dispatch({ type: "touch", field: "email" });
|
|
1091
|
-
const errors = validateDetails(snapshot.details, currentMessages);
|
|
1092
|
-
if (hasErrors(errors) || !snapshot.selectedSlot || !snapshot.selectedDate) return;
|
|
1093
|
-
const request = {
|
|
1094
|
-
slot: { ...snapshot.selectedSlot, end: endOf(snapshot.selectedSlot, snapshot.duration) },
|
|
1095
|
-
date: snapshot.selectedDate,
|
|
1096
|
-
duration: snapshot.duration,
|
|
1097
|
-
timeZone,
|
|
1098
|
-
details: {
|
|
1099
|
-
name: snapshot.details.name.trim(),
|
|
1100
|
-
email: snapshot.details.email.trim(),
|
|
1101
|
-
...snapshot.details.notes ? { notes: snapshot.details.notes } : {}
|
|
1102
|
-
}
|
|
1103
|
-
};
|
|
1104
|
-
const scenario = snapshot.scenarioId;
|
|
1105
|
-
const stillCurrent = () => {
|
|
1106
|
-
const current = stateRef.current;
|
|
1107
|
-
return (
|
|
1108
|
-
// `scenarioId` also covers "reset, then book the very same slot again":
|
|
1109
|
-
// the date and the slot can match a second time, the scenario cannot.
|
|
1110
|
-
current.scenarioId === scenario && current.selectedDate === request.date && current.selectedSlot?.start === request.slot.start
|
|
1111
|
-
);
|
|
1112
|
-
};
|
|
1113
|
-
submitLockRef.current = true;
|
|
1114
|
-
dispatch({ type: "submitStart" });
|
|
1115
|
-
try {
|
|
1116
|
-
const result = await adapterRef.current.createBooking(request);
|
|
1117
|
-
onBookedRef.current?.(result ?? {}, request);
|
|
1118
|
-
if (stillCurrent()) dispatch({ type: "submitSucceeded", scenario, result: result ?? {} });
|
|
1119
|
-
} catch (error) {
|
|
1120
|
-
onErrorRef.current?.(toError(error));
|
|
1121
|
-
if (stillCurrent()) {
|
|
1122
|
-
dispatch({
|
|
1123
|
-
type: "submitFailed",
|
|
1124
|
-
scenario,
|
|
1125
|
-
error: messageOf(error, currentMessages.genericError)
|
|
1126
|
-
});
|
|
1127
|
-
}
|
|
1128
|
-
} finally {
|
|
1129
|
-
submitLockRef.current = false;
|
|
1130
|
-
}
|
|
1131
|
-
}, [timeZone]);
|
|
1132
|
-
const monthGrid = useMemo(
|
|
1133
|
-
() => buildMonthGrid(state.month, weekStartsOn),
|
|
1134
|
-
[state.month, weekStartsOn]
|
|
1135
|
-
);
|
|
1136
|
-
const weekdayHeaders = useMemo(
|
|
1137
|
-
() => rotateToWeekStart(shortWeekdayNames(locale, messages.weekdays), weekStartsOn),
|
|
1138
|
-
[locale, messages.weekdays, weekStartsOn]
|
|
1139
|
-
);
|
|
1140
|
-
const weekdayLong = useMemo(
|
|
1141
|
-
() => rotateToWeekStart(weekdayNames(locale), weekStartsOn),
|
|
1142
|
-
[locale, weekStartsOn]
|
|
1143
|
-
);
|
|
1144
|
-
const fieldErrors = useMemo(
|
|
1145
|
-
() => validateDetails(state.details, messages),
|
|
1146
|
-
[state.details, messages]
|
|
1147
|
-
);
|
|
1148
|
-
const availabilityOf = useCallback((dayKey) => state.days[dayKey], [state.days]);
|
|
1149
|
-
const isDayBookable = useCallback(
|
|
1150
|
-
(dayKey) => {
|
|
1151
|
-
if (compareDayKeys(dayKey, earliestDate) < 0) return false;
|
|
1152
|
-
if (maxDate && compareDayKeys(dayKey, maxDate) > 0) return false;
|
|
1153
|
-
const availability = state.days[dayKey];
|
|
1154
|
-
if (!availability) return false;
|
|
1155
|
-
const count = countSlots(availability);
|
|
1156
|
-
return count === void 0 ? true : count > 0;
|
|
1157
|
-
},
|
|
1158
|
-
[state.days, earliestDate, maxDate]
|
|
1159
|
-
);
|
|
1160
|
-
const selectedDateLabel = state.selectedDate ? formatFullDay(state.selectedDate, locale) : "";
|
|
1161
|
-
const selectedTimeLabel = state.selectedSlot ? formatTime(state.selectedSlot.start, timeZone, locale) : "";
|
|
837
|
+
return defaults;
|
|
838
|
+
}
|
|
839
|
+
function optionsOf(props, browser) {
|
|
1162
840
|
return {
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
timeZoneName: state.step === "date" ? formatTimeZoneName(timeZone, locale) : void 0,
|
|
1168
|
-
theme: resolvedTheme,
|
|
1169
|
-
title: state.step === "confirmed" ? messages.bookedHeader : props.title ?? messages.title,
|
|
1170
|
-
durations: durationOptions,
|
|
1171
|
-
weekStartsOn,
|
|
1172
|
-
...minDate ? { minDate } : {},
|
|
1173
|
-
...maxDate ? { maxDate } : {},
|
|
1174
|
-
monthGrid,
|
|
1175
|
-
weekdayHeaders,
|
|
1176
|
-
weekdayLongNames: weekdayLong,
|
|
1177
|
-
monthLabel: formatMonthYear(state.month, locale),
|
|
1178
|
-
selectedDateLabel,
|
|
1179
|
-
selectedTimeLabel,
|
|
1180
|
-
durationLabel: format(messages.duration, { count: duration }),
|
|
1181
|
-
availabilityOf,
|
|
1182
|
-
isDayBookable,
|
|
1183
|
-
// The confirmation is terminal: there is nothing to go back to, so the back
|
|
1184
|
-
// affordance only appears there when the consumer supplied `onDismiss`.
|
|
1185
|
-
canGoBack: state.step !== "date" && state.step !== "confirmed" || Boolean(onDismiss),
|
|
1186
|
-
fieldErrors,
|
|
1187
|
-
shownFieldErrors: visibleErrors(fieldErrors, state.touched),
|
|
1188
|
-
detailsValid: !hasErrors(fieldErrors),
|
|
1189
|
-
actions: {
|
|
1190
|
-
selectDate,
|
|
1191
|
-
selectSlot,
|
|
1192
|
-
setDuration,
|
|
1193
|
-
setDetails,
|
|
1194
|
-
touchField,
|
|
1195
|
-
back,
|
|
1196
|
-
submit,
|
|
1197
|
-
reset,
|
|
1198
|
-
goToMonth,
|
|
1199
|
-
previousMonth: () => goToMonth(addMonths(stateRef.current.month, -1)),
|
|
1200
|
-
nextMonth: () => goToMonth(addMonths(stateRef.current.month, 1)),
|
|
1201
|
-
reloadAvailability,
|
|
1202
|
-
reloadSlots
|
|
1203
|
-
}
|
|
841
|
+
adapter: adapterOf(props),
|
|
842
|
+
config: configOf(props, browser),
|
|
843
|
+
inputs: inputsOf(props, browser),
|
|
844
|
+
...props.attempt == null ? {} : { initialAttempt: props.attempt }
|
|
1204
845
|
};
|
|
1205
846
|
}
|
|
1206
|
-
function
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
847
|
+
function adapterOf(props) {
|
|
848
|
+
return props.adapter;
|
|
849
|
+
}
|
|
850
|
+
function configOf(props, browser) {
|
|
851
|
+
const locale = props.locale ?? browser?.locale;
|
|
852
|
+
return {
|
|
853
|
+
...locale === void 0 ? {} : { locale },
|
|
854
|
+
...props.messages === void 0 ? {} : { messages: props.messages },
|
|
855
|
+
...props.weekStartsOn === void 0 ? {} : { weekStartsOn: props.weekStartsOn },
|
|
856
|
+
...props.durations === void 0 ? {} : { durationOptions: props.durations },
|
|
857
|
+
...props.minDate === void 0 ? {} : { minDate: props.minDate },
|
|
858
|
+
...props.maxDate === void 0 ? {} : { maxDate: props.maxDate },
|
|
859
|
+
...props.title === void 0 ? {} : { title: props.title },
|
|
860
|
+
...props.context === void 0 ? {} : { context: props.context }
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
function inputsOf(props, browser) {
|
|
864
|
+
const inputs = {};
|
|
865
|
+
if (props.timeZone !== void 0) inputs.timeZone = props.timeZone;
|
|
866
|
+
else {
|
|
867
|
+
const timeZone = props.defaultTimeZone ?? browser?.timeZone;
|
|
868
|
+
if (timeZone !== void 0) inputs.defaultTimeZone = timeZone;
|
|
869
|
+
}
|
|
870
|
+
if (props.duration !== void 0) inputs.duration = props.duration;
|
|
871
|
+
if (props.defaultDuration !== void 0) inputs.defaultDuration = props.defaultDuration;
|
|
872
|
+
if (props.timeFormat !== void 0) inputs.timeFormat = props.timeFormat;
|
|
873
|
+
else if (props.defaultTimeFormat !== void 0)
|
|
874
|
+
inputs.defaultTimeFormat = props.defaultTimeFormat;
|
|
875
|
+
return inputs;
|
|
876
|
+
}
|
|
877
|
+
function useBookingController(props) {
|
|
878
|
+
const browser = useBrowserDefaults();
|
|
879
|
+
const propsRef = useRef(props);
|
|
880
|
+
propsRef.current = props;
|
|
881
|
+
const browserRef = useRef(browser);
|
|
882
|
+
browserRef.current = browser;
|
|
883
|
+
const controllerRef = useRef(null);
|
|
884
|
+
if (controllerRef.current === null) {
|
|
885
|
+
controllerRef.current = createBookingController({
|
|
886
|
+
...optionsOf(props, browser),
|
|
887
|
+
handlers: {
|
|
888
|
+
// Every handler reads the latest props, so a re-render never leaves a
|
|
889
|
+
// stale callback behind.
|
|
890
|
+
onBooked: (result, request) => (
|
|
891
|
+
// The flow carries the payload as plain values; the adapter's own type is
|
|
892
|
+
// what the application declared for its form.
|
|
893
|
+
propsRef.current.onBooked?.(result, request)
|
|
894
|
+
),
|
|
895
|
+
onError: (error, context) => propsRef.current.onError?.(error, context),
|
|
896
|
+
onDismiss: () => propsRef.current.onDismiss?.(),
|
|
897
|
+
onDurationChange: (duration, metadata) => propsRef.current.onDurationChange?.(duration, metadata),
|
|
898
|
+
onTimeFormatChange: (timeFormat, metadata) => propsRef.current.onTimeFormatChange?.(timeFormat, metadata),
|
|
899
|
+
onContextChange: (context, metadata) => propsRef.current.onContextChange?.(context, metadata)
|
|
900
|
+
}
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
const controller = controllerRef.current;
|
|
904
|
+
useEffect(() => {
|
|
905
|
+
controller.updateAdapter(adapterOf(propsRef.current));
|
|
906
|
+
controller.updateConfig(configOf(propsRef.current, browserRef.current));
|
|
907
|
+
controller.updateInputs(inputsOf(propsRef.current, browserRef.current));
|
|
908
|
+
});
|
|
909
|
+
useEffect(() => {
|
|
910
|
+
controller.start();
|
|
911
|
+
return () => controller.stop();
|
|
912
|
+
}, [controller]);
|
|
913
|
+
useEffect(() => observeVisibilityChange(() => controller.refreshTime()), [controller]);
|
|
914
|
+
return controller;
|
|
915
|
+
}
|
|
916
|
+
function useSubscribedFlow(controller) {
|
|
917
|
+
const snapshot = useSyncExternalStore(
|
|
918
|
+
controller.subscribe,
|
|
919
|
+
controller.getSnapshot,
|
|
920
|
+
controller.getSnapshot
|
|
921
|
+
);
|
|
922
|
+
return useMemo(() => ({ snapshot, actions: controller.actions }), [snapshot, controller]);
|
|
923
|
+
}
|
|
924
|
+
function useBookingFlow(props) {
|
|
925
|
+
return useSubscribedFlow(useBookingController(props));
|
|
926
|
+
}
|
|
927
|
+
function useSlotlineUI(props) {
|
|
928
|
+
const browser = useBrowserDefaults();
|
|
929
|
+
const locale = props.locale ?? browser?.locale ?? "en";
|
|
930
|
+
const messages = useMemo(() => resolveMessages(locale, props.messages), [locale, props.messages]);
|
|
931
|
+
const theme = useResolvedTheme(props.theme ?? "dark");
|
|
932
|
+
return useMemo(
|
|
1210
933
|
() => ({
|
|
1211
934
|
animations: props.animations ?? cssAnimations,
|
|
1212
935
|
classNames: props.classNames,
|
|
936
|
+
className: props.className,
|
|
1213
937
|
autoFocus: props.autoFocus ?? false,
|
|
1214
938
|
trapFocus: props.trapFocus ?? false,
|
|
1215
|
-
|
|
939
|
+
canDismiss: Boolean(props.onDismiss),
|
|
940
|
+
onCheckStatus: props.onCheckStatus,
|
|
941
|
+
messages,
|
|
942
|
+
locale,
|
|
943
|
+
theme
|
|
1216
944
|
}),
|
|
1217
945
|
[
|
|
1218
946
|
props.animations,
|
|
1219
947
|
props.classNames,
|
|
948
|
+
props.className,
|
|
1220
949
|
props.autoFocus,
|
|
1221
950
|
props.trapFocus,
|
|
1222
|
-
props.
|
|
951
|
+
props.onDismiss,
|
|
952
|
+
props.onCheckStatus,
|
|
953
|
+
messages,
|
|
954
|
+
locale,
|
|
955
|
+
theme
|
|
1223
956
|
]
|
|
1224
957
|
);
|
|
958
|
+
}
|
|
959
|
+
function SlotlineProvider(props) {
|
|
960
|
+
const { children } = props;
|
|
961
|
+
const controller = useBookingController(props);
|
|
962
|
+
const ui = useSlotlineUI(props);
|
|
963
|
+
const snapshot = useSyncExternalStore(
|
|
964
|
+
controller.subscribe,
|
|
965
|
+
controller.getSnapshot,
|
|
966
|
+
controller.getSnapshot
|
|
967
|
+
);
|
|
968
|
+
const flow = useMemo(() => ({ snapshot, actions: controller.actions }), [snapshot, controller]);
|
|
1225
969
|
return /* @__PURE__ */ jsx(FlowContext.Provider, { value: flow, children: /* @__PURE__ */ jsx(UIContext.Provider, { value: ui, children }) });
|
|
1226
970
|
}
|
|
1227
971
|
function createSlotline(animations) {
|
|
@@ -1236,6 +980,6 @@ createSlotline(cssAnimations);
|
|
|
1236
980
|
// src/motion.ts
|
|
1237
981
|
var Slotline2 = createSlotline(motionAnimations);
|
|
1238
982
|
|
|
1239
|
-
export {
|
|
983
|
+
export { Slotline2 as Slotline, SlotlineProvider, SlotlineView, createSlotline, cssAnimations, motionAnimations, useAnimations, useBookingFlow, useClassNames, useSlotlineContext, useUI };
|
|
1240
984
|
//# sourceMappingURL=motion.js.map
|
|
1241
985
|
//# sourceMappingURL=motion.js.map
|