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