@ohhwells/bridge 0.1.18 → 0.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,10 +1,670 @@
1
1
  "use client";
2
2
 
3
3
  // src/OhhwellsBridge.tsx
4
- import React2, { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
4
+ import React3, { useCallback, useEffect as useEffect2, useLayoutEffect, useRef as useRef2, useState as useState3 } from "react";
5
+ import { createRoot } from "react-dom/client";
6
+
7
+ // src/ui/SchedulingWidget.tsx
8
+ import { useEffect, useMemo, useRef, useState as useState2 } from "react";
9
+
10
+ // src/ui/EmailCaptureModal.tsx
11
+ import { useState } from "react";
12
+ import { Dialog } from "radix-ui";
13
+ import { jsx, jsxs } from "react/jsx-runtime";
14
+ function Spinner() {
15
+ return /* @__PURE__ */ jsxs(
16
+ "svg",
17
+ {
18
+ width: "32",
19
+ height: "32",
20
+ viewBox: "0 0 32 32",
21
+ fill: "none",
22
+ style: { animation: "ohw-spin 0.75s linear infinite" },
23
+ children: [
24
+ /* @__PURE__ */ jsx("style", { children: `@keyframes ohw-spin { to { transform: rotate(360deg); } }` }),
25
+ /* @__PURE__ */ jsx("circle", { cx: "16", cy: "16", r: "12", stroke: "#e5e7eb", strokeWidth: "3" }),
26
+ /* @__PURE__ */ jsx(
27
+ "path",
28
+ {
29
+ d: "M16 4a12 12 0 0 1 12 12",
30
+ stroke: "#3b82f6",
31
+ strokeWidth: "3",
32
+ strokeLinecap: "round"
33
+ }
34
+ )
35
+ ]
36
+ }
37
+ );
38
+ }
39
+ function EmailCaptureModal({ title, subtitle, onSubmit, onClose }) {
40
+ const [email, setEmail] = useState("");
41
+ const [submittedEmail, setSubmittedEmail] = useState("");
42
+ const [loading, setLoading] = useState(false);
43
+ const [success, setSuccess] = useState(false);
44
+ const [error, setError] = useState(null);
45
+ const isBook = title === "Confirm your spot";
46
+ const handleSubmit = async () => {
47
+ if (!email.trim()) return;
48
+ setLoading(true);
49
+ setError(null);
50
+ try {
51
+ await onSubmit(email.trim());
52
+ setSubmittedEmail(email.trim());
53
+ setSuccess(true);
54
+ } catch (e) {
55
+ setError(e instanceof Error ? e.message : "Something went wrong. Please try again.");
56
+ } finally {
57
+ setLoading(false);
58
+ }
59
+ };
60
+ const handleKeyDown = (e) => {
61
+ if (e.key === "Enter" && !loading) handleSubmit();
62
+ };
63
+ return /* @__PURE__ */ jsx(Dialog.Root, { open: true, onOpenChange: (open) => {
64
+ if (!open) onClose();
65
+ }, children: /* @__PURE__ */ jsxs(Dialog.Portal, { children: [
66
+ /* @__PURE__ */ jsx(
67
+ Dialog.Overlay,
68
+ {
69
+ className: "fixed inset-0 z-50",
70
+ style: { background: "rgba(0,0,0,0.45)" }
71
+ }
72
+ ),
73
+ /* @__PURE__ */ jsxs(
74
+ Dialog.Content,
75
+ {
76
+ className: "fixed left-1/2 top-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2 bg-white rounded-xl shadow-xl outline-none font-body box-border overflow-hidden",
77
+ style: { maxWidth: 400 },
78
+ children: [
79
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-3 p-6", children: [
80
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
81
+ /* @__PURE__ */ jsx(
82
+ Dialog.Title,
83
+ {
84
+ className: "font-body text-xl font-bold m-0 leading-tight",
85
+ style: { color: "var(--color-dark,#200C02)" },
86
+ children: success ? isBook ? "Reservation confirmed!" : "You're on the waitlist!" : title
87
+ }
88
+ ),
89
+ /* @__PURE__ */ jsx(
90
+ Dialog.Description,
91
+ {
92
+ className: "font-body text-sm m-0",
93
+ style: { color: "#6B7280" },
94
+ children: success ? isBook ? `A confirmation email has been sent to ${submittedEmail}.` : "We'll let you know via email if someone cancels." : subtitle
95
+ }
96
+ )
97
+ ] }),
98
+ /* @__PURE__ */ jsx(
99
+ Dialog.Close,
100
+ {
101
+ className: "shrink-0 w-7 h-7 flex items-center justify-center rounded-full bg-transparent border-none cursor-pointer mt-0.5 absolute top-2.5 right-1.5",
102
+ style: { color: "#6B7280" },
103
+ "aria-label": "Close",
104
+ children: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
105
+ /* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
106
+ /* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
107
+ ] })
108
+ }
109
+ )
110
+ ] }),
111
+ !success && (loading ? (
112
+ /* Loading state — full body replaced */
113
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center gap-3 px-7 py-12", children: [
114
+ /* @__PURE__ */ jsx(Spinner, {}),
115
+ /* @__PURE__ */ jsx("p", { className: "font-body text-sm m-0", style: { color: "#6B7280" }, children: "Please wait" })
116
+ ] })
117
+ ) : (
118
+ /* Email form */
119
+ /* @__PURE__ */ jsxs("div", { className: "px-7 pt-2 pb-6", children: [
120
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5 mb-8", children: [
121
+ /* @__PURE__ */ jsx("label", { className: "font-body text-sm font-medium", style: { color: "var(--color-dark,#200C02)" }, children: "Email" }),
122
+ /* @__PURE__ */ jsx(
123
+ "input",
124
+ {
125
+ type: "email",
126
+ value: email,
127
+ onChange: (e) => setEmail(e.target.value),
128
+ onKeyDown: handleKeyDown,
129
+ placeholder: "hello@gmail.com",
130
+ autoFocus: true,
131
+ className: "w-full h-10 px-3 rounded-lg border font-body text-sm outline-none box-border",
132
+ style: {
133
+ borderColor: "#E7E5E4",
134
+ background: "#fff",
135
+ color: "var(--color-dark,#200C02)"
136
+ }
137
+ }
138
+ ),
139
+ error && /* @__PURE__ */ jsx("p", { className: "font-body text-xs text-red-500 m-0", children: error })
140
+ ] }),
141
+ /* @__PURE__ */ jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx(
142
+ "button",
143
+ {
144
+ onClick: handleSubmit,
145
+ disabled: !email.trim(),
146
+ className: "h-9 px-5 rounded-lg font-body text-sm font-semibold cursor-pointer border-none disabled:opacity-50 disabled:cursor-not-allowed",
147
+ style: {
148
+ background: "var(--color-primary,#3D312B)",
149
+ color: "var(--color-light,#FEFFF0)"
150
+ },
151
+ children: "Send"
152
+ }
153
+ ) })
154
+ ] })
155
+ ))
156
+ ]
157
+ }
158
+ )
159
+ ] }) });
160
+ }
161
+
162
+ // src/ui/SchedulingWidget.tsx
163
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
164
+ var API_URL = process.env.NEXT_PUBLIC_FLOWOPS_API_URL ?? "https://flowops-backend-staging-2yfdh7pwpq-as.a.run.app";
165
+ var DAY_ABBR = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"];
166
+ function getApiDomain() {
167
+ const h = window.location.hostname;
168
+ for (const base of ["ohhwells.site", "theflowops-staging.com", "theflowops.com"]) {
169
+ if (h.endsWith(`.${base}`)) return h.slice(0, -(base.length + 1));
170
+ }
171
+ return h;
172
+ }
173
+ function classRunsOnDate(cls, date, type) {
174
+ if (type === "FIXED") {
175
+ const d = String(date.getDate()).padStart(2, "0");
176
+ const m = String(date.getMonth() + 1).padStart(2, "0");
177
+ return cls.fixedDates?.some((fd) => fd.date === `${d}-${m}-${date.getFullYear()}`) ?? false;
178
+ }
179
+ return cls.weekdays?.some((w) => w.weekday === date.getDay()) ?? false;
180
+ }
181
+ function formatClassTime(cls) {
182
+ let h = parseInt(cls.startTime.hour, 10);
183
+ const m = parseInt(cls.startTime.minutes, 10);
184
+ if (cls.startTime.time === "PM" && h !== 12) h += 12;
185
+ if (cls.startTime.time === "AM" && h === 12) h = 0;
186
+ const endTotal = h * 60 + m + cls.duration;
187
+ const eh = Math.floor(endTotal / 60) % 24;
188
+ const em = endTotal % 60;
189
+ return `${h}:${cls.startTime.minutes}-${eh}:${String(em).padStart(2, "0")}`;
190
+ }
191
+ function getBookingsOnDate(cls, date) {
192
+ if (!cls.bookings?.length) return 0;
193
+ const ds = date.toISOString().split("T")[0];
194
+ return cls.bookings.filter((b) => {
195
+ try {
196
+ return new Date(b.classDate).toISOString().split("T")[0] === ds;
197
+ } catch {
198
+ return false;
199
+ }
200
+ }).length;
201
+ }
202
+ function buildTimezoneLabel(tz) {
203
+ try {
204
+ const now = /* @__PURE__ */ new Date();
205
+ const timeStr = new Intl.DateTimeFormat("en-US", {
206
+ hour: "2-digit",
207
+ minute: "2-digit",
208
+ hour12: false,
209
+ timeZone: tz
210
+ }).format(now);
211
+ const offsetPart = new Intl.DateTimeFormat("en-US", { timeZoneName: "short", timeZone: tz }).formatToParts(now).find((p) => p.type === "timeZoneName")?.value ?? "";
212
+ const city = tz.split("/").pop()?.replace(/_/g, " ") ?? tz;
213
+ return `${timeStr} (${offsetPart}) ${city} time`;
214
+ } catch {
215
+ return tz;
216
+ }
217
+ }
218
+ function EmptyState({ inEditor }) {
219
+ const handleAddSchedule = () => {
220
+ if (inEditor) {
221
+ window.parent.postMessage({ type: "ow:scheduling-not-connected" }, "*");
222
+ }
223
+ };
224
+ return /* @__PURE__ */ jsxs2("div", { className: "flex flex-col items-center justify-center gap-6 p-12 bg-[#FAFAF9] border-2 border-dashed border-[#E7E5E4] rounded-[10px] h-[264px] w-full box-border", children: [
225
+ /* @__PURE__ */ jsx2("div", { className: "w-10 h-10 bg-[#F5F5F4] rounded-[10px] flex items-center justify-center shrink-0", children: /* @__PURE__ */ jsxs2(
226
+ "svg",
227
+ {
228
+ width: "20",
229
+ height: "20",
230
+ viewBox: "0 0 24 24",
231
+ fill: "none",
232
+ stroke: "var(--color-accent, #A89B83)",
233
+ strokeWidth: "1.5",
234
+ strokeLinecap: "round",
235
+ strokeLinejoin: "round",
236
+ children: [
237
+ /* @__PURE__ */ jsx2("rect", { x: "3", y: "4", width: "18", height: "18", rx: "2" }),
238
+ /* @__PURE__ */ jsx2("line", { x1: "16", y1: "2", x2: "16", y2: "6" }),
239
+ /* @__PURE__ */ jsx2("line", { x1: "8", y1: "2", x2: "8", y2: "6" }),
240
+ /* @__PURE__ */ jsx2("line", { x1: "3", y1: "10", x2: "21", y2: "10" }),
241
+ /* @__PURE__ */ jsx2("line", { x1: "12", y1: "14", x2: "12", y2: "18" }),
242
+ /* @__PURE__ */ jsx2("line", { x1: "10", y1: "16", x2: "14", y2: "16" })
243
+ ]
244
+ }
245
+ ) }),
246
+ /* @__PURE__ */ jsxs2("div", { className: "flex flex-col items-center gap-2", children: [
247
+ /* @__PURE__ */ jsx2("p", { className: "font-body text-lg font-medium text-center text-[#0A0A0A] m-0", children: "No schedule yet" }),
248
+ /* @__PURE__ */ jsx2("p", { className: "font-body text-base font-normal text-center text-(--color-accent,#A89B83) m-0", children: "Add a scheduling widget to get instant bookings." })
249
+ ] }),
250
+ /* @__PURE__ */ jsx2(
251
+ "button",
252
+ {
253
+ onClick: handleAddSchedule,
254
+ className: "h-9 px-2 py-1.5 flex items-center justify-center bg-white border border-[#E7E5E4] rounded-md font-body text-sm font-medium text-(--color-dark,#200C02) cursor-pointer",
255
+ children: "Add Schedule"
256
+ }
257
+ )
258
+ ] });
259
+ }
260
+ function LoadingSkeleton() {
261
+ const shimmer = {
262
+ background: "linear-gradient(90deg,#f0f0f0 25%,#e8e8e8 50%,#f0f0f0 75%)",
263
+ backgroundSize: "200% 100%",
264
+ animation: "ohw-shimmer 1.5s infinite"
265
+ };
266
+ return /* @__PURE__ */ jsxs2("div", { className: "w-full flex flex-col gap-10", children: [
267
+ /* @__PURE__ */ jsx2("style", { children: `@keyframes ohw-shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}` }),
268
+ /* @__PURE__ */ jsx2("div", { className: "flex h-[70px] items-stretch", children: Array.from({ length: 7 }).map((_, i) => /* @__PURE__ */ jsxs2("div", { className: "flex-1 flex flex-col justify-between items-center px-1", children: [
269
+ /* @__PURE__ */ jsx2("div", { style: { ...shimmer, width: "32px", height: "14px", borderRadius: "4px" } }),
270
+ /* @__PURE__ */ jsx2("div", { style: { ...shimmer, width: "40px", height: "40px", borderRadius: "50%" } })
271
+ ] }, i)) }),
272
+ [0, 1, 2].map((i) => /* @__PURE__ */ jsxs2("div", { children: [
273
+ /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-[60px] py-4", children: [
274
+ /* @__PURE__ */ jsx2("div", { style: { ...shimmer, width: "120px", height: "16px", borderRadius: "4px", flexShrink: 0 } }),
275
+ /* @__PURE__ */ jsxs2("div", { className: "flex-1 flex flex-col gap-2", children: [
276
+ /* @__PURE__ */ jsx2("div", { style: { ...shimmer, width: "55%", height: "16px", borderRadius: "4px" } }),
277
+ /* @__PURE__ */ jsx2("div", { style: { ...shimmer, width: "38%", height: "14px", borderRadius: "4px" } })
278
+ ] }),
279
+ /* @__PURE__ */ jsx2("div", { style: { ...shimmer, width: "56px", height: "32px", borderRadius: "4px", flexShrink: 0 } }),
280
+ /* @__PURE__ */ jsx2("div", { style: { ...shimmer, width: "200px", height: "44px", borderRadius: "8px", flexShrink: 0 } })
281
+ ] }),
282
+ i < 2 && /* @__PURE__ */ jsx2("div", { className: "h-px bg-black/20" })
283
+ ] }, i))
284
+ ] });
285
+ }
286
+ function ScheduleView({ schedule, dates, selectedIdx, onSelectDate, onOpenModal }) {
287
+ const todayMs = useMemo(() => {
288
+ const d = /* @__PURE__ */ new Date();
289
+ d.setHours(0, 0, 0, 0);
290
+ return d.getTime();
291
+ }, []);
292
+ const scrollRef = useRef(null);
293
+ const [canScrollLeft, setCanScrollLeft] = useState2(false);
294
+ const [canScrollRight, setCanScrollRight] = useState2(false);
295
+ const updateScrollState = () => {
296
+ const el = scrollRef.current;
297
+ if (!el) return;
298
+ setCanScrollLeft(el.scrollLeft > 0);
299
+ setCanScrollRight(el.scrollLeft + el.clientWidth < el.scrollWidth - 1);
300
+ };
301
+ useEffect(() => {
302
+ const el = scrollRef.current;
303
+ if (!el) return;
304
+ updateScrollState();
305
+ el.addEventListener("scroll", updateScrollState);
306
+ const ro = new ResizeObserver(updateScrollState);
307
+ ro.observe(el);
308
+ return () => {
309
+ el.removeEventListener("scroll", updateScrollState);
310
+ ro.disconnect();
311
+ };
312
+ }, [dates]);
313
+ const scroll = (dir) => {
314
+ scrollRef.current?.scrollBy({ left: dir === "left" ? -216 : 216, behavior: "smooth" });
315
+ };
316
+ const datesWithClasses = useMemo(
317
+ () => new Set(dates.map(
318
+ (d, i) => schedule.classes?.some((c) => classRunsOnDate(c, d, schedule.type)) ? i : -1
319
+ ).filter((i) => i >= 0)),
320
+ [dates, schedule]
321
+ );
322
+ const selectedDate = dates[selectedIdx];
323
+ const selectedClasses = useMemo(
324
+ () => (schedule.classes ?? []).filter((c) => selectedDate && classRunsOnDate(c, selectedDate, schedule.type)),
325
+ [schedule, selectedDate]
326
+ );
327
+ return /* @__PURE__ */ jsxs2("div", { className: "w-full flex flex-col gap-10", children: [
328
+ /* @__PURE__ */ jsxs2("div", { className: "relative w-full", children: [
329
+ canScrollLeft && /* @__PURE__ */ jsx2(
330
+ "button",
331
+ {
332
+ onClick: () => scroll("left"),
333
+ className: "absolute left-0 top-1/2 -translate-y-1/2 z-10 w-8 h-8 flex items-center justify-center rounded-full border-none cursor-pointer",
334
+ style: { background: "var(--color-light,#FEFFF0)", boxShadow: "0 1px 4px rgba(0,0,0,0.15)" },
335
+ "aria-label": "Scroll left",
336
+ children: /* @__PURE__ */ jsx2("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "var(--color-dark,#200C02)", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx2("polyline", { points: "15 18 9 12 15 6" }) })
337
+ }
338
+ ),
339
+ canScrollRight && /* @__PURE__ */ jsx2(
340
+ "button",
341
+ {
342
+ onClick: () => scroll("right"),
343
+ className: "absolute right-0 top-1/2 -translate-y-1/2 z-10 w-8 h-8 flex items-center justify-center rounded-full border-none cursor-pointer",
344
+ style: { background: "var(--color-light,#FEFFF0)", boxShadow: "0 1px 4px rgba(0,0,0,0.15)" },
345
+ "aria-label": "Scroll right",
346
+ children: /* @__PURE__ */ jsx2("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "var(--color-dark,#200C02)", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx2("polyline", { points: "9 18 15 12 9 6" }) })
347
+ }
348
+ ),
349
+ /* @__PURE__ */ jsx2(
350
+ "div",
351
+ {
352
+ ref: scrollRef,
353
+ className: "overflow-x-auto w-full",
354
+ style: {
355
+ scrollbarWidth: "none",
356
+ msOverflowStyle: "none",
357
+ paddingLeft: canScrollLeft ? "36px" : 0,
358
+ paddingRight: canScrollRight ? "36px" : 0
359
+ },
360
+ children: /* @__PURE__ */ jsx2("div", { className: "flex", style: { width: `max(100%, ${dates.length * 60}px)` }, children: dates.map((date, i) => {
361
+ const isToday = date.getTime() === todayMs;
362
+ const isSelected = i === selectedIdx;
363
+ const clickable = datesWithClasses.has(i);
364
+ const label = isToday ? "TODAY" : DAY_ABBR[date.getDay()];
365
+ return /* @__PURE__ */ jsxs2(
366
+ "div",
367
+ {
368
+ onClick: () => clickable && onSelectDate(i),
369
+ className: `flex-1 h-[70px] flex flex-col justify-between items-center ${clickable ? "cursor-pointer opacity-100" : "cursor-default opacity-50"}`,
370
+ children: [
371
+ /* @__PURE__ */ jsx2("span", { className: "font-body text-base font-normal text-center text-(--color-dark,#200C02)", children: label }),
372
+ /* @__PURE__ */ jsx2(
373
+ "div",
374
+ {
375
+ className: "w-10 h-10 rounded-full flex items-center justify-center",
376
+ style: { background: isSelected ? "var(--color-primary, #3D312B)" : "transparent" },
377
+ children: /* @__PURE__ */ jsx2(
378
+ "span",
379
+ {
380
+ className: "font-body text-xl font-bold text-center tracking-display-tight",
381
+ style: { color: isSelected ? "var(--color-light, #FEFFF0)" : "var(--color-dark, #200C02)" },
382
+ children: date.getDate()
383
+ }
384
+ )
385
+ }
386
+ )
387
+ ]
388
+ },
389
+ i
390
+ );
391
+ }) })
392
+ }
393
+ )
394
+ ] }),
395
+ selectedClasses.length === 0 ? /* @__PURE__ */ jsx2("p", { className: "font-body text-base text-(--color-accent,#A89B83) text-center py-8 m-0", children: "No classes scheduled for this day." }) : /* @__PURE__ */ jsx2("div", { className: "flex flex-col", children: selectedClasses.map((cls, i) => {
396
+ const booked = getBookingsOnDate(cls, selectedDate);
397
+ const available = cls.maxParticipants - booked;
398
+ const isFull = available <= 0;
399
+ return /* @__PURE__ */ jsxs2("div", { children: [
400
+ /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-[60px] py-4 box-border", children: [
401
+ /* @__PURE__ */ jsx2("div", { className: "w-[120px] shrink-0", children: /* @__PURE__ */ jsx2("span", { className: "font-body text-base font-bold text-(--color-dark,#200C02) block", children: formatClassTime(cls) }) }),
402
+ /* @__PURE__ */ jsxs2("div", { className: "flex-1 flex flex-col gap-3 min-w-0", children: [
403
+ /* @__PURE__ */ jsx2("span", { className: "font-body text-base font-bold text-(--color-dark,#200C02) block truncate", children: cls.name }),
404
+ cls.hostName && /* @__PURE__ */ jsx2("span", { className: "font-body text-base font-normal text-(--color-dark,#200C02) opacity-80 block truncate", children: cls.hostName })
405
+ ] }),
406
+ /* @__PURE__ */ jsx2("div", { className: "w-14 shrink-0 flex flex-col gap-px", children: isFull ? /* @__PURE__ */ jsx2("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: "Full" }) : /* @__PURE__ */ jsxs2(Fragment, { children: [
407
+ /* @__PURE__ */ jsxs2("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: [
408
+ available,
409
+ "/",
410
+ cls.maxParticipants
411
+ ] }),
412
+ /* @__PURE__ */ jsx2("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: "available" })
413
+ ] }) }),
414
+ /* @__PURE__ */ jsx2(
415
+ "button",
416
+ {
417
+ className: "shrink-0 w-[200px] px-5 py-[10px] flex items-center justify-center rounded-lg font-body text-base font-bold cursor-pointer box-border",
418
+ style: isFull ? {
419
+ background: "transparent",
420
+ border: "1px solid var(--color-dark, #200C02)",
421
+ color: "var(--color-dark, #200C02)"
422
+ } : {
423
+ background: "var(--color-primary, #3D312B)",
424
+ border: "none",
425
+ color: "var(--color-light, #FEFFF0)"
426
+ },
427
+ onClick: () => {
428
+ if (!cls.id) return;
429
+ onOpenModal?.(isFull ? "waitlist" : "book", cls.id, selectedDate);
430
+ },
431
+ children: isFull ? "Join Waitlist" : "Book Now"
432
+ }
433
+ )
434
+ ] }),
435
+ i < selectedClasses.length - 1 && /* @__PURE__ */ jsx2("div", { className: "h-px bg-black/20" })
436
+ ] }, cls.id ?? i);
437
+ }) })
438
+ ] });
439
+ }
440
+ function CalendarFoldIcon() {
441
+ return /* @__PURE__ */ jsxs2(
442
+ "svg",
443
+ {
444
+ width: "16",
445
+ height: "16",
446
+ viewBox: "0 0 24 24",
447
+ fill: "none",
448
+ stroke: "currentColor",
449
+ strokeWidth: "2",
450
+ strokeLinecap: "round",
451
+ strokeLinejoin: "round",
452
+ style: { flexShrink: 0 },
453
+ children: [
454
+ /* @__PURE__ */ jsx2("path", { d: "M8 2v4" }),
455
+ /* @__PURE__ */ jsx2("path", { d: "M16 2v4" }),
456
+ /* @__PURE__ */ jsx2("path", { d: "M21 17V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h11Z" }),
457
+ /* @__PURE__ */ jsx2("path", { d: "M3 10h18" }),
458
+ /* @__PURE__ */ jsx2("path", { d: "M15 22v-4a2 2 0 0 1 2-2h4" })
459
+ ]
460
+ }
461
+ );
462
+ }
463
+ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId }) {
464
+ const [schedule, setSchedule] = useState2(null);
465
+ const [loading, setLoading] = useState2(true);
466
+ const [inEditor, setInEditor] = useState2(false);
467
+ const [isHovered, setIsHovered] = useState2(false);
468
+ const [modalState, setModalState] = useState2(null);
469
+ const switchScheduleIdRef = useRef(null);
470
+ const dates = useMemo(() => {
471
+ const today = /* @__PURE__ */ new Date();
472
+ today.setHours(0, 0, 0, 0);
473
+ return Array.from({ length: 14 }, (_, i) => {
474
+ const d = new Date(today);
475
+ d.setDate(today.getDate() + i);
476
+ return d;
477
+ });
478
+ }, []);
479
+ const [selectedIdx, setSelectedIdx] = useState2(0);
480
+ useEffect(() => {
481
+ if (!schedule?.classes) return;
482
+ for (let i = 0; i < dates.length; i++) {
483
+ if (schedule.classes.some((c) => classRunsOnDate(c, dates[i], schedule.type))) {
484
+ setSelectedIdx(i);
485
+ return;
486
+ }
487
+ }
488
+ }, [schedule, dates]);
489
+ useEffect(() => {
490
+ if (typeof window === "undefined") return;
491
+ const isInEditor = window.self !== window.top;
492
+ setInEditor(isInEditor);
493
+ if (isInEditor) {
494
+ const startEmpty = initialScheduleId === null;
495
+ if (startEmpty) setLoading(false);
496
+ let initialized = startEmpty;
497
+ const timer = !startEmpty ? setTimeout(() => {
498
+ if (!initialized) {
499
+ initialized = true;
500
+ setLoading(false);
501
+ }
502
+ }, 5e3) : null;
503
+ const handler = (e) => {
504
+ if (e.data?.type === "ow:clear-scheduling-widget") {
505
+ setSchedule(null);
506
+ setLoading(false);
507
+ return;
508
+ }
509
+ if (e.data?.type === "ow:switch-schedule") {
510
+ switchScheduleIdRef.current = e.data.scheduleId ?? null;
511
+ initialized = false;
512
+ setLoading(true);
513
+ window.parent.postMessage({ type: "ow:request-user-schedules" }, "*");
514
+ return;
515
+ }
516
+ if (e.data?.type !== "ow:user-schedules-response") return;
517
+ if (initialized && switchScheduleIdRef.current === null) return;
518
+ initialized = true;
519
+ if (timer) clearTimeout(timer);
520
+ const schedules = e.data.schedules ?? [];
521
+ const isSwitching = switchScheduleIdRef.current !== null;
522
+ const target = isSwitching ? schedules.find((s) => s.id === switchScheduleIdRef.current) ?? schedules[0] ?? null : initialScheduleId ? schedules.find((s) => s.id === initialScheduleId) ?? schedules[0] ?? null : schedules[0] ?? null;
523
+ switchScheduleIdRef.current = null;
524
+ setSchedule(target);
525
+ setLoading(false);
526
+ if (notifyOnConnect && target && !isSwitching) {
527
+ window.parent.postMessage({ type: "ow:schedule-connected", schedule: { id: target.id, name: target.name } }, "*");
528
+ }
529
+ };
530
+ window.addEventListener("message", handler);
531
+ if (!startEmpty) window.parent.postMessage({ type: "ow:request-user-schedules" }, "*");
532
+ return () => {
533
+ window.removeEventListener("message", handler);
534
+ if (timer) clearTimeout(timer);
535
+ };
536
+ }
537
+ if (initialScheduleId === null) {
538
+ setLoading(false);
539
+ return;
540
+ }
541
+ ;
542
+ (async () => {
543
+ try {
544
+ if (initialScheduleId) {
545
+ const res = await fetch(`${API_URL}/api/schedule/id/${initialScheduleId}`);
546
+ const data = await res.json();
547
+ setSchedule(data?.id ? data : null);
548
+ } else {
549
+ const domain = getApiDomain();
550
+ const sitemapRes = await fetch(`${API_URL}/api/schedule/sitemap/${domain}`);
551
+ const slugs = await sitemapRes.json();
552
+ if (!Array.isArray(slugs) || !slugs.length) {
553
+ setLoading(false);
554
+ return;
555
+ }
556
+ const { slug } = [...slugs].sort(
557
+ (a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()
558
+ )[0];
559
+ const res = await fetch(`${API_URL}/api/schedule/${domain}/${slug}`);
560
+ const data = await res.json();
561
+ setSchedule(data?.id ? data : null);
562
+ }
563
+ } catch {
564
+ }
565
+ setLoading(false);
566
+ })();
567
+ }, []);
568
+ const timezoneLabel = schedule?.timezone ? (() => {
569
+ try {
570
+ return buildTimezoneLabel(schedule.timezone);
571
+ } catch {
572
+ return void 0;
573
+ }
574
+ })() : void 0;
575
+ const handleModalSubmit = async (email) => {
576
+ if (!modalState) return;
577
+ const { mode, classId, classDate } = modalState;
578
+ const endpoint = mode === "book" ? `${API_URL}/api/schedule/class/${classId}/booking` : `${API_URL}/api/schedule/class/${classId}/waitlist`;
579
+ const res = await fetch(endpoint, {
580
+ method: "POST",
581
+ headers: { "Content-Type": "application/json" },
582
+ body: JSON.stringify({ classDate: classDate.toISOString(), email })
583
+ });
584
+ if (!res.ok) {
585
+ const data = await res.json().catch(() => ({}));
586
+ throw new Error(data?.message ?? "Something went wrong. Please try again.");
587
+ }
588
+ };
589
+ const handleReplaceSchedule = () => {
590
+ setIsHovered(false);
591
+ if (schedule) {
592
+ window.parent.postMessage({ type: "ow:schedule-connected", schedule: { id: schedule.id, name: schedule.name } }, "*");
593
+ } else {
594
+ window.parent.postMessage({ type: "ow:scheduling-not-connected" }, "*");
595
+ }
596
+ };
597
+ return /* @__PURE__ */ jsxs2(
598
+ "section",
599
+ {
600
+ "data-ohw-section": "scheduling",
601
+ className: "w-full box-border py-20 px-16 font-body bg-(--color-light,#FEFFF0) relative",
602
+ onMouseEnter: () => inEditor && setIsHovered(true),
603
+ onMouseLeave: () => setIsHovered(false),
604
+ children: [
605
+ inEditor && isHovered && !!schedule && /* @__PURE__ */ jsxs2(
606
+ "div",
607
+ {
608
+ className: "absolute inset-0 z-10 pointer-events-auto cursor-pointer",
609
+ onClick: handleReplaceSchedule,
610
+ children: [
611
+ /* @__PURE__ */ jsx2("div", { className: "absolute inset-0", style: { background: "#0885FE", opacity: 0.18 } }),
612
+ /* @__PURE__ */ jsx2("div", { className: "absolute inset-0", style: { boxShadow: "inset 0 0 0 1.5px #0885FE" } }),
613
+ /* @__PURE__ */ jsx2("div", { className: "absolute inset-0 flex items-center justify-center pointer-events-none", children: /* @__PURE__ */ jsxs2(
614
+ "div",
615
+ {
616
+ className: "bg-white border border-[#E7E5E4] rounded-md px-3 py-1.5 flex items-center gap-1.5",
617
+ style: {
618
+ fontSize: 14,
619
+ lineHeight: "24px",
620
+ fontFamily: "'Figtree', system-ui, sans-serif",
621
+ fontWeight: 500,
622
+ color: "#0C0A09"
623
+ },
624
+ children: [
625
+ /* @__PURE__ */ jsx2(CalendarFoldIcon, {}),
626
+ "Replace schedule"
627
+ ]
628
+ }
629
+ ) })
630
+ ]
631
+ }
632
+ ),
633
+ /* @__PURE__ */ jsxs2("div", { className: "max-w-[1280px] mx-auto flex flex-col items-center gap-16", children: [
634
+ /* @__PURE__ */ jsxs2("div", { className: "flex flex-col items-center gap-4", children: [
635
+ /* @__PURE__ */ jsx2("h2", { className: "font-display text-5xl font-bold text-center m-0 text-(--color-dark,#200C02)", children: schedule?.name ?? "Book an appointment" }),
636
+ schedule?.description && /* @__PURE__ */ jsx2("p", { className: "font-body text-body text-center m-0 text-(--color-accent,#A89B83)", children: schedule.description })
637
+ ] }),
638
+ /* @__PURE__ */ jsxs2("div", { className: "w-full flex flex-col items-end gap-3", children: [
639
+ timezoneLabel && /* @__PURE__ */ jsx2("span", { className: "font-body text-sm font-normal text-(--color-accent,#A89B83)", children: timezoneLabel }),
640
+ /* @__PURE__ */ jsx2("div", { className: "w-full p-10 box-border", children: loading ? /* @__PURE__ */ jsx2(LoadingSkeleton, {}) : !schedule ? /* @__PURE__ */ jsx2(EmptyState, { inEditor }) : /* @__PURE__ */ jsx2(
641
+ ScheduleView,
642
+ {
643
+ schedule,
644
+ dates,
645
+ selectedIdx,
646
+ onSelectDate: setSelectedIdx,
647
+ onOpenModal: inEditor ? void 0 : (mode, classId, classDate) => setModalState({ mode, classId, classDate })
648
+ }
649
+ ) })
650
+ ] })
651
+ ] }),
652
+ modalState && /* @__PURE__ */ jsx2(
653
+ EmailCaptureModal,
654
+ {
655
+ title: modalState.mode === "book" ? "Confirm your spot" : "Leave your email",
656
+ subtitle: modalState.mode === "book" ? "We'll send your booking confirmation here." : "We'll let you know when spots open up!",
657
+ onSubmit: handleModalSubmit,
658
+ onClose: () => setModalState(null)
659
+ }
660
+ )
661
+ ]
662
+ }
663
+ );
664
+ }
5
665
 
6
666
  // src/ui/toggle-group.tsx
7
- import * as React from "react";
667
+ import * as React2 from "react";
8
668
 
9
669
  // node_modules/clsx/dist/clsx.mjs
10
670
  function r(e) {
@@ -3324,7 +3984,7 @@ var cva = (base, config) => (props) => {
3324
3984
 
3325
3985
  // src/ui/toggle.tsx
3326
3986
  import { Toggle as TogglePrimitive } from "radix-ui";
3327
- import { jsx } from "react/jsx-runtime";
3987
+ import { jsx as jsx3 } from "react/jsx-runtime";
3328
3988
  var toggleVariants = cva(
3329
3989
  "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,box-shadow] outline-none hover:bg-muted hover:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
3330
3990
  {
@@ -3351,7 +4011,7 @@ function Toggle({
3351
4011
  size,
3352
4012
  ...props
3353
4013
  }) {
3354
- return /* @__PURE__ */ jsx(
4014
+ return /* @__PURE__ */ jsx3(
3355
4015
  TogglePrimitive.Root,
3356
4016
  {
3357
4017
  "data-slot": "toggle",
@@ -3362,8 +4022,8 @@ function Toggle({
3362
4022
  }
3363
4023
 
3364
4024
  // src/ui/toggle-group.tsx
3365
- import { jsx as jsx2 } from "react/jsx-runtime";
3366
- var ToggleGroupContext = React.createContext({
4025
+ import { jsx as jsx4 } from "react/jsx-runtime";
4026
+ var ToggleGroupContext = React2.createContext({
3367
4027
  value: "",
3368
4028
  onValueChange: () => {
3369
4029
  }
@@ -3375,13 +4035,13 @@ function ToggleGroup({
3375
4035
  children,
3376
4036
  ...props
3377
4037
  }) {
3378
- return /* @__PURE__ */ jsx2(
4038
+ return /* @__PURE__ */ jsx4(
3379
4039
  "div",
3380
4040
  {
3381
4041
  role: "group",
3382
4042
  className: cn("flex items-center gap-1 p-1 bg-muted rounded-md", className),
3383
4043
  ...props,
3384
- children: /* @__PURE__ */ jsx2(ToggleGroupContext.Provider, { value: { value, onValueChange }, children })
4044
+ children: /* @__PURE__ */ jsx4(ToggleGroupContext.Provider, { value: { value, onValueChange }, children })
3385
4045
  }
3386
4046
  );
3387
4047
  }
@@ -3391,8 +4051,8 @@ function ToggleGroupItem({
3391
4051
  children,
3392
4052
  ...props
3393
4053
  }) {
3394
- const { value: groupValue, onValueChange } = React.useContext(ToggleGroupContext);
3395
- return /* @__PURE__ */ jsx2(
4054
+ const { value: groupValue, onValueChange } = React2.useContext(ToggleGroupContext);
4055
+ return /* @__PURE__ */ jsx4(
3396
4056
  Toggle,
3397
4057
  {
3398
4058
  pressed: groupValue === value,
@@ -3434,7 +4094,7 @@ function isEditSessionActive() {
3434
4094
  }
3435
4095
 
3436
4096
  // src/ui/badge.tsx
3437
- import { jsx as jsx3 } from "react/jsx-runtime";
4097
+ import { jsx as jsx5 } from "react/jsx-runtime";
3438
4098
  var badgeVariants = cva(
3439
4099
  "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
3440
4100
  {
@@ -3452,11 +4112,11 @@ var badgeVariants = cva(
3452
4112
  }
3453
4113
  );
3454
4114
  function Badge({ className, variant, ...props }) {
3455
- return /* @__PURE__ */ jsx3("div", { className: cn(badgeVariants({ variant }), className), ...props });
4115
+ return /* @__PURE__ */ jsx5("div", { className: cn(badgeVariants({ variant }), className), ...props });
3456
4116
  }
3457
4117
 
3458
4118
  // src/OhhwellsBridge.tsx
3459
- import { Fragment, jsx as jsx4, jsxs } from "react/jsx-runtime";
4119
+ import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
3460
4120
  var PRIMARY = "#0885FE";
3461
4121
  var IMAGE_FADE_MS = 300;
3462
4122
  function runOpacityFade(el, onDone) {
@@ -3507,6 +4167,41 @@ function fadeInBgImage(el, url, onReady) {
3507
4167
  if (!prevPos || prevPos === "static") el.style.position = prevPos;
3508
4168
  });
3509
4169
  }
4170
+ function getSectionsTracker() {
4171
+ let el = document.querySelector("[data-ohw-sections-tracker]");
4172
+ if (!el) {
4173
+ el = document.createElement("div");
4174
+ el.setAttribute("data-ohw-sections-tracker", "");
4175
+ el.style.display = "none";
4176
+ document.body.appendChild(el);
4177
+ }
4178
+ return el;
4179
+ }
4180
+ function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId) {
4181
+ const anchor = document.querySelector(`[data-ohw-section="${insertAfter}"]`);
4182
+ if (!anchor) return false;
4183
+ if (anchor.nextElementSibling?.dataset.ohwSectionContainer) return false;
4184
+ const container = document.createElement("div");
4185
+ container.dataset.ohwSectionContainer = "scheduling";
4186
+ anchor.insertAdjacentElement("afterend", container);
4187
+ createRoot(container).render(/* @__PURE__ */ jsx6(SchedulingWidget, { notifyOnConnect, initialScheduleId: scheduleId }));
4188
+ const tracker = getSectionsTracker();
4189
+ let sections = [];
4190
+ try {
4191
+ sections = JSON.parse(tracker.textContent || "[]");
4192
+ } catch {
4193
+ }
4194
+ if (!sections.find((s) => s.type === "scheduling" && s.insertAfter === insertAfter)) {
4195
+ sections.push({
4196
+ type: "scheduling",
4197
+ insertAfter,
4198
+ pagePath: window.location.pathname,
4199
+ ...scheduleId ? { scheduleId } : {}
4200
+ });
4201
+ tracker.textContent = JSON.stringify(sections);
4202
+ }
4203
+ return true;
4204
+ }
3510
4205
  function collectEditableNodes() {
3511
4206
  return Array.from(document.querySelectorAll("[data-ohw-editable]")).map((el) => {
3512
4207
  if (el.dataset.ohwEditable === "image") {
@@ -3664,7 +4359,7 @@ var TOOLBAR_GROUPS = [
3664
4359
  ];
3665
4360
  function GlowFrame({ rect, elRef }) {
3666
4361
  const GAP = 6;
3667
- return /* @__PURE__ */ jsx4(
4362
+ return /* @__PURE__ */ jsx6(
3668
4363
  "div",
3669
4364
  {
3670
4365
  ref: elRef,
@@ -3715,7 +4410,7 @@ function FloatingToolbar({
3715
4410
  activeCommands
3716
4411
  }) {
3717
4412
  const { top, left, transform } = calcToolbarPos(rect, parentScroll);
3718
- return /* @__PURE__ */ jsx4(
4413
+ return /* @__PURE__ */ jsx6(
3719
4414
  "div",
3720
4415
  {
3721
4416
  ref: elRef,
@@ -3738,11 +4433,11 @@ function FloatingToolbar({
3738
4433
  pointerEvents: "auto",
3739
4434
  whiteSpace: "nowrap"
3740
4435
  },
3741
- children: TOOLBAR_GROUPS.map((btns, gi) => /* @__PURE__ */ jsxs(React2.Fragment, { children: [
3742
- gi > 0 && /* @__PURE__ */ jsx4("span", { style: { display: "block", width: 1, height: 24, background: "#E7E5E4", flexShrink: 0 } }),
4436
+ children: TOOLBAR_GROUPS.map((btns, gi) => /* @__PURE__ */ jsxs3(React3.Fragment, { children: [
4437
+ gi > 0 && /* @__PURE__ */ jsx6("span", { style: { display: "block", width: 1, height: 24, background: "#E7E5E4", flexShrink: 0 } }),
3743
4438
  btns.map((btn) => {
3744
4439
  const isActive = activeCommands.has(btn.cmd);
3745
- return /* @__PURE__ */ jsx4(
4440
+ return /* @__PURE__ */ jsx6(
3746
4441
  "button",
3747
4442
  {
3748
4443
  title: btn.title,
@@ -3768,7 +4463,7 @@ function FloatingToolbar({
3768
4463
  flexShrink: 0,
3769
4464
  padding: 6
3770
4465
  },
3771
- children: /* @__PURE__ */ jsx4(
4466
+ children: /* @__PURE__ */ jsx6(
3772
4467
  "svg",
3773
4468
  {
3774
4469
  width: "16",
@@ -3801,7 +4496,7 @@ function StateToggle({
3801
4496
  states,
3802
4497
  onStateChange
3803
4498
  }) {
3804
- return /* @__PURE__ */ jsx4(
4499
+ return /* @__PURE__ */ jsx6(
3805
4500
  ToggleGroup,
3806
4501
  {
3807
4502
  "data-ohw-state-toggle": "",
@@ -3815,7 +4510,7 @@ function StateToggle({
3815
4510
  left: rect.right - 8,
3816
4511
  transform: "translateX(-100%)"
3817
4512
  },
3818
- children: states.map((state) => /* @__PURE__ */ jsx4(ToggleGroupItem, { value: state, size: "sm", children: state }, state))
4513
+ children: states.map((state) => /* @__PURE__ */ jsx6(ToggleGroupItem, { value: state, size: "sm", children: state }, state))
3819
4514
  }
3820
4515
  );
3821
4516
  }
@@ -3825,8 +4520,8 @@ function OhhwellsBridge() {
3825
4520
  const router = useRouter();
3826
4521
  const searchParams = useSearchParams();
3827
4522
  const isEditMode = isEditSessionActive();
3828
- const [bridgeRoot, setBridgeRoot] = useState(null);
3829
- useEffect(() => {
4523
+ const [bridgeRoot, setBridgeRoot] = useState3(null);
4524
+ useEffect2(() => {
3830
4525
  const figtreeFontId = "ohw-figtree-font";
3831
4526
  if (!document.getElementById(figtreeFontId)) {
3832
4527
  const preconnect1 = Object.assign(document.createElement("link"), { rel: "preconnect", href: "https://fonts.googleapis.com" });
@@ -3859,34 +4554,34 @@ function OhhwellsBridge() {
3859
4554
  window.parent.postMessage(data, "*");
3860
4555
  }
3861
4556
  }, []);
3862
- const [fetchState, setFetchState] = useState("idle");
3863
- const autoSaveTimers = useRef(/* @__PURE__ */ new Map());
3864
- const activeElRef = useRef(null);
3865
- const originalContentRef = useRef(null);
3866
- const activeStateElRef = useRef(null);
3867
- const parentScrollRef = useRef(null);
3868
- const toolbarElRef = useRef(null);
3869
- const glowElRef = useRef(null);
3870
- const hoveredImageRef = useRef(null);
3871
- const hoveredImageHasTextOverlapRef = useRef(false);
3872
- const hoveredGapRef = useRef(null);
3873
- const imageUnhoverTimerRef = useRef(null);
3874
- const imageShowTimerRef = useRef(null);
3875
- const editStylesRef = useRef(null);
3876
- const activateRef = useRef(() => {
4557
+ const [fetchState, setFetchState] = useState3("idle");
4558
+ const autoSaveTimers = useRef2(/* @__PURE__ */ new Map());
4559
+ const activeElRef = useRef2(null);
4560
+ const originalContentRef = useRef2(null);
4561
+ const activeStateElRef = useRef2(null);
4562
+ const parentScrollRef = useRef2(null);
4563
+ const toolbarElRef = useRef2(null);
4564
+ const glowElRef = useRef2(null);
4565
+ const hoveredImageRef = useRef2(null);
4566
+ const hoveredImageHasTextOverlapRef = useRef2(false);
4567
+ const hoveredGapRef = useRef2(null);
4568
+ const imageUnhoverTimerRef = useRef2(null);
4569
+ const imageShowTimerRef = useRef2(null);
4570
+ const editStylesRef = useRef2(null);
4571
+ const activateRef = useRef2(() => {
3877
4572
  });
3878
- const deactivateRef = useRef(() => {
4573
+ const deactivateRef = useRef2(() => {
3879
4574
  });
3880
- const refreshActiveCommandsRef = useRef(() => {
4575
+ const refreshActiveCommandsRef = useRef2(() => {
3881
4576
  });
3882
- const postToParentRef = useRef(postToParent);
4577
+ const postToParentRef = useRef2(postToParent);
3883
4578
  postToParentRef.current = postToParent;
3884
- const [toolbarRect, setToolbarRect] = useState(null);
3885
- const [toggleState, setToggleState] = useState(null);
3886
- const [maxBadge, setMaxBadge] = useState(null);
3887
- const [activeCommands, setActiveCommands] = useState(/* @__PURE__ */ new Set());
3888
- const [sectionGap, setSectionGap] = useState(null);
3889
- useEffect(() => {
4579
+ const [toolbarRect, setToolbarRect] = useState3(null);
4580
+ const [toggleState, setToggleState] = useState3(null);
4581
+ const [maxBadge, setMaxBadge] = useState3(null);
4582
+ const [activeCommands, setActiveCommands] = useState3(/* @__PURE__ */ new Set());
4583
+ const [sectionGap, setSectionGap] = useState3(null);
4584
+ useEffect2(() => {
3890
4585
  const update = () => {
3891
4586
  const el = activeElRef.current;
3892
4587
  if (el) setToolbarRect(el.getBoundingClientRect());
@@ -3963,7 +4658,20 @@ function OhhwellsBridge() {
3963
4658
  }
3964
4659
  const applyContent = (content) => {
3965
4660
  const imageLoads = [];
4661
+ document.querySelectorAll("[data-ohw-section-container]").forEach((el) => el.remove());
4662
+ if (content["__ohw_sections"]) {
4663
+ try {
4664
+ const entries = JSON.parse(content["__ohw_sections"]);
4665
+ const currentPath = window.location.pathname;
4666
+ entries.forEach(({ type, insertAfter, scheduleId, pagePath }) => {
4667
+ if (pagePath && pagePath !== currentPath) return;
4668
+ if (type === "scheduling") mountSchedulingWidget(insertAfter, false, scheduleId);
4669
+ });
4670
+ } catch {
4671
+ }
4672
+ }
3966
4673
  for (const [key, val] of Object.entries(content)) {
4674
+ if (key === "__ohw_sections") continue;
3967
4675
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
3968
4676
  if (el.dataset.ohwEditable === "image") {
3969
4677
  const img = el instanceof HTMLImageElement ? el : el.querySelector("img");
@@ -4006,12 +4714,24 @@ function OhhwellsBridge() {
4006
4714
  cancelled = true;
4007
4715
  };
4008
4716
  }, [subdomain, isEditMode, pathname]);
4009
- useEffect(() => {
4717
+ useEffect2(() => {
4010
4718
  if (!subdomain || isEditMode) return;
4011
4719
  const applyFromCache = () => {
4012
4720
  const content = contentCache.get(subdomain);
4013
4721
  if (!content) return;
4722
+ if (content["__ohw_sections"]) {
4723
+ try {
4724
+ const entries = JSON.parse(content["__ohw_sections"]);
4725
+ const currentPath = window.location.pathname;
4726
+ entries.forEach(({ type, insertAfter, scheduleId, pagePath }) => {
4727
+ if (pagePath && pagePath !== currentPath) return;
4728
+ if (type === "scheduling") mountSchedulingWidget(insertAfter, false, scheduleId);
4729
+ });
4730
+ } catch {
4731
+ }
4732
+ }
4014
4733
  for (const [key, val] of Object.entries(content)) {
4734
+ if (key === "__ohw_sections") continue;
4015
4735
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
4016
4736
  if (el.dataset.ohwEditable === "image") {
4017
4737
  const img = el instanceof HTMLImageElement ? el : el.querySelector("img");
@@ -4036,10 +4756,10 @@ function OhhwellsBridge() {
4036
4756
  const visible = Boolean(subdomain) && fetchState !== "done";
4037
4757
  el.style.display = visible ? "flex" : "none";
4038
4758
  }, [subdomain, fetchState]);
4039
- useEffect(() => {
4759
+ useEffect2(() => {
4040
4760
  postToParent({ type: "ow:navigation", path: pathname });
4041
4761
  }, [pathname, postToParent]);
4042
- useEffect(() => {
4762
+ useEffect2(() => {
4043
4763
  if (!isEditMode) return;
4044
4764
  const measure = () => {
4045
4765
  const h = document.body.scrollHeight;
@@ -4063,7 +4783,7 @@ function OhhwellsBridge() {
4063
4783
  window.removeEventListener("resize", handleResize);
4064
4784
  };
4065
4785
  }, [pathname, isEditMode, postToParent]);
4066
- useEffect(() => {
4786
+ useEffect2(() => {
4067
4787
  if (!subdomainFromQuery || isEditMode) return;
4068
4788
  const handleClick = (e) => {
4069
4789
  const anchor = e.target.closest("a");
@@ -4079,7 +4799,7 @@ function OhhwellsBridge() {
4079
4799
  document.addEventListener("click", handleClick, true);
4080
4800
  return () => document.removeEventListener("click", handleClick, true);
4081
4801
  }, [subdomainFromQuery, isEditMode, router]);
4082
- useEffect(() => {
4802
+ useEffect2(() => {
4083
4803
  if (!isEditMode) {
4084
4804
  editStylesRef.current?.base.remove();
4085
4805
  editStylesRef.current?.forceHover.remove();
@@ -4339,6 +5059,15 @@ function OhhwellsBridge() {
4339
5059
  textEditable.setAttribute("data-ohw-hovered", "");
4340
5060
  return;
4341
5061
  }
5062
+ if (hoveredGapRef.current) {
5063
+ if (hoveredImageRef.current) {
5064
+ hoveredImageRef.current = null;
5065
+ resumeAnimTracks();
5066
+ postToParentRef.current({ type: "ow:image-unhover" });
5067
+ }
5068
+ document.querySelectorAll("[data-ohw-hovered]").forEach((el) => el.removeAttribute("data-ohw-hovered"));
5069
+ return;
5070
+ }
4342
5071
  document.querySelectorAll("[data-ohw-hovered]").forEach((el) => el.removeAttribute("data-ohw-hovered"));
4343
5072
  if (imgEl !== hoveredImageRef.current || hoveredImageHasTextOverlapRef.current) {
4344
5073
  hoveredImageRef.current = imgEl;
@@ -4427,17 +5156,17 @@ function OhhwellsBridge() {
4427
5156
  };
4428
5157
  const handleMouseMove = (e) => {
4429
5158
  const { clientX, clientY } = e;
5159
+ probeSectionGapAt(clientX, clientY);
4430
5160
  probeImageAt(clientX, clientY);
4431
5161
  probeHoverCardsAt(clientX, clientY);
4432
- probeSectionGapAt(clientX, clientY);
4433
5162
  };
4434
5163
  const handlePointerSync = (e) => {
4435
5164
  if (e.data?.type !== "ow:pointer-sync") return;
4436
5165
  const { clientX, clientY } = e.data;
4437
5166
  if (typeof clientX !== "number" || typeof clientY !== "number") return;
5167
+ probeSectionGapAt(clientX, clientY);
4438
5168
  probeImageAt(clientX, clientY);
4439
5169
  probeHoverCardsAt(clientX, clientY);
4440
- probeSectionGapAt(clientX, clientY);
4441
5170
  };
4442
5171
  const handleDragOver = (e) => {
4443
5172
  e.preventDefault();
@@ -4594,6 +5323,18 @@ function OhhwellsBridge() {
4594
5323
  const content = e.data.content;
4595
5324
  if (!content) return;
4596
5325
  for (const [key, val] of Object.entries(content)) {
5326
+ if (key === "__ohw_sections") {
5327
+ try {
5328
+ const entries = JSON.parse(val);
5329
+ const tracker = getSectionsTracker();
5330
+ tracker.textContent = val;
5331
+ entries.forEach(({ type, insertAfter, scheduleId }) => {
5332
+ if (type === "scheduling") mountSchedulingWidget(insertAfter, false, scheduleId);
5333
+ });
5334
+ } catch {
5335
+ }
5336
+ continue;
5337
+ }
4597
5338
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
4598
5339
  if (el.dataset.ohwEditable === "image") {
4599
5340
  const img = el instanceof HTMLImageElement ? el : el.querySelector("img");
@@ -4648,7 +5389,71 @@ function OhhwellsBridge() {
4648
5389
  };
4649
5390
  const handleSave = (e) => {
4650
5391
  if (e.data?.type !== "ow:save") return;
4651
- postToParentRef.current({ type: "ow:save-result", nodes: collectEditableNodes() });
5392
+ const nodes = collectEditableNodes();
5393
+ const tracker = document.querySelector("[data-ohw-sections-tracker]");
5394
+ if (tracker?.textContent) nodes.push({ key: "__ohw_sections", type: "sections", text: tracker.textContent });
5395
+ postToParentRef.current({ type: "ow:save-result", nodes });
5396
+ };
5397
+ const handleInsertSection = (e) => {
5398
+ if (e.data?.type !== "ow:insert-section") return;
5399
+ const { widgetType, insertAfter } = e.data;
5400
+ if (widgetType !== "scheduling") return;
5401
+ const inserted = mountSchedulingWidget(insertAfter, true);
5402
+ if (inserted) {
5403
+ const tracker = getSectionsTracker();
5404
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: "__ohw_sections", text: tracker.textContent ?? "[]" }] });
5405
+ const h = document.documentElement.scrollHeight;
5406
+ if (h > 50) postToParentRef.current({ type: "ow:height", height: h });
5407
+ }
5408
+ };
5409
+ const handleSwitchSchedule = (e) => {
5410
+ if (e.data?.type !== "ow:switch-schedule") return;
5411
+ const scheduleId = e.data.scheduleId;
5412
+ if (!scheduleId) return;
5413
+ const tracker = getSectionsTracker();
5414
+ let sections = [];
5415
+ try {
5416
+ sections = JSON.parse(tracker.textContent || "[]");
5417
+ } catch {
5418
+ }
5419
+ const currentPath = window.location.pathname;
5420
+ const updated = sections.map(
5421
+ (s) => s.type === "scheduling" && s.pagePath === currentPath ? { ...s, scheduleId } : s
5422
+ );
5423
+ tracker.textContent = JSON.stringify(updated);
5424
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: "__ohw_sections", text: tracker.textContent }] });
5425
+ };
5426
+ const handleClearSchedulingWidget = (e) => {
5427
+ if (e.data?.type !== "ow:clear-scheduling-widget") return;
5428
+ const tracker = getSectionsTracker();
5429
+ let sections = [];
5430
+ try {
5431
+ sections = JSON.parse(tracker.textContent || "[]");
5432
+ } catch {
5433
+ }
5434
+ const currentPath = window.location.pathname;
5435
+ const updated = sections.map(
5436
+ (s) => s.type === "scheduling" && s.pagePath === currentPath ? { ...s, scheduleId: null } : s
5437
+ );
5438
+ tracker.textContent = JSON.stringify(updated);
5439
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: "__ohw_sections", text: tracker.textContent }] });
5440
+ };
5441
+ const handleRemoveSchedulingSection = (e) => {
5442
+ if (e.data?.type !== "ow:remove-scheduling-section") return;
5443
+ const els = document.querySelectorAll('[data-ohw-section="scheduling"]');
5444
+ els.forEach((el) => el.parentElement?.removeChild(el));
5445
+ const tracker = getSectionsTracker();
5446
+ let sections = [];
5447
+ try {
5448
+ sections = JSON.parse(tracker.textContent || "[]");
5449
+ } catch {
5450
+ }
5451
+ const currentPath = window.location.pathname;
5452
+ const updated = sections.filter((s) => !(s.type === "scheduling" && s.pagePath === currentPath));
5453
+ tracker.textContent = JSON.stringify(updated);
5454
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: "__ohw_sections", text: tracker.textContent }] });
5455
+ const h = document.documentElement.scrollHeight;
5456
+ if (h > 50) postToParentRef.current({ type: "ow:height", height: h });
4652
5457
  };
4653
5458
  const handleSelectionChange = () => {
4654
5459
  if (!activeElRef.current) return;
@@ -4753,6 +5558,10 @@ function OhhwellsBridge() {
4753
5558
  deactivateRef.current();
4754
5559
  };
4755
5560
  window.addEventListener("message", handleSave);
5561
+ window.addEventListener("message", handleInsertSection);
5562
+ window.addEventListener("message", handleSwitchSchedule);
5563
+ window.addEventListener("message", handleClearSchedulingWidget);
5564
+ window.addEventListener("message", handleRemoveSchedulingSection);
4756
5565
  window.addEventListener("message", handleImageUrl);
4757
5566
  window.addEventListener("message", handleAnimLock);
4758
5567
  window.addEventListener("message", handleCanvasHeight);
@@ -4787,6 +5596,10 @@ function OhhwellsBridge() {
4787
5596
  document.removeEventListener("selectionchange", handleSelectionChange);
4788
5597
  window.removeEventListener("scroll", handleScroll, true);
4789
5598
  window.removeEventListener("message", handleSave);
5599
+ window.removeEventListener("message", handleInsertSection);
5600
+ window.removeEventListener("message", handleSwitchSchedule);
5601
+ window.removeEventListener("message", handleClearSchedulingWidget);
5602
+ window.removeEventListener("message", handleRemoveSchedulingSection);
4790
5603
  window.removeEventListener("message", handleImageUrl);
4791
5604
  window.removeEventListener("message", handleAnimLock);
4792
5605
  window.removeEventListener("message", handleCanvasHeight);
@@ -4801,7 +5614,7 @@ function OhhwellsBridge() {
4801
5614
  if (imageShowTimerRef.current) clearTimeout(imageShowTimerRef.current);
4802
5615
  };
4803
5616
  }, [isEditMode, refreshStateRules]);
4804
- useEffect(() => {
5617
+ useEffect2(() => {
4805
5618
  if (!isEditMode) return;
4806
5619
  document.querySelectorAll("[data-ohw-active-state]").forEach((el) => {
4807
5620
  el.removeAttribute("data-ohw-active-state");
@@ -4843,12 +5656,12 @@ function OhhwellsBridge() {
4843
5656
  setToggleState((prev) => prev ? { ...prev, activeState: state } : null);
4844
5657
  }, [deactivate]);
4845
5658
  return bridgeRoot ? createPortal(
4846
- /* @__PURE__ */ jsxs(Fragment, { children: [
4847
- toolbarRect && /* @__PURE__ */ jsxs(Fragment, { children: [
4848
- /* @__PURE__ */ jsx4(GlowFrame, { rect: toolbarRect, elRef: glowElRef }),
4849
- /* @__PURE__ */ jsx4(FloatingToolbar, { rect: toolbarRect, parentScroll: parentScrollRef.current, elRef: toolbarElRef, onCommand: handleCommand, activeCommands })
5659
+ /* @__PURE__ */ jsxs3(Fragment2, { children: [
5660
+ toolbarRect && /* @__PURE__ */ jsxs3(Fragment2, { children: [
5661
+ /* @__PURE__ */ jsx6(GlowFrame, { rect: toolbarRect, elRef: glowElRef }),
5662
+ /* @__PURE__ */ jsx6(FloatingToolbar, { rect: toolbarRect, parentScroll: parentScrollRef.current, elRef: toolbarElRef, onCommand: handleCommand, activeCommands })
4850
5663
  ] }),
4851
- maxBadge && /* @__PURE__ */ jsxs(
5664
+ maxBadge && /* @__PURE__ */ jsxs3(
4852
5665
  "div",
4853
5666
  {
4854
5667
  "data-ohw-max-badge": "",
@@ -4874,7 +5687,7 @@ function OhhwellsBridge() {
4874
5687
  ]
4875
5688
  }
4876
5689
  ),
4877
- toggleState && /* @__PURE__ */ jsx4(
5690
+ toggleState && /* @__PURE__ */ jsx6(
4878
5691
  StateToggle,
4879
5692
  {
4880
5693
  rect: toggleState.rect,
@@ -4883,15 +5696,15 @@ function OhhwellsBridge() {
4883
5696
  onStateChange: handleStateChange
4884
5697
  }
4885
5698
  ),
4886
- sectionGap && /* @__PURE__ */ jsxs(
5699
+ sectionGap && /* @__PURE__ */ jsxs3(
4887
5700
  "div",
4888
5701
  {
4889
5702
  "data-ohw-section-insert-line": "",
4890
5703
  className: "fixed left-0 w-full z-2147483646 flex items-center pointer-events-none",
4891
5704
  style: { top: sectionGap.y, transform: "translateY(-50%)" },
4892
5705
  children: [
4893
- /* @__PURE__ */ jsx4("div", { className: "flex-1 bg-primary", style: { height: 3 } }),
4894
- /* @__PURE__ */ jsx4(
5706
+ /* @__PURE__ */ jsx6("div", { className: "flex-1 bg-primary", style: { height: 3 } }),
5707
+ /* @__PURE__ */ jsx6(
4895
5708
  Badge,
4896
5709
  {
4897
5710
  className: "px-8 py-1 bg-primary hover:bg-primary text-primary-foreground text-xs font-medium shrink-0 rounded-full cursor-pointer pointer-events-auto",
@@ -4904,7 +5717,7 @@ function OhhwellsBridge() {
4904
5717
  children: "Add Section"
4905
5718
  }
4906
5719
  ),
4907
- /* @__PURE__ */ jsx4("div", { className: "flex-1 bg-primary", style: { height: 3 } })
5720
+ /* @__PURE__ */ jsx6("div", { className: "flex-1 bg-primary", style: { height: 3 } })
4908
5721
  ]
4909
5722
  }
4910
5723
  )
@@ -4914,6 +5727,7 @@ function OhhwellsBridge() {
4914
5727
  }
4915
5728
  export {
4916
5729
  OhhwellsBridge,
5730
+ SchedulingWidget,
4917
5731
  Toggle,
4918
5732
  ToggleGroup,
4919
5733
  ToggleGroupItem,