@ohhwells/bridge 0.1.15 → 0.1.16-next.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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,8 +4173,8 @@ 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,
@@ -3451,7 +4194,7 @@ function ToggleGroupItem({
3451
4194
  }
3452
4195
 
3453
4196
  // src/OhhwellsBridge.tsx
3454
- var import_react_dom = require("react-dom");
4197
+ var import_react_dom2 = require("react-dom");
3455
4198
  var import_navigation = require("next/navigation");
3456
4199
 
3457
4200
  // src/lib/session-search.ts
@@ -3472,8 +4215,30 @@ function isEditSessionActive() {
3472
4215
  return new URLSearchParams(q).get("mode") === "edit";
3473
4216
  }
3474
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
+
3475
4240
  // src/OhhwellsBridge.tsx
3476
- var import_jsx_runtime3 = require("react/jsx-runtime");
4241
+ var import_jsx_runtime6 = require("react/jsx-runtime");
3477
4242
  var PRIMARY = "#0885FE";
3478
4243
  var IMAGE_FADE_MS = 300;
3479
4244
  function runOpacityFade(el, onDone) {
@@ -3524,6 +4289,163 @@ function fadeInBgImage(el, url, onReady) {
3524
4289
  if (!prevPos || prevPos === "static") el.style.position = prevPos;
3525
4290
  });
3526
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
+ }
3527
4449
  function collectEditableNodes() {
3528
4450
  return Array.from(document.querySelectorAll("[data-ohw-editable]")).map((el) => {
3529
4451
  if (el.dataset.ohwEditable === "image") {
@@ -3681,7 +4603,7 @@ var TOOLBAR_GROUPS = [
3681
4603
  ];
3682
4604
  function GlowFrame({ rect, elRef }) {
3683
4605
  const GAP = 6;
3684
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
4606
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3685
4607
  "div",
3686
4608
  {
3687
4609
  ref: elRef,
@@ -3732,7 +4654,7 @@ function FloatingToolbar({
3732
4654
  activeCommands
3733
4655
  }) {
3734
4656
  const { top, left, transform } = calcToolbarPos(rect, parentScroll);
3735
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
4657
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3736
4658
  "div",
3737
4659
  {
3738
4660
  ref: elRef,
@@ -3755,11 +4677,11 @@ function FloatingToolbar({
3755
4677
  pointerEvents: "auto",
3756
4678
  whiteSpace: "nowrap"
3757
4679
  },
3758
- children: TOOLBAR_GROUPS.map((btns, gi) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react.default.Fragment, { children: [
3759
- 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 } }),
3760
4682
  btns.map((btn) => {
3761
4683
  const isActive = activeCommands.has(btn.cmd);
3762
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
4684
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3763
4685
  "button",
3764
4686
  {
3765
4687
  title: btn.title,
@@ -3785,7 +4707,7 @@ function FloatingToolbar({
3785
4707
  flexShrink: 0,
3786
4708
  padding: 6
3787
4709
  },
3788
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
4710
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3789
4711
  "svg",
3790
4712
  {
3791
4713
  width: "16",
@@ -3818,7 +4740,7 @@ function StateToggle({
3818
4740
  states,
3819
4741
  onStateChange
3820
4742
  }) {
3821
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
4743
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
3822
4744
  ToggleGroup,
3823
4745
  {
3824
4746
  "data-ohw-state-toggle": "",
@@ -3832,18 +4754,35 @@ function StateToggle({
3832
4754
  left: rect.right - 8,
3833
4755
  transform: "translateX(-100%)"
3834
4756
  },
3835
- children: states.map((state) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ToggleGroupItem, { value: state, size: "sm", children: state }, state))
4757
+ children: states.map((state) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ToggleGroupItem, { value: state, size: "sm", children: state }, state))
3836
4758
  }
3837
4759
  );
3838
4760
  }
3839
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
+ }
3840
4779
  function OhhwellsBridge() {
3841
4780
  const pathname = (0, import_navigation.usePathname)();
3842
4781
  const router = (0, import_navigation.useRouter)();
3843
4782
  const searchParams = (0, import_navigation.useSearchParams)();
3844
4783
  const isEditMode = isEditSessionActive();
3845
- const [bridgeRoot, setBridgeRoot] = (0, import_react.useState)(null);
3846
- (0, import_react.useEffect)(() => {
4784
+ const [bridgeRoot, setBridgeRoot] = (0, import_react3.useState)(null);
4785
+ (0, import_react3.useEffect)(() => {
3847
4786
  const figtreeFontId = "ohw-figtree-font";
3848
4787
  if (!document.getElementById(figtreeFontId)) {
3849
4788
  const preconnect1 = Object.assign(document.createElement("link"), { rel: "preconnect", href: "https://fonts.googleapis.com" });
@@ -3866,42 +4805,40 @@ function OhhwellsBridge() {
3866
4805
  };
3867
4806
  }, []);
3868
4807
  const subdomainFromQuery = searchParams.get("subdomain");
3869
- const subdomain = subdomainFromQuery ?? (() => {
3870
- if (typeof window === "undefined") return "";
3871
- const parts = window.location.hostname.split(".");
3872
- return parts.length >= 3 && parts[0] !== "www" ? parts[0] : "";
3873
- })();
3874
- const postToParent = (0, import_react.useCallback)((data) => {
4808
+ const subdomain = resolveSubdomain(subdomainFromQuery);
4809
+ const postToParent = (0, import_react3.useCallback)((data) => {
3875
4810
  if (typeof window !== "undefined" && window.parent !== window) {
3876
4811
  window.parent.postMessage(data, "*");
3877
4812
  }
3878
4813
  }, []);
3879
- const [fetchState, setFetchState] = (0, import_react.useState)("idle");
3880
- const autoSaveTimers = (0, import_react.useRef)(/* @__PURE__ */ new Map());
3881
- const activeElRef = (0, import_react.useRef)(null);
3882
- const originalContentRef = (0, import_react.useRef)(null);
3883
- const activeStateElRef = (0, import_react.useRef)(null);
3884
- const parentScrollRef = (0, import_react.useRef)(null);
3885
- const toolbarElRef = (0, import_react.useRef)(null);
3886
- const glowElRef = (0, import_react.useRef)(null);
3887
- const hoveredImageRef = (0, import_react.useRef)(null);
3888
- const hoveredImageHasTextOverlapRef = (0, import_react.useRef)(false);
3889
- const imageUnhoverTimerRef = (0, import_react.useRef)(null);
3890
- const imageShowTimerRef = (0, import_react.useRef)(null);
3891
- const editStylesRef = (0, import_react.useRef)(null);
3892
- 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)(() => {
3893
4829
  });
3894
- const deactivateRef = (0, import_react.useRef)(() => {
4830
+ const deactivateRef = (0, import_react3.useRef)(() => {
3895
4831
  });
3896
- const refreshActiveCommandsRef = (0, import_react.useRef)(() => {
4832
+ const refreshActiveCommandsRef = (0, import_react3.useRef)(() => {
3897
4833
  });
3898
- const postToParentRef = (0, import_react.useRef)(postToParent);
4834
+ const postToParentRef = (0, import_react3.useRef)(postToParent);
3899
4835
  postToParentRef.current = postToParent;
3900
- const [toolbarRect, setToolbarRect] = (0, import_react.useState)(null);
3901
- const [toggleState, setToggleState] = (0, import_react.useState)(null);
3902
- const [maxBadge, setMaxBadge] = (0, import_react.useState)(null);
3903
- const [activeCommands, setActiveCommands] = (0, import_react.useState)(/* @__PURE__ */ new Set());
3904
- (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)(() => {
3905
4842
  const update = () => {
3906
4843
  const el = activeElRef.current;
3907
4844
  if (el) setToolbarRect(el.getBoundingClientRect());
@@ -3909,6 +4846,12 @@ function OhhwellsBridge() {
3909
4846
  if (!prev || !activeStateElRef.current) return prev;
3910
4847
  return { ...prev, rect: activeStateElRef.current.getBoundingClientRect() };
3911
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
+ });
3912
4855
  };
3913
4856
  const vvp = window.visualViewport;
3914
4857
  if (!vvp) return;
@@ -3919,10 +4862,10 @@ function OhhwellsBridge() {
3919
4862
  vvp.removeEventListener("resize", update);
3920
4863
  };
3921
4864
  }, []);
3922
- const refreshStateRules = (0, import_react.useCallback)(() => {
4865
+ const refreshStateRules = (0, import_react3.useCallback)(() => {
3923
4866
  editStylesRef.current?.forceHover && (editStylesRef.current.forceHover.textContent = collectStateRules());
3924
4867
  }, []);
3925
- const deactivate = (0, import_react.useCallback)(() => {
4868
+ const deactivate = (0, import_react3.useCallback)(() => {
3926
4869
  const el = activeElRef.current;
3927
4870
  if (!el) return;
3928
4871
  const key = el.dataset.ohwKey;
@@ -3947,7 +4890,7 @@ function OhhwellsBridge() {
3947
4890
  setActiveCommands(/* @__PURE__ */ new Set());
3948
4891
  postToParent({ type: "ow:exit-edit" });
3949
4892
  }, [postToParent]);
3950
- const activate = (0, import_react.useCallback)((el) => {
4893
+ const activate = (0, import_react3.useCallback)((el) => {
3951
4894
  if (activeElRef.current === el) return;
3952
4895
  deactivate();
3953
4896
  if (hoveredImageRef.current) {
@@ -3965,7 +4908,7 @@ function OhhwellsBridge() {
3965
4908
  }, [deactivate, postToParent]);
3966
4909
  activateRef.current = activate;
3967
4910
  deactivateRef.current = deactivate;
3968
- (0, import_react.useLayoutEffect)(() => {
4911
+ (0, import_react3.useLayoutEffect)(() => {
3969
4912
  if (!subdomain || isEditMode) {
3970
4913
  setFetchState("done");
3971
4914
  return;
@@ -3973,6 +4916,7 @@ function OhhwellsBridge() {
3973
4916
  const applyContent = (content) => {
3974
4917
  const imageLoads = [];
3975
4918
  for (const [key, val] of Object.entries(content)) {
4919
+ if (key === "__ohw_sections") continue;
3976
4920
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
3977
4921
  if (el.dataset.ohwEditable === "image") {
3978
4922
  const img = el instanceof HTMLImageElement ? el : el.querySelector("img");
@@ -3991,6 +4935,7 @@ function OhhwellsBridge() {
3991
4935
  }
3992
4936
  });
3993
4937
  }
4938
+ initSectionsFromContent(content, true);
3994
4939
  return imageLoads.length > 0 ? Promise.all(imageLoads).then(() => {
3995
4940
  }) : Promise.resolve();
3996
4941
  };
@@ -4001,7 +4946,7 @@ function OhhwellsBridge() {
4001
4946
  }
4002
4947
  let cancelled = false;
4003
4948
  setFetchState("loading");
4004
- const apiUrl = globalThis.process?.env?.NEXT_PUBLIC_FLOWOPS_API_URL ?? "https://flowops-backend-staging-2yfdh7pwpq-as.a.run.app";
4949
+ const apiUrl = process.env.NEXT_PUBLIC_FLOWOPS_API_URL ?? "https://flowops-backend-staging-2yfdh7pwpq-as.a.run.app";
4005
4950
  fetch(`${apiUrl}/api/public/sites/${subdomain}/content`, { cache: "no-store" }).then((r2) => r2.ok ? r2.json() : null).then((data) => {
4006
4951
  if (cancelled) return;
4007
4952
  const content = data?.content ?? {};
@@ -4015,12 +4960,15 @@ function OhhwellsBridge() {
4015
4960
  cancelled = true;
4016
4961
  };
4017
4962
  }, [subdomain, isEditMode, pathname]);
4018
- (0, import_react.useEffect)(() => {
4963
+ (0, import_react3.useEffect)(() => {
4019
4964
  if (!subdomain || isEditMode) return;
4965
+ let debounceTimer = null;
4020
4966
  const applyFromCache = () => {
4021
4967
  const content = contentCache.get(subdomain);
4022
4968
  if (!content) return;
4969
+ retryMissingSchedulingMounts(getPageSchedulingEntries(content["__ohw_sections"]), false);
4023
4970
  for (const [key, val] of Object.entries(content)) {
4971
+ if (key === "__ohw_sections") continue;
4024
4972
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
4025
4973
  if (el.dataset.ohwEditable === "image") {
4026
4974
  const img = el instanceof HTMLImageElement ? el : el.querySelector("img");
@@ -4034,21 +4982,28 @@ function OhhwellsBridge() {
4034
4982
  });
4035
4983
  }
4036
4984
  };
4037
- applyFromCache();
4038
- 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);
4039
4991
  observer.observe(document.body, { childList: true, subtree: true });
4040
- return () => observer.disconnect();
4041
- }, [subdomain, isEditMode]);
4042
- (0, import_react.useLayoutEffect)(() => {
4992
+ return () => {
4993
+ observer.disconnect();
4994
+ if (debounceTimer) clearTimeout(debounceTimer);
4995
+ };
4996
+ }, [subdomain, isEditMode, pathname]);
4997
+ (0, import_react3.useLayoutEffect)(() => {
4043
4998
  const el = document.getElementById("ohw-loader");
4044
4999
  if (!el) return;
4045
5000
  const visible = Boolean(subdomain) && fetchState !== "done";
4046
5001
  el.style.display = visible ? "flex" : "none";
4047
5002
  }, [subdomain, fetchState]);
4048
- (0, import_react.useEffect)(() => {
5003
+ (0, import_react3.useEffect)(() => {
4049
5004
  postToParent({ type: "ow:navigation", path: pathname });
4050
5005
  }, [pathname, postToParent]);
4051
- (0, import_react.useEffect)(() => {
5006
+ (0, import_react3.useEffect)(() => {
4052
5007
  if (!isEditMode) return;
4053
5008
  const measure = () => {
4054
5009
  const h = document.body.scrollHeight;
@@ -4072,7 +5027,7 @@ function OhhwellsBridge() {
4072
5027
  window.removeEventListener("resize", handleResize);
4073
5028
  };
4074
5029
  }, [pathname, isEditMode, postToParent]);
4075
- (0, import_react.useEffect)(() => {
5030
+ (0, import_react3.useEffect)(() => {
4076
5031
  if (!subdomainFromQuery || isEditMode) return;
4077
5032
  const handleClick = (e) => {
4078
5033
  const anchor = e.target.closest("a");
@@ -4088,7 +5043,7 @@ function OhhwellsBridge() {
4088
5043
  document.addEventListener("click", handleClick, true);
4089
5044
  return () => document.removeEventListener("click", handleClick, true);
4090
5045
  }, [subdomainFromQuery, isEditMode, router]);
4091
- (0, import_react.useEffect)(() => {
5046
+ (0, import_react3.useEffect)(() => {
4092
5047
  if (!isEditMode) {
4093
5048
  editStylesRef.current?.base.remove();
4094
5049
  editStylesRef.current?.forceHover.remove();
@@ -4348,6 +5303,15 @@ function OhhwellsBridge() {
4348
5303
  textEditable.setAttribute("data-ohw-hovered", "");
4349
5304
  return;
4350
5305
  }
5306
+ if (hoveredGapRef.current) {
5307
+ if (hoveredImageRef.current) {
5308
+ hoveredImageRef.current = null;
5309
+ resumeAnimTracks();
5310
+ postToParentRef.current({ type: "ow:image-unhover" });
5311
+ }
5312
+ document.querySelectorAll("[data-ohw-hovered]").forEach((el) => el.removeAttribute("data-ohw-hovered"));
5313
+ return;
5314
+ }
4351
5315
  document.querySelectorAll("[data-ohw-hovered]").forEach((el) => el.removeAttribute("data-ohw-hovered"));
4352
5316
  if (imgEl !== hoveredImageRef.current || hoveredImageHasTextOverlapRef.current) {
4353
5317
  hoveredImageRef.current = imgEl;
@@ -4413,8 +5377,30 @@ function OhhwellsBridge() {
4413
5377
  }
4414
5378
  }
4415
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);
5399
+ }
5400
+ };
4416
5401
  const handleMouseMove = (e) => {
4417
5402
  const { clientX, clientY } = e;
5403
+ probeSectionGapAt(clientX, clientY);
4418
5404
  probeImageAt(clientX, clientY);
4419
5405
  probeHoverCardsAt(clientX, clientY);
4420
5406
  };
@@ -4422,6 +5408,7 @@ function OhhwellsBridge() {
4422
5408
  if (e.data?.type !== "ow:pointer-sync") return;
4423
5409
  const { clientX, clientY } = e.data;
4424
5410
  if (typeof clientX !== "number" || typeof clientY !== "number") return;
5411
+ probeSectionGapAt(clientX, clientY);
4425
5412
  probeImageAt(clientX, clientY);
4426
5413
  probeHoverCardsAt(clientX, clientY);
4427
5414
  };
@@ -4579,7 +5566,12 @@ function OhhwellsBridge() {
4579
5566
  if (e.data?.type !== "ow:hydrate") return;
4580
5567
  const content = e.data.content;
4581
5568
  if (!content) return;
5569
+ let sectionsJson = null;
4582
5570
  for (const [key, val] of Object.entries(content)) {
5571
+ if (key === "__ohw_sections") {
5572
+ sectionsJson = val;
5573
+ continue;
5574
+ }
4583
5575
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
4584
5576
  if (el.dataset.ohwEditable === "image") {
4585
5577
  const img = el instanceof HTMLImageElement ? el : el.querySelector("img");
@@ -4591,6 +5583,9 @@ function OhhwellsBridge() {
4591
5583
  }
4592
5584
  });
4593
5585
  }
5586
+ if (sectionsJson) {
5587
+ initSectionsFromContent({ __ohw_sections: sectionsJson }, true);
5588
+ }
4594
5589
  postToParentRef.current({ type: "ow:hydrate-done" });
4595
5590
  };
4596
5591
  window.addEventListener("message", handleHydrate);
@@ -4634,7 +5629,63 @@ function OhhwellsBridge() {
4634
5629
  };
4635
5630
  const handleSave = (e) => {
4636
5631
  if (e.data?.type !== "ow:save") return;
4637
- 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 });
4638
5689
  };
4639
5690
  const handleSelectionChange = () => {
4640
5691
  if (!activeElRef.current) return;
@@ -4739,6 +5790,11 @@ function OhhwellsBridge() {
4739
5790
  deactivateRef.current();
4740
5791
  };
4741
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);
4742
5798
  window.addEventListener("message", handleImageUrl);
4743
5799
  window.addEventListener("message", handleAnimLock);
4744
5800
  window.addEventListener("message", handleCanvasHeight);
@@ -4773,6 +5829,11 @@ function OhhwellsBridge() {
4773
5829
  document.removeEventListener("selectionchange", handleSelectionChange);
4774
5830
  window.removeEventListener("scroll", handleScroll, true);
4775
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);
4776
5837
  window.removeEventListener("message", handleImageUrl);
4777
5838
  window.removeEventListener("message", handleAnimLock);
4778
5839
  window.removeEventListener("message", handleCanvasHeight);
@@ -4787,7 +5848,7 @@ function OhhwellsBridge() {
4787
5848
  if (imageShowTimerRef.current) clearTimeout(imageShowTimerRef.current);
4788
5849
  };
4789
5850
  }, [isEditMode, refreshStateRules]);
4790
- (0, import_react.useEffect)(() => {
5851
+ (0, import_react3.useEffect)(() => {
4791
5852
  if (!isEditMode) return;
4792
5853
  document.querySelectorAll("[data-ohw-active-state]").forEach((el) => {
4793
5854
  el.removeAttribute("data-ohw-active-state");
@@ -4809,13 +5870,13 @@ function OhhwellsBridge() {
4809
5870
  clearTimeout(timer);
4810
5871
  };
4811
5872
  }, [pathname, isEditMode, refreshStateRules, postToParent]);
4812
- const handleCommand = (0, import_react.useCallback)((cmd) => {
5873
+ const handleCommand = (0, import_react3.useCallback)((cmd) => {
4813
5874
  document.execCommand(cmd, false);
4814
5875
  activeElRef.current?.focus();
4815
5876
  if (activeElRef.current) setToolbarRect(activeElRef.current.getBoundingClientRect());
4816
5877
  refreshActiveCommandsRef.current();
4817
5878
  }, []);
4818
- const handleStateChange = (0, import_react.useCallback)((state) => {
5879
+ const handleStateChange = (0, import_react3.useCallback)((state) => {
4819
5880
  if (!activeStateElRef.current) return;
4820
5881
  const el = activeStateElRef.current;
4821
5882
  if (state === "Default") {
@@ -4828,13 +5889,13 @@ function OhhwellsBridge() {
4828
5889
  }
4829
5890
  setToggleState((prev) => prev ? { ...prev, activeState: state } : null);
4830
5891
  }, [deactivate]);
4831
- return bridgeRoot ? (0, import_react_dom.createPortal)(
4832
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
4833
- toolbarRect && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
4834
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(GlowFrame, { rect: toolbarRect, elRef: glowElRef }),
4835
- /* @__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 })
4836
5897
  ] }),
4837
- maxBadge && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
5898
+ maxBadge && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
4838
5899
  "div",
4839
5900
  {
4840
5901
  "data-ohw-max-badge": "",
@@ -4860,7 +5921,7 @@ function OhhwellsBridge() {
4860
5921
  ]
4861
5922
  }
4862
5923
  ),
4863
- toggleState && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
5924
+ toggleState && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
4864
5925
  StateToggle,
4865
5926
  {
4866
5927
  rect: toggleState.rect,
@@ -4868,6 +5929,31 @@ function OhhwellsBridge() {
4868
5929
  states: toggleState.states,
4869
5930
  onStateChange: handleStateChange
4870
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
+ }
4871
5957
  )
4872
5958
  ] }),
4873
5959
  bridgeRoot
@@ -4876,6 +5962,7 @@ function OhhwellsBridge() {
4876
5962
  // Annotate the CommonJS export names for ESM import in node:
4877
5963
  0 && (module.exports = {
4878
5964
  OhhwellsBridge,
5965
+ SchedulingWidget,
4879
5966
  Toggle,
4880
5967
  ToggleGroup,
4881
5968
  ToggleGroupItem,