@ohhwells/bridge 0.1.20 → 0.1.21

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";
@@ -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,22 @@ 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
+ const sectionId = `scheduling-${insertAfter}`;
597
678
  return /* @__PURE__ */ jsxs2(
598
679
  "section",
599
680
  {
600
- "data-ohw-section": "scheduling",
601
- className: "w-full box-border py-20 px-16 font-body bg-(--color-light,#FEFFF0) relative",
681
+ "data-ohw-section": sectionId,
682
+ "data-ohw-scheduling-anchor": insertAfter,
683
+ className: "w-full box-border py-10 px-5 sm:py-20 sm:px-16 font-body bg-(--color-light,#FEFFF0) relative",
602
684
  onMouseEnter: () => inEditor && setIsHovered(true),
603
685
  onMouseLeave: () => setIsHovered(false),
604
686
  children: [
@@ -632,12 +714,12 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId }) {
632
714
  ),
633
715
  /* @__PURE__ */ jsxs2("div", { className: "max-w-[1280px] mx-auto flex flex-col items-center gap-16", children: [
634
716
  /* @__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" }),
717
+ /* @__PURE__ */ jsx2("h2", { className: "font-display text-4xl sm:text-5xl font-bold text-center m-0 text-(--color-dark,#200C02)", children: schedule?.name ?? "Book an appointment" }),
636
718
  schedule?.description && /* @__PURE__ */ jsx2("p", { className: "font-body text-body text-center m-0 text-(--color-accent,#A89B83)", children: schedule.description })
637
719
  ] }),
638
720
  /* @__PURE__ */ jsxs2("div", { className: "w-full flex flex-col items-end gap-3", children: [
639
721
  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(
722
+ /* @__PURE__ */ jsx2("div", { className: "w-full p-0 sm:p-10 box-border", children: loading ? /* @__PURE__ */ jsx2(LoadingSkeleton, {}) : !schedule ? /* @__PURE__ */ jsx2(EmptyState, { inEditor }) : /* @__PURE__ */ jsx2(
641
723
  ScheduleView,
642
724
  {
643
725
  schedule,
@@ -4177,24 +4259,135 @@ function getSectionsTracker() {
4177
4259
  }
4178
4260
  return el;
4179
4261
  }
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;
4262
+ function updateSectionScheduleId(insertAfter, scheduleId) {
4263
+ const tracker = getSectionsTracker();
4264
+ let sections = [];
4265
+ try {
4266
+ sections = JSON.parse(tracker.textContent || "[]");
4267
+ } catch {
4268
+ }
4269
+ const currentPath = window.location.pathname;
4270
+ const updated = sections.map((s) => {
4271
+ if (s.type !== "scheduling" || s.insertAfter !== insertAfter) return s;
4272
+ if (s.pagePath && s.pagePath !== currentPath) return s;
4273
+ return { ...s, scheduleId };
4274
+ });
4275
+ tracker.textContent = JSON.stringify(updated);
4276
+ return tracker.textContent ?? "[]";
4277
+ }
4278
+ function schedulingSectionId(insertAfter) {
4279
+ return `scheduling-${insertAfter}`;
4280
+ }
4281
+ var INSERT_BEFORE_MARKER = ":before:";
4282
+ function parseSchedulingInsertAfter(insertAfter) {
4283
+ const idx = insertAfter.indexOf(INSERT_BEFORE_MARKER);
4284
+ if (idx < 0) return { anchor: insertAfter, insertBefore: null };
4285
+ return {
4286
+ anchor: insertAfter.slice(0, idx),
4287
+ insertBefore: insertAfter.slice(idx + INSERT_BEFORE_MARKER.length)
4288
+ };
4289
+ }
4290
+ function resolveSchedulingInsert(insertAfter, explicitInsertBefore) {
4291
+ const parsed = parseSchedulingInsertAfter(insertAfter);
4292
+ const insertBefore = explicitInsertBefore ?? parsed.insertBefore;
4293
+ const effectiveInsertAfter = insertBefore && parsed.insertBefore === null ? `${insertAfter}${INSERT_BEFORE_MARKER}${insertBefore}` : insertAfter;
4294
+ return { effectiveInsertAfter, insertBefore };
4295
+ }
4296
+ function getSchedulingMountPoint(insertAfter) {
4297
+ const { anchor } = parseSchedulingInsertAfter(insertAfter);
4298
+ let anchorEl = document.querySelector(`[data-ohw-section="${anchor}"]`);
4299
+ if (!anchorEl && anchor === "scheduling") {
4300
+ const widgets = Array.from(document.querySelectorAll('[data-ohw-section^="scheduling-"]'));
4301
+ anchorEl = widgets.at(-1) ?? null;
4302
+ }
4303
+ if (!anchorEl) return null;
4304
+ return anchorEl.closest('[data-ohw-section-container="scheduling"]') ?? anchorEl;
4305
+ }
4306
+ function schedulingMountDepth(insertAfter) {
4307
+ if (!insertAfter.includes(INSERT_BEFORE_MARKER)) return 0;
4308
+ return insertAfter.split(INSERT_BEFORE_MARKER).length - 1;
4309
+ }
4310
+ function getPageSchedulingEntries(raw) {
4311
+ if (!raw) return [];
4312
+ try {
4313
+ const entries = JSON.parse(raw);
4314
+ const currentPath = window.location.pathname;
4315
+ return entries.filter((e) => e.type === "scheduling" && (!e.pagePath || e.pagePath === currentPath));
4316
+ } catch {
4317
+ return [];
4318
+ }
4319
+ }
4320
+ function isSchedulingWidgetMissing(entry) {
4321
+ const { effectiveInsertAfter } = resolveSchedulingInsert(entry.insertAfter);
4322
+ return !document.querySelector(`[data-ohw-section="${schedulingSectionId(effectiveInsertAfter)}"]`);
4323
+ }
4324
+ function hasMissingSchedulingWidgets(entries) {
4325
+ return entries.some(isSchedulingWidgetMissing);
4326
+ }
4327
+ function retryMissingSchedulingMounts(entries, notifyOnConnect = false) {
4328
+ if (!hasMissingSchedulingWidgets(entries)) return;
4329
+ mountSchedulingEntries(entries, notifyOnConnect);
4330
+ }
4331
+ function initSectionsFromContent(content, removeExisting = false) {
4332
+ const raw = content["__ohw_sections"];
4333
+ if (!raw) return;
4334
+ try {
4335
+ if (removeExisting) {
4336
+ document.querySelectorAll('[data-ohw-section-container="scheduling"]').forEach((el) => el.remove());
4337
+ }
4338
+ const inEditor = typeof window !== "undefined" && window.self !== window.top;
4339
+ if (inEditor) getSectionsTracker().textContent = raw;
4340
+ const pageEntries = getPageSchedulingEntries(raw);
4341
+ mountSchedulingEntries(pageEntries, false);
4342
+ requestAnimationFrame(() => retryMissingSchedulingMounts(pageEntries, false));
4343
+ setTimeout(() => retryMissingSchedulingMounts(pageEntries, false), 250);
4344
+ } catch {
4345
+ }
4346
+ }
4347
+ function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId, explicitInsertBefore) {
4348
+ const { effectiveInsertAfter, insertBefore } = resolveSchedulingInsert(insertAfter, explicitInsertBefore);
4349
+ const sectionId = schedulingSectionId(effectiveInsertAfter);
4350
+ if (document.querySelector(`[data-ohw-section="${sectionId}"]`)) return false;
4351
+ const mountPoint = getSchedulingMountPoint(effectiveInsertAfter);
4352
+ if (!mountPoint) return false;
4184
4353
  const container = document.createElement("div");
4185
4354
  container.dataset.ohwSectionContainer = "scheduling";
4186
- anchor.insertAdjacentElement("afterend", container);
4187
- createRoot(container).render(/* @__PURE__ */ jsx6(SchedulingWidget, { notifyOnConnect, initialScheduleId: scheduleId }));
4355
+ if (insertBefore) {
4356
+ const beforeAnchor = document.querySelector(`[data-ohw-section="${insertBefore}"]`);
4357
+ const beforePoint = beforeAnchor?.closest('[data-ohw-section-container="scheduling"]') ?? beforeAnchor;
4358
+ if (!beforePoint) return false;
4359
+ beforePoint.insertAdjacentElement("beforebegin", container);
4360
+ } else {
4361
+ let tail = mountPoint;
4362
+ while (tail.nextElementSibling?.dataset.ohwSectionContainer === "scheduling") {
4363
+ tail = tail.nextElementSibling;
4364
+ }
4365
+ tail.insertAdjacentElement("afterend", container);
4366
+ }
4367
+ const root = createRoot(container);
4368
+ flushSync(() => {
4369
+ root.render(
4370
+ /* @__PURE__ */ jsx6(
4371
+ SchedulingWidget,
4372
+ {
4373
+ notifyOnConnect,
4374
+ initialScheduleId: scheduleId,
4375
+ insertAfter: effectiveInsertAfter
4376
+ }
4377
+ )
4378
+ );
4379
+ });
4188
4380
  const tracker = getSectionsTracker();
4189
4381
  let sections = [];
4190
4382
  try {
4191
4383
  sections = JSON.parse(tracker.textContent || "[]");
4192
4384
  } catch {
4193
4385
  }
4194
- if (!sections.find((s) => s.type === "scheduling" && s.insertAfter === insertAfter)) {
4386
+ const inEditor = typeof window !== "undefined" && window.self !== window.top;
4387
+ if (inEditor && !sections.find((s) => s.type === "scheduling" && s.insertAfter === effectiveInsertAfter)) {
4195
4388
  sections.push({
4196
4389
  type: "scheduling",
4197
- insertAfter,
4390
+ insertAfter: effectiveInsertAfter,
4198
4391
  pagePath: window.location.pathname,
4199
4392
  ...scheduleId ? { scheduleId } : {}
4200
4393
  });
@@ -4202,6 +4395,17 @@ function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId)
4202
4395
  }
4203
4396
  return true;
4204
4397
  }
4398
+ function mountSchedulingEntries(entries, notifyOnConnect = false) {
4399
+ const pending = entries.filter((e) => e.type === "scheduling").sort((a, b) => schedulingMountDepth(a.insertAfter) - schedulingMountDepth(b.insertAfter));
4400
+ for (let attempt = 0; attempt < pending.length + 1 && pending.length > 0; attempt++) {
4401
+ for (let i = pending.length - 1; i >= 0; i--) {
4402
+ const { insertAfter, scheduleId } = pending[i];
4403
+ if (mountSchedulingWidget(insertAfter, notifyOnConnect, scheduleId)) {
4404
+ pending.splice(i, 1);
4405
+ }
4406
+ }
4407
+ }
4408
+ }
4205
4409
  function collectEditableNodes() {
4206
4410
  return Array.from(document.querySelectorAll("[data-ohw-editable]")).map((el) => {
4207
4411
  if (el.dataset.ohwEditable === "image") {
@@ -4515,6 +4719,23 @@ function StateToggle({
4515
4719
  );
4516
4720
  }
4517
4721
  var contentCache = /* @__PURE__ */ new Map();
4722
+ function resolveSubdomain(subdomainFromQuery) {
4723
+ if (subdomainFromQuery) return subdomainFromQuery;
4724
+ if (typeof window !== "undefined") {
4725
+ const parts = window.location.hostname.split(".");
4726
+ if (parts.length >= 3 && parts[0] !== "www") return parts[0];
4727
+ }
4728
+ const siteUrl = process.env.NEXT_PUBLIC_SITE_URL;
4729
+ if (siteUrl) {
4730
+ try {
4731
+ const host = new URL(siteUrl).hostname;
4732
+ const siteParts = host.split(".");
4733
+ if (siteParts.length >= 3 && siteParts[0] !== "www") return siteParts[0];
4734
+ } catch {
4735
+ }
4736
+ }
4737
+ return "";
4738
+ }
4518
4739
  function OhhwellsBridge() {
4519
4740
  const pathname = usePathname();
4520
4741
  const router = useRouter();
@@ -4544,11 +4765,7 @@ function OhhwellsBridge() {
4544
4765
  };
4545
4766
  }, []);
4546
4767
  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
- })();
4768
+ const subdomain = resolveSubdomain(subdomainFromQuery);
4552
4769
  const postToParent = useCallback((data) => {
4553
4770
  if (typeof window !== "undefined" && window.parent !== window) {
4554
4771
  window.parent.postMessage(data, "*");
@@ -4658,18 +4875,6 @@ function OhhwellsBridge() {
4658
4875
  }
4659
4876
  const applyContent = (content) => {
4660
4877
  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
4878
  for (const [key, val] of Object.entries(content)) {
4674
4879
  if (key === "__ohw_sections") continue;
4675
4880
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
@@ -4690,6 +4895,7 @@ function OhhwellsBridge() {
4690
4895
  }
4691
4896
  });
4692
4897
  }
4898
+ initSectionsFromContent(content, true);
4693
4899
  return imageLoads.length > 0 ? Promise.all(imageLoads).then(() => {
4694
4900
  }) : Promise.resolve();
4695
4901
  };
@@ -4716,20 +4922,11 @@ function OhhwellsBridge() {
4716
4922
  }, [subdomain, isEditMode, pathname]);
4717
4923
  useEffect2(() => {
4718
4924
  if (!subdomain || isEditMode) return;
4925
+ let debounceTimer = null;
4719
4926
  const applyFromCache = () => {
4720
4927
  const content = contentCache.get(subdomain);
4721
4928
  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
- }
4929
+ retryMissingSchedulingMounts(getPageSchedulingEntries(content["__ohw_sections"]), false);
4733
4930
  for (const [key, val] of Object.entries(content)) {
4734
4931
  if (key === "__ohw_sections") continue;
4735
4932
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
@@ -4745,11 +4942,18 @@ function OhhwellsBridge() {
4745
4942
  });
4746
4943
  }
4747
4944
  };
4748
- applyFromCache();
4749
- const observer = new MutationObserver(applyFromCache);
4945
+ const scheduleApply = () => {
4946
+ if (debounceTimer) clearTimeout(debounceTimer);
4947
+ debounceTimer = setTimeout(applyFromCache, 150);
4948
+ };
4949
+ scheduleApply();
4950
+ const observer = new MutationObserver(scheduleApply);
4750
4951
  observer.observe(document.body, { childList: true, subtree: true });
4751
- return () => observer.disconnect();
4752
- }, [subdomain, isEditMode]);
4952
+ return () => {
4953
+ observer.disconnect();
4954
+ if (debounceTimer) clearTimeout(debounceTimer);
4955
+ };
4956
+ }, [subdomain, isEditMode, pathname]);
4753
4957
  useLayoutEffect(() => {
4754
4958
  const el = document.getElementById("ohw-loader");
4755
4959
  if (!el) return;
@@ -5322,17 +5526,10 @@ function OhhwellsBridge() {
5322
5526
  if (e.data?.type !== "ow:hydrate") return;
5323
5527
  const content = e.data.content;
5324
5528
  if (!content) return;
5529
+ let sectionsJson = null;
5325
5530
  for (const [key, val] of Object.entries(content)) {
5326
5531
  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
- }
5532
+ sectionsJson = val;
5336
5533
  continue;
5337
5534
  }
5338
5535
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
@@ -5346,6 +5543,9 @@ function OhhwellsBridge() {
5346
5543
  }
5347
5544
  });
5348
5545
  }
5546
+ if (sectionsJson) {
5547
+ initSectionsFromContent({ __ohw_sections: sectionsJson }, true);
5548
+ }
5349
5549
  postToParentRef.current({ type: "ow:hydrate-done" });
5350
5550
  };
5351
5551
  window.addEventListener("message", handleHydrate);
@@ -5396,9 +5596,9 @@ function OhhwellsBridge() {
5396
5596
  };
5397
5597
  const handleInsertSection = (e) => {
5398
5598
  if (e.data?.type !== "ow:insert-section") return;
5399
- const { widgetType, insertAfter } = e.data;
5599
+ const { widgetType, insertAfter, insertBefore } = e.data;
5400
5600
  if (widgetType !== "scheduling") return;
5401
- const inserted = mountSchedulingWidget(insertAfter, true);
5601
+ const inserted = mountSchedulingWidget(insertAfter, true, void 0, insertBefore ?? null);
5402
5602
  if (inserted) {
5403
5603
  const tracker = getSectionsTracker();
5404
5604
  postToParentRef.current({ type: "ow:change", nodes: [{ key: "__ohw_sections", text: tracker.textContent ?? "[]" }] });
@@ -5409,39 +5609,31 @@ function OhhwellsBridge() {
5409
5609
  const handleSwitchSchedule = (e) => {
5410
5610
  if (e.data?.type !== "ow:switch-schedule") return;
5411
5611
  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 }] });
5612
+ const insertAfter = e.data.insertAfter;
5613
+ if (!scheduleId || !insertAfter) return;
5614
+ const text = updateSectionScheduleId(insertAfter, scheduleId);
5615
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: "__ohw_sections", text }] });
5616
+ };
5617
+ const handleScheduleLinked = (e) => {
5618
+ if (e.data?.type !== "ow:schedule-linked") return;
5619
+ const scheduleId = e.data.scheduleId;
5620
+ const insertAfter = e.data.insertAfter;
5621
+ if (!scheduleId || !insertAfter) return;
5622
+ const text = updateSectionScheduleId(insertAfter, scheduleId);
5623
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: "__ohw_sections", text }] });
5425
5624
  };
