@ohhwells/bridge 0.1.14 → 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.cjs CHANGED
@@ -32,6 +32,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
32
32
  var index_exports = {};
33
33
  __export(index_exports, {
34
34
  OhhwellsBridge: () => OhhwellsBridge,
35
+ SchedulingWidget: () => SchedulingWidget,
35
36
  Toggle: () => Toggle,
36
37
  ToggleGroup: () => ToggleGroup,
37
38
  ToggleGroupItem: () => ToggleGroupItem,
@@ -40,10 +41,752 @@ __export(index_exports, {
40
41
  module.exports = __toCommonJS(index_exports);
41
42
 
42
43
  // src/OhhwellsBridge.tsx
43
- var import_react = __toESM(require("react"), 1);
44
+ var import_react3 = __toESM(require("react"), 1);
45
+ var import_client = require("react-dom/client");
46
+ var import_react_dom = require("react-dom");
47
+
48
+ // src/ui/SchedulingWidget.tsx
49
+ var import_react2 = require("react");
50
+
51
+ // src/ui/EmailCaptureModal.tsx
52
+ var import_react = require("react");
53
+ var import_radix_ui = require("radix-ui");
54
+ var import_jsx_runtime = require("react/jsx-runtime");
55
+ function Spinner() {
56
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
57
+ "svg",
58
+ {
59
+ width: "32",
60
+ height: "32",
61
+ viewBox: "0 0 32 32",
62
+ fill: "none",
63
+ style: { animation: "ohw-spin 0.75s linear infinite" },
64
+ children: [
65
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: `@keyframes ohw-spin { to { transform: rotate(360deg); } }` }),
66
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "16", cy: "16", r: "12", stroke: "#e5e7eb", strokeWidth: "3" }),
67
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
68
+ "path",
69
+ {
70
+ d: "M16 4a12 12 0 0 1 12 12",
71
+ stroke: "#3b82f6",
72
+ strokeWidth: "3",
73
+ strokeLinecap: "round"
74
+ }
75
+ )
76
+ ]
77
+ }
78
+ );
79
+ }
80
+ function EmailCaptureModal({ title, subtitle, onSubmit, onClose }) {
81
+ const [email, setEmail] = (0, import_react.useState)("");
82
+ const [submittedEmail, setSubmittedEmail] = (0, import_react.useState)("");
83
+ const [loading, setLoading] = (0, import_react.useState)(false);
84
+ const [success, setSuccess] = (0, import_react.useState)(false);
85
+ const [error, setError] = (0, import_react.useState)(null);
86
+ const isBook = title === "Confirm your spot";
87
+ const handleSubmit = async () => {
88
+ if (!email.trim()) return;
89
+ setLoading(true);
90
+ setError(null);
91
+ try {
92
+ await onSubmit(email.trim());
93
+ setSubmittedEmail(email.trim());
94
+ setSuccess(true);
95
+ } catch (e) {
96
+ setError(e instanceof Error ? e.message : "Something went wrong. Please try again.");
97
+ } finally {
98
+ setLoading(false);
99
+ }
100
+ };
101
+ const handleKeyDown = (e) => {
102
+ if (e.key === "Enter" && !loading) handleSubmit();
103
+ };
104
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_radix_ui.Dialog.Root, { open: true, onOpenChange: (open) => {
105
+ if (!open) onClose();
106
+ }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_radix_ui.Dialog.Portal, { children: [
107
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
108
+ import_radix_ui.Dialog.Overlay,
109
+ {
110
+ className: "fixed inset-0 z-50",
111
+ style: { background: "rgba(0,0,0,0.45)" }
112
+ }
113
+ ),
114
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
115
+ import_radix_ui.Dialog.Content,
116
+ {
117
+ 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",
118
+ style: { maxWidth: 400 },
119
+ children: [
120
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-start justify-between gap-3 p-6", children: [
121
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex flex-col gap-1", children: [
122
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
123
+ import_radix_ui.Dialog.Title,
124
+ {
125
+ className: "font-body text-xl font-bold m-0 leading-tight",
126
+ style: { color: "var(--color-dark,#200C02)" },
127
+ children: success ? isBook ? "Reservation confirmed!" : "You're on the waitlist!" : title
128
+ }
129
+ ),
130
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
131
+ import_radix_ui.Dialog.Description,
132
+ {
133
+ className: "font-body text-sm m-0",
134
+ style: { color: "#6B7280" },
135
+ children: success ? isBook ? `A confirmation email has been sent to ${submittedEmail}.` : "We'll let you know via email if someone cancels." : subtitle
136
+ }
137
+ )
138
+ ] }),
139
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
140
+ import_radix_ui.Dialog.Close,
141
+ {
142
+ 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",
143
+ style: { color: "#6B7280" },
144
+ "aria-label": "Close",
145
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
146
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
147
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
148
+ ] })
149
+ }
150
+ )
151
+ ] }),
152
+ !success && (loading ? (
153
+ /* Loading state — full body replaced */
154
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex flex-col items-center justify-center gap-3 px-7 py-12", children: [
155
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {}),
156
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "font-body text-sm m-0", style: { color: "#6B7280" }, children: "Please wait" })
157
+ ] })
158
+ ) : (
159
+ /* Email form */
160
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "px-7 pt-2 pb-6", children: [
161
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex flex-col gap-1.5 mb-8", children: [
162
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { className: "font-body text-sm font-medium", style: { color: "var(--color-dark,#200C02)" }, children: "Email" }),
163
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
164
+ "input",
165
+ {
166
+ type: "email",
167
+ value: email,
168
+ onChange: (e) => setEmail(e.target.value),
169
+ onKeyDown: handleKeyDown,
170
+ placeholder: "hello@gmail.com",
171
+ autoFocus: true,
172
+ className: "w-full h-10 px-3 rounded-lg border font-body text-sm outline-none box-border",
173
+ style: {
174
+ borderColor: "#E7E5E4",
175
+ background: "#fff",
176
+ color: "var(--color-dark,#200C02)"
177
+ }
178
+ }
179
+ ),
180
+ error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "font-body text-xs text-red-500 m-0", children: error })
181
+ ] }),
182
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex justify-end", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
183
+ "button",
184
+ {
185
+ onClick: handleSubmit,
186
+ disabled: !email.trim(),
187
+ className: "h-9 px-5 rounded-lg font-body text-sm font-semibold cursor-pointer border-none disabled:opacity-50 disabled:cursor-not-allowed",
188
+ style: {
189
+ background: "var(--color-primary,#3D312B)",
190
+ color: "var(--color-light,#FEFFF0)"
191
+ },
192
+ children: "Send"
193
+ }
194
+ ) })
195
+ ] })
196
+ ))
197
+ ]
198
+ }
199
+ )
200
+ ] }) });
201
+ }
202
+
203
+ // src/ui/SchedulingWidget.tsx
204
+ var import_jsx_runtime2 = require("react/jsx-runtime");
205
+ var API_URL = process.env.NEXT_PUBLIC_FLOWOPS_API_URL ?? "https://flowops-backend-staging-2yfdh7pwpq-as.a.run.app";
206
+ var DAY_ABBR = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"];
207
+ function getApiDomain() {
208
+ const h = window.location.hostname;
209
+ for (const base of ["ohhwells.site", "theflowops-staging.com", "theflowops.com"]) {
210
+ if (h.endsWith(`.${base}`)) return h.slice(0, -(base.length + 1));
211
+ }
212
+ return h;
213
+ }
214
+ function classRunsOnDate(cls, date, type) {
215
+ if (type === "FIXED") {
216
+ const d = String(date.getDate()).padStart(2, "0");
217
+ const m = String(date.getMonth() + 1).padStart(2, "0");
218
+ return cls.fixedDates?.some((fd) => fd.date === `${d}-${m}-${date.getFullYear()}`) ?? false;
219
+ }
220
+ return cls.weekdays?.some((w) => w.weekday === date.getDay()) ?? false;
221
+ }
222
+ function formatClassTime(cls) {
223
+ let h = parseInt(cls.startTime.hour, 10);
224
+ const m = parseInt(cls.startTime.minutes, 10);
225
+ if (cls.startTime.time === "PM" && h !== 12) h += 12;
226
+ if (cls.startTime.time === "AM" && h === 12) h = 0;
227
+ const endTotal = h * 60 + m + cls.duration;
228
+ const eh = Math.floor(endTotal / 60) % 24;
229
+ const em = endTotal % 60;
230
+ return `${h}:${cls.startTime.minutes}-${eh}:${String(em).padStart(2, "0")}`;
231
+ }
232
+ function getBookingsOnDate(cls, date) {
233
+ if (!cls.bookings?.length) return 0;
234
+ const ds = date.toISOString().split("T")[0];
235
+ return cls.bookings.filter((b) => {
236
+ try {
237
+ return new Date(b.classDate).toISOString().split("T")[0] === ds;
238
+ } catch {
239
+ return false;
240
+ }
241
+ }).length;
242
+ }
243
+ function buildTimezoneLabel(tz) {
244
+ try {
245
+ const now = /* @__PURE__ */ new Date();
246
+ const timeStr = new Intl.DateTimeFormat("en-US", {
247
+ hour: "2-digit",
248
+ minute: "2-digit",
249
+ hour12: false,
250
+ timeZone: tz
251
+ }).format(now);
252
+ const offsetPart = new Intl.DateTimeFormat("en-US", { timeZoneName: "short", timeZone: tz }).formatToParts(now).find((p) => p.type === "timeZoneName")?.value ?? "";
253
+ const city = tz.split("/").pop()?.replace(/_/g, " ") ?? tz;
254
+ return `${timeStr} (${offsetPart}) ${city} time`;
255
+ } catch {
256
+ return tz;
257
+ }
258
+ }
259
+ function EmptyState({ inEditor }) {
260
+ const handleAddSchedule = () => {
261
+ if (inEditor) {
262
+ window.parent.postMessage({ type: "ow:scheduling-not-connected" }, "*");
263
+ }
264
+ };
265
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("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: [
266
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "w-10 h-10 bg-[#F5F5F4] rounded-[10px] flex items-center justify-center shrink-0", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
267
+ "svg",
268
+ {
269
+ width: "20",
270
+ height: "20",
271
+ viewBox: "0 0 24 24",
272
+ fill: "none",
273
+ stroke: "var(--color-accent, #A89B83)",
274
+ strokeWidth: "1.5",
275
+ strokeLinecap: "round",
276
+ strokeLinejoin: "round",
277
+ children: [
278
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("rect", { x: "3", y: "4", width: "18", height: "18", rx: "2" }),
279
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "16", y1: "2", x2: "16", y2: "6" }),
280
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "8", y1: "2", x2: "8", y2: "6" }),
281
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "3", y1: "10", x2: "21", y2: "10" }),
282
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "12", y1: "14", x2: "12", y2: "18" }),
283
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "10", y1: "16", x2: "14", y2: "16" })
284
+ ]
285
+ }
286
+ ) }),
287
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex flex-col items-center gap-2", children: [
288
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "font-body text-lg font-medium text-center text-[#0A0A0A] m-0", children: "No schedule yet" }),
289
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("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." })
290
+ ] }),
291
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
292
+ "button",
293
+ {
294
+ onClick: handleAddSchedule,
295
+ 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",
296
+ children: "Add Schedule"
297
+ }
298
+ )
299
+ ] });
300
+ }
301
+ function LoadingSkeleton() {
302
+ const shimmer = {
303
+ background: "linear-gradient(90deg,#f0f0f0 25%,#e8e8e8 50%,#f0f0f0 75%)",
304
+ backgroundSize: "200% 100%",
305
+ animation: "ohw-shimmer 1.5s infinite"
306
+ };
307
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "w-full flex flex-col gap-10", children: [
308
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("style", { children: `@keyframes ohw-shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}` }),
309
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "flex h-[70px] items-stretch", children: Array.from({ length: 7 }).map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex-1 flex flex-col justify-between items-center px-1", children: [
310
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { ...shimmer, width: "32px", height: "14px", borderRadius: "4px" } }),
311
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { ...shimmer, width: "40px", height: "40px", borderRadius: "50%" } })
312
+ ] }, i)) }),
313
+ [0, 1, 2].map((i) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
314
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex gap-4 py-4 sm:hidden", children: [
315
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex flex-col gap-2 shrink-0", children: [
316
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { ...shimmer, width: "100px", height: "16px", borderRadius: "4px" } }),
317
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { ...shimmer, width: "52px", height: "20px", borderRadius: "999px" } }),
318
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { ...shimmer, width: "60px", height: "14px", borderRadius: "4px" } })
319
+ ] }),
320
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex flex-1 flex-col gap-2", children: [
321
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { ...shimmer, width: "75%", height: "16px", borderRadius: "4px" } }),
322
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { ...shimmer, width: "50%", height: "14px", borderRadius: "4px" } }),
323
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { ...shimmer, width: "100%", height: "44px", borderRadius: "8px" } })
324
+ ] })
325
+ ] }),
326
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "hidden sm:flex items-center gap-[60px] py-4", children: [
327
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { ...shimmer, width: "120px", height: "16px", borderRadius: "4px", flexShrink: 0 } }),
328
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex-1 flex flex-col gap-2", children: [
329
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { ...shimmer, width: "55%", height: "16px", borderRadius: "4px" } }),
330
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { ...shimmer, width: "38%", height: "14px", borderRadius: "4px" } })
331
+ ] }),
332
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { ...shimmer, width: "56px", height: "32px", borderRadius: "4px", flexShrink: 0 } }),
333
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { ...shimmer, width: "200px", height: "44px", borderRadius: "8px", flexShrink: 0 } })
334
+ ] }),
335
+ i < 2 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "h-px bg-black/20" })
336
+ ] }, i))
337
+ ] });
338
+ }
339
+ function ScheduleView({ schedule, dates, selectedIdx, onSelectDate, onOpenModal }) {
340
+ const todayMs = (0, import_react2.useMemo)(() => {
341
+ const d = /* @__PURE__ */ new Date();
342
+ d.setHours(0, 0, 0, 0);
343
+ return d.getTime();
344
+ }, []);
345
+ const scrollRef = (0, import_react2.useRef)(null);
346
+ const [canScrollLeft, setCanScrollLeft] = (0, import_react2.useState)(false);
347
+ const [canScrollRight, setCanScrollRight] = (0, import_react2.useState)(false);
348
+ const updateScrollState = () => {
349
+ const el = scrollRef.current;
350
+ if (!el) return;
351
+ setCanScrollLeft(el.scrollLeft > 0);
352
+ setCanScrollRight(el.scrollLeft + el.clientWidth < el.scrollWidth - 1);
353
+ };
354
+ (0, import_react2.useEffect)(() => {
355
+ const el = scrollRef.current;
356
+ if (!el) return;
357
+ updateScrollState();
358
+ el.addEventListener("scroll", updateScrollState);
359
+ const ro = new ResizeObserver(updateScrollState);
360
+ ro.observe(el);
361
+ return () => {
362
+ el.removeEventListener("scroll", updateScrollState);
363
+ ro.disconnect();
364
+ };
365
+ }, [dates]);
366
+ const scroll = (dir) => {
367
+ scrollRef.current?.scrollBy({ left: dir === "left" ? -216 : 216, behavior: "smooth" });
368
+ };
369
+ const isDragging = (0, import_react2.useRef)(false);
370
+ const dragStartX = (0, import_react2.useRef)(0);
371
+ const dragStartScrollLeft = (0, import_react2.useRef)(0);
372
+ const onDragStart = (e) => {
373
+ if (!scrollRef.current) return;
374
+ isDragging.current = true;
375
+ dragStartX.current = e.clientX;
376
+ dragStartScrollLeft.current = scrollRef.current.scrollLeft;
377
+ scrollRef.current.style.cursor = "grabbing";
378
+ scrollRef.current.style.userSelect = "none";
379
+ };
380
+ const onDragMove = (e) => {
381
+ if (!isDragging.current || !scrollRef.current) return;
382
+ const delta = e.clientX - dragStartX.current;
383
+ scrollRef.current.scrollLeft = dragStartScrollLeft.current - delta;
384
+ };
385
+ const onDragEnd = () => {
386
+ if (!scrollRef.current) return;
387
+ isDragging.current = false;
388
+ scrollRef.current.style.cursor = "grab";
389
+ scrollRef.current.style.userSelect = "";
390
+ };
391
+ const datesWithClasses = (0, import_react2.useMemo)(
392
+ () => new Set(dates.map(
393
+ (d, i) => schedule.classes?.some((c) => classRunsOnDate(c, d, schedule.type)) ? i : -1
394
+ ).filter((i) => i >= 0)),
395
+ [dates, schedule]
396
+ );
397
+ const selectedDate = dates[selectedIdx];
398
+ const selectedClasses = (0, import_react2.useMemo)(
399
+ () => (schedule.classes ?? []).filter((c) => selectedDate && classRunsOnDate(c, selectedDate, schedule.type)),
400
+ [schedule, selectedDate]
401
+ );
402
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "w-full flex flex-col gap-10", children: [
403
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "relative w-full", children: [
404
+ canScrollLeft && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
405
+ "button",
406
+ {
407
+ onClick: () => scroll("left"),
408
+ 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",
409
+ style: { background: "var(--color-light,#FEFFF0)", boxShadow: "0 1px 4px rgba(0,0,0,0.15)" },
410
+ "aria-label": "Scroll left",
411
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("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__ */ (0, import_jsx_runtime2.jsx)("polyline", { points: "15 18 9 12 15 6" }) })
412
+ }
413
+ ),
414
+ canScrollRight && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
415
+ "button",
416
+ {
417
+ onClick: () => scroll("right"),
418
+ 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",
419
+ style: { background: "var(--color-light,#FEFFF0)", boxShadow: "0 1px 4px rgba(0,0,0,0.15)" },
420
+ "aria-label": "Scroll right",
421
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("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__ */ (0, import_jsx_runtime2.jsx)("polyline", { points: "9 18 15 12 9 6" }) })
422
+ }
423
+ ),
424
+ canScrollLeft && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
425
+ "div",
426
+ {
427
+ className: "sm:hidden absolute left-0 top-0 bottom-0 w-10 z-10 pointer-events-none",
428
+ style: { background: "linear-gradient(to right, var(--color-light,#FEFFF0), transparent)" }
429
+ }
430
+ ),
431
+ canScrollRight && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
432
+ "div",
433
+ {
434
+ className: "sm:hidden absolute right-0 top-0 bottom-0 w-10 z-10 pointer-events-none",
435
+ style: { background: "linear-gradient(to left, var(--color-light,#FEFFF0), transparent)" }
436
+ }
437
+ ),
438
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
439
+ "div",
440
+ {
441
+ ref: scrollRef,
442
+ className: "overflow-x-auto w-full",
443
+ style: { scrollbarWidth: "none", msOverflowStyle: "none", touchAction: "pan-x", cursor: "grab" },
444
+ onMouseDown: onDragStart,
445
+ onMouseMove: onDragMove,
446
+ onMouseUp: onDragEnd,
447
+ onMouseLeave: onDragEnd,
448
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "flex", style: { width: `max(100%, ${dates.length * 60}px)` }, children: dates.map((date, i) => {
449
+ const isToday = date.getTime() === todayMs;
450
+ const isSelected = i === selectedIdx;
451
+ const clickable = datesWithClasses.has(i);
452
+ const label = isToday ? "TODAY" : DAY_ABBR[date.getDay()];
453
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
454
+ "div",
455
+ {
456
+ onClick: () => clickable && onSelectDate(i),
457
+ className: `flex-1 h-[70px] flex flex-col justify-between items-center ${clickable ? "cursor-pointer opacity-100" : "cursor-default opacity-50"}`,
458
+ children: [
459
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "font-body text-base font-normal text-center text-(--color-dark,#200C02)", children: label }),
460
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
461
+ "div",
462
+ {
463
+ className: "w-10 h-10 rounded-full flex items-center justify-center",
464
+ style: { background: isSelected ? "var(--color-primary, #3D312B)" : "transparent" },
465
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
466
+ "span",
467
+ {
468
+ className: "font-body text-xl font-bold text-center tracking-display-tight",
469
+ style: { color: isSelected ? "var(--color-light, #FEFFF0)" : "var(--color-dark, #200C02)" },
470
+ children: date.getDate()
471
+ }
472
+ )
473
+ }
474
+ )
475
+ ]
476
+ },
477
+ i
478
+ );
479
+ }) })
480
+ }
481
+ )
482
+ ] }),
483
+ selectedClasses.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "font-body text-base text-(--color-accent,#A89B83) text-center py-8 m-0", children: "No classes scheduled for this day." }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "flex flex-col", children: selectedClasses.map((cls, i) => {
484
+ const booked = getBookingsOnDate(cls, selectedDate);
485
+ const available = cls.maxParticipants - booked;
486
+ const isFull = available <= 0;
487
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
488
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex gap-4 py-4 sm:items-center sm:gap-[60px] box-border", children: [
489
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex flex-col gap-2 shrink-0 sm:contents", children: [
490
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "sm:w-[120px] sm:shrink-0", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "font-body text-base font-bold text-(--color-dark,#200C02) block", children: formatClassTime(cls) }) }),
491
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
492
+ "span",
493
+ {
494
+ className: "inline-flex items-center px-2 py-0.5 rounded-full border text-xs font-medium sm:hidden",
495
+ style: { borderColor: "#0885FE", color: "#0885FE" },
496
+ children: "GROUP"
497
+ }
498
+ ),
499
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "sm:hidden flex flex-col gap-px", children: isFull ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: "Full" }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
500
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: [
501
+ available,
502
+ "/",
503
+ cls.maxParticipants
504
+ ] }),
505
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: "available" })
506
+ ] }) })
507
+ ] }),
508
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex flex-1 flex-col gap-2 min-w-0 sm:contents", children: [
509
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex-1 flex flex-col gap-2 min-w-0", children: [
510
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "font-body text-base font-bold text-(--color-dark,#200C02) block truncate", children: cls.name }),
511
+ cls.hostName && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "font-body text-base font-normal text-(--color-dark,#200C02) opacity-80 block truncate", children: cls.hostName })
512
+ ] }),
513
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "hidden sm:flex w-14 shrink-0 flex-col gap-px", children: isFull ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: "Full" }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
514
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: [
515
+ available,
516
+ "/",
517
+ cls.maxParticipants
518
+ ] }),
519
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: "available" })
520
+ ] }) }),
521
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
522
+ "button",
523
+ {
524
+ 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",
525
+ style: isFull ? {
526
+ background: "transparent",
527
+ border: "1px solid var(--color-dark, #200C02)",
528
+ color: "var(--color-dark, #200C02)"
529
+ } : {
530
+ background: "var(--color-primary, #3D312B)",
531
+ border: "none",
532
+ color: "var(--color-light, #FEFFF0)"
533
+ },
534
+ onClick: () => {
535
+ if (!cls.id) return;
536
+ onOpenModal?.(isFull ? "waitlist" : "book", cls.id, selectedDate);
537
+ },
538
+ children: isFull ? "Join Waitlist" : "Book Now"
539
+ }
540
+ )
541
+ ] })
542
+ ] }),
543
+ i < selectedClasses.length - 1 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "h-px bg-black/20" })
544
+ ] }, cls.id ?? i);
545
+ }) })
546
+ ] });
547
+ }
548
+ function CalendarFoldIcon() {
549
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
550
+ "svg",
551
+ {
552
+ width: "16",
553
+ height: "16",
554
+ viewBox: "0 0 24 24",
555
+ fill: "none",
556
+ stroke: "currentColor",
557
+ strokeWidth: "2",
558
+ strokeLinecap: "round",
559
+ strokeLinejoin: "round",
560
+ style: { flexShrink: 0 },
561
+ children: [
562
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M8 2v4" }),
563
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M16 2v4" }),
564
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M21 17V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h11Z" }),
565
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M3 10h18" }),
566
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M15 22v-4a2 2 0 0 1 2-2h4" })
567
+ ]
568
+ }
569
+ );
570
+ }
571
+ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAfter }) {
572
+ const [schedule, setSchedule] = (0, import_react2.useState)(null);
573
+ const [loading, setLoading] = (0, import_react2.useState)(true);
574
+ const [inEditor, setInEditor] = (0, import_react2.useState)(false);
575
+ const [isHovered, setIsHovered] = (0, import_react2.useState)(false);
576
+ const [modalState, setModalState] = (0, import_react2.useState)(null);
577
+ const switchScheduleIdRef = (0, import_react2.useRef)(null);
578
+ const dates = (0, import_react2.useMemo)(() => {
579
+ const today = /* @__PURE__ */ new Date();
580
+ today.setHours(0, 0, 0, 0);
581
+ return Array.from({ length: 14 }, (_, i) => {
582
+ const d = new Date(today);
583
+ d.setDate(today.getDate() + i);
584
+ return d;
585
+ });
586
+ }, []);
587
+ const [selectedIdx, setSelectedIdx] = (0, import_react2.useState)(0);
588
+ (0, import_react2.useEffect)(() => {
589
+ if (!schedule?.classes) return;
590
+ for (let i = 0; i < dates.length; i++) {
591
+ if (schedule.classes.some((c) => classRunsOnDate(c, dates[i], schedule.type))) {
592
+ setSelectedIdx(i);
593
+ return;
594
+ }
595
+ }
596
+ }, [schedule, dates]);
597
+ (0, import_react2.useEffect)(() => {
598
+ if (typeof window === "undefined") return;
599
+ const isInEditor = window.self !== window.top;
600
+ setInEditor(isInEditor);
601
+ if (isInEditor) {
602
+ const startEmpty = initialScheduleId === null;
603
+ if (startEmpty) setLoading(false);
604
+ let initialized = startEmpty;
605
+ const timer = !startEmpty ? setTimeout(() => {
606
+ if (!initialized) {
607
+ initialized = true;
608
+ setLoading(false);
609
+ }
610
+ }, 5e3) : null;
611
+ const handler = (e) => {
612
+ if (e.data?.type === "ow:clear-scheduling-widget") {
613
+ if (!e.data.insertAfter || e.data.insertAfter !== insertAfter) return;
614
+ setSchedule(null);
615
+ setLoading(false);
616
+ return;
617
+ }
618
+ if (e.data?.type === "ow:switch-schedule") {
619
+ if (!e.data.insertAfter || e.data.insertAfter !== insertAfter) return;
620
+ switchScheduleIdRef.current = e.data.scheduleId ?? null;
621
+ initialized = false;
622
+ setLoading(true);
623
+ window.parent.postMessage({ type: "ow:request-user-schedules", insertAfter }, "*");
624
+ return;
625
+ }
626
+ if (e.data?.type !== "ow:user-schedules-response") return;
627
+ if (e.data.insertAfter && e.data.insertAfter !== insertAfter) return;
628
+ if (initialized && switchScheduleIdRef.current === null) return;
629
+ initialized = true;
630
+ if (timer) clearTimeout(timer);
631
+ const schedules = e.data.schedules ?? [];
632
+ const isSwitching = switchScheduleIdRef.current !== null;
633
+ 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;
634
+ switchScheduleIdRef.current = null;
635
+ setSchedule(target);
636
+ setLoading(false);
637
+ if (notifyOnConnect && target && !isSwitching) {
638
+ window.parent.postMessage({
639
+ type: "ow:schedule-connected",
640
+ schedule: { id: target.id, name: target.name },
641
+ insertAfter
642
+ }, "*");
643
+ window.postMessage({ type: "ow:schedule-linked", scheduleId: target.id, insertAfter }, "*");
644
+ }
645
+ };
646
+ window.addEventListener("message", handler);
647
+ if (!startEmpty) window.parent.postMessage({ type: "ow:request-user-schedules", insertAfter }, "*");
648
+ return () => {
649
+ window.removeEventListener("message", handler);
650
+ if (timer) clearTimeout(timer);
651
+ };
652
+ }
653
+ if (initialScheduleId === null) {
654
+ setLoading(false);
655
+ return;
656
+ }
657
+ ;
658
+ (async () => {
659
+ try {
660
+ if (initialScheduleId) {
661
+ const res = await fetch(`${API_URL}/api/schedule/id/${initialScheduleId}`);
662
+ const data = await res.json();
663
+ setSchedule(data?.id ? data : null);
664
+ } else {
665
+ const domain = getApiDomain();
666
+ const sitemapRes = await fetch(`${API_URL}/api/schedule/sitemap/${domain}`);
667
+ const slugs = await sitemapRes.json();
668
+ if (!Array.isArray(slugs) || !slugs.length) {
669
+ setLoading(false);
670
+ return;
671
+ }
672
+ const { slug } = [...slugs].sort(
673
+ (a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()
674
+ )[0];
675
+ const res = await fetch(`${API_URL}/api/schedule/${domain}/${slug}`);
676
+ const data = await res.json();
677
+ setSchedule(data?.id ? data : null);
678
+ }
679
+ } catch {
680
+ }
681
+ setLoading(false);
682
+ })();
683
+ }, []);
684
+ const timezoneLabel = schedule?.timezone ? (() => {
685
+ try {
686
+ return buildTimezoneLabel(schedule.timezone);
687
+ } catch {
688
+ return void 0;
689
+ }
690
+ })() : void 0;
691
+ const handleModalSubmit = async (email) => {
692
+ if (!modalState) return;
693
+ const { mode, classId, classDate } = modalState;
694
+ const endpoint = mode === "book" ? `${API_URL}/api/schedule/class/${classId}/booking` : `${API_URL}/api/schedule/class/${classId}/waitlist`;
695
+ const res = await fetch(endpoint, {
696
+ method: "POST",
697
+ headers: { "Content-Type": "application/json" },
698
+ body: JSON.stringify({ classDate: classDate.toISOString(), email })
699
+ });
700
+ if (!res.ok) {
701
+ const data = await res.json().catch(() => ({}));
702
+ throw new Error(data?.message ?? "Something went wrong. Please try again.");
703
+ }
704
+ };
705
+ const handleReplaceSchedule = () => {
706
+ setIsHovered(false);
707
+ if (schedule) {
708
+ window.parent.postMessage({
709
+ type: "ow:schedule-connected",
710
+ schedule: { id: schedule.id, name: schedule.name },
711
+ insertAfter
712
+ }, "*");
713
+ } else {
714
+ window.parent.postMessage({ type: "ow:scheduling-not-connected", insertAfter }, "*");
715
+ }
716
+ };
717
+ const sectionId = `scheduling-${insertAfter}`;
718
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
719
+ "section",
720
+ {
721
+ "data-ohw-section": sectionId,
722
+ "data-ohw-scheduling-anchor": insertAfter,
723
+ className: "w-full box-border py-10 px-5 sm:py-20 sm:px-16 font-body bg-(--color-light,#FEFFF0) relative",
724
+ onMouseEnter: () => inEditor && setIsHovered(true),
725
+ onMouseLeave: () => setIsHovered(false),
726
+ children: [
727
+ inEditor && isHovered && !!schedule && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
728
+ "div",
729
+ {
730
+ className: "absolute inset-0 z-10 pointer-events-auto cursor-pointer",
731
+ onClick: handleReplaceSchedule,
732
+ children: [
733
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "absolute inset-0", style: { background: "#0885FE", opacity: 0.18 } }),
734
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "absolute inset-0", style: { boxShadow: "inset 0 0 0 1.5px #0885FE" } }),
735
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "absolute inset-0 flex items-center justify-center pointer-events-none", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
736
+ "div",
737
+ {
738
+ className: "bg-white border border-[#E7E5E4] rounded-md px-3 py-1.5 flex items-center gap-1.5",
739
+ style: {
740
+ fontSize: 14,
741
+ lineHeight: "24px",
742
+ fontFamily: "'Figtree', system-ui, sans-serif",
743
+ fontWeight: 500,
744
+ color: "#0C0A09"
745
+ },
746
+ children: [
747
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(CalendarFoldIcon, {}),
748
+ "Replace schedule"
749
+ ]
750
+ }
751
+ ) })
752
+ ]
753
+ }
754
+ ),
755
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "max-w-[1280px] mx-auto flex flex-col items-center gap-16", children: [
756
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex flex-col items-center gap-4", children: [
757
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("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" }),
758
+ schedule?.description && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "font-body text-body text-center m-0 text-(--color-accent,#A89B83)", children: schedule.description })
759
+ ] }),
760
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "w-full flex flex-col items-end gap-3", children: [
761
+ timezoneLabel && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "font-body text-sm font-normal text-(--color-accent,#A89B83)", children: timezoneLabel }),
762
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "w-full p-0 sm:p-10 box-border", children: loading ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(LoadingSkeleton, {}) : !schedule ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(EmptyState, { inEditor }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
763
+ ScheduleView,
764
+ {
765
+ schedule,
766
+ dates,
767
+ selectedIdx,
768
+ onSelectDate: setSelectedIdx,
769
+ onOpenModal: inEditor ? void 0 : (mode, classId, classDate) => setModalState({ mode, classId, classDate })
770
+ }
771
+ ) })
772
+ ] })
773
+ ] }),
774
+ modalState && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
775
+ EmailCaptureModal,
776
+ {
777
+ title: modalState.mode === "book" ? "Confirm your spot" : "Leave your email",
778
+ subtitle: modalState.mode === "book" ? "We'll send your booking confirmation here." : "We'll let you know when spots open up!",
779
+ onSubmit: handleModalSubmit,
780
+ onClose: () => setModalState(null)
781
+ }
782
+ )
783
+ ]
784
+ }
785
+ );
786
+ }
44
787
 
