@ohhwells/bridge 0.1.17 → 0.1.19

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,428 @@ __export(index_exports, {
40
41
  module.exports = __toCommonJS(index_exports);
41
42
 
42
43
  // src/OhhwellsBridge.tsx
44
+ var import_react2 = __toESM(require("react"), 1);
45
+ var import_client = require("react-dom/client");
46
+
47
+ // src/ui/SchedulingWidget.tsx
43
48
  var import_react = __toESM(require("react"), 1);
49
+ var import_jsx_runtime = require("react/jsx-runtime");
50
+ var API_URL = process.env.NEXT_PUBLIC_FLOWOPS_API_URL ?? "https://flowops-backend-staging-2yfdh7pwpq-as.a.run.app";
51
+ var DAY_ABBR = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"];
52
+ function getApiDomain() {
53
+ const h = window.location.hostname;
54
+ for (const base of ["ohhwells.site", "theflowops-staging.com", "theflowops.com"]) {
55
+ if (h.endsWith(`.${base}`)) return h.slice(0, -(base.length + 1));
56
+ }
57
+ return h;
58
+ }
59
+ function classRunsOnDate(cls, date, type) {
60
+ if (type === "FIXED") {
61
+ const d = String(date.getDate()).padStart(2, "0");
62
+ const m = String(date.getMonth() + 1).padStart(2, "0");
63
+ return cls.fixedDates?.some((fd) => fd.date === `${d}-${m}-${date.getFullYear()}`) ?? false;
64
+ }
65
+ return cls.weekdays?.some((w) => w.weekday === date.getDay()) ?? false;
66
+ }
67
+ function formatClassTime(cls) {
68
+ let h = parseInt(cls.startTime.hour, 10);
69
+ const m = parseInt(cls.startTime.minutes, 10);
70
+ if (cls.startTime.time === "PM" && h !== 12) h += 12;
71
+ if (cls.startTime.time === "AM" && h === 12) h = 0;
72
+ const endTotal = h * 60 + m + cls.duration;
73
+ const eh = Math.floor(endTotal / 60) % 24;
74
+ const em = endTotal % 60;
75
+ return `${h}:${cls.startTime.minutes}-${eh}:${String(em).padStart(2, "0")}`;
76
+ }
77
+ function getBookingsOnDate(cls, date) {
78
+ if (!cls.bookings?.length) return 0;
79
+ const ds = date.toISOString().split("T")[0];
80
+ return cls.bookings.filter((b) => {
81
+ try {
82
+ return new Date(b.classDate).toISOString().split("T")[0] === ds;
83
+ } catch {
84
+ return false;
85
+ }
86
+ }).length;
87
+ }
88
+ function buildTimezoneLabel(tz) {
89
+ try {
90
+ const now = /* @__PURE__ */ new Date();
91
+ const timeStr = new Intl.DateTimeFormat("en-US", {
92
+ hour: "2-digit",
93
+ minute: "2-digit",
94
+ hour12: false,
95
+ timeZone: tz
96
+ }).format(now);
97
+ const offsetPart = new Intl.DateTimeFormat("en-US", { timeZoneName: "short", timeZone: tz }).formatToParts(now).find((p) => p.type === "timeZoneName")?.value ?? "";
98
+ const city = tz.split("/").pop()?.replace(/_/g, " ") ?? tz;
99
+ return `${timeStr} (${offsetPart}) ${city} time`;
100
+ } catch {
101
+ return tz;
102
+ }
103
+ }
104
+ function EmptyState({ inEditor }) {
105
+ const handleAddSchedule = () => {
106
+ if (inEditor) {
107
+ window.parent.postMessage({ type: "ow:scheduling-not-connected" }, "*");
108
+ }
109
+ };
110
+ return /* @__PURE__ */ (0, import_jsx_runtime.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: [
111
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "w-10 h-10 bg-[#F5F5F4] rounded-[10px] flex items-center justify-center shrink-0", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
112
+ "svg",
113
+ {
114
+ width: "20",
115
+ height: "20",
116
+ viewBox: "0 0 24 24",
117
+ fill: "none",
118
+ stroke: "var(--color-accent, #A89B83)",
119
+ strokeWidth: "1.5",
120
+ strokeLinecap: "round",
121
+ strokeLinejoin: "round",
122
+ children: [
123
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: "3", y: "4", width: "18", height: "18", rx: "2" }),
124
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "16", y1: "2", x2: "16", y2: "6" }),
125
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "8", y1: "2", x2: "8", y2: "6" }),
126
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "3", y1: "10", x2: "21", y2: "10" }),
127
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "12", y1: "14", x2: "12", y2: "18" }),
128
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "10", y1: "16", x2: "14", y2: "16" })
129
+ ]
130
+ }
131
+ ) }),
132
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex flex-col items-center gap-2", children: [
133
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "font-body text-lg font-medium text-center text-[#0A0A0A] m-0", children: "No schedule yet" }),
134
+ /* @__PURE__ */ (0, import_jsx_runtime.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." })
135
+ ] }),
136
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
137
+ "button",
138
+ {
139
+ onClick: handleAddSchedule,
140
+ 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",
141
+ children: "Add Schedule"
142
+ }
143
+ )
144
+ ] });
145
+ }
146
+ function LoadingSkeleton() {
147
+ const shimmer = {
148
+ background: "linear-gradient(90deg,#f0f0f0 25%,#e8e8e8 50%,#f0f0f0 75%)",
149
+ backgroundSize: "200% 100%",
150
+ animation: "ohw-shimmer 1.5s infinite"
151
+ };
152
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "w-full flex flex-col gap-10", children: [
153
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: `@keyframes ohw-shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}` }),
154
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex h-[70px] items-stretch", children: Array.from({ length: 7 }).map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex-1 flex flex-col justify-between items-center px-1", children: [
155
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { ...shimmer, width: "32px", height: "14px", borderRadius: "4px" } }),
156
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { ...shimmer, width: "40px", height: "40px", borderRadius: "50%" } })
157
+ ] }, i)) }),
158
+ [0, 1, 2].map((i) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
159
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center gap-[60px] py-4", children: [
160
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { ...shimmer, width: "120px", height: "16px", borderRadius: "4px", flexShrink: 0 } }),
161
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex-1 flex flex-col gap-2", children: [
162
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { ...shimmer, width: "55%", height: "16px", borderRadius: "4px" } }),
163
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { ...shimmer, width: "38%", height: "14px", borderRadius: "4px" } })
164
+ ] }),
165
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { ...shimmer, width: "56px", height: "32px", borderRadius: "4px", flexShrink: 0 } }),
166
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { ...shimmer, width: "200px", height: "44px", borderRadius: "8px", flexShrink: 0 } })
167
+ ] }),
168
+ i < 2 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "h-px bg-black/20" })
169
+ ] }, i))
170
+ ] });
171
+ }
172
+ function ScheduleView({ schedule, dates, selectedIdx, onSelectDate }) {
173
+ const todayMs = (0, import_react.useMemo)(() => {
174
+ const d = /* @__PURE__ */ new Date();
175
+ d.setHours(0, 0, 0, 0);
176
+ return d.getTime();
177
+ }, []);
178
+ const datesWithClasses = (0, import_react.useMemo)(
179
+ () => new Set(dates.map(
180
+ (d, i) => schedule.classes?.some((c) => classRunsOnDate(c, d, schedule.type)) ? i : -1
181
+ ).filter((i) => i >= 0)),
182
+ [dates, schedule]
183
+ );
184
+ const selectedDate = dates[selectedIdx];
185
+ const selectedClasses = (0, import_react.useMemo)(
186
+ () => (schedule.classes ?? []).filter((c) => selectedDate && classRunsOnDate(c, selectedDate, schedule.type)),
187
+ [schedule, selectedDate]
188
+ );
189
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "w-full flex flex-col gap-10", children: [
190
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "overflow-x-auto w-full", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex min-w-max", children: dates.map((date, i) => {
191
+ const isToday = date.getTime() === todayMs;
192
+ const isSelected = i === selectedIdx;
193
+ const clickable = datesWithClasses.has(i);
194
+ const label = isToday ? "TODAY" : DAY_ABBR[date.getDay()];
195
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
196
+ "div",
197
+ {
198
+ onClick: () => clickable && onSelectDate(i),
199
+ className: `flex-none w-[72px] h-[70px] flex flex-col justify-between items-center ${clickable ? "cursor-pointer opacity-100" : "cursor-default opacity-50"}`,
200
+ children: [
201
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "font-body text-base font-normal text-center text-(--color-dark,#200C02)", children: label }),
202
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
203
+ "div",
204
+ {
205
+ className: "w-10 h-10 rounded-full flex items-center justify-center",
206
+ style: { background: isSelected ? "var(--color-primary, #3D312B)" : "transparent" },
207
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
208
+ "span",
209
+ {
210
+ className: "font-body text-xl font-bold text-center tracking-display-tight",
211
+ style: { color: isSelected ? "var(--color-light, #FEFFF0)" : "var(--color-dark, #200C02)" },
212
+ children: date.getDate()
213
+ }
214
+ )
215
+ }
216
+ )
217
+ ]
218
+ },
219
+ i
220
+ );
221
+ }) }) }),
222
+ selectedClasses.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.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_runtime.jsx)("div", { className: "flex flex-col", children: selectedClasses.map((cls, i) => {
223
+ const booked = getBookingsOnDate(cls, selectedDate);
224
+ const available = cls.maxParticipants - booked;
225
+ const isFull = available <= 0;
226
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
227
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center gap-[60px] py-4 box-border", children: [
228
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "w-[120px] shrink-0", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "font-body text-base font-bold text-(--color-dark,#200C02) block", children: formatClassTime(cls) }) }),
229
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex-1 flex flex-col gap-3 min-w-0", children: [
230
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "font-body text-base font-bold text-(--color-dark,#200C02) block truncate", children: cls.name }),
231
+ cls.hostName && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "font-body text-base font-normal text-(--color-dark,#200C02) opacity-80 block truncate", children: cls.hostName })
232
+ ] }),
233
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "w-14 shrink-0 flex flex-col gap-px", children: isFull ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: "Full" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
234
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: [
235
+ available,
236
+ "/",
237
+ cls.maxParticipants
238
+ ] }),
239
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: "available" })
240
+ ] }) }),
241
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
242
+ "button",
243
+ {
244
+ className: "shrink-0 w-[200px] px-5 py-[10px] flex items-center justify-center rounded-lg font-body text-base font-bold cursor-pointer box-border",
245
+ style: isFull ? {
246
+ background: "transparent",
247
+ border: "1px solid var(--color-dark, #200C02)",
248
+ color: "var(--color-dark, #200C02)"
249
+ } : {
250
+ background: "var(--color-primary, #3D312B)",
251
+ border: "none",
252
+ color: "var(--color-light, #FEFFF0)"
253
+ },
254
+ children: isFull ? "Join Waitlist" : "Book Now"
255
+ }
256
+ )
257
+ ] }),
258
+ i < selectedClasses.length - 1 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "h-px bg-black/20" })
259
+ ] }, cls.id ?? i);
260
+ }) })
261
+ ] });
262
+ }
263
+ function CalendarFoldIcon() {
264
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
265
+ "svg",
266
+ {
267
+ width: "16",
268
+ height: "16",
269
+ viewBox: "0 0 24 24",
270
+ fill: "none",
271
+ stroke: "currentColor",
272
+ strokeWidth: "2",
273
+ strokeLinecap: "round",
274
+ strokeLinejoin: "round",
275
+ style: { flexShrink: 0 },
276
+ children: [
277
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M8 2v4" }),
278
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M16 2v4" }),
279
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M21 17V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h11Z" }),
280
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M3 10h18" }),
281
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M15 22v-4a2 2 0 0 1 2-2h4" })
282
+ ]
283
+ }
284
+ );
285
+ }
286
+ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId }) {
287
+ const [schedule, setSchedule] = (0, import_react.useState)(null);
288
+ const [loading, setLoading] = (0, import_react.useState)(true);
289
+ const [inEditor, setInEditor] = (0, import_react.useState)(false);
290
+ const [isHovered, setIsHovered] = (0, import_react.useState)(false);
291
+ const switchScheduleIdRef = import_react.default.useRef(null);
292
+ const dates = (0, import_react.useMemo)(() => {
293
+ const today = /* @__PURE__ */ new Date();
294
+ today.setHours(0, 0, 0, 0);
295
+ return Array.from({ length: 14 }, (_, i) => {
296
+ const d = new Date(today);
297
+ d.setDate(today.getDate() + i);
298
+ return d;
299
+ });
300
+ }, []);
301
+ const [selectedIdx, setSelectedIdx] = (0, import_react.useState)(0);
302
+ (0, import_react.useEffect)(() => {
303
+ if (!schedule?.classes) return;
304
+ for (let i = 0; i < dates.length; i++) {
305
+ if (schedule.classes.some((c) => classRunsOnDate(c, dates[i], schedule.type))) {
306
+ setSelectedIdx(i);
307
+ return;
308
+ }
309
+ }
310
+ }, [schedule, dates]);
311
+ (0, import_react.useEffect)(() => {
312
+ if (typeof window === "undefined") return;
313
+ const isInEditor = window.self !== window.top;
314
+ setInEditor(isInEditor);
315
+ if (isInEditor) {
316
+ const startEmpty = initialScheduleId === null;
317
+ if (startEmpty) setLoading(false);
318
+ let initialized = startEmpty;
319
+ const timer = !startEmpty ? setTimeout(() => {
320
+ if (!initialized) {
321
+ initialized = true;
322
+ setLoading(false);
323
+ }
324
+ }, 5e3) : null;
325
+ const handler = (e) => {
326
+ if (e.data?.type === "ow:clear-scheduling-widget") {
327
+ setSchedule(null);
328
+ setLoading(false);
329
+ return;
330
+ }
331
+ if (e.data?.type === "ow:switch-schedule") {
332
+ switchScheduleIdRef.current = e.data.scheduleId ?? null;
333
+ initialized = false;
334
+ setLoading(true);
335
+ window.parent.postMessage({ type: "ow:request-user-schedules" }, "*");
336
+ return;
337
+ }
338
+ if (e.data?.type !== "ow:user-schedules-response") return;
339
+ if (initialized && switchScheduleIdRef.current === null) return;
340
+ initialized = true;
341
+ if (timer) clearTimeout(timer);
342
+ const schedules = e.data.schedules ?? [];
343
+ const isSwitching = switchScheduleIdRef.current !== null;
344
+ 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;
345
+ switchScheduleIdRef.current = null;
346
+ setSchedule(target);
347
+ setLoading(false);
348
+ if (notifyOnConnect && target && !isSwitching) {
349
+ window.parent.postMessage({ type: "ow:schedule-connected", schedule: { id: target.id, name: target.name } }, "*");
350
+ }
351
+ };
352
+ window.addEventListener("message", handler);
353
+ if (!startEmpty) window.parent.postMessage({ type: "ow:request-user-schedules" }, "*");
354
+ return () => {
355
+ window.removeEventListener("message", handler);
356
+ if (timer) clearTimeout(timer);
357
+ };
358
+ }
359
+ if (initialScheduleId === null) {
360
+ setLoading(false);
361
+ return;
362
+ }
363
+ ;
364
+ (async () => {
365
+ try {
366
+ if (initialScheduleId) {
367
+ const res = await fetch(`${API_URL}/api/schedule/id/${initialScheduleId}`);
368
+ const data = await res.json();
369
+ setSchedule(data?.id ? data : null);
370
+ } else {
371
+ const domain = getApiDomain();
372
+ const sitemapRes = await fetch(`${API_URL}/api/schedule/sitemap/${domain}`);
373
+ const slugs = await sitemapRes.json();
374
+ if (!Array.isArray(slugs) || !slugs.length) {
375
+ setLoading(false);
376
+ return;
377
+ }
378
+ const { slug } = [...slugs].sort(
379
+ (a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()
380
+ )[0];
381
+ const res = await fetch(`${API_URL}/api/schedule/${domain}/${slug}`);
382
+ const data = await res.json();
383
+ setSchedule(data?.id ? data : null);
384
+ }
385
+ } catch {
386
+ }
387
+ setLoading(false);
388
+ })();
389
+ }, []);
390
+ const timezoneLabel = schedule?.timezone ? (() => {
391
+ try {
392
+ return buildTimezoneLabel(schedule.timezone);
393
+ } catch {
394
+ return void 0;
395
+ }
396
+ })() : void 0;
397
+ const handleReplaceSchedule = () => {
398
+ setIsHovered(false);
399
+ if (schedule) {
400
+ window.parent.postMessage({ type: "ow:schedule-connected", schedule: { id: schedule.id, name: schedule.name } }, "*");
401
+ } else {
402
+ window.parent.postMessage({ type: "ow:scheduling-not-connected" }, "*");
403
+ }
404
+ };
405
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
406
+ "section",
407
+ {
408
+ "data-ohw-section": "scheduling",
409
+ className: "w-full box-border py-20 px-16 font-body bg-(--color-light,#FEFFF0) relative",
410
+ onMouseEnter: () => inEditor && setIsHovered(true),
411
+ onMouseLeave: () => setIsHovered(false),
412
+ children: [
413
+ inEditor && isHovered && !!schedule && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
414
+ "div",
415
+ {
416
+ className: "absolute inset-0 z-10 pointer-events-auto cursor-pointer",
417
+ onClick: handleReplaceSchedule,
418
+ children: [
419
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "absolute inset-0", style: { background: "#0885FE", opacity: 0.18 } }),
420
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "absolute inset-0", style: { boxShadow: "inset 0 0 0 1.5px #0885FE" } }),
421
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "absolute inset-0 flex items-center justify-center pointer-events-none", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
422
+ "div",
423
+ {
424
+ className: "bg-white border border-[#E7E5E4] rounded-md px-3 py-1.5 flex items-center gap-1.5",
425
+ style: {
426
+ fontSize: 14,
427
+ lineHeight: "24px",
428
+ fontFamily: "'Figtree', system-ui, sans-serif",
429
+ fontWeight: 500,
430
+ color: "#0C0A09"
431
+ },
432
+ children: [
433
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CalendarFoldIcon, {}),
434
+ "Replace schedule"
435
+ ]
436
+ }
437
+ ) })
438
+ ]
439
+ }
440
+ ),
441
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "max-w-[1280px] mx-auto flex flex-col items-center gap-16", children: [
442
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex flex-col items-center gap-4", children: [
443
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: "font-display text-5xl font-bold text-center m-0 text-(--color-dark,#200C02)", children: schedule?.name ?? "Book an appointment" }),
444
+ schedule?.description && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "font-body text-body text-center m-0 text-(--color-accent,#A89B83)", children: schedule.description })
445
+ ] }),
446
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "w-full flex flex-col items-end gap-3", children: [
447
+ timezoneLabel && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "font-body text-sm font-normal text-(--color-accent,#A89B83)", children: timezoneLabel }),
448
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "w-full p-10 box-border", children: loading ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LoadingSkeleton, {}) : !schedule ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EmptyState, { inEditor }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
449
+ ScheduleView,
450
+ {
451
+ schedule,
452
+ dates,
453
+ selectedIdx,
454
+ onSelectDate: setSelectedIdx
455
+ }
456
+ ) })
457
+ ] })
458
+ ] })
459
+ ]
460
+ }
461
+ );
462
+ }
44
463
 
