@ohhwells/bridge 0.1.20 → 0.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@
3
3
  // src/OhhwellsBridge.tsx
4
4
  import React3, { useCallback, useEffect as useEffect2, useLayoutEffect, useRef as useRef2, useState as useState3 } from "react";
5
5
  import { createRoot } from "react-dom/client";
6
+ import { flushSync } from "react-dom";
6
7
 
7
8
  // src/ui/SchedulingWidget.tsx
8
9
  import { useEffect, useMemo, useRef, useState as useState2 } from "react";
@@ -215,10 +216,10 @@ function buildTimezoneLabel(tz) {
215
216
  return tz;
216
217
  }
217
218
  }
218
- function EmptyState({ inEditor }) {
219
+ function EmptyState({ inEditor, insertAfter }) {
219
220
  const handleAddSchedule = () => {
220
221
  if (inEditor) {
221
- window.parent.postMessage({ type: "ow:scheduling-not-connected" }, "*");
222
+ window.parent.postMessage({ type: "ow:scheduling-not-connected", insertAfter }, "*");
222
223
  }
223
224
  };
224
225
  return /* @__PURE__ */ jsxs2("div", { className: "flex flex-col items-center justify-center gap-6 p-12 bg-[#FAFAF9] border-2 border-dashed border-[#E7E5E4] rounded-[10px] h-[264px] w-full box-border", children: [
@@ -270,7 +271,19 @@ function LoadingSkeleton() {
270
271
  /* @__PURE__ */ jsx2("div", { style: { ...shimmer, width: "40px", height: "40px", borderRadius: "50%" } })
271
272
  ] }, i)) }),