45
788
  // src/ui/toggle-group.tsx
46
- var React = __toESM(require("react"), 1);
789
+ var React2 = __toESM(require("react"), 1);
47
790
 
48
791
  // node_modules/clsx/dist/clsx.mjs
49
792
  function r(e) {
@@ -3362,8 +4105,8 @@ var cva = (base, config) => (props) => {
3362
4105
  };
3363
4106
 
3364
4107
  // src/ui/toggle.tsx
3365
- var import_radix_ui = require("radix-ui");
3366
- var import_jsx_runtime = require("react/jsx-runtime");
4108
+ var import_radix_ui2 = require("radix-ui");
4109
+ var import_jsx_runtime3 = require("react/jsx-runtime");
3367
4110
  var toggleVariants = cva(
3368
4111
  "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",
3369
4112
  {
@@ -3390,8 +4133,8 @@ function Toggle({
3390
4133
  size,
3391
4134
  ...props
3392
4135
  }) {
3393
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
3394
- import_radix_ui.Toggle.Root,
4136
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
4137
+ import_radix_ui2.Toggle.Root,
3395
4138
  {
3396
4139
  "data-slot": "toggle",
3397
4140
  className: cn(toggleVariants({ variant, size, className })),
@@ -3401,8 +4144,8 @@ function Toggle({
3401
4144
  }
3402
4145
 
3403
4146
  // src/ui/toggle-group.tsx
3404
- var import_jsx_runtime2 = require("react/jsx-runtime");
3405
- var ToggleGroupContext = React.createContext({
4147
+ var import_jsx_runtime4 = require("react/jsx-runtime");
4148
+ var ToggleGroupContext = React2.createContext({
3406
4149
  value: "",
3407
4150
  onValueChange: () => {
3408
4151
  }
@@ -3414,13 +4157,13 @@ function ToggleGroup({
3414
4157
  children,
3415
4158
  ...props
3416
4159
  }) {
3417
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
4160
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
3418
4161
  "div",
3419
4162
  {
3420
4163
  role: "group",
3421
4164
  className: cn("flex items-center gap-1 p-1 bg-muted rounded-md", className),
3422
4165
  ...props,
3423
- children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ToggleGroupContext.Provider, { value: { value, onValueChange }, children })
4166
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ToggleGroupContext.Provider, { value: { value, onValueChange }, children })
3424
4167
  }
3425
4168
  );
3426
4169
  }
@@ -3430,12 +4173,14 @@ function ToggleGroupItem({
3430
4173
  children,
3431
4174
  ...props
3432
4175
  }) {
3433
- const { value: groupValue, onValueChange } = React.useContext(ToggleGroupContext);
3434
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
4176
+ const { value: groupValue, onValueChange } = React2.useContext(ToggleGroupContext);
4177
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
3435
4178
  Toggle,
3436
4179
  {
3437
4180
  pressed: groupValue === value,
3438
- onPressedChange: () => onValueChange(value),
4181
+ onPressedChange: (pressed) => {
4182
+ if (pressed) onValueChange(value);
4183
+ },
3439
4184
  className: cn(
3440
4185
  "text-foreground cursor-pointer hover:bg-transparent hover:text-foreground",
3441
4186
  "data-[state=on]:bg-background data-[state=on]:shadow-sm",
@@ -3449,7 +4194,7 @@ function ToggleGroupItem({
3449
4194
  }
3450
4195
 
3451
4196
  // src/OhhwellsBridge.tsx
3452
- var import_react_dom = require("react-dom");
4197
+ var import_react_dom2 = require("react-dom");
3453
4198
  var import_navigation = require("next/navigation");
3454
4199
 
3455
4200
  // src/lib/session-search.ts
@@ -3470,8 +4215,30 @@ function isEditSessionActive() {
3470
4215
  return new URLSearchParams(q).get("mode") === "edit";
3471
4216
  }
3472
4217
 
4218
+ // src/ui/badge.tsx
4219
+ var import_jsx_runtime5 = require("react/jsx-runtime");
4220
+ var badgeVariants = cva(
4221
+ "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",
4222
+ {
4223
+ variants: {
4224
+ variant: {
4225
+ default: "border-transparent bg-primary text-primary-foreground",
4226
+ secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
4227
+ destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
4228
+ outline: "text-foreground"
4229
+ }
4230
+ },
4231
+ defaultVariants: {
4232
+ variant: "default"
4233
+ }
4234
+ }
4235
+ );
4236
+ function Badge({ className, variant, ...props }) {
4237
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: cn(badgeVariants({ variant }), className), ...props });
4238
+ }
4239
+
3473
4240
  // src/OhhwellsBridge.tsx
3474
- var import_jsx_runtime3 = require("react/jsx-runtime");
4241
+ var import_jsx_runtime6 = require("react/jsx-runtime");
3475
4242
  var PRIMARY = "#0885FE";
3476
4243
  var IMAGE_FADE_MS = 300;
3477
4244
  function runOpacityFade(el, onDone) {
@@ -3522,6 +4289,163 @@ function fadeInBgImage(el, url, onReady) {
3522
4289
  if (!prevPos || prevPos === "static") el.style.position = prevPos;
3523
4290
  });
3524
4291
  }
4292
+ function getSectionsTracker() {
4293
+ let el = document.querySelector("[data-ohw-sections-tracker]");
4294
+ if (!el) {
4295
+ el = document.createElement("div");
4296
+ el.setAttribute("data-ohw-sections-tracker", "");
4297
+ el.style.display = "none";
4298
+ document.body.appendChild(el);
4299
+ }
4300
+ return el;
4301
+ }
4302
+ function updateSectionScheduleId(insertAfter, scheduleId) {
4303
+ const tracker = getSectionsTracker();
4304
+ let sections = [];
4305
+ try {
4306
+ sections = JSON.parse(tracker.textContent || "[]");
4307
+ } catch {
4308
+ }
4309
+ const currentPath = window.location.pathname;
4310
+ const updated = sections.map((s) => {
4311
+ if (s.type !== "scheduling" || s.insertAfter !== insertAfter) return s;
4312
+ if (s.pagePath && s.pagePath !== currentPath) return s;
4313
+ return { ...s, scheduleId };
4314
+ });
4315
+ tracker.textContent = JSON.stringify(updated);
4316
+ return tracker.textContent ?? "[]";
4317
+ }
4318
+ function schedulingSectionId(insertAfter) {
4319
+ return `scheduling-${insertAfter}`;
4320
+ }
4321
+ var INSERT_BEFORE_MARKER = ":before:";
4322
+ function parseSchedulingInsertAfter(insertAfter) {
4323
+ const idx = insertAfter.indexOf(INSERT_BEFORE_MARKER);
4324
+ if (idx < 0) return { anchor: insertAfter, insertBefore: null };
4325
+ return {
4326
+ anchor: insertAfter.slice(0, idx),
4327
+ insertBefore: insertAfter.slice(idx + INSERT_BEFORE_MARKER.length)
4328
+ };
4329
+ }
4330
+ function resolveSchedulingInsert(insertAfter, explicitInsertBefore) {
4331
+ const parsed = parseSchedulingInsertAfter(insertAfter);
4332
+ const insertBefore = explicitInsertBefore ?? parsed.insertBefore;
4333
+ const effectiveInsertAfter = insertBefore && parsed.insertBefore === null ? `${insertAfter}${INSERT_BEFORE_MARKER}${insertBefore}` : insertAfter;
4334
+ return { effectiveInsertAfter, insertBefore };
4335
+ }
4336
+ function getSchedulingMountPoint(insertAfter) {
4337
+ const { anchor } = parseSchedulingInsertAfter(insertAfter);
4338
+ let anchorEl = document.querySelector(`[data-ohw-section="${anchor}"]`);
4339
+ if (!anchorEl && anchor === "scheduling") {
4340
+ const widgets = Array.from(document.querySelectorAll('[data-ohw-section^="scheduling-"]'));
4341
+ anchorEl = widgets.at(-1) ?? null;
4342
+ }
4343
+ if (!anchorEl) return null;
4344
+ return anchorEl.closest('[data-ohw-section-container="scheduling"]') ?? anchorEl;
4345
+ }
4346
+ function schedulingMountDepth(insertAfter) {
4347
+ if (!insertAfter.includes(INSERT_BEFORE_MARKER)) return 0;
4348
+ return insertAfter.split(INSERT_BEFORE_MARKER).length - 1;
4349
+ }
4350
+ function getPageSchedulingEntries(raw) {
4351
+ if (!raw) return [];
4352
+ try {
4353
+ const entries = JSON.parse(raw);
4354
+ const currentPath = window.location.pathname;
4355
+ return entries.filter((e) => e.type === "scheduling" && (!e.pagePath || e.pagePath === currentPath));
4356
+ } catch {
4357
+ return [];
4358
+ }
4359
+ }
4360
+ function isSchedulingWidgetMissing(entry) {
4361
+ const { effectiveInsertAfter } = resolveSchedulingInsert(entry.insertAfter);
4362
+ return !document.querySelector(`[data-ohw-section="${schedulingSectionId(effectiveInsertAfter)}"]`);
4363
+ }
4364
+ function hasMissingSchedulingWidgets(entries) {
4365
+ return entries.some(isSchedulingWidgetMissing);
4366
+ }
4367
+ function retryMissingSchedulingMounts(entries, notifyOnConnect = false) {
4368
+ if (!hasMissingSchedulingWidgets(entries)) return;
4369
+ mountSchedulingEntries(entries, notifyOnConnect);
4370
+ }
4371
+ function initSectionsFromContent(content, removeExisting = false) {
4372
+ const raw = content["__ohw_sections"];
4373
+ if (!raw) return;
4374
+ try {
4375
+ if (removeExisting) {
4376
+ document.querySelectorAll('[data-ohw-section-container="scheduling"]').forEach((el) => el.remove());
4377
+ }
4378
+ const inEditor = typeof window !== "undefined" && window.self !== window.top;
4379
+ if (inEditor) getSectionsTracker().textContent = raw;
4380
+ const pageEntries = getPageSchedulingEntries(raw);
4381
+ mountSchedulingEntries(pageEntries, false);
4382
+ requestAnimationFrame(() => retryMissingSchedulingMounts(pageEntries, false));
4383
+ setTimeout(() => retryMissingSchedulingMounts(pageEntries, false), 250);
4384
+ } catch {
4385
+ }
4386
+ }
4387
+ function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId, explicitInsertBefore) {
4388
+ const { effectiveInsertAfter, insertBefore } = resolveSchedulingInsert(insertAfter, explicitInsertBefore);
4389
+ const sectionId = schedulingSectionId(effectiveInsertAfter);
4390
+ if (document.querySelector(`[data-ohw-section="${sectionId}"]`)) return false;
4391
+ const mountPoint = getSchedulingMountPoint(effectiveInsertAfter);
4392
+ if (!mountPoint) return false;
4393
+ const container = document.createElement("div");
4394
+ container.dataset.ohwSectionContainer = "scheduling";
4395
+ if (insertBefore) {
4396
+ const beforeAnchor = document.querySelector(`[data-ohw-section="${insertBefore}"]`);
4397
+ const beforePoint = beforeAnchor?.closest('[data-ohw-section-container="scheduling"]') ?? beforeAnchor;
4398
+ if (!beforePoint) return false;
4399
+ beforePoint.insertAdjacentElement("beforebegin", container);
4400
+ } else {
4401
+ let tail = mountPoint;
4402
+ while (tail.nextElementSibling?.dataset.ohwSectionContainer === "scheduling") {
4403
+ tail = tail.nextElementSibling;
4404
+ }
4405
+ tail.insertAdjacentElement("afterend", container);
4406
+ }
4407
+ const root = (0, import_client.createRoot)(container);
4408
+ (0, import_react_dom.flushSync)(() => {
4409
+ root.render(
4410
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
4411
+ SchedulingWidget,
4412
+ {
4413
+ notifyOnConnect,
4414
+ initialScheduleId: scheduleId,
4415
+ insertAfter: effectiveInsertAfter
4416
+ }
4417
+ )
4418
+ );
4419
+ });
4420
+ const tracker = getSectionsTracker();
4421
+ let sections = [];
4422
+ try {
4423
+ sections = JSON.parse(tracker.textContent || "[]");
4424
+ } catch {
4425
+ }
4426
+ const inEditor = typeof window !== "undefined" && window.self !== window.top;
4427
+ if (inEditor && !sections.find((s) => s.type === "scheduling" && s.insertAfter === effectiveInsertAfter)) {
4428
+ sections.push({
4429
+ type: "scheduling",
4430
+ insertAfter: effectiveInsertAfter,
4431
+ pagePath: window.location.pathname,
4432
+ ...scheduleId ? { scheduleId } : {}
4433
+ });
4434
+ tracker.textContent = JSON.stringify(sections);
4435
+ }
4436
+ return true;
4437
+ }
4438
+ function mountSchedulingEntries(entries, notifyOnConnect = false) {
4439
+ const pending = entries.filter((e) => e.type === "scheduling").sort((a, b) => schedulingMountDepth(a.insertAfter) - schedulingMountDepth(b.insertAfter));
4440
+ for (let attempt = 0; attempt < pending.length + 1 && pending.length > 0; attempt++) {
4441
+ for (let i = pending.length - 1; i >= 0; i--) {
4442
+ const { insertAfter, scheduleId } = pending[i];
4443
+ if (mountSchedulingWidget(insertAfter, notifyOnConnect, scheduleId)) {
4444
+ pending.splice(i, 1);
4445
+ }
4446
+ }
4447
+ }
4448
+ }
3525
4449
  function collectEditableNodes() {
3526
4450
  return Array.from(document.querySelectorAll("[data-ohw-editable]")).map((el) => {
3527
4451
  if (el.dataset.ohwEditable === "image") {
@@ -3679,7 +4603,7 @@ var TOOLBAR_GROUPS = [
3679
4603
  ];
3680
4604
  function GlowFrame({ rect, elRef }) {
3681
4605
  const GAP = 6;
3682
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
4606
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3683
4607
  "div",
3684
4608
  {
3685
4609
  ref: elRef,
@@ -3730,7 +4654,7 @@ function FloatingToolbar({
3730
4654
  activeCommands
3731
4655
  }) {
3732
4656
  const { top, left, transform } = calcToolbarPos(rect, parentScroll);
3733
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
4657
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3734
4658
  "div",
3735
4659
  {
3736
4660
  ref: elRef,
@@ -3753,11 +4677,11 @@ function FloatingToolbar({
3753
4677
  pointerEvents: "auto",
3754
4678
  whiteSpace: "nowrap"
3755
4679
  },
3756
- children: TOOLBAR_GROUPS.map((btns, gi) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react.default.Fragment, { children: [
3757
- gi > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { display: "block", width: 1, height: 24, background: "#E7E5E4", flexShrink: 0 } }),
4680
+ children: TOOLBAR_GROUPS.map((btns, gi) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react3.default.Fragment, { children: [
4681
+ gi > 0 && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { style: { display: "block", width: 1, height: 24, background: "#E7E5E4", flexShrink: 0 } }),
3758
4682
  btns.map((btn) => {
3759
4683
  const isActive = activeCommands.has(btn.cmd);
3760
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
4684
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3761
4685
  "button",
3762
4686
  {
3763
4687
  title: btn.title,
@@ -3783,7 +4707,7 @@ function FloatingToolbar({
3783
4707
  flexShrink: 0,
3784
4708
  padding: 6
3785
4709
  },
3786
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
4710
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3787
4711
  "svg",
3788
4712
  {
3789
4713
  width: "16",
@@ -3816,7 +4740,7 @@ function StateToggle({
3816
4740
  states,
3817
4741
  onStateChange
3818
4742
  }) {
3819
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
4743
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3820
4744
  ToggleGroup,
3821
4745
  {
3822
4746
  "data-ohw-state-toggle": "",
@@ -3830,30 +4754,46 @@ function StateToggle({
3830
4754
  left: rect.right - 8,
3831
4755
  transform: "translateX(-100%)"
3832
4756
  },
3833
- children: states.map((state) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ToggleGroupItem, { value: state, children: state }, state))
4757
+ children: states.map((state) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ToggleGroupItem, { value: state, size: "sm", children: state }, state))
3834
4758
  }
3835
4759
  );
3836
4760
  }
3837
4761
  var contentCache = /* @__PURE__ */ new Map();
4762
+ function resolveSubdomain(subdomainFromQuery) {
4763
+ if (subdomainFromQuery) return subdomainFromQuery;
4764
+ if (typeof window !== "undefined") {
4765
+ const parts = window.location.hostname.split(".");
4766
+ if (parts.length >= 3 && parts[0] !== "www") return parts[0];
4767
+ }
4768
+ const siteUrl = process.env.NEXT_PUBLIC_SITE_URL;
4769
+ if (siteUrl) {
4770
+ try {
4771
+ const host = new URL(siteUrl).hostname;
4772
+ const siteParts = host.split(".");
4773
+ if (siteParts.length >= 3 && siteParts[0] !== "www") return siteParts[0];
4774
+ } catch {
4775
+ }
4776
+ }
4777
+ return "";
4778
+ }
3838
4779
  function OhhwellsBridge() {
3839
4780
  const pathname = (0, import_navigation.usePathname)();
3840
4781
  const router = (0, import_navigation.useRouter)();
3841
4782
  const searchParams = (0, import_navigation.useSearchParams)();
3842
4783
  const isEditMode = isEditSessionActive();
3843
- (0, import_react.useEffect)(() => {
3844
- if (!isEditMode) return;
3845
- const preconnect1 = Object.assign(document.createElement("link"), { rel: "preconnect", href: "https://fonts.googleapis.com" });
3846
- const preconnect2 = Object.assign(document.createElement("link"), { rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "" });
3847
- const stylesheet = Object.assign(document.createElement("link"), { rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Figtree:ital,wght@0,300..900;1,300..900&display=swap" });
3848
- document.head.append(preconnect1, preconnect2, stylesheet);
3849
- return () => {
3850
- preconnect1.remove();
3851
- preconnect2.remove();
3852
- stylesheet.remove();
3853
- };
3854
- }, [isEditMode]);
3855
- const [bridgeRoot, setBridgeRoot] = (0, import_react.useState)(null);
3856
- (0, import_react.useEffect)(() => {
4784
+ const [bridgeRoot, setBridgeRoot] = (0, import_react3.useState)(null);
4785
+ (0, import_react3.useEffect)(() => {
4786
+ const figtreeFontId = "ohw-figtree-font";
4787
+ if (!document.getElementById(figtreeFontId)) {
4788
+ const preconnect1 = Object.assign(document.createElement("link"), { rel: "preconnect", href: "https://fonts.googleapis.com" });
4789
+ const preconnect2 = Object.assign(document.createElement("link"), { rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "" });
4790
+ const stylesheet = Object.assign(document.createElement("link"), {
4791
+ id: figtreeFontId,
4792
+ rel: "stylesheet",
4793
+ href: "https://fonts.googleapis.com/css2?family=Figtree:ital,wght@0,300..900;1,300..900&display=swap"
4794
+ });
4795
+ document.head.append(preconnect1, preconnect2, stylesheet);
4796
+ }
3857
4797
  const el = document.createElement("div");
3858
4798
  el.setAttribute("data-ohw-bridge-root", "");
3859
4799
  el.setAttribute("data-ohw-bridge", "");
@@ -3865,39 +4805,40 @@ function OhhwellsBridge() {
3865
4805
  };
3866
4806
  }, []);
3867
4807
  const subdomainFromQuery = searchParams.get("subdomain");
3868
- const subdomain = subdomainFromQuery ?? (() => {
3869
- if (typeof window === "undefined") return "";
3870
- const parts = window.location.hostname.split(".");
3871
- return parts.length >= 3 && parts[0] !== "www" ? parts[0] : "";
3872
- })();
3873
- const postToParent = (0, import_react.useCallback)((data) => {
4808
+ const subdomain = resolveSubdomain(subdomainFromQuery);
4809
+ const postToParent = (0, import_react3.useCallback)((data) => {
3874
4810
  if (typeof window !== "undefined" && window.parent !== window) {
3875
4811
  window.parent.postMessage(data, "*");
3876
4812
  }
3877
4813
  }, []);
3878
- const [fetchState, setFetchState] = (0, import_react.useState)("idle");
3879
- const autoSaveTimers = (0, import_react.useRef)(/* @__PURE__ */ new Map());
3880
- const activeElRef = (0, import_react.useRef)(null);
3881
- const originalContentRef = (0, import_react.useRef)(null);
3882
- const activeStateElRef = (0, import_react.useRef)(null);
3883
- const parentScrollRef = (0, import_react.useRef)(null);
3884
- const toolbarElRef = (0, import_react.useRef)(null);
3885
- const glowElRef = (0, import_react.useRef)(null);
3886
- const hoveredImageRef = (0, import_react.useRef)(null);
3887
- const editStylesRef = (0, import_react.useRef)(null);
3888
- const activateRef = (0, import_react.useRef)(() => {
4814
+ const [fetchState, setFetchState] = (0, import_react3.useState)("idle");
4815
+ const autoSaveTimers = (0, import_react3.useRef)(/* @__PURE__ */ new Map());
4816
+ const activeElRef = (0, import_react3.useRef)(null);
4817
+ const originalContentRef = (0, import_react3.useRef)(null);
4818
+ const activeStateElRef = (0, import_react3.useRef)(null);
4819
+ const parentScrollRef = (0, import_react3.useRef)(null);
4820
+ const toolbarElRef = (0, import_react3.useRef)(null);
4821
+ const glowElRef = (0, import_react3.useRef)(null);
4822
+ const hoveredImageRef = (0, import_react3.useRef)(null);
4823
+ const hoveredImageHasTextOverlapRef = (0, import_react3.useRef)(false);
4824
+ const hoveredGapRef = (0, import_react3.useRef)(null);
4825
+ const imageUnhoverTimerRef = (0, import_react3.useRef)(null);
4826
+ const imageShowTimerRef = (0, import_react3.useRef)(null);
4827
+ const editStylesRef = (0, import_react3.useRef)(null);
4828
+ const activateRef = (0, import_react3.useRef)(() => {
3889
4829
  });
3890
- const deactivateRef = (0, import_react.useRef)(() => {
4830
+ const deactivateRef = (0, import_react3.useRef)(() => {
3891
4831
  });
3892
- const refreshActiveCommandsRef = (0, import_react.useRef)(() => {
4832
+ const refreshActiveCommandsRef = (0, import_react3.useRef)(() => {
3893
4833
  });
3894
- const postToParentRef = (0, import_react.useRef)(postToParent);
4834
+ const postToParentRef = (0, import_react3.useRef)(postToParent);
3895
4835
  postToParentRef.current = postToParent;
3896
- const [toolbarRect, setToolbarRect] = (0, import_react.useState)(null);
3897
- const [toggleState, setToggleState] = (0, import_react.useState)(null);
3898
- const [maxBadge, setMaxBadge] = (0, import_react.useState)(null);
3899
- const [activeCommands, setActiveCommands] = (0, import_react.useState)(/* @__PURE__ */ new Set());
3900
- (0, import_react.useEffect)(() => {
4836
+ const [toolbarRect, setToolbarRect] = (0, import_react3.useState)(null);
4837
+ const [toggleState, setToggleState] = (0, import_react3.useState)(null);
4838
+ const [maxBadge, setMaxBadge] = (0, import_react3.useState)(null);
4839
+ const [activeCommands, setActiveCommands] = (0, import_react3.useState)(/* @__PURE__ */ new Set());
4840
+ const [sectionGap, setSectionGap] = (0, import_react3.useState)(null);
4841
+ (0, import_react3.useEffect)(() => {
3901
4842
  const update = () => {
3902
4843
  const el = activeElRef.current;
3903
4844
  if (el) setToolbarRect(el.getBoundingClientRect());
@@ -3905,6 +4846,12 @@ function OhhwellsBridge() {
3905
4846
  if (!prev || !activeStateElRef.current) return prev;
3906
4847
  return { ...prev, rect: activeStateElRef.current.getBoundingClientRect() };
3907
4848
  });
4849
+ setSectionGap((prev) => {
4850
+ if (!prev) return prev;
4851
+ const anchor = document.querySelector(`[data-ohw-section="${prev.insertAfter}"]`);
4852
+ if (!anchor) return prev;
4853
+ return { ...prev, y: anchor.getBoundingClientRect().bottom };
4854
+ });
3908
4855
  };
3909
4856
  const vvp = window.visualViewport;
3910
4857
  if (!vvp) return;
@@ -3915,10 +4862,10 @@ function OhhwellsBridge() {
3915
4862
  vvp.removeEventListener("resize", update);
3916
4863
  };
3917
4864
  }, []);
3918
- const refreshStateRules = (0, import_react.useCallback)(() => {
4865
+ const refreshStateRules = (0, import_react3.useCallback)(() => {
3919
4866
  editStylesRef.current?.forceHover && (editStylesRef.current.forceHover.textContent = collectStateRules());
3920
4867
  }, []);
3921
- const deactivate = (0, import_react.useCallback)(() => {
4868
+ const deactivate = (0, import_react3.useCallback)(() => {
3922
4869
  const el = activeElRef.current;
3923
4870
  if (!el) return;
3924
4871
  const key = el.dataset.ohwKey;
@@ -3943,9 +4890,13 @@ function OhhwellsBridge() {
3943
4890
  setActiveCommands(/* @__PURE__ */ new Set());
3944
4891
  postToParent({ type: "ow:exit-edit" });
3945
4892
  }, [postToParent]);
3946
- const activate = (0, import_react.useCallback)((el) => {
4893
+ const activate = (0, import_react3.useCallback)((el) => {
3947
4894
  if (activeElRef.current === el) return;
3948
4895
  deactivate();
4896
+ if (hoveredImageRef.current) {
4897
+ hoveredImageRef.current = null;
4898
+ postToParentRef.current({ type: "ow:image-unhover" });
4899
+ }
3949
4900
  el.setAttribute("contenteditable", "true");
3950
4901
  el.removeAttribute("data-ohw-hovered");
3951
4902
  activeElRef.current = el;
@@ -3957,7 +4908,7 @@ function OhhwellsBridge() {
3957
4908
  }, [deactivate, postToParent]);
3958
4909
  activateRef.current = activate;
3959
4910
  deactivateRef.current = deactivate;
3960
- (0, import_react.useLayoutEffect)(() => {
4911
+ (0, import_react3.useLayoutEffect)(() => {
3961
4912
  if (!subdomain || isEditMode) {
3962
4913
  setFetchState("done");
3963
4914
  return;
@@ -3965,6 +4916,7 @@ function OhhwellsBridge() {
3965
4916
  const applyContent = (content) => {
3966
4917
  const imageLoads = [];
3967
4918
  for (const [key, val] of Object.entries(content)) {
4919
+ if (key === "__ohw_sections") continue;
3968
4920
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
3969
4921
  if (el.dataset.ohwEditable === "image") {
3970
4922
  const img = el instanceof HTMLImageElement ? el : el.querySelector("img");
@@ -3983,6 +4935,7 @@ function OhhwellsBridge() {
3983
4935
  }
3984
4936
  });
3985
4937
  }
4938
+ initSectionsFromContent(content, true);
3986
4939
  return imageLoads.length > 0 ? Promise.all(imageLoads).then(() => {
3987
4940
  }) : Promise.resolve();
3988
4941
  };
@@ -3993,7 +4946,7 @@ function OhhwellsBridge() {
3993
4946
  }
3994
4947
  let cancelled = false;
3995
4948
  setFetchState("loading");
3996
- const apiUrl = globalThis.process?.env?.NEXT_PUBLIC_FLOWOPS_API_URL ?? "http://localhost:4005";
4949
+ const apiUrl = process.env.NEXT_PUBLIC_FLOWOPS_API_URL ?? "https://flowops-backend-staging-2yfdh7pwpq-as.a.run.app";
3997
4950
  fetch(`${apiUrl}/api/public/sites/${subdomain}/content`, { cache: "no-store" }).then((r2) => r2.ok ? r2.json() : null).then((data) => {
3998
4951
  if (cancelled) return;
3999
4952
  const content = data?.content ?? {};
@@ -4007,12 +4960,15 @@ function OhhwellsBridge() {
4007
4960
  cancelled = true;
4008
4961
  };
4009
4962
  }, [subdomain, isEditMode, pathname]);
4010
- (0, import_react.useEffect)(() => {
4963
+ (0, import_react3.useEffect)(() => {
4011
4964
  if (!subdomain || isEditMode) return;
4965
+ let debounceTimer = null;
4012
4966
  const applyFromCache = () => {
4013
4967
  const content = contentCache.get(subdomain);
4014
4968
  if (!content) return;
4969
+ retryMissingSchedulingMounts(getPageSchedulingEntries(content["__ohw_sections"]), false);
4015
4970
  for (const [key, val] of Object.entries(content)) {
4971
+ if (key === "__ohw_sections") continue;
4016
4972
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
4017
4973
  if (el.dataset.ohwEditable === "image") {
4018
4974
  const img = el instanceof HTMLImageElement ? el : el.querySelector("img");
@@ -4026,21 +4982,28 @@ function OhhwellsBridge() {
4026
4982
  });
4027
4983
  }
4028
4984
  };
4029
- applyFromCache();
4030
- const observer = new MutationObserver(applyFromCache);
4985
+ const scheduleApply = () => {
4986
+ if (debounceTimer) clearTimeout(debounceTimer);
4987
+ debounceTimer = setTimeout(applyFromCache, 150);
4988
+ };
4989
+ scheduleApply();
4990
+ const observer = new MutationObserver(scheduleApply);
4031
4991
  observer.observe(document.body, { childList: true, subtree: true });
4032
- return () => observer.disconnect();
4033
- }, [subdomain, isEditMode]);
4034
- (0, import_react.useLayoutEffect)(() => {
4992
+ return () => {
4993
+ observer.disconnect();
4994
+ if (debounceTimer) clearTimeout(debounceTimer);
4995
+ };
4996
+ }, [subdomain, isEditMode, pathname]);
4997
+ (0, import_react3.useLayoutEffect)(() => {
4035
4998
  const el = document.getElementById("ohw-loader");
4036
4999
  if (!el) return;
4037
5000
  const visible = Boolean(subdomain) && fetchState !== "done";
4038
5001
  el.style.display = visible ? "flex" : "none";
4039
5002
  }, [subdomain, fetchState]);
4040
- (0, import_react.useEffect)(() => {
5003
+ (0, import_react3.useEffect)(() => {
4041
5004
  postToParent({ type: "ow:navigation", path: pathname });
4042
5005
  }, [pathname, postToParent]);
4043
- (0, import_react.useEffect)(() => {
5006
+ (0, import_react3.useEffect)(() => {
4044
5007
  if (!isEditMode) return;
4045
5008
  const measure = () => {
4046
5009
  const h = document.body.scrollHeight;
@@ -4064,7 +5027,7 @@ function OhhwellsBridge() {
4064
5027
  window.removeEventListener("resize", handleResize);
4065
5028
  };
4066
5029
  }, [pathname, isEditMode, postToParent]);
4067
- (0, import_react.useEffect)(() => {
5030
+ (0, import_react3.useEffect)(() => {
4068
5031
  if (!subdomainFromQuery || isEditMode) return;
4069
5032
  const handleClick = (e) => {
4070
5033
  const anchor = e.target.closest("a");
@@ -4080,7 +5043,7 @@ function OhhwellsBridge() {
4080
5043
  document.addEventListener("click", handleClick, true);
4081
5044
  return () => document.removeEventListener("click", handleClick, true);
4082
5045
  }, [subdomainFromQuery, isEditMode, router]);
4083
- (0, import_react.useEffect)(() => {
5046
+ (0, import_react3.useEffect)(() => {
4084
5047
  if (!isEditMode) {
4085
5048
  editStylesRef.current?.base.remove();
4086
5049
  editStylesRef.current?.forceHover.remove();
@@ -4120,6 +5083,7 @@ function OhhwellsBridge() {
4120
5083
  [data-ohw-editable][contenteditable] *::selection { background: ${PRIMARY}59 !important; }
4121
5084
  [data-ohw-editable-state], [data-ohw-editable-state] * { pointer-events: none !important; }
4122
5085
  [data-ohw-editable-state][data-ohw-active-state] [data-ohw-editable] { pointer-events: auto !important; }
5086
+ [data-ohw-editable-state][data-ohw-active-state][data-ohw-editable] { pointer-events: auto !important; }
4123
5087
  `;
4124
5088
  const forceHover = document.createElement("style");
4125
5089
  forceHover.setAttribute("data-ohw-active-state-style", "");
@@ -4146,10 +5110,12 @@ function OhhwellsBridge() {
4146
5110
  if (editable) {
4147
5111
  if (editable.dataset.ohwEditable === "image" || editable.dataset.ohwEditable === "bg-image") {
4148
5112
  e.preventDefault();
5113
+ e.stopPropagation();
4149
5114
  postToParentRef.current({ type: "ow:image-pick", key: editable.dataset.ohwKey ?? "" });
4150
5115
  return;
4151
5116
  }
4152
5117
  e.preventDefault();
5118
+ e.stopPropagation();
4153
5119
  activateRef.current(editable);
4154
5120
  return;
4155
5121
  }
@@ -4212,12 +5178,15 @@ function OhhwellsBridge() {
4212
5178
  }
4213
5179
  return { top, left, width: Math.max(0, right - left), height: Math.max(0, bottom - top) };
4214
5180
  };
4215
- const postImageHover = (imgEl, isDragOver = false) => {
5181
+ const postImageHover = (imgEl, isDragOver = false, forceTextOverlap) => {
4216
5182
  const r2 = getVisibleRect(imgEl);
5183
+ const hasTextOverlap = forceTextOverlap ?? false;
5184
+ hoveredImageHasTextOverlapRef.current = hasTextOverlap;
4217
5185
  postToParentRef.current({
4218
5186
  type: "ow:image-hover",
4219
5187
  key: imgEl.dataset.ohwKey ?? "",
4220
5188
  rect: { top: r2.top, left: r2.left, width: r2.width, height: r2.height },
5189
+ hasTextOverlap,
4221
5190
  ...isDragOver ? { isDragOver: true } : {}
4222
5191
  });
4223
5192
  const track = imgEl.closest("[data-ohw-hover-pause]");
@@ -4264,7 +5233,7 @@ function OhhwellsBridge() {
4264
5233
  }
4265
5234
  return smallest;
4266
5235
  };
4267
- const probeImageAt = (clientX, clientY, isDragOver = false, fromParentViewport = false, fromOverlay = false) => {
5236
+ const probeImageAt = (clientX, clientY, isDragOver = false, fromParentViewport = false) => {
4268
5237
  const toggleEl = document.querySelector("[data-ohw-state-toggle]");
4269
5238
  if (toggleEl) {
4270
5239
  const { x, y } = toProbeCoords(clientX, clientY, fromParentViewport);
@@ -4290,33 +5259,61 @@ function OhhwellsBridge() {
4290
5259
  }
4291
5260
  return;
4292
5261
  }
4293
- const topEditable = topEl?.closest("[data-ohw-editable]");
4294
- if (topEditable && topEditable.dataset.ohwEditable !== "image" && topEditable.dataset.ohwEditable !== "bg-image") {
4295
- if (topEditable.hasAttribute("contenteditable") && fromOverlay) return;
5262
+ const isStateCardImage = !!imgEl.closest("[data-ohw-editable-state]");
5263
+ const textEditable = Array.from(
5264
+ document.querySelectorAll('[data-ohw-editable]:not([data-ohw-editable="image"]):not([data-ohw-editable="bg-image"])')
5265
+ ).find((el) => {
5266
+ const er = el.getBoundingClientRect();
5267
+ return x >= er.left && x <= er.right && y >= er.top && y <= er.bottom;
5268
+ });
5269
+ if (imageUnhoverTimerRef.current) {
5270
+ clearTimeout(imageUnhoverTimerRef.current);
5271
+ imageUnhoverTimerRef.current = null;
5272
+ }
5273
+ if (imageShowTimerRef.current) {
5274
+ clearTimeout(imageShowTimerRef.current);
5275
+ imageShowTimerRef.current = null;
5276
+ }
5277
+ if (textEditable) {
5278
+ if (textEditable.hasAttribute("contenteditable")) {
5279
+ if (hoveredImageRef.current) {
5280
+ hoveredImageRef.current = null;
5281
+ hoveredImageHasTextOverlapRef.current = false;
5282
+ resumeAnimTracks();
5283
+ postToParentRef.current({ type: "ow:image-unhover" });
5284
+ }
5285
+ return;
5286
+ }
5287
+ if (isStateCardImage) {
5288
+ if (imgEl !== hoveredImageRef.current || !hoveredImageHasTextOverlapRef.current) {
5289
+ hoveredImageRef.current = imgEl;
5290
+ postImageHover(imgEl, isDragOver, true);
5291
+ }
5292
+ document.querySelectorAll("[data-ohw-hovered]").forEach((el) => el.removeAttribute("data-ohw-hovered"));
5293
+ textEditable.setAttribute("data-ohw-hovered", "");
5294
+ return;
5295
+ }
4296
5296
  if (hoveredImageRef.current) {
4297
5297
  hoveredImageRef.current = null;
5298
+ hoveredImageHasTextOverlapRef.current = false;
4298
5299
  resumeAnimTracks();
4299
5300
  postToParentRef.current({ type: "ow:image-unhover" });
4300
5301
  }
4301
5302
  document.querySelectorAll("[data-ohw-hovered]").forEach((el) => el.removeAttribute("data-ohw-hovered"));
4302
- topEditable.setAttribute("data-ohw-hovered", "");
5303
+ textEditable.setAttribute("data-ohw-hovered", "");
4303
5304
  return;
4304
5305
  }
4305
- const activeHovered = document.querySelector("[data-ohw-hovered]");
4306
- if (activeHovered) {
4307
- const outlinePad = 8;
4308
- const hr = activeHovered.getBoundingClientRect();
4309
- if (x >= hr.left - outlinePad && x <= hr.right + outlinePad && y >= hr.top - outlinePad && y <= hr.bottom + outlinePad) {
4310
- if (hoveredImageRef.current) {
4311
- hoveredImageRef.current = null;
4312
- resumeAnimTracks();
4313
- postToParentRef.current({ type: "ow:image-unhover" });
4314
- }
4315
- return;
5306
+ if (hoveredGapRef.current) {
5307
+ if (hoveredImageRef.current) {
5308
+ hoveredImageRef.current = null;
5309
+ resumeAnimTracks();
5310
+ postToParentRef.current({ type: "ow:image-unhover" });
4316
5311
  }
5312
+ document.querySelectorAll("[data-ohw-hovered]").forEach((el) => el.removeAttribute("data-ohw-hovered"));
5313
+ return;
4317
5314
  }
4318
5315
  document.querySelectorAll("[data-ohw-hovered]").forEach((el) => el.removeAttribute("data-ohw-hovered"));
4319
- if (imgEl !== hoveredImageRef.current) {
5316
+ if (imgEl !== hoveredImageRef.current || hoveredImageHasTextOverlapRef.current) {
4320
5317
  hoveredImageRef.current = imgEl;
4321
5318
  postImageHover(imgEl, isDragOver);
4322
5319
  } else if (isDragOver) {
@@ -4325,9 +5322,29 @@ function OhhwellsBridge() {
4325
5322
  } else {
4326
5323
  const inIframeView = clientX >= 0 && clientY >= 0 && clientX <= window.innerWidth && clientY <= window.innerHeight;
4327
5324
  if (!inIframeView) return;
4328
- hoveredImageRef.current = null;
4329
- resumeAnimTracks();
4330
- postToParentRef.current({ type: "ow:image-unhover" });
5325
+ if (imageUnhoverTimerRef.current) {
5326
+ clearTimeout(imageUnhoverTimerRef.current);
5327
+ imageUnhoverTimerRef.current = null;
5328
+ }
5329
+ if (imageShowTimerRef.current) {
5330
+ clearTimeout(imageShowTimerRef.current);
5331
+ imageShowTimerRef.current = null;
5332
+ }
5333
+ if (hoveredImageRef.current) {
5334
+ hoveredImageRef.current = null;
5335
+ hoveredImageHasTextOverlapRef.current = false;
5336
+ resumeAnimTracks();
5337
+ postToParentRef.current({ type: "ow:image-unhover" });
5338
+ }
5339
+ const { x, y } = toProbeCoords(clientX, clientY, fromParentViewport);
5340
+ const textEl = Array.from(
5341
+ document.querySelectorAll('[data-ohw-editable]:not([data-ohw-editable="image"]):not([data-ohw-editable="bg-image"])')
5342
+ ).find((el) => {
5343
+ const er = el.getBoundingClientRect();
5344
+ return x >= er.left && x <= er.right && y >= er.top && y <= er.bottom;
5345
+ });
5346
+ document.querySelectorAll("[data-ohw-hovered]").forEach((el) => el.removeAttribute("data-ohw-hovered"));
5347
+ if (textEl && !textEl.hasAttribute("contenteditable")) textEl.setAttribute("data-ohw-hovered", "");
4331
5348
  }
4332
5349
  };
4333
5350
  const probeHoverCardsAt = (clientX, clientY, fromParentViewport = false) => {
@@ -4346,27 +5363,53 @@ function OhhwellsBridge() {
4346
5363
  }) ?? null;
4347
5364
  const currentLocked = activeStateElRef.current?.hasAttribute("data-ohw-active-state") ? activeStateElRef.current : null;
4348
5365
  const active = found ?? currentLocked;
5366
+ const prev = activeStateElRef.current;
4349
5367
  activeStateElRef.current = active;
4350
- document.querySelectorAll("[data-ohw-state-hovered]").forEach((el) => el.removeAttribute("data-ohw-state-hovered"));
4351
- if (active) {
4352
- if (active.querySelector("[data-ohw-state-view]")) active.setAttribute("data-ohw-state-hovered", "");
4353
- const states = deriveStates(active.dataset.ohwEditableState ?? "");
4354
- const activeState = active.hasAttribute("data-ohw-active-state") ? (active.getAttribute("data-ohw-active-state") ?? "Default").charAt(0).toUpperCase() + (active.getAttribute("data-ohw-active-state") ?? "").slice(1) : "Default";
4355
- setToggleState({ rect: active.getBoundingClientRect(), activeState, states });
4356
- } else {
4357
- setToggleState(null);
5368
+ if (active !== prev) {
5369
+ if (prev) prev.removeAttribute("data-ohw-state-hovered");
5370
+ if (active) {
5371
+ if (active.querySelector("[data-ohw-state-view]")) active.setAttribute("data-ohw-state-hovered", "");
5372
+ const states = deriveStates(active.dataset.ohwEditableState ?? "");
5373
+ const activeState = active.hasAttribute("data-ohw-active-state") ? (active.getAttribute("data-ohw-active-state") ?? "Default").charAt(0).toUpperCase() + (active.getAttribute("data-ohw-active-state") ?? "").slice(1) : "Default";
5374
+ setToggleState({ rect: active.getBoundingClientRect(), activeState, states });
5375
+ } else {
5376
+ setToggleState(null);
5377
+ }
5378
+ }
5379
+ };
5380
+ const probeSectionGapAt = (clientX, clientY, fromParentViewport = false) => {
5381
+ const { y } = toProbeCoords(clientX, clientY, fromParentViewport);
5382
+ const sections = Array.from(document.querySelectorAll("[data-ohw-section]")).sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top);
5383
+ const ZONE = 20;
5384
+ for (let i = 0; i < sections.length; i++) {
5385
+ const a = sections[i];
5386
+ const b = sections[i + 1] ?? null;
5387
+ const boundaryY = a.getBoundingClientRect().bottom;
5388
+ if (Math.abs(y - boundaryY) <= ZONE) {
5389
+ const insertAfter = a.dataset.ohwSection ?? "";
5390
+ const insertBefore = b?.dataset.ohwSection ?? null;
5391
+ hoveredGapRef.current = { insertAfter, insertBefore };
5392
+ setSectionGap({ insertAfter, insertBefore, y: boundaryY });
5393
+ return;
5394
+ }
5395
+ }
5396
+ if (hoveredGapRef.current) {
5397
+ hoveredGapRef.current = null;
5398
+ setSectionGap(null);
4358
5399
  }
4359
5400
  };
4360
5401
  const handleMouseMove = (e) => {
4361
5402
  const { clientX, clientY } = e;
5403
+ probeSectionGapAt(clientX, clientY);
4362
5404
  probeImageAt(clientX, clientY);
4363
5405
  probeHoverCardsAt(clientX, clientY);
4364
5406
  };
4365
5407
  const handlePointerSync = (e) => {
4366
5408
  if (e.data?.type !== "ow:pointer-sync") return;
4367
- const { clientX, clientY, fromOverlay } = e.data;
5409
+ const { clientX, clientY } = e.data;
4368
5410
  if (typeof clientX !== "number" || typeof clientY !== "number") return;
4369
- probeImageAt(clientX, clientY, false, false, fromOverlay ?? false);
5411
+ probeSectionGapAt(clientX, clientY);
5412
+ probeImageAt(clientX, clientY);
4370
5413
  probeHoverCardsAt(clientX, clientY);
4371
5414
  };
4372
5415
  const handleDragOver = (e) => {
@@ -4379,7 +5422,8 @@ function OhhwellsBridge() {
4379
5422
  e.dataTransfer.dropEffect = "copy";
4380
5423
  if (hoveredImageRef.current !== el) {
4381
5424
  hoveredImageRef.current = el;
4382
- postImageHover(el, true);
5425
+ const isStateCard = !!el.closest("[data-ohw-editable-state]");
5426
+ postImageHover(el, true, isStateCard ? true : void 0);
4383
5427
  }
4384
5428
  };
4385
5429
  const handleDragLeave = (e) => {
@@ -4522,7 +5566,12 @@ function OhhwellsBridge() {
4522
5566
  if (e.data?.type !== "ow:hydrate") return;
4523
5567
  const content = e.data.content;
4524
5568
  if (!content) return;
5569
+ let sectionsJson = null;
4525
5570
  for (const [key, val] of Object.entries(content)) {
5571
+ if (key === "__ohw_sections") {
5572
+ sectionsJson = val;
5573
+ continue;
5574
+ }
4526
5575
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
4527
5576
  if (el.dataset.ohwEditable === "image") {
4528
5577
  const img = el instanceof HTMLImageElement ? el : el.querySelector("img");
@@ -4534,6 +5583,9 @@ function OhhwellsBridge() {
4534
5583
  }
4535
5584
  });
4536
5585
  }
5586
+ if (sectionsJson) {
5587
+ initSectionsFromContent({ __ohw_sections: sectionsJson }, true);
5588
+ }
4537
5589
  postToParentRef.current({ type: "ow:hydrate-done" });
4538
5590
  };
4539
5591
  window.addEventListener("message", handleHydrate);
@@ -4565,17 +5617,75 @@ function OhhwellsBridge() {
4565
5617
  setToggleState((prev) => prev ? { ...prev, rect } : null);
4566
5618
  }
4567
5619
  if (hoveredImageRef.current) {
4568
- const r2 = hoveredImageRef.current.getBoundingClientRect();
5620
+ const el = hoveredImageRef.current;
5621
+ const r2 = el.getBoundingClientRect();
4569
5622
  postToParentRef.current({
4570
5623
  type: "ow:image-hover",
4571
- key: hoveredImageRef.current.dataset.ohwKey ?? "",
4572
- rect: { top: r2.top, left: r2.left, width: r2.width, height: r2.height }
5624
+ key: el.dataset.ohwKey ?? "",
5625
+ rect: { top: r2.top, left: r2.left, width: r2.width, height: r2.height },
5626
+ hasTextOverlap: hoveredImageHasTextOverlapRef.current
4573
5627
  });
4574
5628
  }
4575
5629
  };
4576
5630
  const handleSave = (e) => {
4577
5631
  if (e.data?.type !== "ow:save") return;
4578
- postToParentRef.current({ type: "ow:save-result", nodes: collectEditableNodes() });
5632
+ const nodes = collectEditableNodes();
5633
+ const tracker = document.querySelector("[data-ohw-sections-tracker]");
5634
+ if (tracker?.textContent) nodes.push({ key: "__ohw_sections", type: "sections", text: tracker.textContent });
5635
+ postToParentRef.current({ type: "ow:save-result", nodes });
5636
+ };
5637
+ const handleInsertSection = (e) => {
5638
+ if (e.data?.type !== "ow:insert-section") return;
5639
+ const { widgetType, insertAfter, insertBefore } = e.data;
5640
+ if (widgetType !== "scheduling") return;
5641
+ const inserted = mountSchedulingWidget(insertAfter, true, void 0, insertBefore ?? null);
5642
+ if (inserted) {
5643
+ const tracker = getSectionsTracker();
5644
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: "__ohw_sections", text: tracker.textContent ?? "[]" }] });
5645
+ const h = document.documentElement.scrollHeight;
5646
+ if (h > 50) postToParentRef.current({ type: "ow:height", height: h });
5647
+ }
5648
+ };
5649
+ const handleSwitchSchedule = (e) => {
5650
+ if (e.data?.type !== "ow:switch-schedule") return;
5651
+ const scheduleId = e.data.scheduleId;
5652
+ const insertAfter = e.data.insertAfter;
5653
+ if (!scheduleId || !insertAfter) return;
5654
+ const text = updateSectionScheduleId(insertAfter, scheduleId);
5655
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: "__ohw_sections", text }] });
5656
+ };
5657
+ const handleScheduleLinked = (e) => {
5658
+ if (e.data?.type !== "ow:schedule-linked") return;
5659
+ const scheduleId = e.data.scheduleId;
5660
+ const insertAfter = e.data.insertAfter;
5661
+ if (!scheduleId || !insertAfter) return;
5662
+ const text = updateSectionScheduleId(insertAfter, scheduleId);
5663
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: "__ohw_sections", text }] });
5664
+ };
5665
+ const handleClearSchedulingWidget = (e) => {
5666
+ if (e.data?.type !== "ow:clear-scheduling-widget") return;
5667
+ const insertAfter = e.data.insertAfter;
5668
+ if (!insertAfter) return;
5669
+ const text = updateSectionScheduleId(insertAfter, null);
5670
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: "__ohw_sections", text }] });
5671
+ };
5672
+ const handleRemoveSchedulingSection = (e) => {
5673
+ if (e.data?.type !== "ow:remove-scheduling-section") return;
5674
+ document.querySelectorAll('[data-ohw-section-container="scheduling"]').forEach((el) => {
5675
+ el.remove();
5676
+ });
5677
+ const tracker = getSectionsTracker();
5678
+ let sections = [];
5679
+ try {
5680
+ sections = JSON.parse(tracker.textContent || "[]");
5681
+ } catch {
5682
+ }
5683
+ const currentPath = window.location.pathname;
5684
+ const updated = sections.filter((s) => !(s.type === "scheduling" && s.pagePath === currentPath));
5685
+ tracker.textContent = JSON.stringify(updated);
5686
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: "__ohw_sections", text: tracker.textContent }] });
5687
+ const h = document.documentElement.scrollHeight;
5688
+ if (h > 50) postToParentRef.current({ type: "ow:height", height: h });
4579
5689
  };
4580
5690
  const handleSelectionChange = () => {
4581
5691
  if (!activeElRef.current) return;
@@ -4643,12 +5753,54 @@ function OhhwellsBridge() {
4643
5753
  parentScrollRef.current = { iframeOffsetTop, headerH, canvasH };
4644
5754
  if (activeElRef.current) applyToolbarPos(activeElRef.current.getBoundingClientRect());
4645
5755
  };
5756
+ const handleClickAt = (e) => {
5757
+ if (e.data?.type !== "ow:click-at") return;
5758
+ const { clientX, clientY } = e.data;
5759
+ const allImages = Array.from(
5760
+ document.querySelectorAll('[data-ohw-editable="image"], [data-ohw-editable="bg-image"]')
5761
+ ).filter((el) => !!el.closest("[data-ohw-editable-state]"));
5762
+ const stateCardImage = allImages.find((el) => {
5763
+ const ownerCard = el.closest("[data-ohw-editable-state]");
5764
+ if (ownerCard) {
5765
+ const inHoverState = ownerCard.getAttribute("data-ohw-active-state") === "hover";
5766
+ const elState = el.dataset.ohwState;
5767
+ if (el === ownerCard && inHoverState) return false;
5768
+ if (elState === "default" && inHoverState) return false;
5769
+ if (elState === "hover" && !inHoverState) return false;
5770
+ }
5771
+ const r2 = el.getBoundingClientRect();
5772
+ return clientX >= r2.left && clientX <= r2.right && clientY >= r2.top && clientY <= r2.bottom;
5773
+ });
5774
+ if (stateCardImage) {
5775
+ postToParentRef.current({ type: "ow:image-pick", key: stateCardImage.dataset.ohwKey ?? "" });
5776
+ return;
5777
+ }
5778
+ const textEditable = Array.from(
5779
+ document.querySelectorAll(
5780
+ '[data-ohw-editable]:not([data-ohw-editable="image"]):not([data-ohw-editable="bg-image"])'
5781
+ )
5782
+ ).find((el) => {
5783
+ const r2 = el.getBoundingClientRect();
5784
+ return clientX >= r2.left && clientX <= r2.right && clientY >= r2.top && clientY <= r2.bottom;
5785
+ });
5786
+ if (textEditable) {
5787
+ activateRef.current(textEditable);
5788
+ return;
5789
+ }
5790
+ deactivateRef.current();
5791
+ };
4646
5792
  window.addEventListener("message", handleSave);
5793
+ window.addEventListener("message", handleInsertSection);
5794
+ window.addEventListener("message", handleSwitchSchedule);
5795
+ window.addEventListener("message", handleScheduleLinked);
5796
+ window.addEventListener("message", handleClearSchedulingWidget);
5797
+ window.addEventListener("message", handleRemoveSchedulingSection);
4647
5798
  window.addEventListener("message", handleImageUrl);
4648
5799
  window.addEventListener("message", handleAnimLock);
4649
5800
  window.addEventListener("message", handleCanvasHeight);
4650
5801
  window.addEventListener("message", handleParentScroll);
4651
5802
  window.addEventListener("message", handlePointerSync);
5803
+ window.addEventListener("message", handleClickAt);
4652
5804
  document.addEventListener("click", handleClick, true);
4653
5805
  document.addEventListener("paste", handlePaste, true);
4654
5806
  document.addEventListener("input", handleInput, true);
@@ -4677,18 +5829,26 @@ function OhhwellsBridge() {
4677
5829
  document.removeEventListener("selectionchange", handleSelectionChange);
4678
5830
  window.removeEventListener("scroll", handleScroll, true);
4679
5831
  window.removeEventListener("message", handleSave);
5832
+ window.removeEventListener("message", handleInsertSection);
5833
+ window.removeEventListener("message", handleSwitchSchedule);
5834
+ window.removeEventListener("message", handleScheduleLinked);
5835
+ window.removeEventListener("message", handleClearSchedulingWidget);
5836
+ window.removeEventListener("message", handleRemoveSchedulingSection);
4680
5837
  window.removeEventListener("message", handleImageUrl);
4681
5838
  window.removeEventListener("message", handleAnimLock);
4682
5839
  window.removeEventListener("message", handleCanvasHeight);
4683
5840
  window.removeEventListener("message", handleParentScroll);
4684
5841
  window.removeEventListener("message", handlePointerSync);
5842
+ window.removeEventListener("message", handleClickAt);
4685
5843
  window.removeEventListener("message", handleHydrate);
4686
5844
  window.removeEventListener("message", handleDeactivate);
4687
5845
  autoSaveTimers.current.forEach(clearTimeout);
4688
5846
  autoSaveTimers.current.clear();
5847
+ if (imageUnhoverTimerRef.current) clearTimeout(imageUnhoverTimerRef.current);
5848
+ if (imageShowTimerRef.current) clearTimeout(imageShowTimerRef.current);
4689
5849
  };
4690
5850
  }, [isEditMode, refreshStateRules]);
4691
- (0, import_react.useEffect)(() => {
5851
+ (0, import_react3.useEffect)(() => {
4692
5852
  if (!isEditMode) return;
4693
5853
  document.querySelectorAll("[data-ohw-active-state]").forEach((el) => {
4694
5854
  el.removeAttribute("data-ohw-active-state");
@@ -4710,13 +5870,13 @@ function OhhwellsBridge() {
4710
5870
  clearTimeout(timer);
4711
5871
  };
4712
5872
  }, [pathname, isEditMode, refreshStateRules, postToParent]);
4713
- const handleCommand = (0, import_react.useCallback)((cmd) => {
5873
+ const handleCommand = (0, import_react3.useCallback)((cmd) => {
4714
5874
  document.execCommand(cmd, false);
4715
5875
  activeElRef.current?.focus();
4716
5876
  if (activeElRef.current) setToolbarRect(activeElRef.current.getBoundingClientRect());
4717
5877
  refreshActiveCommandsRef.current();
4718
5878
  }, []);
4719
- const handleStateChange = (0, import_react.useCallback)((state) => {
5879
+ const handleStateChange = (0, import_react3.useCallback)((state) => {
4720
5880
  if (!activeStateElRef.current) return;
4721
5881
  const el = activeStateElRef.current;
4722
5882
  if (state === "Default") {
@@ -4729,13 +5889,13 @@ function OhhwellsBridge() {
4729
5889
  }
4730
5890
  setToggleState((prev) => prev ? { ...prev, activeState: state } : null);
4731
5891
  }, [deactivate]);
4732
- return bridgeRoot ? (0, import_react_dom.createPortal)(
4733
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
4734
- toolbarRect && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
4735
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(GlowFrame, { rect: toolbarRect, elRef: glowElRef }),
4736
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(FloatingToolbar, { rect: toolbarRect, parentScroll: parentScrollRef.current, elRef: toolbarElRef, onCommand: handleCommand, activeCommands })
5892
+ return bridgeRoot ? (0, import_react_dom2.createPortal)(
5893
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
5894
+ toolbarRect && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
5895
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(GlowFrame, { rect: toolbarRect, elRef: glowElRef }),
5896
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(FloatingToolbar, { rect: toolbarRect, parentScroll: parentScrollRef.current, elRef: toolbarElRef, onCommand: handleCommand, activeCommands })
4737
5897
  ] }),
4738
- maxBadge && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
5898
+ maxBadge && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
4739
5899
  "div",
4740
5900
  {
4741
5901
  "data-ohw-max-badge": "",
@@ -4761,7 +5921,7 @@ function OhhwellsBridge() {
4761
5921
  ]
4762
5922
  }
4763
5923
  ),
4764
- toggleState && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
5924
+ toggleState && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
4765
5925
  StateToggle,
4766
5926
  {
4767
5927
  rect: toggleState.rect,
@@ -4769,6 +5929,31 @@ function OhhwellsBridge() {
4769
5929
  states: toggleState.states,
4770
5930
  onStateChange: handleStateChange
4771
5931
  }
5932
+ ),
5933
+ sectionGap && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
5934
+ "div",
5935
+ {
5936
+ "data-ohw-section-insert-line": "",
5937
+ className: "fixed left-0 w-full z-2147483646 flex items-center pointer-events-none",
5938
+ style: { top: sectionGap.y, transform: "translateY(-50%)" },
5939
+ children: [
5940
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "flex-1 bg-primary", style: { height: 3 } }),
5941
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
5942
+ Badge,
5943
+ {
5944
+ 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",
5945
+ onClick: () => {
5946
+ window.parent.postMessage(
5947
+ { type: "ow:add-section", insertAfter: sectionGap.insertAfter, insertBefore: sectionGap.insertBefore },
5948
+ "*"
5949
+ );
5950
+ },
5951
+ children: "Add Section"
5952
+ }
5953
+ ),
5954
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "flex-1 bg-primary", style: { height: 3 } })
5955
+ ]
5956
+ }
4772
5957
  )
4773
5958
  ] }),
4774
5959
  bridgeRoot
@@ -4777,6 +5962,7 @@ function OhhwellsBridge() {
4777
5962
  // Annotate the CommonJS export names for ESM import in node:
4778
5963
  0 && (module.exports = {
4779
5964
  OhhwellsBridge,
5965
+ SchedulingWidget,
4780
5966
  Toggle,
4781
5967
  ToggleGroup,
4782
5968
  ToggleGroupItem,