@ohhwells/bridge 0.1.15 → 0.1.16-next.12

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,752 @@
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
+ import { flushSync } from "react-dom";
7
+
8
+ // src/ui/SchedulingWidget.tsx
9
+ import { useEffect, useMemo, useRef, useState as useState2 } from "react";
10
+
11
+ // src/ui/EmailCaptureModal.tsx
12
+ import { useState } from "react";
13
+ import { Dialog } from "radix-ui";
14
+ import { jsx, jsxs } from "react/jsx-runtime";
15
+ function Spinner() {
16
+ return /* @__PURE__ */ jsxs(
17
+ "svg",
18
+ {
19
+ width: "32",
20
+ height: "32",
21
+ viewBox: "0 0 32 32",
22
+ fill: "none",
23
+ style: { animation: "ohw-spin 0.75s linear infinite" },
24
+ children: [
25
+ /* @__PURE__ */ jsx("style", { children: `@keyframes ohw-spin { to { transform: rotate(360deg); } }` }),
26
+ /* @__PURE__ */ jsx("circle", { cx: "16", cy: "16", r: "12", stroke: "#e5e7eb", strokeWidth: "3" }),
27
+ /* @__PURE__ */ jsx(
28
+ "path",
29
+ {
30
+ d: "M16 4a12 12 0 0 1 12 12",
31
+ stroke: "#3b82f6",
32
+ strokeWidth: "3",
33
+ strokeLinecap: "round"
34
+ }
35
+ )
36
+ ]
37
+ }
38
+ );
39
+ }
40
+ function EmailCaptureModal({ title, subtitle, onSubmit, onClose }) {
41
+ const [email, setEmail] = useState("");
42
+ const [submittedEmail, setSubmittedEmail] = useState("");
43
+ const [loading, setLoading] = useState(false);
44
+ const [success, setSuccess] = useState(false);
45
+ const [error, setError] = useState(null);
46
+ const isBook = title === "Confirm your spot";
47
+ const handleSubmit = async () => {
48
+ if (!email.trim()) return;
49
+ setLoading(true);
50
+ setError(null);
51
+ try {
52
+ await onSubmit(email.trim());
53
+ setSubmittedEmail(email.trim());
54
+ setSuccess(true);
55
+ } catch (e) {
56
+ setError(e instanceof Error ? e.message : "Something went wrong. Please try again.");
57
+ } finally {
58
+ setLoading(false);
59
+ }
60
+ };
61
+ const handleKeyDown = (e) => {
62
+ if (e.key === "Enter" && !loading) handleSubmit();
63
+ };
64
+ return /* @__PURE__ */ jsx(Dialog.Root, { open: true, onOpenChange: (open) => {
65
+ if (!open) onClose();
66
+ }, children: /* @__PURE__ */ jsxs(Dialog.Portal, { children: [
67
+ /* @__PURE__ */ jsx(
68
+ Dialog.Overlay,
69
+ {
70
+ className: "fixed inset-0 z-50",
71
+ style: { background: "rgba(0,0,0,0.45)" }
72
+ }
73
+ ),
74
+ /* @__PURE__ */ jsxs(
75
+ Dialog.Content,
76
+ {
77
+ 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",
78
+ style: { maxWidth: 400 },
79
+ children: [
80
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-3 p-6", children: [
81
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
82
+ /* @__PURE__ */ jsx(
83
+ Dialog.Title,
84
+ {
85
+ className: "font-body text-xl font-bold m-0 leading-tight",
86
+ style: { color: "var(--color-dark,#200C02)" },
87
+ children: success ? isBook ? "Reservation confirmed!" : "You're on the waitlist!" : title
88
+ }
89
+ ),
90
+ /* @__PURE__ */ jsx(
91
+ Dialog.Description,
92
+ {
93
+ className: "font-body text-sm m-0",
94
+ style: { color: "#6B7280" },
95
+ children: success ? isBook ? `A confirmation email has been sent to ${submittedEmail}.` : "We'll let you know via email if someone cancels." : subtitle
96
+ }
97
+ )
98
+ ] }),
99
+ /* @__PURE__ */ jsx(
100
+ Dialog.Close,
101
+ {
102
+ 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",
103
+ style: { color: "#6B7280" },
104
+ "aria-label": "Close",
105
+ children: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
106
+ /* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
107
+ /* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
108
+ ] })
109
+ }
110
+ )
111
+ ] }),
112
+ !success && (loading ? (
113
+ /* Loading state — full body replaced */
114
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center gap-3 px-7 py-12", children: [
115
+ /* @__PURE__ */ jsx(Spinner, {}),
116
+ /* @__PURE__ */ jsx("p", { className: "font-body text-sm m-0", style: { color: "#6B7280" }, children: "Please wait" })
117
+ ] })
118
+ ) : (
119
+ /* Email form */
120
+ /* @__PURE__ */ jsxs("div", { className: "px-7 pt-2 pb-6", children: [
121
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5 mb-8", children: [
122
+ /* @__PURE__ */ jsx("label", { className: "font-body text-sm font-medium", style: { color: "var(--color-dark,#200C02)" }, children: "Email" }),
123
+ /* @__PURE__ */ jsx(
124
+ "input",
125
+ {
126
+ type: "email",
127
+ value: email,
128
+ onChange: (e) => setEmail(e.target.value),
129
+ onKeyDown: handleKeyDown,
130
+ placeholder: "hello@gmail.com",
131
+ autoFocus: true,
132
+ className: "w-full h-10 px-3 rounded-lg border font-body text-sm outline-none box-border",
133
+ style: {
134
+ borderColor: "#E7E5E4",
135
+ background: "#fff",
136
+ color: "var(--color-dark,#200C02)"
137
+ }
138
+ }
139
+ ),
140
+ error && /* @__PURE__ */ jsx("p", { className: "font-body text-xs text-red-500 m-0", children: error })
141
+ ] }),
142
+ /* @__PURE__ */ jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx(
143
+ "button",
144
+ {
145
+ onClick: handleSubmit,
146
+ disabled: !email.trim(),
147
+ className: "h-9 px-5 rounded-lg font-body text-sm font-semibold cursor-pointer border-none disabled:opacity-50 disabled:cursor-not-allowed",
148
+ style: {
149
+ background: "var(--color-primary,#3D312B)",
150
+ color: "var(--color-light,#FEFFF0)"
151
+ },
152
+ children: "Send"
153
+ }
154
+ ) })
155
+ ] })
156
+ ))
157
+ ]
158
+ }
159
+ )
160
+ ] }) });
161
+ }
162
+
163
+ // src/ui/SchedulingWidget.tsx
164
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
165
+ var API_URL = process.env.NEXT_PUBLIC_FLOWOPS_API_URL ?? "https://flowops-backend-staging-2yfdh7pwpq-as.a.run.app";
166
+ var DAY_ABBR = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"];
167
+ function getApiDomain() {
168
+ const h = window.location.hostname;
169
+ for (const base of ["ohhwells.site", "theflowops-staging.com", "theflowops.com"]) {
170
+ if (h.endsWith(`.${base}`)) return h.slice(0, -(base.length + 1));
171
+ }
172
+ return h;
173
+ }
174
+ function classRunsOnDate(cls, date, type) {
175
+ if (type === "FIXED") {
176
+ const d = String(date.getDate()).padStart(2, "0");
177
+ const m = String(date.getMonth() + 1).padStart(2, "0");
178
+ return cls.fixedDates?.some((fd) => fd.date === `${d}-${m}-${date.getFullYear()}`) ?? false;
179
+ }
180
+ return cls.weekdays?.some((w) => w.weekday === date.getDay()) ?? false;
181
+ }
182
+ function formatClassTime(cls) {
183
+ let h = parseInt(cls.startTime.hour, 10);
184
+ const m = parseInt(cls.startTime.minutes, 10);
185
+ if (cls.startTime.time === "PM" && h !== 12) h += 12;
186
+ if (cls.startTime.time === "AM" && h === 12) h = 0;
187
+ const endTotal = h * 60 + m + cls.duration;
188
+ const eh = Math.floor(endTotal / 60) % 24;
189
+ const em = endTotal % 60;
190
+ return `${h}:${cls.startTime.minutes}-${eh}:${String(em).padStart(2, "0")}`;
191
+ }
192
+ function getBookingsOnDate(cls, date) {
193
+ if (!cls.bookings?.length) return 0;
194
+ const ds = date.toISOString().split("T")[0];
195
+ return cls.bookings.filter((b) => {
196
+ try {
197
+ return new Date(b.classDate).toISOString().split("T")[0] === ds;
198
+ } catch {
199
+ return false;
200
+ }
201
+ }).length;
202
+ }
203
+ function buildTimezoneLabel(tz) {
204
+ try {
205
+ const now = /* @__PURE__ */ new Date();
206
+ const timeStr = new Intl.DateTimeFormat("en-US", {
207
+ hour: "2-digit",
208
+ minute: "2-digit",
209
+ hour12: false,
210
+ timeZone: tz
211
+ }).format(now);
212
+ const offsetPart = new Intl.DateTimeFormat("en-US", { timeZoneName: "short", timeZone: tz }).formatToParts(now).find((p) => p.type === "timeZoneName")?.value ?? "";
213
+ const city = tz.split("/").pop()?.replace(/_/g, " ") ?? tz;
214
+ return `${timeStr} (${offsetPart}) ${city} time`;
215
+ } catch {
216
+ return tz;
217
+ }
218
+ }
219
+ function EmptyState({ inEditor }) {
220
+ const handleAddSchedule = () => {
221
+ if (inEditor) {
222
+ window.parent.postMessage({ type: "ow:scheduling-not-connected" }, "*");
223
+ }
224
+ };
225
+ 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: [
226
+ /* @__PURE__ */ jsx2("div", { className: "w-10 h-10 bg-[#F5F5F4] rounded-[10px] flex items-center justify-center shrink-0", children: /* @__PURE__ */ jsxs2(
227
+ "svg",
228
+ {
229
+ width: "20",
230
+ height: "20",
231
+ viewBox: "0 0 24 24",
232
+ fill: "none",
233
+ stroke: "var(--color-accent, #A89B83)",
234
+ strokeWidth: "1.5",
235
+ strokeLinecap: "round",
236
+ strokeLinejoin: "round",
237
+ children: [
238
+ /* @__PURE__ */ jsx2("rect", { x: "3", y: "4", width: "18", height: "18", rx: "2" }),
239
+ /* @__PURE__ */ jsx2("line", { x1: "16", y1: "2", x2: "16", y2: "6" }),
240
+ /* @__PURE__ */ jsx2("line", { x1: "8", y1: "2", x2: "8", y2: "6" }),
241
+ /* @__PURE__ */ jsx2("line", { x1: "3", y1: "10", x2: "21", y2: "10" }),
242
+ /* @__PURE__ */ jsx2("line", { x1: "12", y1: "14", x2: "12", y2: "18" }),
243
+ /* @__PURE__ */ jsx2("line", { x1: "10", y1: "16", x2: "14", y2: "16" })
244
+ ]
245
+ }
246
+ ) }),
247
+ /* @__PURE__ */ jsxs2("div", { className: "flex flex-col items-center gap-2", children: [
248
+ /* @__PURE__ */ jsx2("p", { className: "font-body text-lg font-medium text-center text-[#0A0A0A] m-0", children: "No schedule yet" }),
249
+ /* @__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." })
250
+ ] }),
251
+ /* @__PURE__ */ jsx2(
252
+ "button",
253
+ {
254
+ onClick: handleAddSchedule,
255
+ 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",
256
+ children: "Add Schedule"
257
+ }
258
+ )
259
+ ] });
260
+ }
261
+ function LoadingSkeleton() {
262
+ const shimmer = {
263
+ background: "linear-gradient(90deg,#f0f0f0 25%,#e8e8e8 50%,#f0f0f0 75%)",
264
+ backgroundSize: "200% 100%",
265
+ animation: "ohw-shimmer 1.5s infinite"
266
+ };
267
+ return /* @__PURE__ */ jsxs2("div", { className: "w-full flex flex-col gap-10", children: [
268
+ /* @__PURE__ */ jsx2("style", { children: `@keyframes ohw-shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}` }),
269
+ /* @__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: [
270
+ /* @__PURE__ */ jsx2("div", { style: { ...shimmer, width: "32px", height: "14px", borderRadius: "4px" } }),
271
+ /* @__PURE__ */ jsx2("div", { style: { ...shimmer, width: "40px", height: "40px", borderRadius: "50%" } })
272
+ ] }, i)) }),
273
+ [0, 1, 2].map((i) => /* @__PURE__ */ jsxs2("div", { children: [
274
+ /* @__PURE__ */ jsxs2("div", { className: "flex gap-4 py-4 sm:hidden", children: [
275
+ /* @__PURE__ */ jsxs2("div", { className: "flex flex-col gap-2 shrink-0", children: [
276
+ /* @__PURE__ */ jsx2("div", { style: { ...shimmer, width: "100px", height: "16px", borderRadius: "4px" } }),
277
+ /* @__PURE__ */ jsx2("div", { style: { ...shimmer, width: "52px", height: "20px", borderRadius: "999px" } }),
278
+ /* @__PURE__ */ jsx2("div", { style: { ...shimmer, width: "60px", height: "14px", borderRadius: "4px" } })
279
+ ] }),
280
+ /* @__PURE__ */ jsxs2("div", { className: "flex flex-1 flex-col gap-2", children: [
281
+ /* @__PURE__ */ jsx2("div", { style: { ...shimmer, width: "75%", height: "16px", borderRadius: "4px" } }),
282
+ /* @__PURE__ */ jsx2("div", { style: { ...shimmer, width: "50%", height: "14px", borderRadius: "4px" } }),
283
+ /* @__PURE__ */ jsx2("div", { style: { ...shimmer, width: "100%", height: "44px", borderRadius: "8px" } })
284
+ ] })
285
+ ] }),
286
+ /* @__PURE__ */ jsxs2("div", { className: "hidden sm:flex items-center gap-[60px] py-4", children: [
287
+ /* @__PURE__ */ jsx2("div", { style: { ...shimmer, width: "120px", height: "16px", borderRadius: "4px", flexShrink: 0 } }),
288
+ /* @__PURE__ */ jsxs2("div", { className: "flex-1 flex flex-col gap-2", children: [
289
+ /* @__PURE__ */ jsx2("div", { style: { ...shimmer, width: "55%", height: "16px", borderRadius: "4px" } }),
290
+ /* @__PURE__ */ jsx2("div", { style: { ...shimmer, width: "38%", height: "14px", borderRadius: "4px" } })
291
+ ] }),
292
+ /* @__PURE__ */ jsx2("div", { style: { ...shimmer, width: "56px", height: "32px", borderRadius: "4px", flexShrink: 0 } }),
293
+ /* @__PURE__ */ jsx2("div", { style: { ...shimmer, width: "200px", height: "44px", borderRadius: "8px", flexShrink: 0 } })
294
+ ] }),
295
+ i < 2 && /* @__PURE__ */ jsx2("div", { className: "h-px bg-black/20" })
296
+ ] }, i))
297
+ ] });
298
+ }
299
+ function ScheduleView({ schedule, dates, selectedIdx, onSelectDate, onOpenModal }) {
300
+ const todayMs = useMemo(() => {
301
+ const d = /* @__PURE__ */ new Date();
302
+ d.setHours(0, 0, 0, 0);
303
+ return d.getTime();
304
+ }, []);
305
+ const scrollRef = useRef(null);
306
+ const [canScrollLeft, setCanScrollLeft] = useState2(false);
307
+ const [canScrollRight, setCanScrollRight] = useState2(false);
308
+ const updateScrollState = () => {
309
+ const el = scrollRef.current;
310
+ if (!el) return;
311
+ setCanScrollLeft(el.scrollLeft > 0);
312
+ setCanScrollRight(el.scrollLeft + el.clientWidth < el.scrollWidth - 1);
313
+ };
314
+ useEffect(() => {
315
+ const el = scrollRef.current;
316
+ if (!el) return;
317
+ updateScrollState();
318
+ el.addEventListener("scroll", updateScrollState);
319
+ const ro = new ResizeObserver(updateScrollState);
320
+ ro.observe(el);
321
+ return () => {
322
+ el.removeEventListener("scroll", updateScrollState);
323
+ ro.disconnect();
324
+ };
325
+ }, [dates]);
326
+ const scroll = (dir) => {
327
+ scrollRef.current?.scrollBy({ left: dir === "left" ? -216 : 216, behavior: "smooth" });
328
+ };
329
+ const isDragging = useRef(false);
330
+ const dragStartX = useRef(0);
331
+ const dragStartScrollLeft = useRef(0);
332
+ const onDragStart = (e) => {
333
+ if (!scrollRef.current) return;
334
+ isDragging.current = true;
335
+ dragStartX.current = e.clientX;
336
+ dragStartScrollLeft.current = scrollRef.current.scrollLeft;
337
+ scrollRef.current.style.cursor = "grabbing";
338
+ scrollRef.current.style.userSelect = "none";
339
+ };
340
+ const onDragMove = (e) => {
341
+ if (!isDragging.current || !scrollRef.current) return;
342
+ const delta = e.clientX - dragStartX.current;
343
+ scrollRef.current.scrollLeft = dragStartScrollLeft.current - delta;
344
+ };
345
+ const onDragEnd = () => {
346
+ if (!scrollRef.current) return;
347
+ isDragging.current = false;
348
+ scrollRef.current.style.cursor = "grab";
349
+ scrollRef.current.style.userSelect = "";
350
+ };
351
+ const datesWithClasses = useMemo(
352
+ () => new Set(dates.map(
353
+ (d, i) => schedule.classes?.some((c) => classRunsOnDate(c, d, schedule.type)) ? i : -1
354
+ ).filter((i) => i >= 0)),
355
+ [dates, schedule]
356
+ );
357
+ const selectedDate = dates[selectedIdx];
358
+ const selectedClasses = useMemo(
359
+ () => (schedule.classes ?? []).filter((c) => selectedDate && classRunsOnDate(c, selectedDate, schedule.type)),
360
+ [schedule, selectedDate]
361
+ );
362
+ return /* @__PURE__ */ jsxs2("div", { className: "w-full flex flex-col gap-10", children: [
363
+ /* @__PURE__ */ jsxs2("div", { className: "relative w-full", children: [
364
+ canScrollLeft && /* @__PURE__ */ jsx2(
365
+ "button",
366
+ {
367
+ onClick: () => scroll("left"),
368
+ className: "hidden sm:flex absolute left-0 top-1/2 -translate-y-1/2 z-10 w-8 h-8 items-center justify-center rounded-full border-none cursor-pointer",
369
+ style: { background: "var(--color-light,#FEFFF0)", boxShadow: "0 1px 4px rgba(0,0,0,0.15)" },
370
+ "aria-label": "Scroll left",
371
+ 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" }) })
372
+ }
373
+ ),
374
+ canScrollRight && /* @__PURE__ */ jsx2(
375
+ "button",
376
+ {
377
+ onClick: () => scroll("right"),
378
+ className: "hidden sm:flex absolute right-0 top-1/2 -translate-y-1/2 z-10 w-8 h-8 items-center justify-center rounded-full border-none cursor-pointer",
379
+ style: { background: "var(--color-light,#FEFFF0)", boxShadow: "0 1px 4px rgba(0,0,0,0.15)" },
380
+ "aria-label": "Scroll right",
381
+ 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" }) })
382
+ }
383
+ ),
384
+ canScrollLeft && /* @__PURE__ */ jsx2(
385
+ "div",
386
+ {
387
+ className: "sm:hidden absolute left-0 top-0 bottom-0 w-10 z-10 pointer-events-none",
388
+ style: { background: "linear-gradient(to right, var(--color-light,#FEFFF0), transparent)" }
389
+ }
390
+ ),
391
+ canScrollRight && /* @__PURE__ */ jsx2(
392
+ "div",
393
+ {
394
+ className: "sm:hidden absolute right-0 top-0 bottom-0 w-10 z-10 pointer-events-none",
395
+ style: { background: "linear-gradient(to left, var(--color-light,#FEFFF0), transparent)" }
396
+ }
397
+ ),
398
+ /* @__PURE__ */ jsx2(
399
+ "div",
400
+ {
401
+ ref: scrollRef,
402
+ className: "overflow-x-auto w-full",
403
+ style: { scrollbarWidth: "none", msOverflowStyle: "none", touchAction: "pan-x", cursor: "grab" },
404
+ onMouseDown: onDragStart,
405
+ onMouseMove: onDragMove,
406
+ onMouseUp: onDragEnd,
407
+ onMouseLeave: onDragEnd,
408
+ children: /* @__PURE__ */ jsx2("div", { className: "flex", style: { width: `max(100%, ${dates.length * 60}px)` }, children: dates.map((date, i) => {
409
+ const isToday = date.getTime() === todayMs;
410
+ const isSelected = i === selectedIdx;
411
+ const clickable = datesWithClasses.has(i);
412
+ const label = isToday ? "TODAY" : DAY_ABBR[date.getDay()];
413
+ return /* @__PURE__ */ jsxs2(
414
+ "div",
415
+ {
416
+ onClick: () => clickable && onSelectDate(i),
417
+ className: `flex-1 h-[70px] flex flex-col justify-between items-center ${clickable ? "cursor-pointer opacity-100" : "cursor-default opacity-50"}`,
418
+ children: [
419
+ /* @__PURE__ */ jsx2("span", { className: "font-body text-base font-normal text-center text-(--color-dark,#200C02)", children: label }),
420
+ /* @__PURE__ */ jsx2(
421
+ "div",
422
+ {
423
+ className: "w-10 h-10 rounded-full flex items-center justify-center",
424
+ style: { background: isSelected ? "var(--color-primary, #3D312B)" : "transparent" },
425
+ children: /* @__PURE__ */ jsx2(
426
+ "span",
427
+ {
428
+ className: "font-body text-xl font-bold text-center tracking-display-tight",
429
+ style: { color: isSelected ? "var(--color-light, #FEFFF0)" : "var(--color-dark, #200C02)" },
430
+ children: date.getDate()
431
+ }
432
+ )
433
+ }
434
+ )
435
+ ]
436
+ },
437
+ i
438
+ );
439
+ }) })
440
+ }
441
+ )
442
+ ] }),
443
+ 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) => {
444
+ const booked = getBookingsOnDate(cls, selectedDate);
445
+ const available = cls.maxParticipants - booked;
446
+ const isFull = available <= 0;
447
+ return /* @__PURE__ */ jsxs2("div", { children: [
448
+ /* @__PURE__ */ jsxs2("div", { className: "flex gap-4 py-4 sm:items-center sm:gap-[60px] box-border", children: [
449
+ /* @__PURE__ */ jsxs2("div", { className: "flex flex-col gap-2 shrink-0 sm:contents", children: [
450
+ /* @__PURE__ */ jsx2("div", { className: "sm:w-[120px] sm:shrink-0", children: /* @__PURE__ */ jsx2("span", { className: "font-body text-base font-bold text-(--color-dark,#200C02) block", children: formatClassTime(cls) }) }),
451
+ /* @__PURE__ */ jsx2(
452
+ "span",
453
+ {
454
+ className: "inline-flex items-center px-2 py-0.5 rounded-full border text-xs font-medium sm:hidden",
455
+ style: { borderColor: "#0885FE", color: "#0885FE" },
456
+ children: "GROUP"
457
+ }
458
+ ),
459
+ /* @__PURE__ */ jsx2("div", { className: "sm:hidden 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: [
460
+ /* @__PURE__ */ jsxs2("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: [
461
+ available,
462
+ "/",
463
+ cls.maxParticipants
464
+ ] }),
465
+ /* @__PURE__ */ jsx2("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: "available" })
466
+ ] }) })
467
+ ] }),
468
+ /* @__PURE__ */ jsxs2("div", { className: "flex flex-1 flex-col gap-2 min-w-0 sm:contents", children: [
469
+ /* @__PURE__ */ jsxs2("div", { className: "flex-1 flex flex-col gap-2 min-w-0", children: [
470
+ /* @__PURE__ */ jsx2("span", { className: "font-body text-base font-bold text-(--color-dark,#200C02) block truncate", children: cls.name }),
471
+ cls.hostName && /* @__PURE__ */ jsx2("span", { className: "font-body text-base font-normal text-(--color-dark,#200C02) opacity-80 block truncate", children: cls.hostName })
472
+ ] }),
473
+ /* @__PURE__ */ jsx2("div", { className: "hidden sm:flex w-14 shrink-0 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: [
474
+ /* @__PURE__ */ jsxs2("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: [
475
+ available,
476
+ "/",
477
+ cls.maxParticipants
478
+ ] }),
479
+ /* @__PURE__ */ jsx2("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: "available" })
480
+ ] }) }),
481
+ /* @__PURE__ */ jsx2(
482
+ "button",
483
+ {
484
+ className: "w-full sm:w-[200px] sm:shrink-0 px-5 py-[10px] flex items-center justify-center rounded-lg font-body text-base font-bold cursor-pointer box-border",
485
+ style: isFull ? {
486
+ background: "transparent",
487
+ border: "1px solid var(--color-dark, #200C02)",
488
+ color: "var(--color-dark, #200C02)"
489
+ } : {
490
+ background: "var(--color-primary, #3D312B)",
491
+ border: "none",
492
+ color: "var(--color-light, #FEFFF0)"
493
+ },
494
+ onClick: () => {
495
+ if (!cls.id) return;
496
+ onOpenModal?.(isFull ? "waitlist" : "book", cls.id, selectedDate);
497
+ },
498
+ children: isFull ? "Join Waitlist" : "Book Now"
499
+ }
500
+ )
501
+ ] })
502
+ ] }),
503
+ i < selectedClasses.length - 1 && /* @__PURE__ */ jsx2("div", { className: "h-px bg-black/20" })
504
+ ] }, cls.id ?? i);
505
+ }) })
506
+ ] });
507
+ }
508
+ function CalendarFoldIcon() {
509
+ return /* @__PURE__ */ jsxs2(
510
+ "svg",
511
+ {
512
+ width: "16",
513
+ height: "16",
514
+ viewBox: "0 0 24 24",
515
+ fill: "none",
516
+ stroke: "currentColor",
517
+ strokeWidth: "2",
518
+ strokeLinecap: "round",
519
+ strokeLinejoin: "round",
520
+ style: { flexShrink: 0 },
521
+ children: [
522
+ /* @__PURE__ */ jsx2("path", { d: "M8 2v4" }),
523
+ /* @__PURE__ */ jsx2("path", { d: "M16 2v4" }),
524
+ /* @__PURE__ */ jsx2("path", { d: "M21 17V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h11Z" }),
525
+ /* @__PURE__ */ jsx2("path", { d: "M3 10h18" }),
526
+ /* @__PURE__ */ jsx2("path", { d: "M15 22v-4a2 2 0 0 1 2-2h4" })
527
+ ]
528
+ }
529
+ );
530
+ }
531
+ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAfter }) {
532
+ const [schedule, setSchedule] = useState2(null);
533
+ const [loading, setLoading] = useState2(true);
534
+ const [inEditor, setInEditor] = useState2(false);
535
+ const [isHovered, setIsHovered] = useState2(false);
536
+ const [modalState, setModalState] = useState2(null);
537
+ const switchScheduleIdRef = useRef(null);
538
+ const dates = useMemo(() => {
539
+ const today = /* @__PURE__ */ new Date();
540
+ today.setHours(0, 0, 0, 0);
541
+ return Array.from({ length: 14 }, (_, i) => {
542
+ const d = new Date(today);
543
+ d.setDate(today.getDate() + i);
544
+ return d;
545
+ });
546
+ }, []);
547
+ const [selectedIdx, setSelectedIdx] = useState2(0);
548
+ useEffect(() => {
549
+ if (!schedule?.classes) return;
550
+ for (let i = 0; i < dates.length; i++) {
551
+ if (schedule.classes.some((c) => classRunsOnDate(c, dates[i], schedule.type))) {
552
+ setSelectedIdx(i);
553
+ return;
554
+ }
555
+ }
556
+ }, [schedule, dates]);
557
+ useEffect(() => {
558
+ if (typeof window === "undefined") return;
559
+ const isInEditor = window.self !== window.top;
560
+ setInEditor(isInEditor);
561
+ if (isInEditor) {
562
+ const startEmpty = initialScheduleId === null;
563
+ if (startEmpty) setLoading(false);
564
+ let initialized = startEmpty;
565
+ const timer = !startEmpty ? setTimeout(() => {
566
+ if (!initialized) {
567
+ initialized = true;
568
+ setLoading(false);
569
+ }
570
+ }, 5e3) : null;
571
+ const handler = (e) => {
572
+ if (e.data?.type === "ow:clear-scheduling-widget") {
573
+ if (!e.data.insertAfter || e.data.insertAfter !== insertAfter) return;
574
+ setSchedule(null);
575
+ setLoading(false);
576
+ return;
577
+ }
578
+ if (e.data?.type === "ow:switch-schedule") {
579
+ if (!e.data.insertAfter || e.data.insertAfter !== insertAfter) return;
580
+ switchScheduleIdRef.current = e.data.scheduleId ?? null;
581
+ initialized = false;
582
+ setLoading(true);
583
+ window.parent.postMessage({ type: "ow:request-user-schedules", insertAfter }, "*");
584
+ return;
585
+ }
586
+ if (e.data?.type !== "ow:user-schedules-response") return;
587
+ if (e.data.insertAfter && e.data.insertAfter !== insertAfter) return;
588
+ if (initialized && switchScheduleIdRef.current === null) return;
589
+ initialized = true;
590
+ if (timer) clearTimeout(timer);
591
+ const schedules = e.data.schedules ?? [];
592
+ const isSwitching = switchScheduleIdRef.current !== null;
593
+ 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;
594
+ switchScheduleIdRef.current = null;
595
+ setSchedule(target);
596
+ setLoading(false);
597
+ if (notifyOnConnect && target && !isSwitching) {
598
+ window.parent.postMessage({
599
+ type: "ow:schedule-connected",
600
+ schedule: { id: target.id, name: target.name },
601
+ insertAfter
602
+ }, "*");
603
+ window.postMessage({ type: "ow:schedule-linked", scheduleId: target.id, insertAfter }, "*");
604
+ }
605
+ };
606
+ window.addEventListener("message", handler);
607
+ if (!startEmpty) window.parent.postMessage({ type: "ow:request-user-schedules", insertAfter }, "*");
608
+ return () => {
609
+ window.removeEventListener("message", handler);
610
+ if (timer) clearTimeout(timer);
611
+ };
612
+ }
613
+ if (initialScheduleId === null) {
614
+ setLoading(false);
615
+ return;
616
+ }
617
+ ;
618
+ (async () => {
619
+ try {
620
+ if (initialScheduleId) {
621
+ const res = await fetch(`${API_URL}/api/schedule/id/${initialScheduleId}`);
622
+ const data = await res.json();
623
+ setSchedule(data?.id ? data : null);
624
+ } else {
625
+ const domain = getApiDomain();
626
+ const sitemapRes = await fetch(`${API_URL}/api/schedule/sitemap/${domain}`);
627
+ const slugs = await sitemapRes.json();
628
+ if (!Array.isArray(slugs) || !slugs.length) {
629
+ setLoading(false);
630
+ return;
631
+ }
632
+ const { slug } = [...slugs].sort(
633
+ (a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()
634
+ )[0];
635
+ const res = await fetch(`${API_URL}/api/schedule/${domain}/${slug}`);
636
+ const data = await res.json();
637
+ setSchedule(data?.id ? data : null);
638
+ }
639
+ } catch {
640
+ }
641
+ setLoading(false);
642
+ })();
643
+ }, []);
644
+ const timezoneLabel = schedule?.timezone ? (() => {
645
+ try {
646
+ return buildTimezoneLabel(schedule.timezone);
647
+ } catch {
648
+ return void 0;
649
+ }
650
+ })() : void 0;
651
+ const handleModalSubmit = async (email) => {
652
+ if (!modalState) return;
653
+ const { mode, classId, classDate } = modalState;
654
+ const endpoint = mode === "book" ? `${API_URL}/api/schedule/class/${classId}/booking` : `${API_URL}/api/schedule/class/${classId}/waitlist`;
655
+ const res = await fetch(endpoint, {
656
+ method: "POST",
657
+ headers: { "Content-Type": "application/json" },
658
+ body: JSON.stringify({ classDate: classDate.toISOString(), email })
659
+ });
660
+ if (!res.ok) {
661
+ const data = await res.json().catch(() => ({}));
662
+ throw new Error(data?.message ?? "Something went wrong. Please try again.");
663
+ }
664
+ };
665
+ const handleReplaceSchedule = () => {
666
+ setIsHovered(false);
667
+ if (schedule) {
668
+ window.parent.postMessage({
669
+ type: "ow:schedule-connected",
670
+ schedule: { id: schedule.id, name: schedule.name },
671
+ insertAfter
672
+ }, "*");
673
+ } else {
674
+ window.parent.postMessage({ type: "ow:scheduling-not-connected", insertAfter }, "*");
675
+ }
676
+ };
677
+ const sectionId = `scheduling-${insertAfter}`;
678
+ return /* @__PURE__ */ jsxs2(
679
+ "section",
680
+ {
681
+ "data-ohw-section": sectionId,
682
+ "data-ohw-scheduling-anchor": insertAfter,
683
+ className: "w-full box-border py-10 px-5 sm:py-20 sm:px-16 font-body bg-(--color-light,#FEFFF0) relative",
684
+ onMouseEnter: () => inEditor && setIsHovered(true),
685
+ onMouseLeave: () => setIsHovered(false),
686
+ children: [
687
+ inEditor && isHovered && !!schedule && /* @__PURE__ */ jsxs2(
688
+ "div",
689
+ {
690
+ className: "absolute inset-0 z-10 pointer-events-auto cursor-pointer",
691
+ onClick: handleReplaceSchedule,
692
+ children: [
693
+ /* @__PURE__ */ jsx2("div", { className: "absolute inset-0", style: { background: "#0885FE", opacity: 0.18 } }),
694
+ /* @__PURE__ */ jsx2("div", { className: "absolute inset-0", style: { boxShadow: "inset 0 0 0 1.5px #0885FE" } }),
695
+ /* @__PURE__ */ jsx2("div", { className: "absolute inset-0 flex items-center justify-center pointer-events-none", children: /* @__PURE__ */ jsxs2(
696
+ "div",
697
+ {
698
+ className: "bg-white border border-[#E7E5E4] rounded-md px-3 py-1.5 flex items-center gap-1.5",
699
+ style: {
700
+ fontSize: 14,
701
+ lineHeight: "24px",
702
+ fontFamily: "'Figtree', system-ui, sans-serif",
703
+ fontWeight: 500,
704
+ color: "#0C0A09"
705
+ },
706
+ children: [
707
+ /* @__PURE__ */ jsx2(CalendarFoldIcon, {}),
708
+ "Replace schedule"
709
+ ]
710
+ }
711
+ ) })
712
+ ]
713
+ }
714
+ ),
715
+ /* @__PURE__ */ jsxs2("div", { className: "max-w-[1280px] mx-auto flex flex-col items-center gap-16", children: [
716
+ /* @__PURE__ */ jsxs2("div", { className: "flex flex-col items-center gap-4", children: [
717
+ /* @__PURE__ */ jsx2("h2", { className: "font-display text-4xl sm:text-5xl font-bold text-center m-0 text-(--color-dark,#200C02)", children: schedule?.name ?? "Book an appointment" }),
718
+ schedule?.description && /* @__PURE__ */ jsx2("p", { className: "font-body text-body text-center m-0 text-(--color-accent,#A89B83)", children: schedule.description })
719
+ ] }),
720
+ /* @__PURE__ */ jsxs2("div", { className: "w-full flex flex-col items-end gap-3", children: [
721
+ timezoneLabel && /* @__PURE__ */ jsx2("span", { className: "font-body text-sm font-normal text-(--color-accent,#A89B83)", children: timezoneLabel }),
722
+ /* @__PURE__ */ jsx2("div", { className: "w-full p-0 sm:p-10 box-border", children: loading ? /* @__PURE__ */ jsx2(LoadingSkeleton, {}) : !schedule ? /* @__PURE__ */ jsx2(EmptyState, { inEditor }) : /* @__PURE__ */ jsx2(
723
+ ScheduleView,
724
+ {
725
+ schedule,
726
+ dates,
727
+ selectedIdx,
728
+ onSelectDate: setSelectedIdx,
729
+ onOpenModal: inEditor ? void 0 : (mode, classId, classDate) => setModalState({ mode, classId, classDate })
730
+ }
731
+ ) })
732
+ ] })
733
+ ] }),
734
+ modalState && /* @__PURE__ */ jsx2(
735
+ EmailCaptureModal,
736
+ {
737
+ title: modalState.mode === "book" ? "Confirm your spot" : "Leave your email",
738
+ subtitle: modalState.mode === "book" ? "We'll send your booking confirmation here." : "We'll let you know when spots open up!",
739
+ onSubmit: handleModalSubmit,
740
+ onClose: () => setModalState(null)
741
+ }
742
+ )
743
+ ]
744
+ }
745
+ );
746
+ }
5
747
 
6
748
  // src/ui/toggle-group.tsx
7
- import * as React from "react";
749
+ import * as React2 from "react";
8
750
 
9
751
  // node_modules/clsx/dist/clsx.mjs
10
752
  function r(e) {
@@ -3324,7 +4066,7 @@ var cva = (base, config) => (props) => {
3324
4066
 
3325
4067
  // src/ui/toggle.tsx
3326
4068
  import { Toggle as TogglePrimitive } from "radix-ui";
3327
- import { jsx } from "react/jsx-runtime";
4069
+ import { jsx as jsx3 } from "react/jsx-runtime";
3328
4070
  var toggleVariants = cva(
3329
4071
  "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
4072
  {
@@ -3351,7 +4093,7 @@ function Toggle({
3351
4093
  size,
3352
4094
  ...props
3353
4095
  }) {
3354
- return /* @__PURE__ */ jsx(
4096
+ return /* @__PURE__ */ jsx3(
3355
4097
  TogglePrimitive.Root,
3356
4098
  {
3357
4099
  "data-slot": "toggle",
@@ -3362,8 +4104,8 @@ function Toggle({
3362
4104
  }
3363
4105
 
3364
4106
  // src/ui/toggle-group.tsx
3365
- import { jsx as jsx2 } from "react/jsx-runtime";
3366
- var ToggleGroupContext = React.createContext({
4107
+ import { jsx as jsx4 } from "react/jsx-runtime";
4108
+ var ToggleGroupContext = React2.createContext({
3367
4109
  value: "",
3368
4110
  onValueChange: () => {
3369
4111
  }
@@ -3375,13 +4117,13 @@ function ToggleGroup({
3375
4117
  children,
3376
4118
  ...props
3377
4119
  }) {
3378
- return /* @__PURE__ */ jsx2(
4120
+ return /* @__PURE__ */ jsx4(
3379
4121
  "div",
3380
4122
  {
3381
4123
  role: "group",
3382
4124
  className: cn("flex items-center gap-1 p-1 bg-muted rounded-md", className),
3383
4125
  ...props,
3384
- children: /* @__PURE__ */ jsx2(ToggleGroupContext.Provider, { value: { value, onValueChange }, children })
4126
+ children: /* @__PURE__ */ jsx4(ToggleGroupContext.Provider, { value: { value, onValueChange }, children })
3385
4127
  }
3386
4128
  );
3387
4129
  }
@@ -3391,8 +4133,8 @@ function ToggleGroupItem({
3391
4133
  children,
3392
4134
  ...props
3393
4135
  }) {
3394
- const { value: groupValue, onValueChange } = React.useContext(ToggleGroupContext);
3395
- return /* @__PURE__ */ jsx2(
4136
+ const { value: groupValue, onValueChange } = React2.useContext(ToggleGroupContext);
4137
+ return /* @__PURE__ */ jsx4(
3396
4138
  Toggle,
3397
4139
  {
3398
4140
  pressed: groupValue === value,
@@ -3433,8 +4175,30 @@ function isEditSessionActive() {
3433
4175
  return new URLSearchParams(q).get("mode") === "edit";
3434
4176
  }
3435
4177
 
4178
+ // src/ui/badge.tsx
4179
+ import { jsx as jsx5 } from "react/jsx-runtime";
4180
+ var badgeVariants = cva(
4181
+ "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",
4182
+ {
4183
+ variants: {
4184
+ variant: {
4185
+ default: "border-transparent bg-primary text-primary-foreground",
4186
+ secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
4187
+ destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
4188
+ outline: "text-foreground"
4189
+ }
4190
+ },
4191
+ defaultVariants: {
4192
+ variant: "default"
4193
+ }
4194
+ }
4195
+ );
4196
+ function Badge({ className, variant, ...props }) {
4197
+ return /* @__PURE__ */ jsx5("div", { className: cn(badgeVariants({ variant }), className), ...props });
4198
+ }
4199
+
3436
4200
  // src/OhhwellsBridge.tsx
3437
- import { Fragment, jsx as jsx3, jsxs } from "react/jsx-runtime";
4201
+ import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
3438
4202
  var PRIMARY = "#0885FE";
3439
4203
  var IMAGE_FADE_MS = 300;
3440
4204
  function runOpacityFade(el, onDone) {
@@ -3485,6 +4249,163 @@ function fadeInBgImage(el, url, onReady) {
3485
4249
  if (!prevPos || prevPos === "static") el.style.position = prevPos;
3486
4250
  });
3487
4251
  }
4252
+ function getSectionsTracker() {
4253
+ let el = document.querySelector("[data-ohw-sections-tracker]");
4254
+ if (!el) {
4255
+ el = document.createElement("div");
4256
+ el.setAttribute("data-ohw-sections-tracker", "");
4257
+ el.style.display = "none";
4258
+ document.body.appendChild(el);
4259
+ }
4260
+ return el;
4261
+ }
4262
+ function updateSectionScheduleId(insertAfter, scheduleId) {
4263
+ const tracker = getSectionsTracker();
4264
+ let sections = [];
4265
+ try {
4266
+ sections = JSON.parse(tracker.textContent || "[]");
4267
+ } catch {
4268
+ }
4269
+ const currentPath = window.location.pathname;
4270
+ const updated = sections.map((s) => {
4271
+ if (s.type !== "scheduling" || s.insertAfter !== insertAfter) return s;
4272
+ if (s.pagePath && s.pagePath !== currentPath) return s;
4273
+ return { ...s, scheduleId };
4274
+ });
4275
+ tracker.textContent = JSON.stringify(updated);
4276
+ return tracker.textContent ?? "[]";
4277
+ }
4278
+ function schedulingSectionId(insertAfter) {
4279
+ return `scheduling-${insertAfter}`;
4280
+ }
4281
+ var INSERT_BEFORE_MARKER = ":before:";
4282
+ function parseSchedulingInsertAfter(insertAfter) {
4283
+ const idx = insertAfter.indexOf(INSERT_BEFORE_MARKER);
4284
+ if (idx < 0) return { anchor: insertAfter, insertBefore: null };
4285
+ return {
4286
+ anchor: insertAfter.slice(0, idx),
4287
+ insertBefore: insertAfter.slice(idx + INSERT_BEFORE_MARKER.length)
4288
+ };
4289
+ }
4290
+ function resolveSchedulingInsert(insertAfter, explicitInsertBefore) {
4291
+ const parsed = parseSchedulingInsertAfter(insertAfter);
4292
+ const insertBefore = explicitInsertBefore ?? parsed.insertBefore;
4293
+ const effectiveInsertAfter = insertBefore && parsed.insertBefore === null ? `${insertAfter}${INSERT_BEFORE_MARKER}${insertBefore}` : insertAfter;
4294
+ return { effectiveInsertAfter, insertBefore };
4295
+ }
4296
+ function getSchedulingMountPoint(insertAfter) {
4297
+ const { anchor } = parseSchedulingInsertAfter(insertAfter);
4298
+ let anchorEl = document.querySelector(`[data-ohw-section="${anchor}"]`);
4299
+ if (!anchorEl && anchor === "scheduling") {
4300
+ const widgets = Array.from(document.querySelectorAll('[data-ohw-section^="scheduling-"]'));
4301
+ anchorEl = widgets.at(-1) ?? null;
4302
+ }
4303
+ if (!anchorEl) return null;
4304
+ return anchorEl.closest('[data-ohw-section-container="scheduling"]') ?? anchorEl;
4305
+ }
4306
+ function schedulingMountDepth(insertAfter) {
4307
+ if (!insertAfter.includes(INSERT_BEFORE_MARKER)) return 0;
4308
+ return insertAfter.split(INSERT_BEFORE_MARKER).length - 1;
4309
+ }
4310
+ function getPageSchedulingEntries(raw) {
4311
+ if (!raw) return [];
4312
+ try {
4313
+ const entries = JSON.parse(raw);
4314
+ const currentPath = window.location.pathname;
4315
+ return entries.filter((e) => e.type === "scheduling" && (!e.pagePath || e.pagePath === currentPath));
4316
+ } catch {
4317
+ return [];
4318
+ }
4319
+ }
4320
+ function isSchedulingWidgetMissing(entry) {
4321
+ const { effectiveInsertAfter } = resolveSchedulingInsert(entry.insertAfter);
4322
+ return !document.querySelector(`[data-ohw-section="${schedulingSectionId(effectiveInsertAfter)}"]`);
4323
+ }
4324
+ function hasMissingSchedulingWidgets(entries) {
4325
+ return entries.some(isSchedulingWidgetMissing);
4326
+ }
4327
+ function retryMissingSchedulingMounts(entries, notifyOnConnect = false) {
4328
+ if (!hasMissingSchedulingWidgets(entries)) return;
4329
+ mountSchedulingEntries(entries, notifyOnConnect);
4330
+ }
4331
+ function initSectionsFromContent(content, removeExisting = false) {
4332
+ const raw = content["__ohw_sections"];
4333
+ if (!raw) return;
4334
+ try {
4335
+ if (removeExisting) {
4336
+ document.querySelectorAll('[data-ohw-section-container="scheduling"]').forEach((el) => el.remove());
4337
+ }
4338
+ const inEditor = typeof window !== "undefined" && window.self !== window.top;
4339
+ if (inEditor) getSectionsTracker().textContent = raw;
4340
+ const pageEntries = getPageSchedulingEntries(raw);
4341
+ mountSchedulingEntries(pageEntries, false);
4342
+ requestAnimationFrame(() => retryMissingSchedulingMounts(pageEntries, false));
4343
+ setTimeout(() => retryMissingSchedulingMounts(pageEntries, false), 250);
4344
+ } catch {
4345
+ }
4346
+ }
4347
+ function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId, explicitInsertBefore) {
4348
+ const { effectiveInsertAfter, insertBefore } = resolveSchedulingInsert(insertAfter, explicitInsertBefore);
4349
+ const sectionId = schedulingSectionId(effectiveInsertAfter);
4350
+ if (document.querySelector(`[data-ohw-section="${sectionId}"]`)) return false;
4351
+ const mountPoint = getSchedulingMountPoint(effectiveInsertAfter);
4352
+ if (!mountPoint) return false;
4353
+ const container = document.createElement("div");
4354
+ container.dataset.ohwSectionContainer = "scheduling";
4355
+ if (insertBefore) {
4356
+ const beforeAnchor = document.querySelector(`[data-ohw-section="${insertBefore}"]`);
4357
+ const beforePoint = beforeAnchor?.closest('[data-ohw-section-container="scheduling"]') ?? beforeAnchor;
4358
+ if (!beforePoint) return false;
4359
+ beforePoint.insertAdjacentElement("beforebegin", container);
4360
+ } else {
4361
+ let tail = mountPoint;
4362
+ while (tail.nextElementSibling?.dataset.ohwSectionContainer === "scheduling") {
4363
+ tail = tail.nextElementSibling;
4364
+ }
4365
+ tail.insertAdjacentElement("afterend", container);
4366
+ }
4367
+ const root = createRoot(container);
4368
+ flushSync(() => {
4369
+ root.render(
4370
+ /* @__PURE__ */ jsx6(
4371
+ SchedulingWidget,
4372
+ {
4373
+ notifyOnConnect,
4374
+ initialScheduleId: scheduleId,
4375
+ insertAfter: effectiveInsertAfter
4376
+ }
4377
+ )
4378
+ );
4379
+ });
4380
+ const tracker = getSectionsTracker();
4381
+ let sections = [];
4382
+ try {
4383
+ sections = JSON.parse(tracker.textContent || "[]");
4384
+ } catch {
4385
+ }
4386
+ const inEditor = typeof window !== "undefined" && window.self !== window.top;
4387
+ if (inEditor && !sections.find((s) => s.type === "scheduling" && s.insertAfter === effectiveInsertAfter)) {
4388
+ sections.push({
4389
+ type: "scheduling",
4390
+ insertAfter: effectiveInsertAfter,
4391
+ pagePath: window.location.pathname,
4392
+ ...scheduleId ? { scheduleId } : {}
4393
+ });
4394
+ tracker.textContent = JSON.stringify(sections);
4395
+ }
4396
+ return true;
4397
+ }
4398
+ function mountSchedulingEntries(entries, notifyOnConnect = false) {
4399
+ const pending = entries.filter((e) => e.type === "scheduling").sort((a, b) => schedulingMountDepth(a.insertAfter) - schedulingMountDepth(b.insertAfter));
4400
+ for (let attempt = 0; attempt < pending.length + 1 && pending.length > 0; attempt++) {
4401
+ for (let i = pending.length - 1; i >= 0; i--) {
4402
+ const { insertAfter, scheduleId } = pending[i];
4403
+ if (mountSchedulingWidget(insertAfter, notifyOnConnect, scheduleId)) {
4404
+ pending.splice(i, 1);
4405
+ }
4406
+ }
4407
+ }
4408
+ }
3488
4409
  function collectEditableNodes() {
3489
4410
  return Array.from(document.querySelectorAll("[data-ohw-editable]")).map((el) => {
3490
4411
  if (el.dataset.ohwEditable === "image") {
@@ -3642,7 +4563,7 @@ var TOOLBAR_GROUPS = [
3642
4563
  ];
3643
4564
  function GlowFrame({ rect, elRef }) {
3644
4565
  const GAP = 6;
3645
- return /* @__PURE__ */ jsx3(
4566
+ return /* @__PURE__ */ jsx6(
3646
4567
  "div",
3647
4568
  {
3648
4569
  ref: elRef,
@@ -3693,7 +4614,7 @@ function FloatingToolbar({
3693
4614
  activeCommands
3694
4615
  }) {
3695
4616
  const { top, left, transform } = calcToolbarPos(rect, parentScroll);
3696
- return /* @__PURE__ */ jsx3(
4617
+ return /* @__PURE__ */ jsx6(
3697
4618
  "div",
3698
4619
  {
3699
4620
  ref: elRef,
@@ -3716,11 +4637,11 @@ function FloatingToolbar({
3716
4637
  pointerEvents: "auto",
3717
4638
  whiteSpace: "nowrap"
3718
4639
  },
3719
- children: TOOLBAR_GROUPS.map((btns, gi) => /* @__PURE__ */ jsxs(React2.Fragment, { children: [
3720
- gi > 0 && /* @__PURE__ */ jsx3("span", { style: { display: "block", width: 1, height: 24, background: "#E7E5E4", flexShrink: 0 } }),
4640
+ children: TOOLBAR_GROUPS.map((btns, gi) => /* @__PURE__ */ jsxs3(React3.Fragment, { children: [
4641
+ gi > 0 && /* @__PURE__ */ jsx6("span", { style: { display: "block", width: 1, height: 24, background: "#E7E5E4", flexShrink: 0 } }),
3721
4642
  btns.map((btn) => {
3722
4643
  const isActive = activeCommands.has(btn.cmd);
3723
- return /* @__PURE__ */ jsx3(
4644
+ return /* @__PURE__ */ jsx6(
3724
4645
  "button",
3725
4646
  {
3726
4647
  title: btn.title,
@@ -3746,7 +4667,7 @@ function FloatingToolbar({
3746
4667
  flexShrink: 0,
3747
4668
  padding: 6
3748
4669
  },
3749
- children: /* @__PURE__ */ jsx3(
4670
+ children: /* @__PURE__ */ jsx6(
3750
4671
  "svg",
3751
4672
  {
3752
4673
  width: "16",
@@ -3779,7 +4700,7 @@ function StateToggle({
3779
4700
  states,
3780
4701
  onStateChange
3781
4702
  }) {
3782
- return /* @__PURE__ */ jsx3(
4703
+ return /* @__PURE__ */ jsx6(
3783
4704
  ToggleGroup,
3784
4705
  {
3785
4706
  "data-ohw-state-toggle": "",
@@ -3793,18 +4714,35 @@ function StateToggle({
3793
4714
  left: rect.right - 8,
3794
4715
  transform: "translateX(-100%)"
3795
4716
  },
3796
- children: states.map((state) => /* @__PURE__ */ jsx3(ToggleGroupItem, { value: state, size: "sm", children: state }, state))
4717
+ children: states.map((state) => /* @__PURE__ */ jsx6(ToggleGroupItem, { value: state, size: "sm", children: state }, state))
3797
4718
  }
3798
4719
  );
3799
4720
  }
3800
4721
  var contentCache = /* @__PURE__ */ new Map();
4722
+ function resolveSubdomain(subdomainFromQuery) {
4723
+ if (subdomainFromQuery) return subdomainFromQuery;
4724
+ if (typeof window !== "undefined") {
4725
+ const parts = window.location.hostname.split(".");
4726
+ if (parts.length >= 3 && parts[0] !== "www") return parts[0];
4727
+ }
4728
+ const siteUrl = process.env.NEXT_PUBLIC_SITE_URL;
4729
+ if (siteUrl) {
4730
+ try {
4731
+ const host = new URL(siteUrl).hostname;
4732
+ const siteParts = host.split(".");
4733
+ if (siteParts.length >= 3 && siteParts[0] !== "www") return siteParts[0];
4734
+ } catch {
4735
+ }
4736
+ }
4737
+ return "";
4738
+ }
3801
4739
  function OhhwellsBridge() {
3802
4740
  const pathname = usePathname();
3803
4741
  const router = useRouter();
3804
4742
  const searchParams = useSearchParams();
3805
4743
  const isEditMode = isEditSessionActive();
3806
- const [bridgeRoot, setBridgeRoot] = useState(null);
3807
- useEffect(() => {
4744
+ const [bridgeRoot, setBridgeRoot] = useState3(null);
4745
+ useEffect2(() => {
3808
4746
  const figtreeFontId = "ohw-figtree-font";
3809
4747
  if (!document.getElementById(figtreeFontId)) {
3810
4748
  const preconnect1 = Object.assign(document.createElement("link"), { rel: "preconnect", href: "https://fonts.googleapis.com" });
@@ -3827,42 +4765,40 @@ function OhhwellsBridge() {
3827
4765
  };
3828
4766
  }, []);
3829
4767
  const subdomainFromQuery = searchParams.get("subdomain");
3830
- const subdomain = subdomainFromQuery ?? (() => {
3831
- if (typeof window === "undefined") return "";
3832
- const parts = window.location.hostname.split(".");
3833
- return parts.length >= 3 && parts[0] !== "www" ? parts[0] : "";
3834
- })();
4768
+ const subdomain = resolveSubdomain(subdomainFromQuery);
3835
4769
  const postToParent = useCallback((data) => {
3836
4770
  if (typeof window !== "undefined" && window.parent !== window) {
3837
4771
  window.parent.postMessage(data, "*");
3838
4772
  }
3839
4773
  }, []);
3840
- const [fetchState, setFetchState] = useState("idle");
3841
- const autoSaveTimers = useRef(/* @__PURE__ */ new Map());
3842
- const activeElRef = useRef(null);
3843
- const originalContentRef = useRef(null);
3844
- const activeStateElRef = useRef(null);
3845
- const parentScrollRef = useRef(null);
3846
- const toolbarElRef = useRef(null);
3847
- const glowElRef = useRef(null);
3848
- const hoveredImageRef = useRef(null);
3849
- const hoveredImageHasTextOverlapRef = useRef(false);
3850
- const imageUnhoverTimerRef = useRef(null);
3851
- const imageShowTimerRef = useRef(null);
3852
- const editStylesRef = useRef(null);
3853
- const activateRef = useRef(() => {
4774
+ const [fetchState, setFetchState] = useState3("idle");
4775
+ const autoSaveTimers = useRef2(/* @__PURE__ */ new Map());
4776
+ const activeElRef = useRef2(null);
4777
+ const originalContentRef = useRef2(null);
4778
+ const activeStateElRef = useRef2(null);
4779
+ const parentScrollRef = useRef2(null);
4780
+ const toolbarElRef = useRef2(null);
4781
+ const glowElRef = useRef2(null);
4782
+ const hoveredImageRef = useRef2(null);
4783
+ const hoveredImageHasTextOverlapRef = useRef2(false);
4784
+ const hoveredGapRef = useRef2(null);
4785
+ const imageUnhoverTimerRef = useRef2(null);
4786
+ const imageShowTimerRef = useRef2(null);
4787
+ const editStylesRef = useRef2(null);
4788
+ const activateRef = useRef2(() => {
3854
4789
  });
3855
- const deactivateRef = useRef(() => {
4790
+ const deactivateRef = useRef2(() => {
3856
4791
  });
3857
- const refreshActiveCommandsRef = useRef(() => {
4792
+ const refreshActiveCommandsRef = useRef2(() => {
3858
4793
  });
3859
- const postToParentRef = useRef(postToParent);
4794
+ const postToParentRef = useRef2(postToParent);
3860
4795
  postToParentRef.current = postToParent;
3861
- const [toolbarRect, setToolbarRect] = useState(null);
3862
- const [toggleState, setToggleState] = useState(null);
3863
- const [maxBadge, setMaxBadge] = useState(null);
3864
- const [activeCommands, setActiveCommands] = useState(/* @__PURE__ */ new Set());
3865
- useEffect(() => {
4796
+ const [toolbarRect, setToolbarRect] = useState3(null);
4797
+ const [toggleState, setToggleState] = useState3(null);
4798
+ const [maxBadge, setMaxBadge] = useState3(null);
4799
+ const [activeCommands, setActiveCommands] = useState3(/* @__PURE__ */ new Set());
4800
+ const [sectionGap, setSectionGap] = useState3(null);
4801
+ useEffect2(() => {
3866
4802
  const update = () => {
3867
4803
  const el = activeElRef.current;
3868
4804
  if (el) setToolbarRect(el.getBoundingClientRect());
@@ -3870,6 +4806,12 @@ function OhhwellsBridge() {
3870
4806
  if (!prev || !activeStateElRef.current) return prev;
3871
4807
  return { ...prev, rect: activeStateElRef.current.getBoundingClientRect() };
3872
4808
  });
4809
+ setSectionGap((prev) => {
4810
+ if (!prev) return prev;
4811
+ const anchor = document.querySelector(`[data-ohw-section="${prev.insertAfter}"]`);
4812
+ if (!anchor) return prev;
4813
+ return { ...prev, y: anchor.getBoundingClientRect().bottom };
4814
+ });
3873
4815
  };
3874
4816
  const vvp = window.visualViewport;
3875
4817
  if (!vvp) return;
@@ -3934,6 +4876,7 @@ function OhhwellsBridge() {
3934
4876
  const applyContent = (content) => {
3935
4877
  const imageLoads = [];
3936
4878
  for (const [key, val] of Object.entries(content)) {
4879
+ if (key === "__ohw_sections") continue;
3937
4880
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
3938
4881
  if (el.dataset.ohwEditable === "image") {
3939
4882
  const img = el instanceof HTMLImageElement ? el : el.querySelector("img");
@@ -3952,6 +4895,7 @@ function OhhwellsBridge() {
3952
4895
  }
3953
4896
  });
3954
4897
  }
4898
+ initSectionsFromContent(content, true);
3955
4899
  return imageLoads.length > 0 ? Promise.all(imageLoads).then(() => {
3956
4900
  }) : Promise.resolve();
3957
4901
  };
@@ -3962,7 +4906,7 @@ function OhhwellsBridge() {
3962
4906
  }
3963
4907
  let cancelled = false;
3964
4908
  setFetchState("loading");
3965
- const apiUrl = globalThis.process?.env?.NEXT_PUBLIC_FLOWOPS_API_URL ?? "https://flowops-backend-staging-2yfdh7pwpq-as.a.run.app";
4909
+ const apiUrl = process.env.NEXT_PUBLIC_FLOWOPS_API_URL ?? "https://flowops-backend-staging-2yfdh7pwpq-as.a.run.app";
3966
4910
  fetch(`${apiUrl}/api/public/sites/${subdomain}/content`, { cache: "no-store" }).then((r2) => r2.ok ? r2.json() : null).then((data) => {
3967
4911
  if (cancelled) return;
3968
4912
  const content = data?.content ?? {};
@@ -3976,12 +4920,15 @@ function OhhwellsBridge() {
3976
4920
  cancelled = true;
3977
4921
  };
3978
4922
  }, [subdomain, isEditMode, pathname]);
3979
- useEffect(() => {
4923
+ useEffect2(() => {
3980
4924
  if (!subdomain || isEditMode) return;
4925
+ let debounceTimer = null;
3981
4926
  const applyFromCache = () => {
3982
4927
  const content = contentCache.get(subdomain);
3983
4928
  if (!content) return;
4929
+ retryMissingSchedulingMounts(getPageSchedulingEntries(content["__ohw_sections"]), false);
3984
4930
  for (const [key, val] of Object.entries(content)) {
4931
+ if (key === "__ohw_sections") continue;
3985
4932
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
3986
4933
  if (el.dataset.ohwEditable === "image") {
3987
4934
  const img = el instanceof HTMLImageElement ? el : el.querySelector("img");
@@ -3995,21 +4942,28 @@ function OhhwellsBridge() {
3995
4942
  });
3996
4943
  }
3997
4944
  };
3998
- applyFromCache();
3999
- const observer = new MutationObserver(applyFromCache);
4945
+ const scheduleApply = () => {
4946
+ if (debounceTimer) clearTimeout(debounceTimer);
4947
+ debounceTimer = setTimeout(applyFromCache, 150);
4948
+ };
4949
+ scheduleApply();
4950
+ const observer = new MutationObserver(scheduleApply);
4000
4951
  observer.observe(document.body, { childList: true, subtree: true });
4001
- return () => observer.disconnect();
4002
- }, [subdomain, isEditMode]);
4952
+ return () => {
4953
+ observer.disconnect();
4954
+ if (debounceTimer) clearTimeout(debounceTimer);
4955
+ };
4956
+ }, [subdomain, isEditMode, pathname]);
4003
4957
  useLayoutEffect(() => {
4004
4958
  const el = document.getElementById("ohw-loader");
4005
4959
  if (!el) return;
4006
4960
  const visible = Boolean(subdomain) && fetchState !== "done";
4007
4961
  el.style.display = visible ? "flex" : "none";
4008
4962
  }, [subdomain, fetchState]);
4009
- useEffect(() => {
4963
+ useEffect2(() => {
4010
4964
  postToParent({ type: "ow:navigation", path: pathname });
4011
4965
  }, [pathname, postToParent]);
4012
- useEffect(() => {
4966
+ useEffect2(() => {
4013
4967
  if (!isEditMode) return;
4014
4968
  const measure = () => {
4015
4969
  const h = document.body.scrollHeight;
@@ -4033,7 +4987,7 @@ function OhhwellsBridge() {
4033
4987
  window.removeEventListener("resize", handleResize);
4034
4988
  };
4035
4989
  }, [pathname, isEditMode, postToParent]);
4036
- useEffect(() => {
4990
+ useEffect2(() => {
4037
4991
  if (!subdomainFromQuery || isEditMode) return;
4038
4992
  const handleClick = (e) => {
4039
4993
  const anchor = e.target.closest("a");
@@ -4049,7 +5003,7 @@ function OhhwellsBridge() {
4049
5003
  document.addEventListener("click", handleClick, true);
4050
5004
  return () => document.removeEventListener("click", handleClick, true);
4051
5005
  }, [subdomainFromQuery, isEditMode, router]);
4052
- useEffect(() => {
5006
+ useEffect2(() => {
4053
5007
  if (!isEditMode) {
4054
5008
  editStylesRef.current?.base.remove();
4055
5009
  editStylesRef.current?.forceHover.remove();
@@ -4309,6 +5263,15 @@ function OhhwellsBridge() {
4309
5263
  textEditable.setAttribute("data-ohw-hovered", "");
4310
5264
  return;
4311
5265
  }
5266
+ if (hoveredGapRef.current) {
5267
+ if (hoveredImageRef.current) {
5268
+ hoveredImageRef.current = null;
5269
+ resumeAnimTracks();
5270
+ postToParentRef.current({ type: "ow:image-unhover" });
5271
+ }
5272
+ document.querySelectorAll("[data-ohw-hovered]").forEach((el) => el.removeAttribute("data-ohw-hovered"));
5273
+ return;
5274
+ }
4312
5275
  document.querySelectorAll("[data-ohw-hovered]").forEach((el) => el.removeAttribute("data-ohw-hovered"));
4313
5276
  if (imgEl !== hoveredImageRef.current || hoveredImageHasTextOverlapRef.current) {
4314
5277
  hoveredImageRef.current = imgEl;
@@ -4374,8 +5337,30 @@ function OhhwellsBridge() {
4374
5337
  }
4375
5338
  }
4376
5339
  };
5340
+ const probeSectionGapAt = (clientX, clientY, fromParentViewport = false) => {
5341
+ const { y } = toProbeCoords(clientX, clientY, fromParentViewport);
5342
+ const sections = Array.from(document.querySelectorAll("[data-ohw-section]")).sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top);
5343
+ const ZONE = 20;
5344
+ for (let i = 0; i < sections.length; i++) {
5345
+ const a = sections[i];
5346
+ const b = sections[i + 1] ?? null;
5347
+ const boundaryY = a.getBoundingClientRect().bottom;
5348
+ if (Math.abs(y - boundaryY) <= ZONE) {
5349
+ const insertAfter = a.dataset.ohwSection ?? "";
5350
+ const insertBefore = b?.dataset.ohwSection ?? null;
5351
+ hoveredGapRef.current = { insertAfter, insertBefore };
5352
+ setSectionGap({ insertAfter, insertBefore, y: boundaryY });
5353
+ return;
5354
+ }
5355
+ }
5356
+ if (hoveredGapRef.current) {
5357
+ hoveredGapRef.current = null;
5358
+ setSectionGap(null);
5359
+ }
5360
+ };
4377
5361
  const handleMouseMove = (e) => {
4378
5362
  const { clientX, clientY } = e;
5363
+ probeSectionGapAt(clientX, clientY);
4379
5364
  probeImageAt(clientX, clientY);
4380
5365
  probeHoverCardsAt(clientX, clientY);
4381
5366
  };
@@ -4383,6 +5368,7 @@ function OhhwellsBridge() {
4383
5368
  if (e.data?.type !== "ow:pointer-sync") return;
4384
5369
  const { clientX, clientY } = e.data;
4385
5370
  if (typeof clientX !== "number" || typeof clientY !== "number") return;
5371
+ probeSectionGapAt(clientX, clientY);
4386
5372
  probeImageAt(clientX, clientY);
4387
5373
  probeHoverCardsAt(clientX, clientY);
4388
5374
  };
@@ -4540,7 +5526,12 @@ function OhhwellsBridge() {
4540
5526
  if (e.data?.type !== "ow:hydrate") return;
4541
5527
  const content = e.data.content;
4542
5528
  if (!content) return;
5529
+ let sectionsJson = null;
4543
5530
  for (const [key, val] of Object.entries(content)) {
5531
+ if (key === "__ohw_sections") {
5532
+ sectionsJson = val;
5533
+ continue;
5534
+ }
4544
5535
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
4545
5536
  if (el.dataset.ohwEditable === "image") {
4546
5537
  const img = el instanceof HTMLImageElement ? el : el.querySelector("img");
@@ -4552,6 +5543,9 @@ function OhhwellsBridge() {
4552
5543
  }
4553
5544
  });
4554
5545
  }
5546
+ if (sectionsJson) {
5547
+ initSectionsFromContent({ __ohw_sections: sectionsJson }, true);
5548
+ }
4555
5549
  postToParentRef.current({ type: "ow:hydrate-done" });
4556
5550
  };
4557
5551
  window.addEventListener("message", handleHydrate);
@@ -4595,7 +5589,63 @@ function OhhwellsBridge() {
4595
5589
  };
4596
5590
  const handleSave = (e) => {
4597
5591
  if (e.data?.type !== "ow:save") return;
4598
- postToParentRef.current({ type: "ow:save-result", nodes: collectEditableNodes() });
5592
+ const nodes = collectEditableNodes();
5593
+ const tracker = document.querySelector("[data-ohw-sections-tracker]");
5594
+ if (tracker?.textContent) nodes.push({ key: "__ohw_sections", type: "sections", text: tracker.textContent });
5595
+ postToParentRef.current({ type: "ow:save-result", nodes });
5596
+ };
5597
+ const handleInsertSection = (e) => {
5598
+ if (e.data?.type !== "ow:insert-section") return;
5599
+ const { widgetType, insertAfter, insertBefore } = e.data;
5600
+ if (widgetType !== "scheduling") return;
5601
+ const inserted = mountSchedulingWidget(insertAfter, true, void 0, insertBefore ?? null);
5602
+ if (inserted) {
5603
+ const tracker = getSectionsTracker();
5604
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: "__ohw_sections", text: tracker.textContent ?? "[]" }] });
5605
+ const h = document.documentElement.scrollHeight;
5606
+ if (h > 50) postToParentRef.current({ type: "ow:height", height: h });
5607
+ }
5608
+ };
5609
+ const handleSwitchSchedule = (e) => {
5610
+ if (e.data?.type !== "ow:switch-schedule") return;
5611
+ const scheduleId = e.data.scheduleId;
5612
+ const insertAfter = e.data.insertAfter;
5613
+ if (!scheduleId || !insertAfter) return;
5614
+ const text = updateSectionScheduleId(insertAfter, scheduleId);
5615
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: "__ohw_sections", text }] });
5616
+ };
5617
+ const handleScheduleLinked = (e) => {
5618
+ if (e.data?.type !== "ow:schedule-linked") return;
5619
+ const scheduleId = e.data.scheduleId;
5620
+ const insertAfter = e.data.insertAfter;
5621
+ if (!scheduleId || !insertAfter) return;
5622
+ const text = updateSectionScheduleId(insertAfter, scheduleId);
5623
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: "__ohw_sections", text }] });
5624
+ };
5625
+ const handleClearSchedulingWidget = (e) => {
5626
+ if (e.data?.type !== "ow:clear-scheduling-widget") return;
5627
+ const insertAfter = e.data.insertAfter;
5628
+ if (!insertAfter) return;
5629
+ const text = updateSectionScheduleId(insertAfter, null);
5630
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: "__ohw_sections", text }] });
5631
+ };
5632
+ const handleRemoveSchedulingSection = (e) => {
5633
+ if (e.data?.type !== "ow:remove-scheduling-section") return;
5634
+ document.querySelectorAll('[data-ohw-section-container="scheduling"]').forEach((el) => {
5635
+ el.remove();
5636
+ });
5637
+ const tracker = getSectionsTracker();
5638
+ let sections = [];
5639
+ try {
5640
+ sections = JSON.parse(tracker.textContent || "[]");
5641
+ } catch {
5642
+ }
5643
+ const currentPath = window.location.pathname;
5644
+ const updated = sections.filter((s) => !(s.type === "scheduling" && s.pagePath === currentPath));
5645
+ tracker.textContent = JSON.stringify(updated);
5646
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: "__ohw_sections", text: tracker.textContent }] });
5647
+ const h = document.documentElement.scrollHeight;
5648
+ if (h > 50) postToParentRef.current({ type: "ow:height", height: h });
4599
5649
  };
4600
5650
  const handleSelectionChange = () => {
4601
5651
  if (!activeElRef.current) return;
@@ -4700,6 +5750,11 @@ function OhhwellsBridge() {
4700
5750
  deactivateRef.current();
4701
5751
  };
4702
5752
  window.addEventListener("message", handleSave);
5753
+ window.addEventListener("message", handleInsertSection);
5754
+ window.addEventListener("message", handleSwitchSchedule);
5755
+ window.addEventListener("message", handleScheduleLinked);
5756
+ window.addEventListener("message", handleClearSchedulingWidget);
5757
+ window.addEventListener("message", handleRemoveSchedulingSection);
4703
5758
  window.addEventListener("message", handleImageUrl);
4704
5759
  window.addEventListener("message", handleAnimLock);
4705
5760
  window.addEventListener("message", handleCanvasHeight);
@@ -4734,6 +5789,11 @@ function OhhwellsBridge() {
4734
5789
  document.removeEventListener("selectionchange", handleSelectionChange);
4735
5790
  window.removeEventListener("scroll", handleScroll, true);
4736
5791
  window.removeEventListener("message", handleSave);
5792
+ window.removeEventListener("message", handleInsertSection);
5793
+ window.removeEventListener("message", handleSwitchSchedule);
5794
+ window.removeEventListener("message", handleScheduleLinked);
5795
+ window.removeEventListener("message", handleClearSchedulingWidget);
5796
+ window.removeEventListener("message", handleRemoveSchedulingSection);
4737
5797
  window.removeEventListener("message", handleImageUrl);
4738
5798
  window.removeEventListener("message", handleAnimLock);
4739
5799
  window.removeEventListener("message", handleCanvasHeight);
@@ -4748,7 +5808,7 @@ function OhhwellsBridge() {
4748
5808
  if (imageShowTimerRef.current) clearTimeout(imageShowTimerRef.current);
4749
5809
  };
4750
5810
  }, [isEditMode, refreshStateRules]);
4751
- useEffect(() => {
5811
+ useEffect2(() => {
4752
5812
  if (!isEditMode) return;
4753
5813
  document.querySelectorAll("[data-ohw-active-state]").forEach((el) => {
4754
5814
  el.removeAttribute("data-ohw-active-state");
@@ -4790,12 +5850,12 @@ function OhhwellsBridge() {
4790
5850
  setToggleState((prev) => prev ? { ...prev, activeState: state } : null);
4791
5851
  }, [deactivate]);
4792
5852
  return bridgeRoot ? createPortal(
4793
- /* @__PURE__ */ jsxs(Fragment, { children: [
4794
- toolbarRect && /* @__PURE__ */ jsxs(Fragment, { children: [
4795
- /* @__PURE__ */ jsx3(GlowFrame, { rect: toolbarRect, elRef: glowElRef }),
4796
- /* @__PURE__ */ jsx3(FloatingToolbar, { rect: toolbarRect, parentScroll: parentScrollRef.current, elRef: toolbarElRef, onCommand: handleCommand, activeCommands })
5853
+ /* @__PURE__ */ jsxs3(Fragment2, { children: [
5854
+ toolbarRect && /* @__PURE__ */ jsxs3(Fragment2, { children: [
5855
+ /* @__PURE__ */ jsx6(GlowFrame, { rect: toolbarRect, elRef: glowElRef }),
5856
+ /* @__PURE__ */ jsx6(FloatingToolbar, { rect: toolbarRect, parentScroll: parentScrollRef.current, elRef: toolbarElRef, onCommand: handleCommand, activeCommands })
4797
5857
  ] }),
4798
- maxBadge && /* @__PURE__ */ jsxs(
5858
+ maxBadge && /* @__PURE__ */ jsxs3(
4799
5859
  "div",
4800
5860
  {
4801
5861
  "data-ohw-max-badge": "",
@@ -4821,7 +5881,7 @@ function OhhwellsBridge() {
4821
5881
  ]
4822
5882
  }
4823
5883
  ),
4824
- toggleState && /* @__PURE__ */ jsx3(
5884
+ toggleState && /* @__PURE__ */ jsx6(
4825
5885
  StateToggle,
4826
5886
  {
4827
5887
  rect: toggleState.rect,
@@ -4829,6 +5889,31 @@ function OhhwellsBridge() {
4829
5889
  states: toggleState.states,
4830
5890
  onStateChange: handleStateChange
4831
5891
  }
5892
+ ),
5893
+ sectionGap && /* @__PURE__ */ jsxs3(
5894
+ "div",
5895
+ {
5896
+ "data-ohw-section-insert-line": "",
5897
+ className: "fixed left-0 w-full z-2147483646 flex items-center pointer-events-none",
5898
+ style: { top: sectionGap.y, transform: "translateY(-50%)" },
5899
+ children: [
5900
+ /* @__PURE__ */ jsx6("div", { className: "flex-1 bg-primary", style: { height: 3 } }),
5901
+ /* @__PURE__ */ jsx6(
5902
+ Badge,
5903
+ {
5904
+ 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",
5905
+ onClick: () => {
5906
+ window.parent.postMessage(
5907
+ { type: "ow:add-section", insertAfter: sectionGap.insertAfter, insertBefore: sectionGap.insertBefore },
5908
+ "*"
5909
+ );
5910
+ },
5911
+ children: "Add Section"
5912
+ }
5913
+ ),
5914
+ /* @__PURE__ */ jsx6("div", { className: "flex-1 bg-primary", style: { height: 3 } })
5915
+ ]
5916
+ }
4832
5917
  )
4833
5918
  ] }),
4834
5919
  bridgeRoot
@@ -4836,6 +5921,7 @@ function OhhwellsBridge() {
4836
5921
  }
4837
5922
  export {
4838
5923
  OhhwellsBridge,
5924
+ SchedulingWidget,
4839
5925
  Toggle,
4840
5926
  ToggleGroup,
4841
5927
  ToggleGroupItem,