272
273
  [0, 1, 2].map((i) => /* @__PURE__ */ jsxs2("div", { children: [
273
- /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-[60px] py-4", children: [
274
+ /* @__PURE__ */ jsxs2("div", { className: "flex gap-4 py-4 sm:hidden", children: [
275
+ /* @__PURE__ */ jsxs2("div", { className: "flex flex-col gap-2 shrink-0", children: [
276
+ /* @__PURE__ */ jsx2("div", { style: { ...shimmer, width: "100px", height: "16px", borderRadius: "4px" } }),
277
+ /* @__PURE__ */ jsx2("div", { style: { ...shimmer, width: "52px", height: "20px", borderRadius: "999px" } }),
278
+ /* @__PURE__ */ jsx2("div", { style: { ...shimmer, width: "60px", height: "14px", borderRadius: "4px" } })
279
+ ] }),
280
+ /* @__PURE__ */ jsxs2("div", { className: "flex flex-1 flex-col gap-2", children: [
281
+ /* @__PURE__ */ jsx2("div", { style: { ...shimmer, width: "75%", height: "16px", borderRadius: "4px" } }),
282
+ /* @__PURE__ */ jsx2("div", { style: { ...shimmer, width: "50%", height: "14px", borderRadius: "4px" } }),
283
+ /* @__PURE__ */ jsx2("div", { style: { ...shimmer, width: "100%", height: "44px", borderRadius: "8px" } })
284
+ ] })
285
+ ] }),
286
+ /* @__PURE__ */ jsxs2("div", { className: "hidden sm:flex items-center gap-[60px] py-4", children: [
274
287
  /* @__PURE__ */ jsx2("div", { style: { ...shimmer, width: "120px", height: "16px", borderRadius: "4px", flexShrink: 0 } }),
275
288
  /* @__PURE__ */ jsxs2("div", { className: "flex-1 flex flex-col gap-2", children: [
276
289
  /* @__PURE__ */ jsx2("div", { style: { ...shimmer, width: "55%", height: "16px", borderRadius: "4px" } }),
@@ -313,6 +326,28 @@ function ScheduleView({ schedule, dates, selectedIdx, onSelectDate, onOpenModal
313
326
  const scroll = (dir) => {
314
327
  scrollRef.current?.scrollBy({ left: dir === "left" ? -216 : 216, behavior: "smooth" });
315
328
  };
329
+ const isDragging = useRef(false);
330
+ const dragStartX = useRef(0);
331
+ const dragStartScrollLeft = useRef(0);
332
+ const onDragStart = (e) => {
333
+ if (!scrollRef.current) return;
334
+ isDragging.current = true;
335
+ dragStartX.current = e.clientX;
336
+ dragStartScrollLeft.current = scrollRef.current.scrollLeft;
337
+ scrollRef.current.style.cursor = "grabbing";
338
+ scrollRef.current.style.userSelect = "none";
339
+ };
340
+ const onDragMove = (e) => {
341
+ if (!isDragging.current || !scrollRef.current) return;
342
+ const delta = e.clientX - dragStartX.current;
343
+ scrollRef.current.scrollLeft = dragStartScrollLeft.current - delta;
344
+ };
345
+ const onDragEnd = () => {
346
+ if (!scrollRef.current) return;
347
+ isDragging.current = false;
348
+ scrollRef.current.style.cursor = "grab";
349
+ scrollRef.current.style.userSelect = "";
350
+ };
316
351
  const datesWithClasses = useMemo(
317
352
  () => new Set(dates.map(
318
353
  (d, i) => schedule.classes?.some((c) => classRunsOnDate(c, d, schedule.type)) ? i : -1
@@ -330,7 +365,7 @@ function ScheduleView({ schedule, dates, selectedIdx, onSelectDate, onOpenModal
330
365
  "button",
331
366
  {
332
367
  onClick: () => scroll("left"),
333
- className: "absolute left-0 top-1/2 -translate-y-1/2 z-10 w-8 h-8 flex items-center justify-center rounded-full border-none cursor-pointer",
368
+ className: "hidden sm:flex absolute left-0 top-1/2 -translate-y-1/2 z-10 w-8 h-8 items-center justify-center rounded-full border-none cursor-pointer",
334
369
  style: { background: "var(--color-light,#FEFFF0)", boxShadow: "0 1px 4px rgba(0,0,0,0.15)" },
335
370
  "aria-label": "Scroll left",
336
371
  children: /* @__PURE__ */ jsx2("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "var(--color-dark,#200C02)", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx2("polyline", { points: "15 18 9 12 15 6" }) })
@@ -340,23 +375,36 @@ function ScheduleView({ schedule, dates, selectedIdx, onSelectDate, onOpenModal
340
375
  "button",
341
376
  {
342
377
  onClick: () => scroll("right"),
343
- className: "absolute right-0 top-1/2 -translate-y-1/2 z-10 w-8 h-8 flex items-center justify-center rounded-full border-none cursor-pointer",
378
+ className: "hidden sm:flex absolute right-0 top-1/2 -translate-y-1/2 z-10 w-8 h-8 items-center justify-center rounded-full border-none cursor-pointer",
344
379
  style: { background: "var(--color-light,#FEFFF0)", boxShadow: "0 1px 4px rgba(0,0,0,0.15)" },
345
380
  "aria-label": "Scroll right",
346
381
  children: /* @__PURE__ */ jsx2("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "var(--color-dark,#200C02)", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx2("polyline", { points: "9 18 15 12 9 6" }) })
347
382
  }
348
383
  ),
384
+ canScrollLeft && /* @__PURE__ */ jsx2(
385
+ "div",
386
+ {
387
+ className: "sm:hidden absolute left-0 top-0 bottom-0 w-10 z-10 pointer-events-none",
388
+ style: { background: "linear-gradient(to right, var(--color-light,#FEFFF0), transparent)" }
389
+ }
390
+ ),
391
+ canScrollRight && /* @__PURE__ */ jsx2(
392
+ "div",
393
+ {
394
+ className: "sm:hidden absolute right-0 top-0 bottom-0 w-10 z-10 pointer-events-none",
395
+ style: { background: "linear-gradient(to left, var(--color-light,#FEFFF0), transparent)" }
396
+ }
397
+ ),
349
398
  /* @__PURE__ */ jsx2(
350
399
  "div",
351
400
  {
352
401
  ref: scrollRef,
353
402
  className: "overflow-x-auto w-full",
354
- style: {
355
- scrollbarWidth: "none",
356
- msOverflowStyle: "none",
357
- paddingLeft: canScrollLeft ? "36px" : 0,
358
- paddingRight: canScrollRight ? "36px" : 0
359
- },
403
+ style: { scrollbarWidth: "none", msOverflowStyle: "none", touchAction: "pan-x", cursor: "grab" },
404
+ onMouseDown: onDragStart,
405
+ onMouseMove: onDragMove,
406
+ onMouseUp: onDragEnd,
407
+ onMouseLeave: onDragEnd,
360
408
  children: /* @__PURE__ */ jsx2("div", { className: "flex", style: { width: `max(100%, ${dates.length * 60}px)` }, children: dates.map((date, i) => {
361
409
  const isToday = date.getTime() === todayMs;
362
410
  const isSelected = i === selectedIdx;
@@ -397,40 +445,60 @@ function ScheduleView({ schedule, dates, selectedIdx, onSelectDate, onOpenModal
397
445
  const available = cls.maxParticipants - booked;
398
446
  const isFull = available <= 0;
399
447
  return /* @__PURE__ */ jsxs2("div", { children: [
400
- /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-[60px] py-4 box-border", children: [
401
- /* @__PURE__ */ jsx2("div", { className: "w-[120px] shrink-0", children: /* @__PURE__ */ jsx2("span", { className: "font-body text-base font-bold text-(--color-dark,#200C02) block", children: formatClassTime(cls) }) }),
402
- /* @__PURE__ */ jsxs2("div", { className: "flex-1 flex flex-col gap-3 min-w-0", children: [
403
- /* @__PURE__ */ jsx2("span", { className: "font-body text-base font-bold text-(--color-dark,#200C02) block truncate", children: cls.name }),
404
- cls.hostName && /* @__PURE__ */ jsx2("span", { className: "font-body text-base font-normal text-(--color-dark,#200C02) opacity-80 block truncate", children: cls.hostName })
448
+ /* @__PURE__ */ jsxs2("div", { className: "flex gap-4 py-4 sm:items-center sm:gap-[60px] box-border", children: [
449
+ /* @__PURE__ */ jsxs2("div", { className: "flex flex-col gap-2 shrink-0 sm:contents", children: [
450
+ /* @__PURE__ */ jsx2("div", { className: "sm:w-[120px] sm:shrink-0", children: /* @__PURE__ */ jsx2("span", { className: "font-body text-base font-bold text-(--color-dark,#200C02) block", children: formatClassTime(cls) }) }),
451
+ /* @__PURE__ */ jsx2(
452
+ "span",
453
+ {
454
+ className: "inline-flex items-center px-2 py-0.5 rounded-full border text-xs font-medium sm:hidden",
455
+ style: { borderColor: "#0885FE", color: "#0885FE" },
456
+ children: "GROUP"
457
+ }
458
+ ),
459
+ /* @__PURE__ */ jsx2("div", { className: "sm:hidden flex flex-col gap-px", children: isFull ? /* @__PURE__ */ jsx2("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: "Full" }) : /* @__PURE__ */ jsxs2(Fragment, { children: [
460
+ /* @__PURE__ */ jsxs2("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: [
461
+ available,
462
+ "/",
463
+ cls.maxParticipants
464
+ ] }),
465
+ /* @__PURE__ */ jsx2("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: "available" })
466
+ ] }) })
405
467
  ] }),
406
- /* @__PURE__ */ jsx2("div", { className: "w-14 shrink-0 flex flex-col gap-px", children: isFull ? /* @__PURE__ */ jsx2("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: "Full" }) : /* @__PURE__ */ jsxs2(Fragment, { children: [
407
- /* @__PURE__ */ jsxs2("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: [
408
- available,
409
- "/",
410
- cls.maxParticipants
468
+ /* @__PURE__ */ jsxs2("div", { className: "flex flex-1 flex-col gap-2 min-w-0 sm:contents", children: [
469
+ /* @__PURE__ */ jsxs2("div", { className: "flex-1 flex flex-col gap-2 min-w-0", children: [
470
+ /* @__PURE__ */ jsx2("span", { className: "font-body text-base font-bold text-(--color-dark,#200C02) block truncate", children: cls.name }),
471
+ cls.hostName && /* @__PURE__ */ jsx2("span", { className: "font-body text-base font-normal text-(--color-dark,#200C02) opacity-80 block truncate", children: cls.hostName })
411
472
  ] }),
412
- /* @__PURE__ */ jsx2("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: "available" })
413
- ] }) }),
414
- /* @__PURE__ */ jsx2(
415
- "button",
416
- {
417
- className: "shrink-0 w-[200px] px-5 py-[10px] flex items-center justify-center rounded-lg font-body text-base font-bold cursor-pointer box-border",
418
- style: isFull ? {
419
- background: "transparent",
420
- border: "1px solid var(--color-dark, #200C02)",
421
- color: "var(--color-dark, #200C02)"
422
- } : {
423
- background: "var(--color-primary, #3D312B)",
424
- border: "none",
425
- color: "var(--color-light, #FEFFF0)"
426
- },
427
- onClick: () => {
428
- if (!cls.id) return;
429
- onOpenModal?.(isFull ? "waitlist" : "book", cls.id, selectedDate);
430
- },
431
- children: isFull ? "Join Waitlist" : "Book Now"
432
- }
433
- )
473
+ /* @__PURE__ */ jsx2("div", { className: "hidden sm:flex w-14 shrink-0 flex-col gap-px", children: isFull ? /* @__PURE__ */ jsx2("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: "Full" }) : /* @__PURE__ */ jsxs2(Fragment, { children: [
474
+ /* @__PURE__ */ jsxs2("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: [
475
+ available,
476
+ "/",
477
+ cls.maxParticipants
478
+ ] }),
479
+ /* @__PURE__ */ jsx2("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: "available" })
480
+ ] }) }),
481
+ /* @__PURE__ */ jsx2(
482
+ "button",
483
+ {
484
+ className: "w-full sm:w-[200px] sm:shrink-0 px-5 py-[10px] flex items-center justify-center rounded-lg font-body text-base font-bold cursor-pointer box-border",
485
+ style: isFull ? {
486
+ background: "transparent",
487
+ border: "1px solid var(--color-dark, #200C02)",
488
+ color: "var(--color-dark, #200C02)"
489
+ } : {
490
+ background: "var(--color-primary, #3D312B)",
491
+ border: "none",
492
+ color: "var(--color-light, #FEFFF0)"
493
+ },
494
+ onClick: () => {
495
+ if (!cls.id) return;
496
+ onOpenModal?.(isFull ? "waitlist" : "book", cls.id, selectedDate);
497
+ },
498
+ children: isFull ? "Join Waitlist" : "Book Now"
499
+ }
500
+ )
501
+ ] })
434
502
  ] }),
435
503
  i < selectedClasses.length - 1 && /* @__PURE__ */ jsx2("div", { className: "h-px bg-black/20" })
436
504
  ] }, cls.id ?? i);
@@ -460,7 +528,7 @@ function CalendarFoldIcon() {
460
528
  }
461
529
  );
462
530
  }
463
- function SchedulingWidget({ notifyOnConnect = false, initialScheduleId }) {
531
+ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAfter }) {
464
532
  const [schedule, setSchedule] = useState2(null);
465
533
  const [loading, setLoading] = useState2(true);
466
534
  const [inEditor, setInEditor] = useState2(false);
@@ -502,18 +570,21 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId }) {
502
570
  }, 5e3) : null;
503
571
  const handler = (e) => {
504
572
  if (e.data?.type === "ow:clear-scheduling-widget") {
573
+ if (!e.data.insertAfter || e.data.insertAfter !== insertAfter) return;
505
574
  setSchedule(null);
506
575
  setLoading(false);
507
576
  return;
508
577
  }
509
578
  if (e.data?.type === "ow:switch-schedule") {
579
+ if (!e.data.insertAfter || e.data.insertAfter !== insertAfter) return;
510
580
  switchScheduleIdRef.current = e.data.scheduleId ?? null;
511
581
  initialized = false;
512
582
  setLoading(true);
513
- window.parent.postMessage({ type: "ow:request-user-schedules" }, "*");
583
+ window.parent.postMessage({ type: "ow:request-user-schedules", insertAfter }, "*");
514
584
  return;
515
585
  }
516
586
  if (e.data?.type !== "ow:user-schedules-response") return;
587
+ if (e.data.insertAfter && e.data.insertAfter !== insertAfter) return;
517
588
  if (initialized && switchScheduleIdRef.current === null) return;
518
589
  initialized = true;
519
590
  if (timer) clearTimeout(timer);
@@ -524,11 +595,16 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId }) {
524
595
  setSchedule(target);
525
596
  setLoading(false);
526
597
  if (notifyOnConnect && target && !isSwitching) {
527
- window.parent.postMessage({ type: "ow:schedule-connected", schedule: { id: target.id, name: target.name } }, "*");
598
+ window.parent.postMessage({
599
+ type: "ow:schedule-connected",
600
+ schedule: { id: target.id, name: target.name },
601
+ insertAfter
602
+ }, "*");
603
+ window.postMessage({ type: "ow:schedule-linked", scheduleId: target.id, insertAfter }, "*");
528
604
  }
529
605
  };
530
606
  window.addEventListener("message", handler);
531
- if (!startEmpty) window.parent.postMessage({ type: "ow:request-user-schedules" }, "*");
607
+ if (!startEmpty) window.parent.postMessage({ type: "ow:request-user-schedules", insertAfter }, "*");
532
608
  return () => {
533
609
  window.removeEventListener("message", handler);
534
610
  if (timer) clearTimeout(timer);
@@ -589,16 +665,23 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId }) {
589
665
  const handleReplaceSchedule = () => {
590
666
  setIsHovered(false);
591
667
  if (schedule) {
592
- window.parent.postMessage({ type: "ow:schedule-connected", schedule: { id: schedule.id, name: schedule.name } }, "*");
668
+ window.parent.postMessage({
669
+ type: "ow:schedule-connected",
670
+ schedule: { id: schedule.id, name: schedule.name },
671
+ insertAfter
672
+ }, "*");
593
673
  } else {
594
- window.parent.postMessage({ type: "ow:scheduling-not-connected" }, "*");
674
+ window.parent.postMessage({ type: "ow:scheduling-not-connected", insertAfter }, "*");
595
675
  }
596
676
  };
677
+ if (!inEditor && !loading && !schedule) return null;
678
+ const sectionId = `scheduling-${insertAfter}`;
597
679
  return /* @__PURE__ */ jsxs2(
598
680
  "section",
599
681
  {
600
- "data-ohw-section": "scheduling",
601
- className: "w-full box-border py-20 px-16 font-body bg-(--color-light,#FEFFF0) relative",
682
+ "data-ohw-section": sectionId,
683
+ "data-ohw-scheduling-anchor": insertAfter,
684
+ className: "w-full box-border py-10 px-5 sm:py-20 sm:px-16 font-body bg-(--color-light,#FEFFF0) relative",
602
685
  onMouseEnter: () => inEditor && setIsHovered(true),
603
686
  onMouseLeave: () => setIsHovered(false),
604
687
  children: [
@@ -632,12 +715,12 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId }) {
632
715
  ),
633
716
  /* @__PURE__ */ jsxs2("div", { className: "max-w-[1280px] mx-auto flex flex-col items-center gap-16", children: [
634
717
  /* @__PURE__ */ jsxs2("div", { className: "flex flex-col items-center gap-4", children: [
635
- /* @__PURE__ */ jsx2("h2", { className: "font-display text-5xl font-bold text-center m-0 text-(--color-dark,#200C02)", children: schedule?.name ?? "Book an appointment" }),
718
+ /* @__PURE__ */ jsx2("h2", { className: "font-display text-4xl sm:text-5xl font-bold text-center m-0 text-(--color-dark,#200C02)", children: schedule?.name ?? "Book an appointment" }),
636
719
  schedule?.description && /* @__PURE__ */ jsx2("p", { className: "font-body text-body text-center m-0 text-(--color-accent,#A89B83)", children: schedule.description })
637
720
  ] }),
638
721
  /* @__PURE__ */ jsxs2("div", { className: "w-full flex flex-col items-end gap-3", children: [
639
722
  timezoneLabel && /* @__PURE__ */ jsx2("span", { className: "font-body text-sm font-normal text-(--color-accent,#A89B83)", children: timezoneLabel }),
640
- /* @__PURE__ */ jsx2("div", { className: "w-full p-10 box-border", children: loading ? /* @__PURE__ */ jsx2(LoadingSkeleton, {}) : !schedule ? /* @__PURE__ */ jsx2(EmptyState, { inEditor }) : /* @__PURE__ */ jsx2(
723
+ /* @__PURE__ */ jsx2("div", { className: "w-full p-0 sm:p-10 box-border", children: loading ? /* @__PURE__ */ jsx2(LoadingSkeleton, {}) : !schedule ? /* @__PURE__ */ jsx2(EmptyState, { inEditor, insertAfter }) : /* @__PURE__ */ jsx2(
641
724
  ScheduleView,
642
725
  {
643
726
  schedule,
@@ -4177,24 +4260,136 @@ function getSectionsTracker() {
4177
4260
  }
4178
4261
  return el;
4179
4262
  }
4180
- function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId) {
4181
- const anchor = document.querySelector(`[data-ohw-section="${insertAfter}"]`);
4182
- if (!anchor) return false;
4183
- if (anchor.nextElementSibling?.dataset.ohwSectionContainer) return false;
4263
+ function updateSectionScheduleId(insertAfter, scheduleId) {
4264
+ const tracker = getSectionsTracker();
4265
+ let sections = [];
4266
+ try {
4267
+ sections = JSON.parse(tracker.textContent || "[]");
4268
+ } catch {
4269
+ }
4270
+ const currentPath = window.location.pathname;
4271
+ const updated = sections.map((s) => {
4272
+ if (s.type !== "scheduling" || s.insertAfter !== insertAfter) return s;
4273
+ if (s.pagePath && s.pagePath !== currentPath) return s;
4274
+ return { ...s, scheduleId };
4275
+ });
4276
+ tracker.textContent = JSON.stringify(updated);
4277
+ return tracker.textContent ?? "[]";
4278
+ }
4279
+ function schedulingSectionId(insertAfter) {
4280
+ return `scheduling-${insertAfter}`;
4281
+ }
4282
+ var INSERT_BEFORE_MARKER = ":before:";
4283
+ function parseSchedulingInsertAfter(insertAfter) {
4284
+ const idx = insertAfter.indexOf(INSERT_BEFORE_MARKER);
4285
+ if (idx < 0) return { anchor: insertAfter, insertBefore: null };
4286
+ return {
4287
+ anchor: insertAfter.slice(0, idx),
4288
+ insertBefore: insertAfter.slice(idx + INSERT_BEFORE_MARKER.length)
4289
+ };
4290
+ }
4291
+ function resolveSchedulingInsert(insertAfter, explicitInsertBefore) {
4292
+ const parsed = parseSchedulingInsertAfter(insertAfter);
4293
+ const insertBefore = explicitInsertBefore ?? parsed.insertBefore;
4294
+ const effectiveInsertAfter = insertBefore && parsed.insertBefore === null ? `${insertAfter}${INSERT_BEFORE_MARKER}${insertBefore}` : insertAfter;
4295
+ return { effectiveInsertAfter, insertBefore };
4296
+ }
4297
+ function getSchedulingMountPoint(insertAfter) {
4298
+ const { anchor } = parseSchedulingInsertAfter(insertAfter);
4299
+ let anchorEl = document.querySelector(`[data-ohw-section="${anchor}"]`);
4300
+ if (!anchorEl && anchor === "scheduling") {
4301
+ const widgets = Array.from(document.querySelectorAll('[data-ohw-section^="scheduling-"]'));
4302
+ anchorEl = widgets.at(-1) ?? null;
4303
+ }
4304
+ if (!anchorEl) return null;
4305
+ return anchorEl.closest('[data-ohw-section-container="scheduling"]') ?? anchorEl;
4306
+ }
4307
+ function schedulingMountDepth(insertAfter) {
4308
+ if (!insertAfter.includes(INSERT_BEFORE_MARKER)) return 0;
4309
+ return insertAfter.split(INSERT_BEFORE_MARKER).length - 1;
4310
+ }
4311
+ function getPageSchedulingEntries(raw) {
4312
+ if (!raw) return [];
4313
+ try {
4314
+ const entries = JSON.parse(raw);
4315
+ const currentPath = window.location.pathname;
4316
+ return entries.filter((e) => e.type === "scheduling" && (!e.pagePath || e.pagePath === currentPath));
4317
+ } catch {
4318
+ return [];
4319
+ }
4320
+ }
4321
+ function isSchedulingWidgetMissing(entry) {
4322
+ const { effectiveInsertAfter } = resolveSchedulingInsert(entry.insertAfter);
4323
+ return !document.querySelector(`[data-ohw-section="${schedulingSectionId(effectiveInsertAfter)}"]`);
4324
+ }
4325
+ function hasMissingSchedulingWidgets(entries) {
4326
+ return entries.some(isSchedulingWidgetMissing);
4327
+ }
4328
+ function retryMissingSchedulingMounts(entries, notifyOnConnect = false) {
4329
+ if (!hasMissingSchedulingWidgets(entries)) return;
4330
+ mountSchedulingEntries(entries, notifyOnConnect);
4331
+ }
4332
+ function initSectionsFromContent(content, removeExisting = false) {
4333
+ const raw = content["__ohw_sections"];
4334
+ if (!raw) return;
4335
+ try {
4336
+ if (removeExisting) {
4337
+ document.querySelectorAll('[data-ohw-section-container="scheduling"]').forEach((el) => el.remove());
4338
+ }
4339
+ const inEditor = typeof window !== "undefined" && window.self !== window.top;
4340
+ if (inEditor) getSectionsTracker().textContent = raw;
4341
+ const pageEntries = getPageSchedulingEntries(raw);
4342
+ const notifyForEntry = inEditor ? (e) => !e.scheduleId : false;
4343
+ mountSchedulingEntries(pageEntries, notifyForEntry);
4344
+ requestAnimationFrame(() => retryMissingSchedulingMounts(pageEntries, notifyForEntry));
4345
+ setTimeout(() => retryMissingSchedulingMounts(pageEntries, notifyForEntry), 250);
4346
+ } catch {
4347
+ }
4348
+ }
4349
+ function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId, explicitInsertBefore) {
4350
+ const { effectiveInsertAfter, insertBefore } = resolveSchedulingInsert(insertAfter, explicitInsertBefore);
4351
+ const sectionId = schedulingSectionId(effectiveInsertAfter);
4352
+ if (document.querySelector(`[data-ohw-section="${sectionId}"]`)) return false;
4353
+ const mountPoint = getSchedulingMountPoint(effectiveInsertAfter);
4354
+ if (!mountPoint) return false;
4184
4355
  const container = document.createElement("div");
4185
4356
  container.dataset.ohwSectionContainer = "scheduling";
4186
- anchor.insertAdjacentElement("afterend", container);
4187
- createRoot(container).render(/* @__PURE__ */ jsx6(SchedulingWidget, { notifyOnConnect, initialScheduleId: scheduleId }));
4357
+ if (insertBefore) {
4358
+ const beforeAnchor = document.querySelector(`[data-ohw-section="${insertBefore}"]`);
4359
+ const beforePoint = beforeAnchor?.closest('[data-ohw-section-container="scheduling"]') ?? beforeAnchor;
4360
+ if (!beforePoint) return false;
4361
+ beforePoint.insertAdjacentElement("beforebegin", container);
4362
+ } else {
4363
+ let tail = mountPoint;
4364
+ while (tail.nextElementSibling?.dataset.ohwSectionContainer === "scheduling") {
4365
+ tail = tail.nextElementSibling;
4366
+ }
4367
+ tail.insertAdjacentElement("afterend", container);
4368
+ }
4369
+ const root = createRoot(container);
4370
+ flushSync(() => {
4371
+ root.render(
4372
+ /* @__PURE__ */ jsx6(
4373
+ SchedulingWidget,
4374
+ {
4375
+ notifyOnConnect,
4376
+ initialScheduleId: scheduleId,
4377
+ insertAfter: effectiveInsertAfter
4378
+ }
4379
+ )
4380
+ );
4381
+ });
4188
4382
  const tracker = getSectionsTracker();
4189
4383
  let sections = [];
4190
4384
  try {
4191
4385
  sections = JSON.parse(tracker.textContent || "[]");
4192
4386
  } catch {
4193
4387
  }
4194
- if (!sections.find((s) => s.type === "scheduling" && s.insertAfter === insertAfter)) {
4388
+ const inEditor = typeof window !== "undefined" && window.self !== window.top;
4389
+ if (inEditor && !sections.find((s) => s.type === "scheduling" && s.insertAfter === effectiveInsertAfter)) {
4195
4390
  sections.push({
4196
4391
  type: "scheduling",
4197
- insertAfter,
4392
+ insertAfter: effectiveInsertAfter,
4198
4393
  pagePath: window.location.pathname,
4199
4394
  ...scheduleId ? { scheduleId } : {}
4200
4395
  });
@@ -4202,6 +4397,18 @@ function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId)
4202
4397
  }
4203
4398
  return true;
4204
4399
  }
4400
+ function mountSchedulingEntries(entries, notifyOnConnect = false) {
4401
+ const pending = entries.filter((e) => e.type === "scheduling").sort((a, b) => schedulingMountDepth(a.insertAfter) - schedulingMountDepth(b.insertAfter));
4402
+ for (let attempt = 0; attempt < pending.length + 1 && pending.length > 0; attempt++) {
4403
+ for (let i = pending.length - 1; i >= 0; i--) {
4404
+ const entry = pending[i];
4405
+ const shouldNotify = typeof notifyOnConnect === "function" ? notifyOnConnect(entry) : notifyOnConnect;
4406
+ if (mountSchedulingWidget(entry.insertAfter, shouldNotify, entry.scheduleId)) {
4407
+ pending.splice(i, 1);
4408
+ }
4409
+ }
4410
+ }
4411
+ }
4205
4412
  function collectEditableNodes() {
4206
4413
  return Array.from(document.querySelectorAll("[data-ohw-editable]")).map((el) => {
4207
4414
  if (el.dataset.ohwEditable === "image") {
@@ -4515,6 +4722,23 @@ function StateToggle({
4515
4722
  );
4516
4723
  }
4517
4724
  var contentCache = /* @__PURE__ */ new Map();
4725
+ function resolveSubdomain(subdomainFromQuery) {
4726
+ if (subdomainFromQuery) return subdomainFromQuery;
4727
+ if (typeof window !== "undefined") {
4728
+ const parts = window.location.hostname.split(".");
4729
+ if (parts.length >= 3 && parts[0] !== "www") return parts[0];
4730
+ }
4731
+ const siteUrl = process.env.NEXT_PUBLIC_SITE_URL;
4732
+ if (siteUrl) {
4733
+ try {
4734
+ const host = new URL(siteUrl).hostname;
4735
+ const siteParts = host.split(".");
4736
+ if (siteParts.length >= 3 && siteParts[0] !== "www") return siteParts[0];
4737
+ } catch {
4738
+ }
4739
+ }
4740
+ return "";
4741
+ }
4518
4742
  function OhhwellsBridge() {
4519
4743
  const pathname = usePathname();
4520
4744
  const router = useRouter();
@@ -4544,11 +4768,7 @@ function OhhwellsBridge() {
4544
4768
  };
4545
4769
  }, []);
4546
4770
  const subdomainFromQuery = searchParams.get("subdomain");
4547
- const subdomain = subdomainFromQuery ?? (() => {
4548
- if (typeof window === "undefined") return "";
4549
- const parts = window.location.hostname.split(".");
4550
- return parts.length >= 3 && parts[0] !== "www" ? parts[0] : "";
4551
- })();
4771
+ const subdomain = resolveSubdomain(subdomainFromQuery);
4552
4772
  const postToParent = useCallback((data) => {
4553
4773
  if (typeof window !== "undefined" && window.parent !== window) {
4554
4774
  window.parent.postMessage(data, "*");
@@ -4658,18 +4878,6 @@ function OhhwellsBridge() {
4658
4878
  }
4659
4879
  const applyContent = (content) => {
4660
4880
  const imageLoads = [];
4661
- document.querySelectorAll("[data-ohw-section-container]").forEach((el) => el.remove());
4662
- if (content["__ohw_sections"]) {
4663
- try {
4664
- const entries = JSON.parse(content["__ohw_sections"]);
4665
- const currentPath = window.location.pathname;
4666
- entries.forEach(({ type, insertAfter, scheduleId, pagePath }) => {
4667
- if (pagePath && pagePath !== currentPath) return;
4668
- if (type === "scheduling") mountSchedulingWidget(insertAfter, false, scheduleId);
4669
- });
4670
- } catch {
4671
- }
4672
- }
4673
4881
  for (const [key, val] of Object.entries(content)) {
4674
4882
  if (key === "__ohw_sections") continue;
4675
4883
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
@@ -4690,6 +4898,7 @@ function OhhwellsBridge() {
4690
4898
  }
4691
4899
  });
4692
4900
  }
4901
+ initSectionsFromContent(content, true);
4693
4902
  return imageLoads.length > 0 ? Promise.all(imageLoads).then(() => {
4694
4903
  }) : Promise.resolve();
4695
4904
  };
@@ -4716,20 +4925,11 @@ function OhhwellsBridge() {
4716
4925
  }, [subdomain, isEditMode, pathname]);
4717
4926
  useEffect2(() => {
4718
4927
  if (!subdomain || isEditMode) return;
4928
+ let debounceTimer = null;
4719
4929
  const applyFromCache = () => {
4720
4930
  const content = contentCache.get(subdomain);
4721
4931
  if (!content) return;
4722
- if (content["__ohw_sections"]) {
4723
- try {
4724
- const entries = JSON.parse(content["__ohw_sections"]);
4725
- const currentPath = window.location.pathname;
4726
- entries.forEach(({ type, insertAfter, scheduleId, pagePath }) => {
4727
- if (pagePath && pagePath !== currentPath) return;
4728
- if (type === "scheduling") mountSchedulingWidget(insertAfter, false, scheduleId);
4729
- });
4730
- } catch {
4731
- }
4732
- }
4932
+ retryMissingSchedulingMounts(getPageSchedulingEntries(content["__ohw_sections"]), false);
4733
4933
  for (const [key, val] of Object.entries(content)) {
4734
4934
  if (key === "__ohw_sections") continue;
4735
4935
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
@@ -4745,11 +4945,18 @@ function OhhwellsBridge() {
4745
4945
  });
4746
4946
  }
4747
4947
  };
4748
- applyFromCache();
4749
- const observer = new MutationObserver(applyFromCache);
4948
+ const scheduleApply = () => {
4949
+ if (debounceTimer) clearTimeout(debounceTimer);
4950
+ debounceTimer = setTimeout(applyFromCache, 150);
4951
+ };
4952
+ scheduleApply();
4953
+ const observer = new MutationObserver(scheduleApply);
4750
4954
  observer.observe(document.body, { childList: true, subtree: true });
4751
- return () => observer.disconnect();
4752
- }, [subdomain, isEditMode]);
4955
+ return () => {
4956
+ observer.disconnect();
4957
+ if (debounceTimer) clearTimeout(debounceTimer);
4958
+ };
4959
+ }, [subdomain, isEditMode, pathname]);
4753
4960
  useLayoutEffect(() => {
4754
4961
  const el = document.getElementById("ohw-loader");
4755
4962
  if (!el) return;
@@ -5322,17 +5529,10 @@ function OhhwellsBridge() {
5322
5529
  if (e.data?.type !== "ow:hydrate") return;
5323
5530
  const content = e.data.content;
5324
5531
  if (!content) return;
5532
+ let sectionsJson = null;
5325
5533
  for (const [key, val] of Object.entries(content)) {
5326
5534
  if (key === "__ohw_sections") {
5327
- try {
5328
- const entries = JSON.parse(val);
5329
- const tracker = getSectionsTracker();
5330
- tracker.textContent = val;
5331
- entries.forEach(({ type, insertAfter, scheduleId }) => {
5332
- if (type === "scheduling") mountSchedulingWidget(insertAfter, false, scheduleId);
5333
- });
5334
- } catch {
5335
- }
5535
+ sectionsJson = val;
5336
5536
  continue;
5337
5537
  }
5338
5538
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
@@ -5346,6 +5546,9 @@ function OhhwellsBridge() {
5346
5546
  }
5347
5547
  });
5348
5548
  }
5549
+ if (sectionsJson) {
5550
+ initSectionsFromContent({ __ohw_sections: sectionsJson }, true);
5551
+ }
5349
5552
  postToParentRef.current({ type: "ow:hydrate-done" });
5350
5553
  };
5351
5554
  window.addEventListener("message", handleHydrate);
@@ -5396,9 +5599,9 @@ function OhhwellsBridge() {
5396
5599
  };
5397
5600
  const handleInsertSection = (e) => {
5398
5601
  if (e.data?.type !== "ow:insert-section") return;
5399
- const { widgetType, insertAfter } = e.data;
5602
+ const { widgetType, insertAfter, insertBefore } = e.data;
5400
5603
  if (widgetType !== "scheduling") return;
5401
- const inserted = mountSchedulingWidget(insertAfter, true);
5604
+ const inserted = mountSchedulingWidget(insertAfter, true, void 0, insertBefore ?? null);
5402
5605
  if (inserted) {
5403
5606
  const tracker = getSectionsTracker();
5404
5607
  postToParentRef.current({ type: "ow:change", nodes: [{ key: "__ohw_sections", text: tracker.textContent ?? "[]" }] });
@@ -5409,39 +5612,31 @@ function OhhwellsBridge() {
5409
5612
  const handleSwitchSchedule = (e) => {
5410
5613
  if (e.data?.type !== "ow:switch-schedule") return;
5411
5614
  const scheduleId = e.data.scheduleId;
5412
- if (!scheduleId) return;
5413
- const tracker = getSectionsTracker();
5414
- let sections = [];
5415
- try {
5416
- sections = JSON.parse(tracker.textContent || "[]");
5417
- } catch {
5418
- }
5419
- const currentPath = window.location.pathname;
5420
- const updated = sections.map(
5421
- (s) => s.type === "scheduling" && s.pagePath === currentPath ? { ...s, scheduleId } : s
5422
- );
5423
- tracker.textContent = JSON.stringify(updated);
5424
- postToParentRef.current({ type: "ow:change", nodes: [{ key: "__ohw_sections", text: tracker.textContent }] });
5615
+ const insertAfter = e.data.insertAfter;
5616
+ if (!scheduleId || !insertAfter) return;
5617
+ const text = updateSectionScheduleId(insertAfter, scheduleId);
5618
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: "__ohw_sections", text }] });
5619
+ };
5620
+ const handleScheduleLinked = (e) => {
5621
+ if (e.data?.type !== "ow:schedule-linked") return;
5622
+ const scheduleId = e.data.scheduleId;
5623
+ const insertAfter = e.data.insertAfter;
5624
+ if (!scheduleId || !insertAfter) return;
5625
+ const text = updateSectionScheduleId(insertAfter, scheduleId);
5626
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: "__ohw_sections", text }] });
5425
5627
  };
5426
5628
  const handleClearSchedulingWidget = (e) => {
5427
5629
  if (e.data?.type !== "ow:clear-scheduling-widget") return;
5428
- const tracker = getSectionsTracker();
5429
- let sections = [];
5430
- try {
5431
- sections = JSON.parse(tracker.textContent || "[]");
5432
- } catch {
5433
- }
5434
- const currentPath = window.location.pathname;
5435
- const updated = sections.map(
5436
- (s) => s.type === "scheduling" && s.pagePath === currentPath ? { ...s, scheduleId: null } : s
5437
- );
5438
- tracker.textContent = JSON.stringify(updated);
5439
- postToParentRef.current({ type: "ow:change", nodes: [{ key: "__ohw_sections", text: tracker.textContent }] });
5630
+ const insertAfter = e.data.insertAfter;
5631
+ if (!insertAfter) return;
5632
+ const text = updateSectionScheduleId(insertAfter, null);
5633
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: "__ohw_sections", text }] });
5440
5634
  };
5441
5635
  const handleRemoveSchedulingSection = (e) => {
5442
5636
  if (e.data?.type !== "ow:remove-scheduling-section") return;
5443
- const els = document.querySelectorAll('[data-ohw-section="scheduling"]');
5444
- els.forEach((el) => el.parentElement?.removeChild(el));
5637
+ document.querySelectorAll('[data-ohw-section-container="scheduling"]').forEach((el) => {
5638
+ el.remove();
5639
+ });
5445
5640
  const tracker = getSectionsTracker();
5446
5641
  let sections = [];
5447
5642
  try {
@@ -5560,6 +5755,7 @@ function OhhwellsBridge() {
5560
5755
  window.addEventListener("message", handleSave);
5561
5756
  window.addEventListener("message", handleInsertSection);
5562
5757
  window.addEventListener("message", handleSwitchSchedule);
5758
+ window.addEventListener("message", handleScheduleLinked);
5563
5759
  window.addEventListener("message", handleClearSchedulingWidget);
5564
5760
  window.addEventListener("message", handleRemoveSchedulingSection);
5565
5761
  window.addEventListener("message", handleImageUrl);
@@ -5598,6 +5794,7 @@ function OhhwellsBridge() {
5598
5794
  window.removeEventListener("message", handleSave);
5599
5795
  window.removeEventListener("message", handleInsertSection);
5600
5796
  window.removeEventListener("message", handleSwitchSchedule);
5797
+ window.removeEventListener("message", handleScheduleLinked);
5601
5798
  window.removeEventListener("message", handleClearSchedulingWidget);
5602
5799
  window.removeEventListener("message", handleRemoveSchedulingSection);
5603
5800
  window.removeEventListener("message", handleImageUrl);