@ohhwells/bridge 0.1.42-next.93 → 0.1.42-next.95

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/README.md CHANGED
@@ -318,6 +318,18 @@ npm run build
318
318
  npm publish --access public
319
319
  ```
320
320
 
321
+ ### Staging → rebound-template auto-bump
322
+
323
+ When `staging` publishes successfully, the bridge workflow dispatches a `bridge-published` event to `TheFlowOps-Eng/rebound-template`. That repo’s `bump-bridge.yml` workflow checks out `staging`, runs `npm install @ohhwells/bridge@<version> --save-exact`, and pushes the lockfile bump.
324
+
325
+ **One-time setup** (in `ohhwells-bridge` GitHub repo → Settings → Secrets):
326
+
327
+ | Secret | Value |
328
+ |--------|--------|
329
+ | `REBOUND_TEMPLATE_DISPATCH_TOKEN` | Fine-grained or classic PAT with `repo` access to `rebound-template` (needs permission to trigger `repository_dispatch`) |
330
+
331
+ The published staging version looks like `0.1.31-next.42` and is tagged `next` on npm.
332
+
321
333
  ---
322
334
 
323
335
  ## Link dialog (Canvas Editor)
package/dist/index.cjs CHANGED
@@ -115,6 +115,7 @@ var import_react3 = require("react");
115
115
  var import_react2 = require("react");
116
116
  var import_radix_ui = require("radix-ui");
117
117
  var import_jsx_runtime = require("react/jsx-runtime");
118
+ var isValidEmail = (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
118
119
  function Spinner() {
119
120
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
120
121
  "svg",
@@ -148,12 +149,17 @@ function EmailCaptureModal({ title, subtitle, onSubmit, onClose }) {
148
149
  const [error, setError] = (0, import_react2.useState)(null);
149
150
  const isBook = title === "Confirm your spot";
150
151
  const handleSubmit = async () => {
151
- if (!email.trim()) return;
152
+ const trimmed = email.trim();
153
+ if (!trimmed) return;
154
+ if (!isValidEmail(trimmed)) {
155
+ setError("Please enter a valid email address.");
156
+ return;
157
+ }
152
158
  setLoading(true);
153
159
  setError(null);
154
160
  try {
155
- await onSubmit(email.trim());
156
- setSubmittedEmail(email.trim());
161
+ await onSubmit(trimmed);
162
+ setSubmittedEmail(trimmed);
157
163
  setSuccess(true);
158
164
  } catch (e) {
159
165
  setError(e instanceof Error ? e.message : "Something went wrong. Please try again.");
@@ -228,7 +234,10 @@ function EmailCaptureModal({ title, subtitle, onSubmit, onClose }) {
228
234
  {
229
235
  type: "email",
230
236
  value: email,
231
- onChange: (e) => setEmail(e.target.value),
237
+ onChange: (e) => {
238
+ setEmail(e.target.value);
239
+ if (error) setError(null);
240
+ },
232
241
  onKeyDown: handleKeyDown,
233
242
  placeholder: "hello@gmail.com",
234
243
  autoFocus: true,
@@ -285,12 +294,20 @@ function formatClassTime(cls) {
285
294
  const em = endTotal % 60;
286
295
  return `${h}:${cls.startTime.minutes}-${eh}:${String(em).padStart(2, "0")}`;
287
296
  }
297
+ function formatBookingLabel(cls) {
298
+ const price = cls.price;
299
+ const isFree = price === null || price === void 0 || `${price}` === "" || `${price}` === "0";
300
+ return isFree ? "Book for free" : `Book for ${cls.currency ?? ""} ${price}`.trim();
301
+ }
288
302
  function getBookingsOnDate(cls, date) {
289
303
  if (!cls.bookings?.length) return 0;
290
- const ds = date.toISOString().split("T")[0];
304
+ const target = new Date(date);
305
+ target.setHours(0, 0, 0, 0);
291
306
  return cls.bookings.filter((b) => {
292
307
  try {
293
- return new Date(b.classDate).toISOString().split("T")[0] === ds;
308
+ const bookingDate = new Date(b.classDate);
309
+ bookingDate.setHours(0, 0, 0, 0);
310
+ return bookingDate.getTime() === target.getTime();
294
311
  } catch {
295
312
  return false;
296
313
  }
@@ -555,7 +572,7 @@ function ScheduleView({ schedule, dates, selectedIdx, onSelectDate, onOpenModal
555
572
  }
556
573
  )
557
574
  ] }),
558
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "sm:hidden flex flex-col gap-px", children: isFull ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: "Full" }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
575
+ cls.showAvailability !== false && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "sm:hidden flex flex-col gap-px", children: isFull ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: "Full" }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
559
576
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: [
560
577
  available,
561
578
  "/",
@@ -567,9 +584,17 @@ function ScheduleView({ schedule, dates, selectedIdx, onSelectDate, onOpenModal
567
584
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex flex-1 flex-col gap-2 min-w-0 sm:contents", children: [
568
585
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex-1 flex flex-col gap-2 min-w-0", children: [
569
586
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "font-body text-base font-bold text-(--color-dark,#200C02) block truncate", children: cls.name }),
570
- cls.hostName && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "font-body text-base font-normal text-(--color-dark,#200C02) opacity-80 block truncate", children: cls.hostName })
587
+ cls.hostName && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "font-body text-base font-normal text-(--color-dark,#200C02) opacity-80 block truncate", children: cls.hostName }),
588
+ cls.description && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
589
+ "span",
590
+ {
591
+ className: "font-body text-base font-normal text-(--color-dark,#200C02) opacity-80 block",
592
+ style: { wordBreak: "break-word" },
593
+ children: cls.description
594
+ }
595
+ )
571
596
  ] }),
572
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "hidden sm:flex w-14 shrink-0 flex-col gap-px", children: isFull ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: "Full" }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
597
+ cls.showAvailability !== false && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "hidden sm:flex w-14 shrink-0 flex-col gap-px", children: isFull ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: "Full" }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
573
598
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: [
574
599
  available,
575
600
  "/",
@@ -594,7 +619,7 @@ function ScheduleView({ schedule, dates, selectedIdx, onSelectDate, onOpenModal
594
619
  if (!cls.id) return;
595
620
  onOpenModal?.(isFull ? "waitlist" : "book", cls.id, selectedDate);
596
621
  },
597
- children: isFull ? "Join Waitlist" : "Book Now"
622
+ children: isFull ? "Join Waitlist" : formatBookingLabel(cls)
598
623
  }
599
624
  )
600
625
  ] })
@@ -636,6 +661,17 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAf
636
661
  const [isHovered, setIsHovered] = (0, import_react3.useState)(false);
637
662
  const [modalState, setModalState] = (0, import_react3.useState)(null);
638
663
  const switchScheduleIdRef = (0, import_react3.useRef)(null);
664
+ const liveScheduleIdRef = (0, import_react3.useRef)(null);
665
+ const refetchLiveSchedule = (0, import_react3.useCallback)(async () => {
666
+ const id = liveScheduleIdRef.current;
667
+ if (!id) return;
668
+ try {
669
+ const res = await fetch(`${API_URL}/api/schedule/id/${id}`);
670
+ const data = await res.json();
671
+ if (data?.id) setSchedule(data);
672
+ } catch {
673
+ }
674
+ }, []);
639
675
  const dates = (0, import_react3.useMemo)(() => {
640
676
  const today = /* @__PURE__ */ new Date();
641
677
  today.setHours(0, 0, 0, 0);
@@ -655,6 +691,18 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAf
655
691
  }
656
692
  }
657
693
  }, [schedule, dates]);
694
+ (0, import_react3.useEffect)(() => {
695
+ if (typeof window === "undefined") return;
696
+ const onVisible = () => {
697
+ if (!document.hidden) void refetchLiveSchedule();
698
+ };
699
+ window.addEventListener("focus", refetchLiveSchedule);
700
+ document.addEventListener("visibilitychange", onVisible);
701
+ return () => {
702
+ window.removeEventListener("focus", refetchLiveSchedule);
703
+ document.removeEventListener("visibilitychange", onVisible);
704
+ };
705
+ }, [refetchLiveSchedule]);
658
706
  (0, import_react3.useEffect)(() => {
659
707
  if (typeof window === "undefined") return;
660
708
  const isInEditor = window.self !== window.top;
@@ -716,7 +764,7 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAf
716
764
  setLoading(false);
717
765
  return;
718
766
  }
719
- ;
767
+ liveScheduleIdRef.current = effectiveId;
720
768
  (async () => {
721
769
  try {
722
770
  const res = await fetch(`${API_URL}/api/schedule/id/${effectiveId}`);
@@ -773,18 +821,14 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAf
773
821
  const data = await res.json().catch(() => ({}));
774
822
  throw new Error(data?.message ?? "Something went wrong. Please try again.");
775
823
  }
824
+ await refetchLiveSchedule();
776
825
  };
777
826
  const handleReplaceSchedule = () => {
778
827
  setIsHovered(false);
779
- if (schedule) {
780
- window.parent.postMessage({
781
- type: "ow:schedule-connected",
782
- schedule: { id: schedule.id, name: schedule.name },
783
- insertAfter
784
- }, "*");
785
- } else {
786
- window.parent.postMessage({ type: "ow:scheduling-not-connected", insertAfter }, "*");
787
- }
828
+ window.parent.postMessage(
829
+ { type: "ow:scheduling-not-connected", insertAfter, currentScheduleId: schedule?.id },
830
+ "*"
831
+ );
788
832
  };
789
833
  if (!inEditor && !loading && !schedule) return null;
790
834
  const sectionId = `scheduling-${insertAfter}`;
@@ -827,7 +871,19 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAf
827
871
  ),
828
872
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "max-w-[1280px] mx-auto flex flex-col items-center gap-16", children: [
829
873
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex flex-col items-center gap-4", children: [
830
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h2", { className: "font-display text-4xl sm:text-5xl font-bold text-center m-0 text-(--color-dark,#200C02)", children: schedule?.name ?? "Book an appointment" }),
874
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
875
+ "h2",
876
+ {
877
+ className: "font-display text-center m-0 text-(--color-dark,#200C02)",
878
+ style: {
879
+ fontSize: "var(--fs-section-h2, clamp(40px, 4.4vw, 64px))",
880
+ fontWeight: "var(--font-weight-heading, 400)",
881
+ lineHeight: 1.05,
882
+ letterSpacing: "-0.02em"
883
+ },
884
+ children: schedule?.name ?? "Book an appointment"
885
+ }
886
+ ),
831
887
  schedule?.description && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "font-body text-body text-center m-0 text-(--color-accent,#A89B83)", children: schedule.description })
832
888
  ] }),
833
889
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "w-full flex flex-col items-end gap-3", children: [
@@ -6661,6 +6717,8 @@ function applyNavOrderToContainer(container, order) {
6661
6717
  if (current.length === expected.length && current.every((key, i) => key === expected[i])) {
6662
6718
  return;
6663
6719
  }
6720
+ const lastCurrentLinkEl = current.length > 0 ? findCounterpartByHrefKey(current[current.length - 1], container) : null;
6721
+ const anchor = lastCurrentLinkEl?.parentElement === container ? lastCurrentLinkEl.nextElementSibling : null;
6664
6722
  const seen = /* @__PURE__ */ new Set();
6665
6723
  for (const hrefKey of desired) {
6666
6724
  if (seen.has(hrefKey)) continue;
@@ -6675,7 +6733,9 @@ function applyNavOrderToContainer(container, order) {
6675
6733
  orderedEls.push(el);
6676
6734
  }
6677
6735
  for (const el of orderedEls) {
6678
- if (container.lastElementChild !== el) {
6736
+ if (anchor) {
6737
+ if (el.nextSibling !== anchor) container.insertBefore(el, anchor);
6738
+ } else if (container.lastElementChild !== el) {
6679
6739
  container.appendChild(el);
6680
6740
  }
6681
6741
  }
@@ -7017,6 +7077,11 @@ function reconcileFooterOrderFromContent(content) {
7017
7077
  }
7018
7078
  applyFooterOrder(order);
7019
7079
  }
7080
+ function isRowLayoutColumn(links) {
7081
+ if (links.length < 2) return false;
7082
+ const firstTop = links[0].getBoundingClientRect().top;
7083
+ return links.every((link) => Math.abs(link.getBoundingClientRect().top - firstTop) < 4);
7084
+ }
7020
7085
  function buildLinkDropSlots(column, columnIndex) {
7021
7086
  const links = listFooterLinksInColumn(column);
7022
7087
  const colRect = column.getBoundingClientRect();
@@ -7034,6 +7099,32 @@ function buildLinkDropSlots(column, columnIndex) {
7034
7099
  });
7035
7100
  return slots;
7036
7101
  }
7102
+ if (isRowLayoutColumn(links)) {
7103
+ for (let i = 0; i <= links.length; i++) {
7104
+ let left;
7105
+ if (i === 0) {
7106
+ left = links[0].getBoundingClientRect().left - barThickness / 2;
7107
+ } else if (i === links.length) {
7108
+ const last = links[links.length - 1].getBoundingClientRect();
7109
+ left = last.right - barThickness / 2;
7110
+ } else {
7111
+ const prev = links[i - 1].getBoundingClientRect();
7112
+ const next = links[i].getBoundingClientRect();
7113
+ left = (prev.right + next.left) / 2 - barThickness / 2;
7114
+ }
7115
+ const heightRef = i === 0 ? links[0].getBoundingClientRect() : i === links.length ? links[links.length - 1].getBoundingClientRect() : links[i].getBoundingClientRect();
7116
+ slots.push({
7117
+ insertIndex: i,
7118
+ columnIndex,
7119
+ left,
7120
+ top: heightRef.top,
7121
+ width: barThickness,
7122
+ height: Math.max(heightRef.height, colRect.height * 0.8),
7123
+ direction: "vertical"
7124
+ });
7125
+ }
7126
+ return slots;
7127
+ }
7037
7128
  for (let i = 0; i <= links.length; i++) {
7038
7129
  let top;
7039
7130
  if (i === 0) {
@@ -7117,8 +7208,7 @@ function hitTestLinkDropSlot(clientX, clientY, draggedHrefKey) {
7117
7208
  return true;
7118
7209
  });
7119
7210
  for (const slot of slots) {
7120
- const cy = slot.top + slot.height / 2;
7121
- const dist = Math.abs(clientY - cy) + (inColX ? 0 : 1e3);
7211
+ const dist = slot.direction === "vertical" ? Math.abs(clientX - (slot.left + slot.width / 2)) : Math.abs(clientY - (slot.top + slot.height / 2));
7122
7212
  if (!best || dist < best.dist) best = { slot, dist };
7123
7213
  }
7124
7214
  }
@@ -7528,7 +7618,6 @@ function useNavItemDrag({
7528
7618
  const hrefKey = anchor?.getAttribute("data-ohw-href-key") ?? null;
7529
7619
  if (!anchor || !isNavbarHrefKey(hrefKey)) return;
7530
7620
  if (isNavbarButton3(anchor) || isDragHandleDisabled2(anchor)) return;
7531
- armItemPressDrag();
7532
7621
  navPointerDragRef.current = {
7533
7622
  el: anchor,
7534
7623
  startX: e.clientX,
@@ -7555,6 +7644,7 @@ function useNavItemDrag({
7555
7644
  if (dx * dx + dy * dy < THRESHOLD * THRESHOLD) return;
7556
7645
  e.preventDefault();
7557
7646
  pending.started = true;
7647
+ armItemPressDrag();
7558
7648
  clearTextSelection();
7559
7649
  try {
7560
7650
  document.body.setPointerCapture(pending.pointerId);
@@ -7582,7 +7672,8 @@ function useNavItemDrag({
7582
7672
  }
7583
7673
  } catch {
7584
7674
  }
7585
- if (!pending?.started) {
7675
+ if (!pending) return;
7676
+ if (!pending.started) {
7586
7677
  unlockItemDragInteraction();
7587
7678
  return;
7588
7679
  }
@@ -7634,7 +7725,6 @@ function useNavItemDrag({
7634
7725
  const hrefKey = selected.getAttribute("data-ohw-href-key");
7635
7726
  if (!hrefKey || !isNavbarHrefKey(hrefKey)) return false;
7636
7727
  if (isNavbarButton3(selected) || isDragHandleDisabled2(selected)) return false;
7637
- armItemPressDrag();
7638
7728
  navPointerDragRef.current = {
7639
7729
  el: selected,
7640
7730
  startX: clientX,
@@ -8861,6 +8951,7 @@ function OhhwellsBridge() {
8861
8951
  const [fetchState, setFetchState] = (0, import_react9.useState)("idle");
8862
8952
  const autoSaveTimers = (0, import_react9.useRef)(/* @__PURE__ */ new Map());
8863
8953
  const activeElRef = (0, import_react9.useRef)(null);
8954
+ const pointerHeldRef = (0, import_react9.useRef)(false);
8864
8955
  const selectedElRef = (0, import_react9.useRef)(null);
8865
8956
  const selectedHrefKeyRef = (0, import_react9.useRef)(null);
8866
8957
  const selectedFooterColAttrRef = (0, import_react9.useRef)(null);
@@ -9558,7 +9649,6 @@ function OhhwellsBridge() {
9558
9649
  if (e.button !== 0) return;
9559
9650
  const selected = selectedElRef.current;
9560
9651
  if (!selected) return;
9561
- armFooterPressDrag();
9562
9652
  const hrefKey = selected.getAttribute("data-ohw-href-key");
9563
9653
  if (hrefKey && isFooterHrefKey(hrefKey)) {
9564
9654
  footerPointerDragRef.current = {
@@ -9669,6 +9759,8 @@ function OhhwellsBridge() {
9669
9759
  siblingHintElRef.current = null;
9670
9760
  setSiblingHintRect(null);
9671
9761
  setIsItemDragging(false);
9762
+ const preActivationSelection = window.getSelection();
9763
+ const preservedRange = preActivationSelection && preActivationSelection.rangeCount > 0 && !preActivationSelection.isCollapsed && el.contains(preActivationSelection.getRangeAt(0).commonAncestorContainer) ? preActivationSelection.getRangeAt(0).cloneRange() : null;
9672
9764
  el.setAttribute("contenteditable", "true");
9673
9765
  el.setAttribute("data-ohw-editing", "");
9674
9766
  el.removeAttribute("data-ohw-hovered");
@@ -9676,7 +9768,11 @@ function OhhwellsBridge() {
9676
9768
  activeElRef.current = el;
9677
9769
  originalContentRef.current = el.innerHTML;
9678
9770
  el.focus({ preventScroll: true });
9679
- if (options?.caretX !== void 0 && options?.caretY !== void 0) {
9771
+ if (preservedRange) {
9772
+ const selection = window.getSelection();
9773
+ selection?.removeAllRanges();
9774
+ selection?.addRange(preservedRange);
9775
+ } else if (options?.caretX !== void 0 && options?.caretY !== void 0) {
9680
9776
  const { caretX, caretY } = options;
9681
9777
  placeCaretAtPoint(el, caretX, caretY);
9682
9778
  requestAnimationFrame(() => {
@@ -10132,7 +10228,18 @@ function OhhwellsBridge() {
10132
10228
  if (inActive || inActiveNav) {
10133
10229
  e.preventDefault();
10134
10230
  e.stopPropagation();
10135
- if (e.detail < 2) {
10231
+ const selection = window.getSelection();
10232
+ const hasRangeSelection = Boolean(selection && !selection.isCollapsed);
10233
+ console.log(
10234
+ "[OHW DEBUG handleClick] already-active branch",
10235
+ "detail=",
10236
+ e.detail,
10237
+ "hasRangeSelection=",
10238
+ hasRangeSelection,
10239
+ "willPlaceCaret=",
10240
+ e.detail < 2 && !hasRangeSelection
10241
+ );
10242
+ if (e.detail < 2 && !hasRangeSelection) {
10136
10243
  placeCaretAtPoint(active, e.clientX, e.clientY);
10137
10244
  refreshActiveCommandsRef.current();
10138
10245
  }
@@ -10175,6 +10282,15 @@ function OhhwellsBridge() {
10175
10282
  }
10176
10283
  e.preventDefault();
10177
10284
  e.stopPropagation();
10285
+ console.log(
10286
+ "[OHW DEBUG handleClick] first-activation branch",
10287
+ "detail=",
10288
+ e.detail,
10289
+ "selectionAtClick=",
10290
+ JSON.stringify(window.getSelection()?.toString() ?? ""),
10291
+ "collapsedAtClick=",
10292
+ window.getSelection()?.isCollapsed
10293
+ );
10178
10294
  activateRef.current(editable);
10179
10295
  return;
10180
10296
  }
@@ -11285,30 +11401,83 @@ function OhhwellsBridge() {
11285
11401
  const h = document.documentElement.scrollHeight;
11286
11402
  if (h > 50) postToParentRef.current({ type: "ow:height", height: h });
11287
11403
  };
11288
- const handleSelectionChange = () => {
11289
- if (!activeElRef.current) return;
11404
+ let selectionChangeRaf = null;
11405
+ const runSelectionChange = () => {
11406
+ selectionChangeRaf = null;
11407
+ const activeRoot = activeElRef.current;
11408
+ if (!activeRoot) return;
11290
11409
  const next = /* @__PURE__ */ new Set();
11291
- for (const cmd of ["bold", "italic", "underline", "strikeThrough", "insertUnorderedList", "insertOrderedList"]) {
11292
- try {
11293
- if (document.queryCommandState(cmd)) next.add(cmd);
11294
- } catch {
11295
- }
11296
- }
11297
11410
  const sel = window.getSelection();
11298
- const anchor = sel?.anchorNode;
11299
- if (anchor) {
11300
- const el = anchor.nodeType === Node.TEXT_NODE ? anchor.parentElement : anchor;
11301
- const block = el?.closest("div, p, h1, h2, h3, h4, h5, h6, li, td, th") ?? el;
11302
- if (block) {
11303
- const align = getComputedStyle(block).textAlign;
11304
- if (align === "center") next.add("justifyCenter");
11305
- else if (align === "right" || align === "end") next.add("justifyRight");
11306
- else next.add("justifyLeft");
11411
+ let startNode = null;
11412
+ if (sel && sel.rangeCount > 0) {
11413
+ const range = sel.getRangeAt(0);
11414
+ startNode = range.startContainer;
11415
+ if (startNode.nodeType === Node.ELEMENT_NODE) {
11416
+ const el = startNode;
11417
+ startNode = el.childNodes[range.startOffset] ?? el.childNodes[range.startOffset - 1] ?? el;
11307
11418
  }
11308
11419
  }
11309
- setActiveCommands(next);
11420
+ const startEl = startNode ? startNode.nodeType === Node.TEXT_NODE ? startNode.parentElement : startNode : null;
11421
+ if (startEl) {
11422
+ const cs = getComputedStyle(startEl);
11423
+ const weight = parseInt(cs.fontWeight, 10);
11424
+ let hasBold = cs.fontWeight === "bold" || !Number.isNaN(weight) && weight >= 600;
11425
+ let hasItalic = cs.fontStyle === "italic" || cs.fontStyle === "oblique";
11426
+ let hasUnderline = false;
11427
+ let hasStrike = false;
11428
+ let hasUl = false;
11429
+ let hasOl = false;
11430
+ for (let el = startEl; el; el = el.parentElement) {
11431
+ if (el.tagName === "B" || el.tagName === "STRONG") hasBold = true;
11432
+ if (el.tagName === "I" || el.tagName === "EM") hasItalic = true;
11433
+ const decoration = getComputedStyle(el).textDecorationLine;
11434
+ if (decoration.includes("underline") || el.tagName === "U" || el.tagName === "INS") hasUnderline = true;
11435
+ if (decoration.includes("line-through") || el.tagName === "S" || el.tagName === "STRIKE" || el.tagName === "DEL") hasStrike = true;
11436
+ if (el.tagName === "UL") hasUl = true;
11437
+ if (el.tagName === "OL") hasOl = true;
11438
+ if (el === activeRoot) break;
11439
+ }
11440
+ if (hasBold) next.add("bold");
11441
+ if (hasItalic) next.add("italic");
11442
+ if (hasUnderline) next.add("underline");
11443
+ if (hasStrike) next.add("strikeThrough");
11444
+ if (hasUl) next.add("insertUnorderedList");
11445
+ if (hasOl) next.add("insertOrderedList");
11446
+ const block = startEl.closest("div, p, h1, h2, h3, h4, h5, h6, li, td, th") ?? startEl;
11447
+ const align = getComputedStyle(block).textAlign;
11448
+ if (align === "center") next.add("justifyCenter");
11449
+ else if (align === "right" || align === "end") next.add("justifyRight");
11450
+ else next.add("justifyLeft");
11451
+ }
11452
+ setActiveCommands((prev) => {
11453
+ if (prev.size === next.size) {
11454
+ let same = true;
11455
+ for (const cmd of prev) {
11456
+ if (!next.has(cmd)) {
11457
+ same = false;
11458
+ break;
11459
+ }
11460
+ }
11461
+ if (same) return prev;
11462
+ }
11463
+ return next;
11464
+ });
11465
+ };
11466
+ const handleSelectionChange = () => {
11467
+ if (pointerHeldRef.current) return;
11468
+ if (selectionChangeRaf !== null) return;
11469
+ selectionChangeRaf = requestAnimationFrame(runSelectionChange);
11470
+ };
11471
+ const markPointerHeld = (e) => {
11472
+ if (e.button !== 0) return;
11473
+ pointerHeldRef.current = true;
11474
+ };
11475
+ const markPointerReleased = () => {
11476
+ if (!pointerHeldRef.current) return;
11477
+ pointerHeldRef.current = false;
11478
+ handleSelectionChange();
11310
11479
  };
11311
- refreshActiveCommandsRef.current = handleSelectionChange;
11480
+ refreshActiveCommandsRef.current = runSelectionChange;
11312
11481
  const handleDocMouseLeave = () => {
11313
11482
  hoveredImageRef.current = null;
11314
11483
  setMediaHover(null);
@@ -11548,7 +11717,6 @@ function OhhwellsBridge() {
11548
11717
  const anchor = getNavigationItemAnchor(target);
11549
11718
  const hrefKey = anchor?.getAttribute("data-ohw-href-key") ?? null;
11550
11719
  if (anchor && isFooterHrefKey(hrefKey)) {
11551
- armFooterPressDrag();
11552
11720
  footerPointerDragRef.current = {
11553
11721
  el: anchor,
11554
11722
  kind: "link",
@@ -11562,7 +11730,6 @@ function OhhwellsBridge() {
11562
11730
  }
11563
11731
  const col = target.closest("[data-ohw-footer-col]");
11564
11732
  if (col && !getNavigationItemAnchor(target)) {
11565
- armFooterPressDrag();
11566
11733
  footerPointerDragRef.current = {
11567
11734
  el: col,
11568
11735
  kind: "column",
@@ -11591,6 +11758,7 @@ function OhhwellsBridge() {
11591
11758
  if (dx * dx + dy * dy < THRESHOLD * THRESHOLD) return;
11592
11759
  e.preventDefault();
11593
11760
  pending.started = true;
11761
+ armFooterPressDrag();
11594
11762
  clearTextSelection();
11595
11763
  try {
11596
11764
  document.body.setPointerCapture(pending.pointerId);
@@ -11644,7 +11812,8 @@ function OhhwellsBridge() {
11644
11812
  }
11645
11813
  } catch {
11646
11814
  }
11647
- if (!pending?.started) {
11815
+ if (!pending) return;
11816
+ if (!pending.started) {
11648
11817
  unlockFooterDragInteraction();
11649
11818
  return;
11650
11819
  }
@@ -11679,6 +11848,22 @@ function OhhwellsBridge() {
11679
11848
  unlockFooterDragInteraction();
11680
11849
  };
11681
11850
  }, [isEditMode]);
11851
+ (0, import_react9.useEffect)(() => {
11852
+ if (!isEditMode) return;
11853
+ const debugSelectionChange = () => {
11854
+ const sel = window.getSelection();
11855
+ console.log(
11856
+ "[OHW DEBUG selectionchange]",
11857
+ JSON.stringify(sel?.toString() ?? ""),
11858
+ "collapsed=",
11859
+ sel?.isCollapsed,
11860
+ "anchorNode=",
11861
+ sel?.anchorNode,
11862
+ "activeEl=",
11863
+ activeElRef.current
11864
+ );
11865
+ };
11866
+ }, [isEditMode]);
11682
11867
  (0, import_react9.useEffect)(() => {
11683
11868
  const handler = (e) => {
11684
11869
  if (e.data?.type !== "ow:request-schedule-config") return;
@@ -11737,6 +11922,7 @@ function OhhwellsBridge() {
11737
11922
  return () => window.removeEventListener("hashchange", onHashChange);
11738
11923
  }, [pathname]);
11739
11924
  const handleCommand = (0, import_react9.useCallback)((cmd) => {
11925
+ document.execCommand("styleWithCSS", false, true);
11740
11926
  document.execCommand(cmd, false);
11741
11927
  activeElRef.current?.focus();
11742
11928
  if (activeElRef.current) setToolbarRect(getEditMeasureEl(activeElRef.current).getBoundingClientRect());