@realiizlabs/admin 0.1.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 +1 -0
- package/dist/index.cjs +807 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +98 -0
- package/dist/index.d.ts +98 -0
- package/dist/index.js +803 -0
- package/dist/index.js.map +1 -0
- package/package.json +46 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,807 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var react = require('react');
|
|
5
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
6
|
+
|
|
7
|
+
// src/components/BookingWidget.tsx
|
|
8
|
+
|
|
9
|
+
// src/lib/slots.ts
|
|
10
|
+
var DEFAULT_DURATION_MINUTES = 30;
|
|
11
|
+
function parseDurationMinutes(serviceType, fallback = DEFAULT_DURATION_MINUTES) {
|
|
12
|
+
const match = serviceType.match(/(\d+)\s*-?\s*min/i);
|
|
13
|
+
if (match) {
|
|
14
|
+
const n = Number.parseInt(match[1], 10);
|
|
15
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
16
|
+
}
|
|
17
|
+
return fallback;
|
|
18
|
+
}
|
|
19
|
+
function timeToMinutes(time) {
|
|
20
|
+
const [h, m] = time.split(":");
|
|
21
|
+
return Number(h) * 60 + Number(m);
|
|
22
|
+
}
|
|
23
|
+
function localDateKey(d) {
|
|
24
|
+
const y = d.getFullYear();
|
|
25
|
+
const mo = String(d.getMonth() + 1).padStart(2, "0");
|
|
26
|
+
const da = String(d.getDate()).padStart(2, "0");
|
|
27
|
+
return `${y}-${mo}-${da}`;
|
|
28
|
+
}
|
|
29
|
+
function isBlocked(dateKey2, blocked) {
|
|
30
|
+
return blocked.some((b) => dateKey2 >= b.start_date && dateKey2 <= b.end_date);
|
|
31
|
+
}
|
|
32
|
+
function generateSlots(opts) {
|
|
33
|
+
const { rules, blocked, busy, durationMinutes, now, days } = opts;
|
|
34
|
+
const activeRules = rules.filter((r) => r.is_active !== false);
|
|
35
|
+
const busyIntervals = busy.map((b) => {
|
|
36
|
+
const start = new Date(b.requested_at).getTime();
|
|
37
|
+
return [start, start + parseDurationMinutes(b.service_type) * 6e4];
|
|
38
|
+
});
|
|
39
|
+
const slots = [];
|
|
40
|
+
const base = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
41
|
+
for (let offset = 0; offset < days; offset++) {
|
|
42
|
+
const day = new Date(base);
|
|
43
|
+
day.setDate(base.getDate() + offset);
|
|
44
|
+
if (isBlocked(localDateKey(day), blocked)) continue;
|
|
45
|
+
const dow = day.getDay();
|
|
46
|
+
for (const rule of activeRules) {
|
|
47
|
+
if (rule.day_of_week !== dow) continue;
|
|
48
|
+
const startMin = timeToMinutes(rule.start_time);
|
|
49
|
+
const endMin = timeToMinutes(rule.end_time);
|
|
50
|
+
for (let t = startMin; t + durationMinutes <= endMin; t += durationMinutes) {
|
|
51
|
+
const slotStart = new Date(day);
|
|
52
|
+
slotStart.setHours(Math.floor(t / 60), t % 60, 0, 0);
|
|
53
|
+
if (slotStart.getTime() <= now.getTime()) continue;
|
|
54
|
+
const s = slotStart.getTime();
|
|
55
|
+
const e = s + durationMinutes * 6e4;
|
|
56
|
+
const taken = busyIntervals.some(([bs, be]) => s < be && bs < e);
|
|
57
|
+
slots.push({ start: slotStart, available: !taken });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
slots.sort((a, b) => a.start.getTime() - b.start.getTime());
|
|
62
|
+
return slots;
|
|
63
|
+
}
|
|
64
|
+
function normalizeService(s) {
|
|
65
|
+
return typeof s === "string" ? { label: s, durationMinutes: parseDurationMinutes(s) } : s;
|
|
66
|
+
}
|
|
67
|
+
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
68
|
+
function formatPhone(input) {
|
|
69
|
+
const d = input.replace(/\D/g, "").slice(0, 10);
|
|
70
|
+
if (d.length === 0) return "";
|
|
71
|
+
if (d.length < 4) return `(${d}`;
|
|
72
|
+
if (d.length < 7) return `(${d.slice(0, 3)}) ${d.slice(3)}`;
|
|
73
|
+
return `(${d.slice(0, 3)}) ${d.slice(3, 6)}-${d.slice(6)}`;
|
|
74
|
+
}
|
|
75
|
+
function BookingWidget({
|
|
76
|
+
supabase,
|
|
77
|
+
serviceTypes,
|
|
78
|
+
onBookingComplete,
|
|
79
|
+
confirmationMessage,
|
|
80
|
+
daysAhead = 14,
|
|
81
|
+
className
|
|
82
|
+
}) {
|
|
83
|
+
const services = react.useMemo(() => serviceTypes.map(normalizeService), [serviceTypes]);
|
|
84
|
+
const [step, setStep] = react.useState("service");
|
|
85
|
+
const [service, setService] = react.useState(null);
|
|
86
|
+
const [slots, setSlots] = react.useState([]);
|
|
87
|
+
const [slotsLoading, setSlotsLoading] = react.useState(false);
|
|
88
|
+
const [loadError, setLoadError] = react.useState(null);
|
|
89
|
+
const [retakenNotice, setRetakenNotice] = react.useState(null);
|
|
90
|
+
const [selectedSlot, setSelectedSlot] = react.useState(null);
|
|
91
|
+
const [name, setName] = react.useState("");
|
|
92
|
+
const [email, setEmail] = react.useState("");
|
|
93
|
+
const [phone, setPhone] = react.useState("");
|
|
94
|
+
const [smsConsent, setSmsConsent] = react.useState(false);
|
|
95
|
+
const [submitting, setSubmitting] = react.useState(false);
|
|
96
|
+
const [formError, setFormError] = react.useState(null);
|
|
97
|
+
const loadSlots = react.useCallback(
|
|
98
|
+
async (svc) => {
|
|
99
|
+
setSlotsLoading(true);
|
|
100
|
+
setLoadError(null);
|
|
101
|
+
const now = /* @__PURE__ */ new Date();
|
|
102
|
+
const rangeEnd = new Date(now.getFullYear(), now.getMonth(), now.getDate() + daysAhead);
|
|
103
|
+
try {
|
|
104
|
+
const [availabilityRes, blockedRes, busyRes] = await Promise.all([
|
|
105
|
+
supabase.from("availability").select("day_of_week,start_time,end_time,is_active").eq("is_active", true),
|
|
106
|
+
supabase.from("blocked_dates").select("start_date,end_date"),
|
|
107
|
+
supabase.rpc("get_busy_slots", {
|
|
108
|
+
range_start: now.toISOString(),
|
|
109
|
+
range_end: rangeEnd.toISOString()
|
|
110
|
+
})
|
|
111
|
+
]);
|
|
112
|
+
const firstError = availabilityRes.error || blockedRes.error || busyRes.error;
|
|
113
|
+
if (firstError) throw firstError;
|
|
114
|
+
const computed = generateSlots({
|
|
115
|
+
rules: availabilityRes.data ?? [],
|
|
116
|
+
blocked: blockedRes.data ?? [],
|
|
117
|
+
busy: busyRes.data ?? [],
|
|
118
|
+
durationMinutes: svc.durationMinutes,
|
|
119
|
+
now,
|
|
120
|
+
days: daysAhead
|
|
121
|
+
});
|
|
122
|
+
setSlots(computed);
|
|
123
|
+
} catch {
|
|
124
|
+
setLoadError("Couldn't load available times. Please try again.");
|
|
125
|
+
setSlots([]);
|
|
126
|
+
} finally {
|
|
127
|
+
setSlotsLoading(false);
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
[supabase, daysAhead]
|
|
131
|
+
);
|
|
132
|
+
const pickService = react.useCallback(
|
|
133
|
+
(svc) => {
|
|
134
|
+
setService(svc);
|
|
135
|
+
setSelectedSlot(null);
|
|
136
|
+
setRetakenNotice(null);
|
|
137
|
+
setStep("slot");
|
|
138
|
+
void loadSlots(svc);
|
|
139
|
+
},
|
|
140
|
+
[loadSlots]
|
|
141
|
+
);
|
|
142
|
+
const pickSlot = react.useCallback((slot) => {
|
|
143
|
+
setSelectedSlot(slot);
|
|
144
|
+
setFormError(null);
|
|
145
|
+
setStep("details");
|
|
146
|
+
}, []);
|
|
147
|
+
const handlePhoneChange = react.useCallback((e) => {
|
|
148
|
+
const el = e.target;
|
|
149
|
+
const raw = el.value;
|
|
150
|
+
const caret = el.selectionStart ?? raw.length;
|
|
151
|
+
const digitsBeforeCaret = raw.slice(0, caret).replace(/\D/g, "").length;
|
|
152
|
+
const formatted = formatPhone(raw);
|
|
153
|
+
let pos = formatted.length;
|
|
154
|
+
if (digitsBeforeCaret === 0) {
|
|
155
|
+
pos = 0;
|
|
156
|
+
} else {
|
|
157
|
+
let seen = 0;
|
|
158
|
+
for (let i = 0; i < formatted.length; i++) {
|
|
159
|
+
if (formatted[i] >= "0" && formatted[i] <= "9") {
|
|
160
|
+
seen++;
|
|
161
|
+
if (seen === digitsBeforeCaret) {
|
|
162
|
+
pos = i + 1;
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
el.value = formatted;
|
|
169
|
+
el.setSelectionRange(pos, pos);
|
|
170
|
+
setPhone(formatted);
|
|
171
|
+
}, []);
|
|
172
|
+
const submit = react.useCallback(async () => {
|
|
173
|
+
if (!service || !selectedSlot) return;
|
|
174
|
+
const cleanPhone = phone.replace(/\D/g, "");
|
|
175
|
+
if (!name.trim()) return setFormError("Please enter your name.");
|
|
176
|
+
if (!EMAIL_RE.test(email)) return setFormError("Please enter a valid email address.");
|
|
177
|
+
if (cleanPhone.length !== 10) return setFormError("Please enter a valid phone number.");
|
|
178
|
+
setSubmitting(true);
|
|
179
|
+
setFormError(null);
|
|
180
|
+
const { error } = await supabase.from("bookings").insert({
|
|
181
|
+
service_type: service.label,
|
|
182
|
+
name: name.trim(),
|
|
183
|
+
email: email.trim(),
|
|
184
|
+
phone: cleanPhone,
|
|
185
|
+
requested_at: selectedSlot.toISOString(),
|
|
186
|
+
status: "pending",
|
|
187
|
+
sms_consent: smsConsent
|
|
188
|
+
});
|
|
189
|
+
setSubmitting(false);
|
|
190
|
+
if (error) {
|
|
191
|
+
if (error.code === "23505") {
|
|
192
|
+
setRetakenNotice("That slot was just booked \u2014 please pick another.");
|
|
193
|
+
setSelectedSlot(null);
|
|
194
|
+
setStep("slot");
|
|
195
|
+
if (service) void loadSlots(service);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
setFormError("Something went wrong submitting your booking. Please try again.");
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const completed = {
|
|
202
|
+
service_type: service.label,
|
|
203
|
+
requested_at: selectedSlot.toISOString(),
|
|
204
|
+
name: name.trim(),
|
|
205
|
+
email: email.trim(),
|
|
206
|
+
phone: cleanPhone,
|
|
207
|
+
sms_consent: smsConsent
|
|
208
|
+
};
|
|
209
|
+
setStep("done");
|
|
210
|
+
onBookingComplete?.(completed);
|
|
211
|
+
}, [service, selectedSlot, name, email, phone, smsConsent, supabase, loadSlots, onBookingComplete]);
|
|
212
|
+
const reset = react.useCallback(() => {
|
|
213
|
+
setStep("service");
|
|
214
|
+
setService(null);
|
|
215
|
+
setSlots([]);
|
|
216
|
+
setSelectedSlot(null);
|
|
217
|
+
setName("");
|
|
218
|
+
setEmail("");
|
|
219
|
+
setPhone("");
|
|
220
|
+
setSmsConsent(false);
|
|
221
|
+
setFormError(null);
|
|
222
|
+
setRetakenNotice(null);
|
|
223
|
+
}, []);
|
|
224
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
225
|
+
"div",
|
|
226
|
+
{
|
|
227
|
+
"data-realiiz-booking-widget": "",
|
|
228
|
+
className: ["realiiz-bw", className].filter(Boolean).join(" "),
|
|
229
|
+
style: st.root,
|
|
230
|
+
children: [
|
|
231
|
+
/* @__PURE__ */ jsxRuntime.jsx("style", { children: CALENDAR_CSS }),
|
|
232
|
+
step === "service" && /* @__PURE__ */ jsxRuntime.jsxs("fieldset", { style: st.section, className: "realiiz-bw__step realiiz-bw__step--service", children: [
|
|
233
|
+
/* @__PURE__ */ jsxRuntime.jsx("legend", { style: st.legend, children: "Choose a service" }),
|
|
234
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: st.list, children: services.map((svc) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
235
|
+
"button",
|
|
236
|
+
{
|
|
237
|
+
type: "button",
|
|
238
|
+
style: st.optionButton,
|
|
239
|
+
className: "realiiz-bw__service",
|
|
240
|
+
onClick: () => pickService(svc),
|
|
241
|
+
children: [
|
|
242
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "realiiz-bw__service-main", children: [
|
|
243
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "realiiz-bw__service-name", children: svc.label }),
|
|
244
|
+
svc.description && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "realiiz-bw__service-desc", children: svc.description })
|
|
245
|
+
] }),
|
|
246
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { style: st.muted, className: "realiiz-bw__service-duration", children: [
|
|
247
|
+
svc.durationMinutes,
|
|
248
|
+
" min"
|
|
249
|
+
] })
|
|
250
|
+
]
|
|
251
|
+
},
|
|
252
|
+
svc.label
|
|
253
|
+
)) })
|
|
254
|
+
] }),
|
|
255
|
+
step === "slot" && service && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: st.section, className: "realiiz-bw__step realiiz-bw__step--slot", children: [
|
|
256
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: st.header, children: /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "realiiz-bw__back", onClick: reset, children: "\u2190 Back" }) }),
|
|
257
|
+
retakenNotice && /* @__PURE__ */ jsxRuntime.jsx("p", { style: st.notice, role: "status", children: retakenNotice }),
|
|
258
|
+
slotsLoading && /* @__PURE__ */ jsxRuntime.jsx("p", { style: st.muted, children: "Loading available times\u2026" }),
|
|
259
|
+
loadError && /* @__PURE__ */ jsxRuntime.jsx("p", { style: st.error, role: "alert", children: loadError }),
|
|
260
|
+
!slotsLoading && !loadError && slots.length === 0 && /* @__PURE__ */ jsxRuntime.jsxs("p", { style: st.muted, children: [
|
|
261
|
+
"No times available in the next ",
|
|
262
|
+
daysAhead,
|
|
263
|
+
" days."
|
|
264
|
+
] }),
|
|
265
|
+
!slotsLoading && !loadError && slots.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(CalendarSlotPicker, { slots, daysAhead, service, onPick: pickSlot })
|
|
266
|
+
] }),
|
|
267
|
+
step === "details" && service && selectedSlot && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
268
|
+
"form",
|
|
269
|
+
{
|
|
270
|
+
style: st.section,
|
|
271
|
+
className: "realiiz-bw__step realiiz-bw__step--details",
|
|
272
|
+
onSubmit: (e) => {
|
|
273
|
+
e.preventDefault();
|
|
274
|
+
void submit();
|
|
275
|
+
},
|
|
276
|
+
children: [
|
|
277
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: st.header, children: /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "realiiz-bw__back", onClick: () => setStep("slot"), children: "\u2190 Back" }) }),
|
|
278
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "realiiz-bw__summary", children: [
|
|
279
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "realiiz-bw__summary-service", children: service.label }),
|
|
280
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "realiiz-bw__summary-when", children: formatSlot(selectedSlot) })
|
|
281
|
+
] }),
|
|
282
|
+
/* @__PURE__ */ jsxRuntime.jsxs("label", { className: "realiiz-bw__field", children: [
|
|
283
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "realiiz-bw__field-label", children: "Name" }),
|
|
284
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
285
|
+
"input",
|
|
286
|
+
{
|
|
287
|
+
className: "realiiz-bw__input",
|
|
288
|
+
value: name,
|
|
289
|
+
onChange: (e) => setName(e.target.value),
|
|
290
|
+
required: true
|
|
291
|
+
}
|
|
292
|
+
)
|
|
293
|
+
] }),
|
|
294
|
+
/* @__PURE__ */ jsxRuntime.jsxs("label", { className: "realiiz-bw__field", children: [
|
|
295
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "realiiz-bw__field-label", children: "Email" }),
|
|
296
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
297
|
+
"input",
|
|
298
|
+
{
|
|
299
|
+
className: "realiiz-bw__input",
|
|
300
|
+
type: "email",
|
|
301
|
+
value: email,
|
|
302
|
+
onChange: (e) => setEmail(e.target.value),
|
|
303
|
+
required: true
|
|
304
|
+
}
|
|
305
|
+
)
|
|
306
|
+
] }),
|
|
307
|
+
/* @__PURE__ */ jsxRuntime.jsxs("label", { className: "realiiz-bw__field", children: [
|
|
308
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "realiiz-bw__field-label", children: "Phone" }),
|
|
309
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
310
|
+
"input",
|
|
311
|
+
{
|
|
312
|
+
className: "realiiz-bw__input",
|
|
313
|
+
type: "tel",
|
|
314
|
+
inputMode: "tel",
|
|
315
|
+
autoComplete: "tel",
|
|
316
|
+
placeholder: "(555) 123-4567",
|
|
317
|
+
value: phone,
|
|
318
|
+
onChange: handlePhoneChange,
|
|
319
|
+
required: true
|
|
320
|
+
}
|
|
321
|
+
)
|
|
322
|
+
] }),
|
|
323
|
+
/* @__PURE__ */ jsxRuntime.jsxs("label", { className: "realiiz-bw__consent", children: [
|
|
324
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
325
|
+
"input",
|
|
326
|
+
{
|
|
327
|
+
type: "checkbox",
|
|
328
|
+
className: "realiiz-bw__consent-check",
|
|
329
|
+
checked: smsConsent,
|
|
330
|
+
onChange: (e) => setSmsConsent(e.target.checked)
|
|
331
|
+
}
|
|
332
|
+
),
|
|
333
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "realiiz-bw__consent-text", children: "By providing my phone number, I agree to receive appointment-related and occasional follow-up text messages from Realiiz. Message frequency varies. Message and data rates may apply. Reply STOP to opt out at any time. Consent is not a condition of booking." })
|
|
334
|
+
] }),
|
|
335
|
+
formError && /* @__PURE__ */ jsxRuntime.jsx("p", { style: st.error, role: "alert", children: formError }),
|
|
336
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "submit", className: "realiiz-bw__submit", disabled: submitting, children: submitting ? "Booking\u2026" : "Confirm booking" })
|
|
337
|
+
]
|
|
338
|
+
}
|
|
339
|
+
),
|
|
340
|
+
step === "done" && service && selectedSlot && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: st.section, className: "realiiz-bw__step realiiz-bw__step--done", children: [
|
|
341
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { style: st.doneTitle, children: "Request received \u2713" }),
|
|
342
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: st.muted, className: "realiiz-bw__confirmation", children: resolveConfirmation(confirmationMessage, {
|
|
343
|
+
service_type: service.label,
|
|
344
|
+
requested_at: selectedSlot.toISOString(),
|
|
345
|
+
name: name.trim(),
|
|
346
|
+
email: email.trim(),
|
|
347
|
+
phone: phone.replace(/\D/g, "") || null,
|
|
348
|
+
sms_consent: smsConsent
|
|
349
|
+
}, selectedSlot) }),
|
|
350
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", style: st.linkButton, className: "realiiz-bw__again", onClick: reset, children: "Book another" })
|
|
351
|
+
] })
|
|
352
|
+
]
|
|
353
|
+
}
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
function dateKey(d) {
|
|
357
|
+
const y = d.getFullYear();
|
|
358
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
359
|
+
const da = String(d.getDate()).padStart(2, "0");
|
|
360
|
+
return `${y}-${m}-${da}`;
|
|
361
|
+
}
|
|
362
|
+
function startOfMonth(d) {
|
|
363
|
+
return new Date(d.getFullYear(), d.getMonth(), 1);
|
|
364
|
+
}
|
|
365
|
+
function addMonths(d, n) {
|
|
366
|
+
return new Date(d.getFullYear(), d.getMonth() + n, 1);
|
|
367
|
+
}
|
|
368
|
+
function sameMonth(a, b) {
|
|
369
|
+
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth();
|
|
370
|
+
}
|
|
371
|
+
function groupTimesByPartOfDay(slots) {
|
|
372
|
+
const buckets = [
|
|
373
|
+
{ key: "morning", label: "Morning", slots: [] },
|
|
374
|
+
{ key: "afternoon", label: "Afternoon", slots: [] },
|
|
375
|
+
{ key: "evening", label: "Evening", slots: [] }
|
|
376
|
+
];
|
|
377
|
+
for (const slot of slots) {
|
|
378
|
+
const h = slot.start.getHours();
|
|
379
|
+
const idx = h < 12 ? 0 : h < 17 ? 1 : 2;
|
|
380
|
+
buckets[idx].slots.push(slot);
|
|
381
|
+
}
|
|
382
|
+
return buckets.filter((b) => b.slots.length > 0);
|
|
383
|
+
}
|
|
384
|
+
function CalendarSlotPicker({
|
|
385
|
+
slots,
|
|
386
|
+
daysAhead,
|
|
387
|
+
service,
|
|
388
|
+
onPick
|
|
389
|
+
}) {
|
|
390
|
+
const now = react.useMemo(() => /* @__PURE__ */ new Date(), []);
|
|
391
|
+
const todayKey = dateKey(now);
|
|
392
|
+
const minMonth = react.useMemo(() => startOfMonth(now), [now]);
|
|
393
|
+
const maxMonth = react.useMemo(
|
|
394
|
+
() => startOfMonth(new Date(now.getFullYear(), now.getMonth(), now.getDate() + (daysAhead - 1))),
|
|
395
|
+
[now, daysAhead]
|
|
396
|
+
);
|
|
397
|
+
const slotsByDay = react.useMemo(() => {
|
|
398
|
+
const map = /* @__PURE__ */ new Map();
|
|
399
|
+
for (const slot of slots) {
|
|
400
|
+
const k = dateKey(slot.start);
|
|
401
|
+
const arr = map.get(k);
|
|
402
|
+
if (arr) arr.push(slot);
|
|
403
|
+
else map.set(k, [slot]);
|
|
404
|
+
}
|
|
405
|
+
return map;
|
|
406
|
+
}, [slots]);
|
|
407
|
+
const availableDays = react.useMemo(() => {
|
|
408
|
+
const set = /* @__PURE__ */ new Set();
|
|
409
|
+
for (const [k, arr] of slotsByDay) {
|
|
410
|
+
if (arr.some((s) => s.available)) set.add(k);
|
|
411
|
+
}
|
|
412
|
+
return set;
|
|
413
|
+
}, [slotsByDay]);
|
|
414
|
+
const weekdays = react.useMemo(() => {
|
|
415
|
+
const base = new Date(2023, 0, 1);
|
|
416
|
+
return Array.from({ length: 7 }, (_, i) => {
|
|
417
|
+
const d = new Date(base);
|
|
418
|
+
d.setDate(base.getDate() + i);
|
|
419
|
+
return d.toLocaleDateString(void 0, { weekday: "short" });
|
|
420
|
+
});
|
|
421
|
+
}, []);
|
|
422
|
+
const [viewMonth, setViewMonth] = react.useState(minMonth);
|
|
423
|
+
const [selectedDay, setSelectedDay] = react.useState(null);
|
|
424
|
+
const canPrev = viewMonth.getTime() > minMonth.getTime();
|
|
425
|
+
const canNext = viewMonth.getTime() < maxMonth.getTime();
|
|
426
|
+
const cells = react.useMemo(() => {
|
|
427
|
+
const first = startOfMonth(viewMonth);
|
|
428
|
+
const gridStart = new Date(first);
|
|
429
|
+
gridStart.setDate(first.getDate() - first.getDay());
|
|
430
|
+
return Array.from({ length: 42 }, (_, i) => {
|
|
431
|
+
const d = new Date(gridStart);
|
|
432
|
+
d.setDate(gridStart.getDate() + i);
|
|
433
|
+
return d;
|
|
434
|
+
});
|
|
435
|
+
}, [viewMonth]);
|
|
436
|
+
const goMonth = (delta) => {
|
|
437
|
+
setSelectedDay(null);
|
|
438
|
+
setViewMonth((m) => addMonths(m, delta));
|
|
439
|
+
};
|
|
440
|
+
const daySlots = selectedDay ? slotsByDay.get(selectedDay)?.filter((s) => s.available) ?? [] : [];
|
|
441
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "realiiz-bw__scheduler", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "realiiz-bw__scheduler-inner", children: [
|
|
442
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "realiiz-bw__calendar", role: "group", "aria-label": "Choose a date", children: [
|
|
443
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "realiiz-bw__service-header", children: [
|
|
444
|
+
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "realiiz-bw__service-title", children: service.label }),
|
|
445
|
+
service.description && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "realiiz-bw__service-desc", children: service.description })
|
|
446
|
+
] }),
|
|
447
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "realiiz-bw__cal-header", children: [
|
|
448
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "realiiz-bw__cal-title", "aria-live": "polite", children: viewMonth.toLocaleDateString(void 0, { month: "long", year: "numeric" }) }),
|
|
449
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "realiiz-bw__cal-nav", children: [
|
|
450
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "aria-label": "Previous month", disabled: !canPrev, onClick: () => goMonth(-1), children: "\u2039" }),
|
|
451
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "aria-label": "Next month", disabled: !canNext, onClick: () => goMonth(1), children: "\u203A" })
|
|
452
|
+
] })
|
|
453
|
+
] }),
|
|
454
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "realiiz-bw__weekdays", "aria-hidden": "true", children: weekdays.map((w) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "realiiz-bw__weekday", children: w }, w)) }),
|
|
455
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "realiiz-bw__days", children: cells.map((cell) => {
|
|
456
|
+
const k = dateKey(cell);
|
|
457
|
+
if (!sameMonth(cell, viewMonth)) {
|
|
458
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "realiiz-bw__day realiiz-bw__day--outside", "aria-hidden": "true" }, k);
|
|
459
|
+
}
|
|
460
|
+
const isAvailable = availableDays.has(k);
|
|
461
|
+
const isToday = k === todayKey;
|
|
462
|
+
const isSelected = k === selectedDay;
|
|
463
|
+
const className = [
|
|
464
|
+
"realiiz-bw__day",
|
|
465
|
+
isToday && "realiiz-bw__day--today",
|
|
466
|
+
isAvailable ? "realiiz-bw__day--available" : "realiiz-bw__day--disabled",
|
|
467
|
+
isSelected && "realiiz-bw__day--selected"
|
|
468
|
+
].filter(Boolean).join(" ");
|
|
469
|
+
const fullDate = cell.toLocaleDateString(void 0, {
|
|
470
|
+
weekday: "long",
|
|
471
|
+
month: "long",
|
|
472
|
+
day: "numeric",
|
|
473
|
+
year: "numeric"
|
|
474
|
+
});
|
|
475
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
476
|
+
"button",
|
|
477
|
+
{
|
|
478
|
+
type: "button",
|
|
479
|
+
className,
|
|
480
|
+
disabled: !isAvailable,
|
|
481
|
+
"aria-pressed": isSelected,
|
|
482
|
+
"aria-current": isToday ? "date" : void 0,
|
|
483
|
+
"aria-label": isAvailable ? fullDate : `${fullDate}, unavailable`,
|
|
484
|
+
onClick: () => setSelectedDay(k),
|
|
485
|
+
children: cell.getDate()
|
|
486
|
+
},
|
|
487
|
+
k
|
|
488
|
+
);
|
|
489
|
+
}) })
|
|
490
|
+
] }),
|
|
491
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "realiiz-bw__times", role: "group", "aria-label": "Choose a time", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "realiiz-bw__times-inner", children: [
|
|
492
|
+
!selectedDay && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "realiiz-bw__times-empty", children: "Select a date to see available times." }),
|
|
493
|
+
selectedDay && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
494
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "realiiz-bw__times-title", children: (/* @__PURE__ */ new Date(`${selectedDay}T00:00:00`)).toLocaleDateString(void 0, {
|
|
495
|
+
weekday: "short",
|
|
496
|
+
month: "short",
|
|
497
|
+
day: "numeric"
|
|
498
|
+
}) }),
|
|
499
|
+
daySlots.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "realiiz-bw__times-empty", children: "No times available." }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "realiiz-bw__times-list", children: groupTimesByPartOfDay(daySlots).map((group) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "realiiz-bw__times-group", children: [
|
|
500
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "realiiz-bw__times-group-label", children: group.label }),
|
|
501
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "realiiz-bw__times-group-slots", children: group.slots.map((slot) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
502
|
+
"button",
|
|
503
|
+
{
|
|
504
|
+
type: "button",
|
|
505
|
+
className: "realiiz-bw__time",
|
|
506
|
+
"aria-label": slot.start.toLocaleString(void 0, {
|
|
507
|
+
weekday: "long",
|
|
508
|
+
month: "long",
|
|
509
|
+
day: "numeric",
|
|
510
|
+
hour: "numeric",
|
|
511
|
+
minute: "2-digit"
|
|
512
|
+
}),
|
|
513
|
+
onClick: () => onPick(slot.start),
|
|
514
|
+
children: slot.start.toLocaleTimeString(void 0, { hour: "numeric", minute: "2-digit" })
|
|
515
|
+
},
|
|
516
|
+
slot.start.toISOString()
|
|
517
|
+
)) })
|
|
518
|
+
] }, group.key)) })
|
|
519
|
+
] })
|
|
520
|
+
] }) })
|
|
521
|
+
] }) });
|
|
522
|
+
}
|
|
523
|
+
function resolveConfirmation(message, booking, slot) {
|
|
524
|
+
if (typeof message === "function") return message(booking);
|
|
525
|
+
if (message != null) return message;
|
|
526
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
527
|
+
"Your ",
|
|
528
|
+
/* @__PURE__ */ jsxRuntime.jsx("strong", { children: booking.service_type }),
|
|
529
|
+
" is requested for ",
|
|
530
|
+
formatSlot(slot),
|
|
531
|
+
"."
|
|
532
|
+
] });
|
|
533
|
+
}
|
|
534
|
+
function formatSlot(d) {
|
|
535
|
+
return d.toLocaleString(void 0, {
|
|
536
|
+
weekday: "short",
|
|
537
|
+
month: "short",
|
|
538
|
+
day: "numeric",
|
|
539
|
+
hour: "numeric",
|
|
540
|
+
minute: "2-digit"
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
var CALENDAR_CSS = `
|
|
544
|
+
.realiiz-bw__scheduler { container-type: inline-size; }
|
|
545
|
+
.realiiz-bw__scheduler-inner { display: flex; flex-direction: column; gap: 1rem; box-sizing: border-box; }
|
|
546
|
+
.realiiz-bw__scheduler-inner * { box-sizing: border-box; }
|
|
547
|
+
/* DESKTOP ONLY (mobile/stacked layout above is intentionally untouched).
|
|
548
|
+
Lay the two columns side by side. The widget renders "naked" \u2014 no outer
|
|
549
|
+
card chrome (no background, border, accent edge, radius, or padding) so the
|
|
550
|
+
host can wrap it in whatever container treatment it wants. */
|
|
551
|
+
@container (min-width: 28rem) {
|
|
552
|
+
.realiiz-bw__scheduler-inner {
|
|
553
|
+
flex-direction: row;
|
|
554
|
+
align-items: stretch;
|
|
555
|
+
gap: 1.25rem;
|
|
556
|
+
}
|
|
557
|
+
.realiiz-bw__calendar { flex: 1 1 auto; min-width: 0; }
|
|
558
|
+
/* Slim times column, separated by an internal divider so the columns feel joined.
|
|
559
|
+
Its content is taken out of flow (see __times-inner) so the column never
|
|
560
|
+
inflates the card height \u2014 the left column (title + description + calendar)
|
|
561
|
+
drives it. position: relative makes it the containing block for that inner. */
|
|
562
|
+
.realiiz-bw__times {
|
|
563
|
+
flex: 0 0 9rem;
|
|
564
|
+
border-left: 1px solid var(--realiiz-bw-border, #d4d4d8);
|
|
565
|
+
position: relative;
|
|
566
|
+
}
|
|
567
|
+
/* Absolutely fill the (stretched) times column. Because it's out of flow, the
|
|
568
|
+
times side contributes no height of its own, so the card is sized to the
|
|
569
|
+
calendar side \u2014 no stranded void under the calendar \u2014 while the list below
|
|
570
|
+
flexes to fill this height and scrolls, reaching the bottom in alignment. */
|
|
571
|
+
.realiiz-bw__times-inner {
|
|
572
|
+
position: absolute;
|
|
573
|
+
inset: 0;
|
|
574
|
+
padding-left: 1.25rem;
|
|
575
|
+
display: flex;
|
|
576
|
+
flex-direction: column;
|
|
577
|
+
min-height: 0;
|
|
578
|
+
}
|
|
579
|
+
.realiiz-bw__times-list {
|
|
580
|
+
flex: 1 1 0;
|
|
581
|
+
min-height: 0;
|
|
582
|
+
overflow-y: auto;
|
|
583
|
+
/* Breathing room so the pills don't crowd the (subtle) scrollbar. */
|
|
584
|
+
padding-right: 0.4rem;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
/* Prominent service header at the top of the left column: clear title, with
|
|
588
|
+
the description and duration beneath so users know what they're booking. */
|
|
589
|
+
.realiiz-bw__service-header { margin-bottom: 0.85rem; }
|
|
590
|
+
.realiiz-bw__service-title { margin: 0; font-size: 1.05rem; font-weight: 700; line-height: 1.25; color: var(--realiiz-bw-fg, inherit); }
|
|
591
|
+
.realiiz-bw__service-desc { margin: 0.3rem 0 0; font-size: 0.78rem; line-height: 1.4; color: var(--realiiz-bw-muted, #71717a); }
|
|
592
|
+
/* Service picker option: stacked title + supporting description, duration kept at the right. */
|
|
593
|
+
.realiiz-bw__service-main { display: flex; flex-direction: column; min-width: 0; }
|
|
594
|
+
.realiiz-bw__service-name { font-weight: 600; }
|
|
595
|
+
.realiiz-bw__service-duration { flex: none; white-space: nowrap; }
|
|
596
|
+
.realiiz-bw__cal-header { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; margin-bottom: 0.5rem; }
|
|
597
|
+
.realiiz-bw__cal-title { font-weight: 600; }
|
|
598
|
+
.realiiz-bw__cal-nav { display: flex; gap: 0.25rem; }
|
|
599
|
+
.realiiz-bw__cal-nav button {
|
|
600
|
+
width: 2rem; height: 2rem; line-height: 1; cursor: pointer; font: inherit; color: inherit;
|
|
601
|
+
border: 1px solid var(--realiiz-bw-border, #d4d4d8);
|
|
602
|
+
border-radius: var(--realiiz-bw-radius, 8px);
|
|
603
|
+
background: var(--realiiz-bw-surface, transparent);
|
|
604
|
+
}
|
|
605
|
+
.realiiz-bw__cal-nav button:hover:not(:disabled) { background: var(--realiiz-bw-available-hover-bg, #f4f4f5); }
|
|
606
|
+
.realiiz-bw__cal-nav button:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
607
|
+
.realiiz-bw__weekdays, .realiiz-bw__days { display: grid; grid-template-columns: repeat(7, 1fr); gap: 0.25rem; }
|
|
608
|
+
.realiiz-bw__weekday { text-align: center; font-size: 0.7rem; padding: 0.25rem 0; color: var(--realiiz-bw-muted, #71717a); }
|
|
609
|
+
.realiiz-bw__day {
|
|
610
|
+
position: relative;
|
|
611
|
+
aspect-ratio: 1 / 1; min-width: 2.25rem; display: flex; align-items: center; justify-content: center;
|
|
612
|
+
padding: 0; font: inherit; font-size: 0.9rem; color: inherit;
|
|
613
|
+
border: 1px solid transparent; border-radius: var(--realiiz-bw-radius, 8px); background: transparent;
|
|
614
|
+
}
|
|
615
|
+
.realiiz-bw__day--outside { visibility: hidden; }
|
|
616
|
+
.realiiz-bw__day--disabled { color: var(--realiiz-bw-disabled-fg, #c4c4c8); cursor: not-allowed; }
|
|
617
|
+
/* Available days get a subtle surface fill + bold dark text so they read as
|
|
618
|
+
confident, tappable targets that clearly stand apart from disabled days \u2014
|
|
619
|
+
without competing with the selected (accent fill) state. */
|
|
620
|
+
.realiiz-bw__day--available {
|
|
621
|
+
cursor: pointer; font-weight: 600;
|
|
622
|
+
color: var(--realiiz-bw-available-fg, inherit);
|
|
623
|
+
background: var(--realiiz-bw-available-bg, #fafafb);
|
|
624
|
+
border-color: var(--realiiz-bw-available-border, var(--realiiz-bw-border, #d4d4d8));
|
|
625
|
+
}
|
|
626
|
+
/* Hover only shifts the background; text stays dark. Scoped to :not(--selected)
|
|
627
|
+
so it can never override the selected fill and leave white text on a light bg. */
|
|
628
|
+
.realiiz-bw__day--available:not(.realiiz-bw__day--selected):hover {
|
|
629
|
+
background: var(--realiiz-bw-available-hover-bg, #f4f4f5);
|
|
630
|
+
color: var(--realiiz-bw-available-fg, inherit);
|
|
631
|
+
}
|
|
632
|
+
/* Today: a small dot centered under the day number (no underline bar). */
|
|
633
|
+
.realiiz-bw__day--today::after {
|
|
634
|
+
content: ""; position: absolute; left: 50%; bottom: 0.3rem; transform: translateX(-50%);
|
|
635
|
+
width: 0.32rem; height: 0.32rem; border-radius: 50%;
|
|
636
|
+
background: var(--realiiz-bw-today-marker, var(--realiiz-bw-accent, #18181b));
|
|
637
|
+
}
|
|
638
|
+
.realiiz-bw__day--selected {
|
|
639
|
+
border-color: transparent;
|
|
640
|
+
background: var(--realiiz-bw-selected-bg, var(--realiiz-bw-accent, #18181b));
|
|
641
|
+
color: var(--realiiz-bw-selected-fg, var(--realiiz-bw-accent-fg, #ffffff));
|
|
642
|
+
}
|
|
643
|
+
/* Selected always wins: keep the fill on hover, and hide the today dot when a
|
|
644
|
+
day is both today and selected so the two markers never clash. */
|
|
645
|
+
.realiiz-bw__day--selected:hover {
|
|
646
|
+
background: var(--realiiz-bw-selected-bg, var(--realiiz-bw-accent, #18181b));
|
|
647
|
+
color: var(--realiiz-bw-selected-fg, var(--realiiz-bw-accent-fg, #ffffff));
|
|
648
|
+
}
|
|
649
|
+
.realiiz-bw__day--today.realiiz-bw__day--selected::after { display: none; }
|
|
650
|
+
.realiiz-bw__times-title { margin: 0 0 0.5rem; font-weight: 600; font-size: 0.875rem; }
|
|
651
|
+
.realiiz-bw__times-empty { margin: 0; color: var(--realiiz-bw-muted, #71717a); font-size: 0.875rem; }
|
|
652
|
+
/* Single vertical list: one time slot per row, full width of the times column,
|
|
653
|
+
broken into Morning/Afternoon/Evening sections for scannability. */
|
|
654
|
+
.realiiz-bw__times-list {
|
|
655
|
+
display: flex; flex-direction: column; gap: 0.9rem;
|
|
656
|
+
/* Quiet scrollbar (only shows on desktop, where the list is height-capped):
|
|
657
|
+
no track, just a faint slim thumb in a low-contrast neutral. Firefox. */
|
|
658
|
+
scrollbar-width: thin;
|
|
659
|
+
scrollbar-color: var(--realiiz-bw-scrollbar-thumb, color-mix(in srgb, var(--realiiz-bw-muted, #71717a) 18%, transparent)) transparent;
|
|
660
|
+
}
|
|
661
|
+
/* WebKit (Chrome/Safari/Edge): track removed entirely, thumb only. */
|
|
662
|
+
.realiiz-bw__times-list::-webkit-scrollbar { width: 6px; }
|
|
663
|
+
.realiiz-bw__times-list::-webkit-scrollbar-track { background: transparent; }
|
|
664
|
+
.realiiz-bw__times-list::-webkit-scrollbar-thumb {
|
|
665
|
+
background: var(--realiiz-bw-scrollbar-thumb, color-mix(in srgb, var(--realiiz-bw-muted, #71717a) 18%, transparent));
|
|
666
|
+
border-radius: 999px;
|
|
667
|
+
}
|
|
668
|
+
.realiiz-bw__times-group { display: flex; flex-direction: column; gap: 0.4rem; }
|
|
669
|
+
.realiiz-bw__times-group-label {
|
|
670
|
+
margin: 0; font-size: 0.68rem; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase;
|
|
671
|
+
color: var(--realiiz-bw-muted, #71717a);
|
|
672
|
+
}
|
|
673
|
+
.realiiz-bw__times-group-slots { display: flex; flex-direction: column; gap: 0.4rem; }
|
|
674
|
+
.realiiz-bw__time {
|
|
675
|
+
width: 100%; padding: 0.6rem 0.75rem; cursor: pointer; font: inherit; color: inherit;
|
|
676
|
+
white-space: nowrap; text-align: center;
|
|
677
|
+
border: 1px solid var(--realiiz-bw-border, #d4d4d8);
|
|
678
|
+
border-radius: var(--realiiz-bw-radius, 8px);
|
|
679
|
+
background: var(--realiiz-bw-surface, transparent);
|
|
680
|
+
}
|
|
681
|
+
/* Hover shifts background only \u2014 text stays readable. */
|
|
682
|
+
.realiiz-bw__time:hover { background: var(--realiiz-bw-available-hover-bg, #f4f4f5); color: inherit; }
|
|
683
|
+
/* Back: a quiet ghost/bordered secondary button (no underline). Deliberately
|
|
684
|
+
muted \u2014 it must not compete with the accent-filled primary booking action. */
|
|
685
|
+
.realiiz-bw__back {
|
|
686
|
+
display: inline-flex; align-items: center; gap: 0.25rem;
|
|
687
|
+
padding: 0.3rem 0.6rem; font: inherit; font-size: 0.8rem; line-height: 1;
|
|
688
|
+
cursor: pointer; text-decoration: none;
|
|
689
|
+
color: var(--realiiz-bw-muted, #71717a);
|
|
690
|
+
background: var(--realiiz-bw-surface, transparent);
|
|
691
|
+
border: 1px solid var(--realiiz-bw-border, #d4d4d8);
|
|
692
|
+
border-radius: var(--realiiz-bw-radius, 8px);
|
|
693
|
+
}
|
|
694
|
+
.realiiz-bw__back:hover { background: var(--realiiz-bw-available-hover-bg, #f4f4f5); color: var(--realiiz-bw-fg, inherit); }
|
|
695
|
+
.realiiz-bw__day--available:focus-visible,
|
|
696
|
+
.realiiz-bw__time:focus-visible,
|
|
697
|
+
.realiiz-bw__back:focus-visible,
|
|
698
|
+
.realiiz-bw__consent-check:focus-visible,
|
|
699
|
+
.realiiz-bw__cal-nav button:focus-visible {
|
|
700
|
+
outline: 2px solid var(--realiiz-bw-focus-ring, var(--realiiz-bw-accent, #18181b));
|
|
701
|
+
outline-offset: 2px;
|
|
702
|
+
}
|
|
703
|
+
/* Details form fields \u2014 part of the same designed system as the calendar:
|
|
704
|
+
refined surface + border, tight label hierarchy, comfortable padding, and an
|
|
705
|
+
elegant accent focus ring (replacing the browser default outline). */
|
|
706
|
+
.realiiz-bw__field { display: flex; flex-direction: column; gap: 0.35rem; }
|
|
707
|
+
.realiiz-bw__field-label { font-size: 0.8rem; font-weight: 500; color: var(--realiiz-bw-fg, inherit); }
|
|
708
|
+
/* SMS consent: small, quiet supporting text that doesn't compete with the fields. */
|
|
709
|
+
.realiiz-bw__consent { display: flex; align-items: flex-start; gap: 0.5rem; cursor: pointer; }
|
|
710
|
+
.realiiz-bw__consent-check {
|
|
711
|
+
margin: 0.1rem 0 0; flex: none; cursor: pointer;
|
|
712
|
+
accent-color: var(--realiiz-bw-accent, #18181b);
|
|
713
|
+
}
|
|
714
|
+
.realiiz-bw__consent-text { font-size: 0.7rem; line-height: 1.45; color: var(--realiiz-bw-muted, #71717a); }
|
|
715
|
+
.realiiz-bw__input {
|
|
716
|
+
width: 100%; box-sizing: border-box;
|
|
717
|
+
padding: 0.65rem 0.8rem;
|
|
718
|
+
font: inherit; color: inherit;
|
|
719
|
+
background: var(--realiiz-bw-surface, transparent);
|
|
720
|
+
border: 1px solid var(--realiiz-bw-border, #d4d4d8);
|
|
721
|
+
border-radius: var(--realiiz-bw-radius, 8px);
|
|
722
|
+
outline: none;
|
|
723
|
+
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
|
724
|
+
}
|
|
725
|
+
.realiiz-bw__input::placeholder { color: var(--realiiz-bw-muted, #71717a); opacity: 0.7; }
|
|
726
|
+
.realiiz-bw__input:hover { border-color: var(--realiiz-bw-muted, #71717a); }
|
|
727
|
+
.realiiz-bw__input:focus {
|
|
728
|
+
border-color: var(--realiiz-bw-focus-ring, var(--realiiz-bw-accent, #18181b));
|
|
729
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--realiiz-bw-focus-ring, var(--realiiz-bw-accent, #18181b)) 22%, transparent);
|
|
730
|
+
}
|
|
731
|
+
/* Selected service + time, so the user sees WHAT they're booking, not just when.
|
|
732
|
+
Service name is the prominent, readable line; the date/time is muted beneath. */
|
|
733
|
+
.realiiz-bw__summary { display: flex; flex-direction: column; gap: 0.1rem; }
|
|
734
|
+
.realiiz-bw__summary-service { font-weight: 600; font-size: 0.95rem; color: var(--realiiz-bw-fg, inherit); }
|
|
735
|
+
.realiiz-bw__summary-when { font-size: 0.8rem; color: var(--realiiz-bw-muted, #71717a); }
|
|
736
|
+
/* Primary action. Styled as a class (like the inputs) so the two share the
|
|
737
|
+
exact same corner radius \u2014 both use --realiiz-bw-radius, so they agree
|
|
738
|
+
instead of one being a rounded rect and the other a pill. */
|
|
739
|
+
.realiiz-bw__submit {
|
|
740
|
+
padding: 0.7rem 1rem;
|
|
741
|
+
border: 1px solid transparent;
|
|
742
|
+
border-radius: var(--realiiz-bw-radius, 8px);
|
|
743
|
+
background: var(--realiiz-bw-accent, #18181b);
|
|
744
|
+
color: var(--realiiz-bw-accent-fg, #ffffff);
|
|
745
|
+
font: inherit; font-weight: 600;
|
|
746
|
+
cursor: pointer;
|
|
747
|
+
transition: filter 0.15s ease;
|
|
748
|
+
}
|
|
749
|
+
.realiiz-bw__submit:hover:not(:disabled) { filter: brightness(0.94); }
|
|
750
|
+
.realiiz-bw__submit:disabled { opacity: 0.6; cursor: not-allowed; }
|
|
751
|
+
.realiiz-bw__submit:focus-visible {
|
|
752
|
+
outline: 2px solid var(--realiiz-bw-focus-ring, var(--realiiz-bw-accent, #18181b));
|
|
753
|
+
outline-offset: 2px;
|
|
754
|
+
}
|
|
755
|
+
`;
|
|
756
|
+
var st = {
|
|
757
|
+
root: {
|
|
758
|
+
fontFamily: "var(--realiiz-bw-font, inherit)",
|
|
759
|
+
color: "var(--realiiz-bw-fg, inherit)",
|
|
760
|
+
maxWidth: "var(--realiiz-bw-width, 34rem)",
|
|
761
|
+
boxSizing: "border-box"
|
|
762
|
+
},
|
|
763
|
+
section: { display: "flex", flexDirection: "column", gap: "0.75rem" },
|
|
764
|
+
legend: { fontWeight: 600, padding: 0 },
|
|
765
|
+
header: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: "0.5rem" },
|
|
766
|
+
list: { display: "flex", flexDirection: "column", gap: "0.5rem" },
|
|
767
|
+
optionButton: {
|
|
768
|
+
display: "flex",
|
|
769
|
+
justifyContent: "space-between",
|
|
770
|
+
alignItems: "flex-start",
|
|
771
|
+
gap: "0.5rem",
|
|
772
|
+
padding: "0.75rem 1rem",
|
|
773
|
+
border: "1px solid var(--realiiz-bw-border, #d4d4d8)",
|
|
774
|
+
borderRadius: "var(--realiiz-bw-radius, 8px)",
|
|
775
|
+
background: "var(--realiiz-bw-surface, transparent)",
|
|
776
|
+
color: "inherit",
|
|
777
|
+
font: "inherit",
|
|
778
|
+
cursor: "pointer",
|
|
779
|
+
textAlign: "left"
|
|
780
|
+
},
|
|
781
|
+
linkButton: {
|
|
782
|
+
background: "none",
|
|
783
|
+
border: "none",
|
|
784
|
+
padding: 0,
|
|
785
|
+
color: "var(--realiiz-bw-muted, #71717a)",
|
|
786
|
+
font: "inherit",
|
|
787
|
+
cursor: "pointer",
|
|
788
|
+
textDecoration: "underline"
|
|
789
|
+
},
|
|
790
|
+
muted: { color: "var(--realiiz-bw-muted, #71717a)", fontSize: "0.875rem" },
|
|
791
|
+
notice: {
|
|
792
|
+
margin: 0,
|
|
793
|
+
padding: "0.5rem 0.75rem",
|
|
794
|
+
borderRadius: "var(--realiiz-bw-radius, 8px)",
|
|
795
|
+
background: "var(--realiiz-bw-notice-surface, #fef9c3)",
|
|
796
|
+
color: "var(--realiiz-bw-notice-fg, #713f12)",
|
|
797
|
+
fontSize: "0.875rem"
|
|
798
|
+
},
|
|
799
|
+
error: { margin: 0, color: "var(--realiiz-bw-danger, #b91c1c)", fontSize: "0.875rem" },
|
|
800
|
+
doneTitle: { margin: 0, fontWeight: 600, fontSize: "1.125rem" }
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
exports.BookingWidget = BookingWidget;
|
|
804
|
+
exports.generateSlots = generateSlots;
|
|
805
|
+
exports.parseDurationMinutes = parseDurationMinutes;
|
|
806
|
+
//# sourceMappingURL=index.cjs.map
|
|
807
|
+
//# sourceMappingURL=index.cjs.map
|