45
464
  // src/ui/toggle-group.tsx
46
- var React = __toESM(require("react"), 1);
465
+ var React2 = __toESM(require("react"), 1);
47
466
 
48
467
  // node_modules/clsx/dist/clsx.mjs
49
468
  function r(e) {
@@ -3363,7 +3782,7 @@ var cva = (base, config) => (props) => {
3363
3782
 
3364
3783
  // src/ui/toggle.tsx
3365
3784
  var import_radix_ui = require("radix-ui");
3366
- var import_jsx_runtime = require("react/jsx-runtime");
3785
+ var import_jsx_runtime2 = require("react/jsx-runtime");
3367
3786
  var toggleVariants = cva(
3368
3787
  "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
3788
  {
@@ -3390,7 +3809,7 @@ function Toggle({
3390
3809
  size,
3391
3810
  ...props
3392
3811
  }) {
3393
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
3812
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
3394
3813
  import_radix_ui.Toggle.Root,
3395
3814
  {
3396
3815
  "data-slot": "toggle",
@@ -3401,8 +3820,8 @@ function Toggle({
3401
3820
  }
3402
3821
 
3403
3822
  // src/ui/toggle-group.tsx
3404
- var import_jsx_runtime2 = require("react/jsx-runtime");
3405
- var ToggleGroupContext = React.createContext({
3823
+ var import_jsx_runtime3 = require("react/jsx-runtime");
3824
+ var ToggleGroupContext = React2.createContext({
3406
3825
  value: "",
3407
3826
  onValueChange: () => {
3408
3827
  }
@@ -3414,13 +3833,13 @@ function ToggleGroup({
3414
3833
  children,
3415
3834
  ...props
3416
3835
  }) {
3417
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
3836
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3418
3837
  "div",
3419
3838
  {
3420
3839
  role: "group",
3421
3840
  className: cn("flex items-center gap-1 p-1 bg-muted rounded-md", className),
3422
3841
  ...props,
3423
- children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ToggleGroupContext.Provider, { value: { value, onValueChange }, children })
3842
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ToggleGroupContext.Provider, { value: { value, onValueChange }, children })
3424
3843
  }
3425
3844
  );
3426
3845
  }
@@ -3430,8 +3849,8 @@ function ToggleGroupItem({
3430
3849
  children,
3431
3850
  ...props
3432
3851
  }) {
3433
- const { value: groupValue, onValueChange } = React.useContext(ToggleGroupContext);
3434
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
3852
+ const { value: groupValue, onValueChange } = React2.useContext(ToggleGroupContext);
3853
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3435
3854
  Toggle,
3436
3855
  {
3437
3856
  pressed: groupValue === value,
@@ -3472,8 +3891,30 @@ function isEditSessionActive() {
3472
3891
  return new URLSearchParams(q).get("mode") === "edit";
3473
3892
  }
3474
3893
 
3894
+ // src/ui/badge.tsx
3895
+ var import_jsx_runtime4 = require("react/jsx-runtime");
3896
+ var badgeVariants = cva(
3897
+ "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",
3898
+ {
3899
+ variants: {
3900
+ variant: {
3901
+ default: "border-transparent bg-primary text-primary-foreground",
3902
+ secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
3903
+ destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
3904
+ outline: "text-foreground"
3905
+ }
3906
+ },
3907
+ defaultVariants: {
3908
+ variant: "default"
3909
+ }
3910
+ }
3911
+ );
3912
+ function Badge({ className, variant, ...props }) {
3913
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: cn(badgeVariants({ variant }), className), ...props });
3914
+ }
3915
+
3475
3916
  // src/OhhwellsBridge.tsx
3476
- var import_jsx_runtime3 = require("react/jsx-runtime");
3917
+ var import_jsx_runtime5 = require("react/jsx-runtime");
3477
3918
  var PRIMARY = "#0885FE";
3478
3919
  var IMAGE_FADE_MS = 300;
3479
3920
  function runOpacityFade(el, onDone) {
@@ -3524,6 +3965,41 @@ function fadeInBgImage(el, url, onReady) {
3524
3965
  if (!prevPos || prevPos === "static") el.style.position = prevPos;
3525
3966
  });
3526
3967
  }
3968
+ function getSectionsTracker() {
3969
+ let el = document.querySelector("[data-ohw-sections-tracker]");
3970
+ if (!el) {
3971
+ el = document.createElement("div");
3972
+ el.setAttribute("data-ohw-sections-tracker", "");
3973
+ el.style.display = "none";
3974
+ document.body.appendChild(el);
3975
+ }
3976
+ return el;
3977
+ }
3978
+ function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId) {
3979
+ const anchor = document.querySelector(`[data-ohw-section="${insertAfter}"]`);
3980
+ if (!anchor) return false;
3981
+ if (anchor.nextElementSibling?.dataset.ohwSectionContainer) return false;
3982
+ const container = document.createElement("div");
3983
+ container.dataset.ohwSectionContainer = "scheduling";
3984
+ anchor.insertAdjacentElement("afterend", container);
3985
+ (0, import_client.createRoot)(container).render(/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(SchedulingWidget, { notifyOnConnect, initialScheduleId: scheduleId }));
3986
+ const tracker = getSectionsTracker();
3987
+ let sections = [];
3988
+ try {
3989
+ sections = JSON.parse(tracker.textContent || "[]");
3990
+ } catch {
3991
+ }
3992
+ if (!sections.find((s) => s.type === "scheduling" && s.insertAfter === insertAfter)) {
3993
+ sections.push({
3994
+ type: "scheduling",
3995
+ insertAfter,
3996
+ pagePath: window.location.pathname,
3997
+ ...scheduleId ? { scheduleId } : {}
3998
+ });
3999
+ tracker.textContent = JSON.stringify(sections);
4000
+ }
4001
+ return true;
4002
+ }
3527
4003
  function collectEditableNodes() {
3528
4004
  return Array.from(document.querySelectorAll("[data-ohw-editable]")).map((el) => {
3529
4005
  if (el.dataset.ohwEditable === "image") {
@@ -3681,7 +4157,7 @@ var TOOLBAR_GROUPS = [
3681
4157
  ];
3682
4158
  function GlowFrame({ rect, elRef }) {
3683
4159
  const GAP = 6;
3684
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
4160
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3685
4161
  "div",
3686
4162
  {
3687
4163
  ref: elRef,
@@ -3732,7 +4208,7 @@ function FloatingToolbar({
3732
4208
  activeCommands
3733
4209
  }) {
3734
4210
  const { top, left, transform } = calcToolbarPos(rect, parentScroll);
3735
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
4211
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3736
4212
  "div",
3737
4213
  {
3738
4214
  ref: elRef,
@@ -3755,11 +4231,11 @@ function FloatingToolbar({
3755
4231
  pointerEvents: "auto",
3756
4232
  whiteSpace: "nowrap"
3757
4233
  },
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 } }),
4234
+ children: TOOLBAR_GROUPS.map((btns, gi) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react2.default.Fragment, { children: [
4235
+ gi > 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: { display: "block", width: 1, height: 24, background: "#E7E5E4", flexShrink: 0 } }),
3760
4236
  btns.map((btn) => {
3761
4237
  const isActive = activeCommands.has(btn.cmd);
3762
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
4238
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3763
4239
  "button",
3764
4240
  {
3765
4241
  title: btn.title,
@@ -3785,7 +4261,7 @@ function FloatingToolbar({
3785
4261
  flexShrink: 0,
3786
4262
  padding: 6
3787
4263
  },
3788
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
4264
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3789
4265
  "svg",
3790
4266
  {
3791
4267
  width: "16",
@@ -3818,7 +4294,7 @@ function StateToggle({
3818
4294
  states,
3819
4295
  onStateChange
3820
4296
  }) {
3821
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
4297
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3822
4298
  ToggleGroup,
3823
4299
  {
3824
4300
  "data-ohw-state-toggle": "",
@@ -3832,7 +4308,7 @@ function StateToggle({
3832
4308
  left: rect.right - 8,
3833
4309
  transform: "translateX(-100%)"
3834
4310
  },
3835
- children: states.map((state) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ToggleGroupItem, { value: state, size: "sm", children: state }, state))
4311
+ children: states.map((state) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ToggleGroupItem, { value: state, size: "sm", children: state }, state))
3836
4312
  }
3837
4313
  );
3838
4314
  }
@@ -3842,8 +4318,8 @@ function OhhwellsBridge() {
3842
4318
  const router = (0, import_navigation.useRouter)();
3843
4319
  const searchParams = (0, import_navigation.useSearchParams)();
3844
4320
  const isEditMode = isEditSessionActive();
3845
- const [bridgeRoot, setBridgeRoot] = (0, import_react.useState)(null);
3846
- (0, import_react.useEffect)(() => {
4321
+ const [bridgeRoot, setBridgeRoot] = (0, import_react2.useState)(null);
4322
+ (0, import_react2.useEffect)(() => {
3847
4323
  const figtreeFontId = "ohw-figtree-font";
3848
4324
  if (!document.getElementById(figtreeFontId)) {
3849
4325
  const preconnect1 = Object.assign(document.createElement("link"), { rel: "preconnect", href: "https://fonts.googleapis.com" });
@@ -3871,37 +4347,39 @@ function OhhwellsBridge() {
3871
4347
  const parts = window.location.hostname.split(".");
3872
4348
  return parts.length >= 3 && parts[0] !== "www" ? parts[0] : "";
3873
4349
  })();
3874
- const postToParent = (0, import_react.useCallback)((data) => {
4350
+ const postToParent = (0, import_react2.useCallback)((data) => {
3875
4351
  if (typeof window !== "undefined" && window.parent !== window) {
3876
4352
  window.parent.postMessage(data, "*");
3877
4353
  }
3878
4354
  }, []);
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)(() => {
4355
+ const [fetchState, setFetchState] = (0, import_react2.useState)("idle");
4356
+ const autoSaveTimers = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
4357
+ const activeElRef = (0, import_react2.useRef)(null);
4358
+ const originalContentRef = (0, import_react2.useRef)(null);
4359
+ const activeStateElRef = (0, import_react2.useRef)(null);
4360
+ const parentScrollRef = (0, import_react2.useRef)(null);
4361
+ const toolbarElRef = (0, import_react2.useRef)(null);
4362
+ const glowElRef = (0, import_react2.useRef)(null);
4363
+ const hoveredImageRef = (0, import_react2.useRef)(null);
4364
+ const hoveredImageHasTextOverlapRef = (0, import_react2.useRef)(false);
4365
+ const hoveredGapRef = (0, import_react2.useRef)(null);
4366
+ const imageUnhoverTimerRef = (0, import_react2.useRef)(null);
4367
+ const imageShowTimerRef = (0, import_react2.useRef)(null);
4368
+ const editStylesRef = (0, import_react2.useRef)(null);
4369
+ const activateRef = (0, import_react2.useRef)(() => {
3893
4370
  });
3894
- const deactivateRef = (0, import_react.useRef)(() => {
4371
+ const deactivateRef = (0, import_react2.useRef)(() => {
3895
4372
  });
3896
- const refreshActiveCommandsRef = (0, import_react.useRef)(() => {
4373
+ const refreshActiveCommandsRef = (0, import_react2.useRef)(() => {
3897
4374
  });
3898
- const postToParentRef = (0, import_react.useRef)(postToParent);
4375
+ const postToParentRef = (0, import_react2.useRef)(postToParent);
3899
4376
  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)(() => {
4377
+ const [toolbarRect, setToolbarRect] = (0, import_react2.useState)(null);
4378
+ const [toggleState, setToggleState] = (0, import_react2.useState)(null);
4379
+ const [maxBadge, setMaxBadge] = (0, import_react2.useState)(null);
4380
+ const [activeCommands, setActiveCommands] = (0, import_react2.useState)(/* @__PURE__ */ new Set());
4381
+ const [sectionGap, setSectionGap] = (0, import_react2.useState)(null);
4382
+ (0, import_react2.useEffect)(() => {
3905
4383
  const update = () => {
3906
4384
  const el = activeElRef.current;
3907
4385
  if (el) setToolbarRect(el.getBoundingClientRect());
@@ -3909,6 +4387,12 @@ function OhhwellsBridge() {
3909
4387
  if (!prev || !activeStateElRef.current) return prev;
3910
4388
  return { ...prev, rect: activeStateElRef.current.getBoundingClientRect() };
3911
4389
  });
4390
+ setSectionGap((prev) => {
4391
+ if (!prev) return prev;
4392
+ const anchor = document.querySelector(`[data-ohw-section="${prev.insertAfter}"]`);
4393
+ if (!anchor) return prev;
4394
+ return { ...prev, y: anchor.getBoundingClientRect().bottom };
4395
+ });
3912
4396
  };
3913
4397
  const vvp = window.visualViewport;
3914
4398
  if (!vvp) return;
@@ -3919,10 +4403,10 @@ function OhhwellsBridge() {
3919
4403
  vvp.removeEventListener("resize", update);
3920
4404
  };
3921
4405
  }, []);
3922
- const refreshStateRules = (0, import_react.useCallback)(() => {
4406
+ const refreshStateRules = (0, import_react2.useCallback)(() => {
3923
4407
  editStylesRef.current?.forceHover && (editStylesRef.current.forceHover.textContent = collectStateRules());
3924
4408
  }, []);
3925
- const deactivate = (0, import_react.useCallback)(() => {
4409
+ const deactivate = (0, import_react2.useCallback)(() => {
3926
4410
  const el = activeElRef.current;
3927
4411
  if (!el) return;
3928
4412
  const key = el.dataset.ohwKey;
@@ -3947,7 +4431,7 @@ function OhhwellsBridge() {
3947
4431
  setActiveCommands(/* @__PURE__ */ new Set());
3948
4432
  postToParent({ type: "ow:exit-edit" });
3949
4433
  }, [postToParent]);
3950
- const activate = (0, import_react.useCallback)((el) => {
4434
+ const activate = (0, import_react2.useCallback)((el) => {
3951
4435
  if (activeElRef.current === el) return;
3952
4436
  deactivate();
3953
4437
  if (hoveredImageRef.current) {
@@ -3965,14 +4449,27 @@ function OhhwellsBridge() {
3965
4449
  }, [deactivate, postToParent]);
3966
4450
  activateRef.current = activate;
3967
4451
  deactivateRef.current = deactivate;
3968
- (0, import_react.useLayoutEffect)(() => {
4452
+ (0, import_react2.useLayoutEffect)(() => {
3969
4453
  if (!subdomain || isEditMode) {
3970
4454
  setFetchState("done");
3971
4455
  return;
3972
4456
  }
3973
4457
  const applyContent = (content) => {
3974
4458
  const imageLoads = [];
4459
+ document.querySelectorAll("[data-ohw-section-container]").forEach((el) => el.remove());
4460
+ if (content["__ohw_sections"]) {
4461
+ try {
4462
+ const entries = JSON.parse(content["__ohw_sections"]);
4463
+ const currentPath = window.location.pathname;
4464
+ entries.forEach(({ type, insertAfter, scheduleId, pagePath }) => {
4465
+ if (pagePath && pagePath !== currentPath) return;
4466
+ if (type === "scheduling") mountSchedulingWidget(insertAfter, false, scheduleId);
4467
+ });
4468
+ } catch {
4469
+ }
4470
+ }
3975
4471
  for (const [key, val] of Object.entries(content)) {
4472
+ if (key === "__ohw_sections") continue;
3976
4473
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
3977
4474
  if (el.dataset.ohwEditable === "image") {
3978
4475
  const img = el instanceof HTMLImageElement ? el : el.querySelector("img");
@@ -4015,12 +4512,24 @@ function OhhwellsBridge() {
4015
4512
  cancelled = true;
4016
4513
  };
4017
4514
  }, [subdomain, isEditMode, pathname]);
4018
- (0, import_react.useEffect)(() => {
4515
+ (0, import_react2.useEffect)(() => {
4019
4516
  if (!subdomain || isEditMode) return;
4020
4517
  const applyFromCache = () => {
4021
4518
  const content = contentCache.get(subdomain);
4022
4519
  if (!content) return;
4520
+ if (content["__ohw_sections"]) {
4521
+ try {
4522
+ const entries = JSON.parse(content["__ohw_sections"]);
4523
+ const currentPath = window.location.pathname;
4524
+ entries.forEach(({ type, insertAfter, scheduleId, pagePath }) => {
4525
+ if (pagePath && pagePath !== currentPath) return;
4526
+ if (type === "scheduling") mountSchedulingWidget(insertAfter, false, scheduleId);
4527
+ });
4528
+ } catch {
4529
+ }
4530
+ }
4023
4531
  for (const [key, val] of Object.entries(content)) {
4532
+ if (key === "__ohw_sections") continue;
4024
4533
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
4025
4534
  if (el.dataset.ohwEditable === "image") {
4026
4535
  const img = el instanceof HTMLImageElement ? el : el.querySelector("img");
@@ -4039,16 +4548,16 @@ function OhhwellsBridge() {
4039
4548
  observer.observe(document.body, { childList: true, subtree: true });
4040
4549
  return () => observer.disconnect();
4041
4550
  }, [subdomain, isEditMode]);
4042
- (0, import_react.useLayoutEffect)(() => {
4551
+ (0, import_react2.useLayoutEffect)(() => {
4043
4552
  const el = document.getElementById("ohw-loader");
4044
4553
  if (!el) return;
4045
4554
  const visible = Boolean(subdomain) && fetchState !== "done";
4046
4555
  el.style.display = visible ? "flex" : "none";
4047
4556
  }, [subdomain, fetchState]);
4048
- (0, import_react.useEffect)(() => {
4557
+ (0, import_react2.useEffect)(() => {
4049
4558
  postToParent({ type: "ow:navigation", path: pathname });
4050
4559
  }, [pathname, postToParent]);
4051
- (0, import_react.useEffect)(() => {
4560
+ (0, import_react2.useEffect)(() => {
4052
4561
  if (!isEditMode) return;
4053
4562
  const measure = () => {
4054
4563
  const h = document.body.scrollHeight;
@@ -4072,7 +4581,7 @@ function OhhwellsBridge() {
4072
4581
  window.removeEventListener("resize", handleResize);
4073
4582
  };
4074
4583
  }, [pathname, isEditMode, postToParent]);
4075
- (0, import_react.useEffect)(() => {
4584
+ (0, import_react2.useEffect)(() => {
4076
4585
  if (!subdomainFromQuery || isEditMode) return;
4077
4586
  const handleClick = (e) => {
4078
4587
  const anchor = e.target.closest("a");
@@ -4088,7 +4597,7 @@ function OhhwellsBridge() {
4088
4597
  document.addEventListener("click", handleClick, true);
4089
4598
  return () => document.removeEventListener("click", handleClick, true);
4090
4599
  }, [subdomainFromQuery, isEditMode, router]);
4091
- (0, import_react.useEffect)(() => {
4600
+ (0, import_react2.useEffect)(() => {
4092
4601
  if (!isEditMode) {
4093
4602
  editStylesRef.current?.base.remove();
4094
4603
  editStylesRef.current?.forceHover.remove();
@@ -4348,6 +4857,15 @@ function OhhwellsBridge() {
4348
4857
  textEditable.setAttribute("data-ohw-hovered", "");
4349
4858
  return;
4350
4859
  }
4860
+ if (hoveredGapRef.current) {
4861
+ if (hoveredImageRef.current) {
4862
+ hoveredImageRef.current = null;
4863
+ resumeAnimTracks();
4864
+ postToParentRef.current({ type: "ow:image-unhover" });
4865
+ }
4866
+ document.querySelectorAll("[data-ohw-hovered]").forEach((el) => el.removeAttribute("data-ohw-hovered"));
4867
+ return;
4868
+ }
4351
4869
  document.querySelectorAll("[data-ohw-hovered]").forEach((el) => el.removeAttribute("data-ohw-hovered"));
4352
4870
  if (imgEl !== hoveredImageRef.current || hoveredImageHasTextOverlapRef.current) {
4353
4871
  hoveredImageRef.current = imgEl;
@@ -4413,8 +4931,30 @@ function OhhwellsBridge() {
4413
4931
  }
4414
4932
  }
4415
4933
  };
4934
+ const probeSectionGapAt = (clientX, clientY, fromParentViewport = false) => {
4935
+ const { y } = toProbeCoords(clientX, clientY, fromParentViewport);
4936
+ const sections = Array.from(document.querySelectorAll("[data-ohw-section]")).sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top);
4937
+ const ZONE = 20;
4938
+ for (let i = 0; i < sections.length; i++) {
4939
+ const a = sections[i];
4940
+ const b = sections[i + 1] ?? null;
4941
+ const boundaryY = a.getBoundingClientRect().bottom;
4942
+ if (Math.abs(y - boundaryY) <= ZONE) {
4943
+ const insertAfter = a.dataset.ohwSection ?? "";
4944
+ const insertBefore = b?.dataset.ohwSection ?? null;
4945
+ hoveredGapRef.current = { insertAfter, insertBefore };
4946
+ setSectionGap({ insertAfter, insertBefore, y: boundaryY });
4947
+ return;
4948
+ }
4949
+ }
4950
+ if (hoveredGapRef.current) {
4951
+ hoveredGapRef.current = null;
4952
+ setSectionGap(null);
4953
+ }
4954
+ };
4416
4955
  const handleMouseMove = (e) => {
4417
4956
  const { clientX, clientY } = e;
4957
+ probeSectionGapAt(clientX, clientY);
4418
4958
  probeImageAt(clientX, clientY);
4419
4959
  probeHoverCardsAt(clientX, clientY);
4420
4960
  };
@@ -4422,6 +4962,7 @@ function OhhwellsBridge() {
4422
4962
  if (e.data?.type !== "ow:pointer-sync") return;
4423
4963
  const { clientX, clientY } = e.data;
4424
4964
  if (typeof clientX !== "number" || typeof clientY !== "number") return;
4965
+ probeSectionGapAt(clientX, clientY);
4425
4966
  probeImageAt(clientX, clientY);
4426
4967
  probeHoverCardsAt(clientX, clientY);
4427
4968
  };
@@ -4580,6 +5121,18 @@ function OhhwellsBridge() {
4580
5121
  const content = e.data.content;
4581
5122
  if (!content) return;
4582
5123
  for (const [key, val] of Object.entries(content)) {
5124
+ if (key === "__ohw_sections") {
5125
+ try {
5126
+ const entries = JSON.parse(val);
5127
+ const tracker = getSectionsTracker();
5128
+ tracker.textContent = val;
5129
+ entries.forEach(({ type, insertAfter, scheduleId }) => {
5130
+ if (type === "scheduling") mountSchedulingWidget(insertAfter, false, scheduleId);
5131
+ });
5132
+ } catch {
5133
+ }
5134
+ continue;
5135
+ }
4583
5136
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
4584
5137
  if (el.dataset.ohwEditable === "image") {
4585
5138
  const img = el instanceof HTMLImageElement ? el : el.querySelector("img");
@@ -4634,7 +5187,71 @@ function OhhwellsBridge() {
4634
5187
  };
4635
5188
  const handleSave = (e) => {
4636
5189
  if (e.data?.type !== "ow:save") return;
4637
- postToParentRef.current({ type: "ow:save-result", nodes: collectEditableNodes() });
5190
+ const nodes = collectEditableNodes();
5191
+ const tracker = document.querySelector("[data-ohw-sections-tracker]");
5192
+ if (tracker?.textContent) nodes.push({ key: "__ohw_sections", type: "sections", text: tracker.textContent });
5193
+ postToParentRef.current({ type: "ow:save-result", nodes });
5194
+ };
5195
+ const handleInsertSection = (e) => {
5196
+ if (e.data?.type !== "ow:insert-section") return;
5197
+ const { widgetType, insertAfter } = e.data;
5198
+ if (widgetType !== "scheduling") return;
5199
+ const inserted = mountSchedulingWidget(insertAfter, true);
5200
+ if (inserted) {
5201
+ const tracker = getSectionsTracker();
5202
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: "__ohw_sections", text: tracker.textContent ?? "[]" }] });
5203
+ const h = document.documentElement.scrollHeight;
5204
+ if (h > 50) postToParentRef.current({ type: "ow:height", height: h });
5205
+ }
5206
+ };
5207
+ const handleSwitchSchedule = (e) => {
5208
+ if (e.data?.type !== "ow:switch-schedule") return;
5209
+ const scheduleId = e.data.scheduleId;
5210
+ if (!scheduleId) return;
5211
+ const tracker = getSectionsTracker();
5212
+ let sections = [];
5213
+ try {
5214
+ sections = JSON.parse(tracker.textContent || "[]");
5215
+ } catch {
5216
+ }
5217
+ const currentPath = window.location.pathname;
5218
+ const updated = sections.map(
5219
+ (s) => s.type === "scheduling" && s.pagePath === currentPath ? { ...s, scheduleId } : s
5220
+ );
5221
+ tracker.textContent = JSON.stringify(updated);
5222
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: "__ohw_sections", text: tracker.textContent }] });
5223
+ };
5224
+ const handleClearSchedulingWidget = (e) => {
5225
+ if (e.data?.type !== "ow:clear-scheduling-widget") return;
5226
+ const tracker = getSectionsTracker();
5227
+ let sections = [];
5228
+ try {
5229
+ sections = JSON.parse(tracker.textContent || "[]");
5230
+ } catch {
5231
+ }
5232
+ const currentPath = window.location.pathname;
5233
+ const updated = sections.map(
5234
+ (s) => s.type === "scheduling" && s.pagePath === currentPath ? { ...s, scheduleId: null } : s
5235
+ );
5236
+ tracker.textContent = JSON.stringify(updated);
5237
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: "__ohw_sections", text: tracker.textContent }] });
5238
+ };
5239
+ const handleRemoveSchedulingSection = (e) => {
5240
+ if (e.data?.type !== "ow:remove-scheduling-section") return;
5241
+ const els = document.querySelectorAll('[data-ohw-section="scheduling"]');
5242
+ els.forEach((el) => el.parentElement?.removeChild(el));
5243
+ const tracker = getSectionsTracker();
5244
+ let sections = [];
5245
+ try {
5246
+ sections = JSON.parse(tracker.textContent || "[]");
5247
+ } catch {
5248
+ }
5249
+ const currentPath = window.location.pathname;
5250
+ const updated = sections.filter((s) => !(s.type === "scheduling" && s.pagePath === currentPath));
5251
+ tracker.textContent = JSON.stringify(updated);
5252
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: "__ohw_sections", text: tracker.textContent }] });
5253
+ const h = document.documentElement.scrollHeight;
5254
+ if (h > 50) postToParentRef.current({ type: "ow:height", height: h });
4638
5255
  };
4639
5256
  const handleSelectionChange = () => {
4640
5257
  if (!activeElRef.current) return;
@@ -4739,6 +5356,10 @@ function OhhwellsBridge() {
4739
5356
  deactivateRef.current();
4740
5357
  };
4741
5358
  window.addEventListener("message", handleSave);
5359
+ window.addEventListener("message", handleInsertSection);
5360
+ window.addEventListener("message", handleSwitchSchedule);
5361
+ window.addEventListener("message", handleClearSchedulingWidget);
5362
+ window.addEventListener("message", handleRemoveSchedulingSection);
4742
5363
  window.addEventListener("message", handleImageUrl);
4743
5364
  window.addEventListener("message", handleAnimLock);
4744
5365
  window.addEventListener("message", handleCanvasHeight);
@@ -4773,6 +5394,10 @@ function OhhwellsBridge() {
4773
5394
  document.removeEventListener("selectionchange", handleSelectionChange);
4774
5395
  window.removeEventListener("scroll", handleScroll, true);
4775
5396
  window.removeEventListener("message", handleSave);
5397
+ window.removeEventListener("message", handleInsertSection);
5398
+ window.removeEventListener("message", handleSwitchSchedule);
5399
+ window.removeEventListener("message", handleClearSchedulingWidget);
5400
+ window.removeEventListener("message", handleRemoveSchedulingSection);
4776
5401
  window.removeEventListener("message", handleImageUrl);
4777
5402
  window.removeEventListener("message", handleAnimLock);
4778
5403
  window.removeEventListener("message", handleCanvasHeight);
@@ -4787,7 +5412,7 @@ function OhhwellsBridge() {
4787
5412
  if (imageShowTimerRef.current) clearTimeout(imageShowTimerRef.current);
4788
5413
  };
4789
5414
  }, [isEditMode, refreshStateRules]);
4790
- (0, import_react.useEffect)(() => {
5415
+ (0, import_react2.useEffect)(() => {
4791
5416
  if (!isEditMode) return;
4792
5417
  document.querySelectorAll("[data-ohw-active-state]").forEach((el) => {
4793
5418
  el.removeAttribute("data-ohw-active-state");
@@ -4809,13 +5434,13 @@ function OhhwellsBridge() {
4809
5434
  clearTimeout(timer);
4810
5435
  };
4811
5436
  }, [pathname, isEditMode, refreshStateRules, postToParent]);
4812
- const handleCommand = (0, import_react.useCallback)((cmd) => {
5437
+ const handleCommand = (0, import_react2.useCallback)((cmd) => {
4813
5438
  document.execCommand(cmd, false);
4814
5439
  activeElRef.current?.focus();
4815
5440
  if (activeElRef.current) setToolbarRect(activeElRef.current.getBoundingClientRect());
4816
5441
  refreshActiveCommandsRef.current();
4817
5442
  }, []);
4818
- const handleStateChange = (0, import_react.useCallback)((state) => {
5443
+ const handleStateChange = (0, import_react2.useCallback)((state) => {
4819
5444
  if (!activeStateElRef.current) return;
4820
5445
  const el = activeStateElRef.current;
4821
5446
  if (state === "Default") {
@@ -4829,12 +5454,12 @@ function OhhwellsBridge() {
4829
5454
  setToggleState((prev) => prev ? { ...prev, activeState: state } : null);
4830
5455
  }, [deactivate]);
4831
5456
  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 })
5457
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
5458
+ toolbarRect && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
5459
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(GlowFrame, { rect: toolbarRect, elRef: glowElRef }),
5460
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(FloatingToolbar, { rect: toolbarRect, parentScroll: parentScrollRef.current, elRef: toolbarElRef, onCommand: handleCommand, activeCommands })
4836
5461
  ] }),
4837
- maxBadge && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
5462
+ maxBadge && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
4838
5463
  "div",
4839
5464
  {
4840
5465
  "data-ohw-max-badge": "",
@@ -4860,7 +5485,7 @@ function OhhwellsBridge() {
4860
5485
  ]
4861
5486
  }
4862
5487
  ),
4863
- toggleState && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
5488
+ toggleState && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
4864
5489
  StateToggle,
4865
5490
  {
4866
5491
  rect: toggleState.rect,
@@ -4868,6 +5493,31 @@ function OhhwellsBridge() {
4868
5493
  states: toggleState.states,
4869
5494
  onStateChange: handleStateChange
4870
5495
  }
5496
+ ),
5497
+ sectionGap && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
5498
+ "div",
5499
+ {
5500
+ "data-ohw-section-insert-line": "",
5501
+ className: "fixed left-0 w-full z-2147483646 flex items-center pointer-events-none",
5502
+ style: { top: sectionGap.y, transform: "translateY(-50%)" },
5503
+ children: [
5504
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "flex-1 bg-primary", style: { height: 3 } }),
5505
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
5506
+ Badge,
5507
+ {
5508
+ 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",
5509
+ onClick: () => {
5510
+ window.parent.postMessage(
5511
+ { type: "ow:add-section", insertAfter: sectionGap.insertAfter, insertBefore: sectionGap.insertBefore },
5512
+ "*"
5513
+ );
5514
+ },
5515
+ children: "Add Section"
5516
+ }
5517
+ ),
5518
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "flex-1 bg-primary", style: { height: 3 } })
5519
+ ]
5520
+ }
4871
5521
  )
4872
5522
  ] }),
4873
5523
  bridgeRoot
@@ -4876,6 +5526,7 @@ function OhhwellsBridge() {
4876
5526
  // Annotate the CommonJS export names for ESM import in node:
4877
5527
  0 && (module.exports = {
4878
5528
  OhhwellsBridge,
5529
+ SchedulingWidget,
4879
5530
  Toggle,
4880
5531
  ToggleGroup,
4881
5532
  ToggleGroupItem,