5426
5625
  const handleClearSchedulingWidget = (e) => {
5427
5626
  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 }] });
5627
+ const insertAfter = e.data.insertAfter;
5628
+ if (!insertAfter) return;
5629
+ const text = updateSectionScheduleId(insertAfter, null);
5630
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: "__ohw_sections", text }] });
5440
5631
  };
5441
5632
  const handleRemoveSchedulingSection = (e) => {
5442
5633
  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));
5634
+ document.querySelectorAll('[data-ohw-section-container="scheduling"]').forEach((el) => {
5635
+ el.remove();
5636
+ });
5445
5637
  const tracker = getSectionsTracker();
5446
5638
  let sections = [];
5447
5639
  try {
@@ -5560,6 +5752,7 @@ function OhhwellsBridge() {
5560
5752
  window.addEventListener("message", handleSave);
5561
5753
  window.addEventListener("message", handleInsertSection);
5562
5754
  window.addEventListener("message", handleSwitchSchedule);
5755
+ window.addEventListener("message", handleScheduleLinked);
5563
5756
  window.addEventListener("message", handleClearSchedulingWidget);
5564
5757
  window.addEventListener("message", handleRemoveSchedulingSection);
5565
5758
  window.addEventListener("message", handleImageUrl);
@@ -5598,6 +5791,7 @@ function OhhwellsBridge() {
5598
5791
  window.removeEventListener("message", handleSave);
5599
5792
  window.removeEventListener("message", handleInsertSection);
5600
5793
  window.removeEventListener("message", handleSwitchSchedule);
5794
+ window.removeEventListener("message", handleScheduleLinked);
5601
5795
  window.removeEventListener("message", handleClearSchedulingWidget);
5602
5796
  window.removeEventListener("message", handleRemoveSchedulingSection);
5603
5797
  window.removeEventListener("message", handleImageUrl);