@ohhwells/bridge 0.1.71-next.216 → 0.1.71-next.218

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -6791,6 +6791,7 @@ function ItemActionToolbar({
6791
6791
  var import_react7 = require("react");
6792
6792
  var import_jsx_runtime12 = require("react/jsx-runtime");
6793
6793
  var PRIMARY = "var(--ohw-primary, #0885FE)";
6794
+ var CHROME_RADIUS = 6;
6794
6795
  var FOCUS_RING = `0 0 0 4px color-mix(in srgb, ${PRIMARY} 12%, transparent)`;
6795
6796
  var DRAG_SHADOW = "0px 4px 6px -1px rgba(0, 0, 0, 0.1), 0px 2px 4px -2px rgba(0, 0, 0, 0.1)";
6796
6797
  var TOOLBAR_EDGE_MARGIN = 4;
@@ -6985,15 +6986,16 @@ function ItemInteractionLayer({
6985
6986
  "div",
6986
6987
  {
6987
6988
  "aria-hidden": true,
6988
- className: "absolute inset-0 rounded-lg",
6989
- style: getChromeStyle(state)
6989
+ className: "absolute inset-0",
6990
+ style: { ...getChromeStyle(state), borderRadius: CHROME_RADIUS }
6990
6991
  }
6991
6992
  ),
6992
6993
  itemDragEnabled && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
6993
6994
  "div",
6994
6995
  {
6995
6996
  "data-ohw-item-drag-surface": "",
6996
- className: "pointer-events-auto absolute inset-0 cursor-text rounded-lg",
6997
+ className: "pointer-events-auto absolute inset-0 cursor-text",
6998
+ style: { borderRadius: CHROME_RADIUS },
6997
6999
  onPointerDown: (e) => {
6998
7000
  if (e.button !== 0) return;
6999
7001
  onItemPointerDown?.(e);
@@ -7745,6 +7747,15 @@ var SKELETON_CSS = `
7745
7747
  animation: ohw-media-shimmer 1.4s infinite;
7746
7748
  }
7747
7749
  `;
7750
+ var REPLACE_BUTTON_INSET = 12;
7751
+ function replaceButtonMode(image, button) {
7752
+ if (!button.fullWidth || !button.height) return "none";
7753
+ const room = REPLACE_BUTTON_INSET * 2;
7754
+ if (image.height < button.height + room) return "none";
7755
+ if (image.width >= button.fullWidth + room) return "full";
7756
+ if (image.width >= button.height + room) return "icon";
7757
+ return "none";
7758
+ }
7748
7759
  function MediaOverlay({
7749
7760
  hover,
7750
7761
  isUploading,
@@ -7762,6 +7773,19 @@ function MediaOverlay({
7762
7773
  const showChrome = !selected || hovered;
7763
7774
  const autoplay = hover.videoAutoplay ?? true;
7764
7775
  const muted = hover.videoMuted ?? true;
7776
+ const probeRef = React8.useRef(null);
7777
+ const [buttonSize, setButtonSize] = React8.useState(null);
7778
+ React8.useLayoutEffect(() => {
7779
+ const probe = probeRef.current;
7780
+ if (!probe) return;
7781
+ const { width, height } = probe.getBoundingClientRect();
7782
+ if (!width || !height) return;
7783
+ setButtonSize(
7784
+ (prev) => prev && prev.fullWidth === width && prev.height === height ? prev : { fullWidth: width, height }
7785
+ );
7786
+ }, [isVideo]);
7787
+ const replaceLabel = isVideo ? "Replace video" : hover.elementType === "bg-image" ? "Replace background" : "Replace image";
7788
+ const replaceMode = buttonSize ? replaceButtonMode({ width: rect.width, height: rect.height }, buttonSize) : "none";
7765
7789
  const box = {
7766
7790
  position: "fixed",
7767
7791
  top: rect.top,
@@ -7851,7 +7875,7 @@ function MediaOverlay({
7851
7875
  ) : null;
7852
7876
  return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_jsx_runtime15.Fragment, { children: [
7853
7877
  settingsBar,
7854
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
7878
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
7855
7879
  "div",
7856
7880
  {
7857
7881
  "data-ohw-bridge": "",
@@ -7870,29 +7894,64 @@ function MediaOverlay({
7870
7894
  background: selected ? "transparent" : "color-mix(in srgb, var(--color-primary) 20%, transparent)"
7871
7895
  },
7872
7896
  onClick: () => onSelect && !selected ? onSelect(hover.key) : onReplace(hover.key),
7873
- children: showChrome && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
7874
- Button,
7875
- {
7876
- "data-ohw-media-overlay": "",
7877
- variant: "outline",
7878
- size: "sm",
7879
- className: "gap-1.5 cursor-pointer hover:bg-background",
7880
- style: {
7881
- ...OVERLAY_BUTTON_STYLE,
7882
- // Don't let Replace steal clicks when text (e.g. nav) sits on top of this media.
7883
- pointerEvents: hover.hasTextOverlap ? "none" : "auto"
7884
- },
7885
- onMouseDown: (e) => e.preventDefault(),
7886
- onClick: (e) => {
7887
- e.stopPropagation();
7888
- onReplace(hover.key);
7889
- },
7890
- children: [
7891
- isVideo ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_lucide_react5.Film, { size: 14 }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_lucide_react5.ImageIcon, { size: 14 }),
7892
- isVideo ? "Replace video" : hover.elementType === "bg-image" ? "Replace background" : "Replace image"
7893
- ]
7894
- }
7895
- )
7897
+ children: [
7898
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
7899
+ Button,
7900
+ {
7901
+ ref: probeRef,
7902
+ "aria-hidden": true,
7903
+ tabIndex: -1,
7904
+ variant: "outline",
7905
+ size: "sm",
7906
+ className: "gap-1.5",
7907
+ style: {
7908
+ ...OVERLAY_BUTTON_STYLE,
7909
+ position: "fixed",
7910
+ top: 0,
7911
+ left: 0,
7912
+ visibility: "hidden",
7913
+ pointerEvents: "none"
7914
+ },
7915
+ children: [
7916
+ isVideo ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_lucide_react5.Film, { size: 14 }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_lucide_react5.ImageIcon, { size: 14 }),
7917
+ replaceLabel
7918
+ ]
7919
+ }
7920
+ ),
7921
+ showChrome && replaceMode !== "none" && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
7922
+ Button,
7923
+ {
7924
+ "data-ohw-media-overlay": "",
7925
+ variant: "outline",
7926
+ size: "sm",
7927
+ "aria-label": replaceLabel,
7928
+ className: "gap-1.5 cursor-pointer hover:bg-background",
7929
+ style: {
7930
+ ...OVERLAY_BUTTON_STYLE,
7931
+ // Icon-only is a square of the same height, so the pair can never differ vertically.
7932
+ // `minWidth` has to go too: the button's own size carries `min-w-[64px]`, and a
7933
+ // minimum beats a width — the square came out wider than the avatar it sat on.
7934
+ ...replaceMode === "icon" && buttonSize ? {
7935
+ width: buttonSize.height,
7936
+ minWidth: buttonSize.height,
7937
+ padding: 0,
7938
+ justifyContent: "center"
7939
+ } : null,
7940
+ // Don't let Replace steal clicks when text (e.g. nav) sits on top of this media.
7941
+ pointerEvents: hover.hasTextOverlap ? "none" : "auto"
7942
+ },
7943
+ onMouseDown: (e) => e.preventDefault(),
7944
+ onClick: (e) => {
7945
+ e.stopPropagation();
7946
+ onReplace(hover.key);
7947
+ },
7948
+ children: [
7949
+ isVideo ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_lucide_react5.Film, { size: 14 }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_lucide_react5.ImageIcon, { size: 14 }),
7950
+ replaceMode === "full" ? replaceLabel : null
7951
+ ]
7952
+ }
7953
+ )
7954
+ ]
7896
7955
  }
7897
7956
  )
7898
7957
  ] });
@@ -14882,9 +14941,21 @@ function getLinkHref4(el) {
14882
14941
  const anchor = el instanceof HTMLAnchorElement ? el : el.querySelector("a");
14883
14942
  return anchor?.getAttribute("href") ?? "";
14884
14943
  }
14944
+ function normalizeLinkHref(value) {
14945
+ const href = value.trim();
14946
+ if (!href) return href;
14947
+ if (/^[a-z][a-z0-9+.-]*:/i.test(href)) return href;
14948
+ if (href.startsWith("/") || href.startsWith("#") || href.startsWith("?")) return href;
14949
+ if (!/^[^\s/]+\.[a-z]{2,}(?:[/:?#]|$)/i.test(href)) return href;
14950
+ return `https://${href}`;
14951
+ }
14952
+ function isPageFrameSection(el) {
14953
+ const id = el.dataset.ohwSection ?? "";
14954
+ return el.tagName === "FOOTER" || el.tagName === "HEADER" || Boolean(el.closest("footer")) || /(^|[-_])(footer|navbar)$/i.test(id);
14955
+ }
14885
14956
  function applyLinkHref(el, val) {
14886
14957
  const anchor = el instanceof HTMLAnchorElement ? el : el.querySelector("a");
14887
- if (anchor) anchor.setAttribute("href", val);
14958
+ if (anchor) anchor.setAttribute("href", normalizeLinkHref(val));
14888
14959
  }
14889
14960
  function currentIconRef(el) {
14890
14961
  const uploaded = el instanceof HTMLImageElement ? el : el.querySelector("img");
@@ -15014,7 +15085,7 @@ function applyLinkByKey(key, val) {
15014
15085
  });
15015
15086
  const hrefAnchors = document.querySelectorAll(`[data-ohw-href-key="${key}"]`);
15016
15087
  if (hrefAnchors.length > 0) {
15017
- setStoredLinkHref(key, val);
15088
+ setStoredLinkHref(key, normalizeLinkHref(val));
15018
15089
  hrefAnchors.forEach((el) => applyLinkHref(el, val));
15019
15090
  }
15020
15091
  }
@@ -15556,7 +15627,8 @@ function EditGlowChrome({
15556
15627
  position: "absolute",
15557
15628
  inset: 0,
15558
15629
  border: "2px solid var(--ohw-primary, #0885FE)",
15559
- borderRadius: 8,
15630
+ // Same radius as every other interaction frame — this one was drawing 8 (OHH-701/722).
15631
+ borderRadius: CHROME_RADIUS,
15560
15632
  boxShadow: "0 0 0 4px color-mix(in srgb, var(--ohw-primary, #0885FE) 12%, transparent)",
15561
15633
  pointerEvents: "none"
15562
15634
  }
@@ -16300,7 +16372,8 @@ function OhhwellsBridge() {
16300
16372
  const el = document.querySelector(
16301
16373
  '[data-ohw-hovered]:not([contenteditable]):not([data-ohw-href-key]):not([data-ohw-editable="form"] *)'
16302
16374
  );
16303
- const target = el && !el.closest("[data-ohw-href-key]") ? el : null;
16375
+ const busy = el?.querySelector('[contenteditable="true"]') ?? null;
16376
+ const target = el && !el.closest("[data-ohw-href-key]") && !busy ? el : null;
16304
16377
  if (!target) {
16305
16378
  setHoveredTextRect(null);
16306
16379
  return;
@@ -16311,7 +16384,10 @@ function OhhwellsBridge() {
16311
16384
  const observer = new MutationObserver(sync);
16312
16385
  observer.observe(document.documentElement, {
16313
16386
  attributes: true,
16314
- attributeFilter: ["data-ohw-hovered"],
16387
+ // `contenteditable` too: the box is recomputed only when one of these changes, and a click
16388
+ // that opens an element for typing changes nothing else — so the frame measured a moment
16389
+ // earlier, while merely hovering, stayed on screen inside the solid one (OHH-762).
16390
+ attributeFilter: ["data-ohw-hovered", "contenteditable"],
16315
16391
  subtree: true
16316
16392
  });
16317
16393
  return () => observer.disconnect();
@@ -16621,6 +16697,14 @@ function OhhwellsBridge() {
16621
16697
  el.removeAttribute("data-ohw-hovered");
16622
16698
  el.setAttribute("data-ohw-selected", "");
16623
16699
  }, [clearSelectedAttr]);
16700
+ const isSelectedForHover = (0, import_react17.useCallback)((el) => {
16701
+ if (!el) return false;
16702
+ return [selectedElRef.current, activeElRef.current].some(
16703
+ (busy) => busy && (el === busy || busy.contains(el) || el.contains(busy))
16704
+ );
16705
+ }, []);
16706
+ const isSelectedForHoverRef = (0, import_react17.useRef)(isSelectedForHover);
16707
+ isSelectedForHoverRef.current = isSelectedForHover;
16624
16708
  const resolveHrefKeyElement = (0, import_react17.useCallback)((hrefKey) => {
16625
16709
  if (isFooterHrefKey(hrefKey)) {
16626
16710
  return document.querySelector(
@@ -17538,6 +17622,12 @@ function OhhwellsBridge() {
17538
17622
  );
17539
17623
  const activate = (0, import_react17.useCallback)((el, options) => {
17540
17624
  if (activeElRef.current === el) return;
17625
+ document.querySelectorAll("[data-ohw-hovered]").forEach((hovered) => {
17626
+ hovered.removeAttribute("data-ohw-hovered");
17627
+ });
17628
+ setHoveredTextRect(null);
17629
+ hoveredItemElRef.current = null;
17630
+ setHoveredItemRect(null);
17541
17631
  if (isIconEditable(el)) {
17542
17632
  const social = getSocialItem(el);
17543
17633
  const words = social ? social.querySelector('[data-ohw-editable="text"], [data-ohw-editable="plain"]') : null;
@@ -18776,7 +18866,7 @@ function OhhwellsBridge() {
18776
18866
  clearHrefKeyHover(hoverTarget);
18777
18867
  hoveredItemElRef.current = hoverTarget;
18778
18868
  setHoveredItemRect(hoverTarget.getBoundingClientRect());
18779
- } else if (!isInsideNavigationItem(editable) && hoverTarget !== selectedElRef.current) {
18869
+ } else if (!isInsideNavigationItem(editable) && !isSelectedForHoverRef.current(hoverTarget)) {
18780
18870
  hoverTarget.setAttribute("data-ohw-hovered", "");
18781
18871
  if (editable.closest("footer") || editable.closest('[data-ohw-section="footer"]')) {
18782
18872
  hoveredNavContainerRef.current = null;
@@ -19236,7 +19326,7 @@ function OhhwellsBridge() {
19236
19326
  showImageHover(imgEl, isDragOver, true);
19237
19327
  }
19238
19328
  document.querySelectorAll("[data-ohw-hovered]").forEach((el) => el.removeAttribute("data-ohw-hovered"));
19239
- if (textEditable && !textEditable.closest("[data-ohw-href-key]")) {
19329
+ if (textEditable && !textEditable.closest("[data-ohw-href-key]") && !isSelectedForHoverRef.current(textEditable)) {
19240
19330
  textEditable.setAttribute("data-ohw-hovered", "");
19241
19331
  }
19242
19332
  return;
@@ -19248,7 +19338,7 @@ function OhhwellsBridge() {
19248
19338
  clearImageHover();
19249
19339
  }
19250
19340
  document.querySelectorAll("[data-ohw-hovered]").forEach((el) => el.removeAttribute("data-ohw-hovered"));
19251
- if (textEditable && !textEditable.closest("[data-ohw-href-key]")) {
19341
+ if (textEditable && !textEditable.closest("[data-ohw-href-key]") && !isSelectedForHoverRef.current(textEditable)) {
19252
19342
  textEditable.setAttribute("data-ohw-hovered", "");
19253
19343
  }
19254
19344
  return;
@@ -19322,7 +19412,7 @@ function OhhwellsBridge() {
19322
19412
  hoveredItemElRef.current = navAnchor;
19323
19413
  setHoveredItemRect(navAnchor.getBoundingClientRect());
19324
19414
  }
19325
- } else if (!textEl.closest("[data-ohw-href-key]")) {
19415
+ } else if (!textEl.closest("[data-ohw-href-key]") && !isSelectedForHoverRef.current(textEl)) {
19326
19416
  hoveredItemElRef.current = null;
19327
19417
  setHoveredItemRect(null);
19328
19418
  textEl.setAttribute("data-ohw-hovered", "");
@@ -19418,7 +19508,7 @@ function OhhwellsBridge() {
19418
19508
  const ZONE = 20;
19419
19509
  for (let i = 0; i < sections.length; i++) {
19420
19510
  const a = sections[i];
19421
- if (a.dataset.ohwSection === "footer") continue;
19511
+ if (isPageFrameSection(a)) continue;
19422
19512
  const b = sections[i + 1] ?? null;
19423
19513
  const boundaryY = a.getBoundingClientRect().bottom;
19424
19514
  if (Math.abs(y - boundaryY) <= ZONE) {
@@ -19910,9 +20000,10 @@ function OhhwellsBridge() {
19910
20000
  const handleAiDeleteSection = (e) => {
19911
20001
  if (e.data?.type !== "ow:ai-delete-section") return;
19912
20002
  const sectionId = typeof e.data.sectionId === "string" ? e.data.sectionId : "";
19913
- if (!sectionId || sectionId === "navbar" || sectionId === "footer") return;
20003
+ if (!sectionId) return;
19914
20004
  const exists = document.querySelector(`[data-ohw-section="${CSS.escape(sectionId)}"]`);
19915
20005
  if (!exists) return;
20006
+ if (isPageFrameSection(exists)) return;
19916
20007
  clearInteractionChrome();
19917
20008
  const previous = aiSectionsRef.current;
19918
20009
  const nextState = deleteSectionFromState(parseAiSectionsState(previous), sectionId);
@@ -20653,6 +20744,15 @@ function OhhwellsBridge() {
20653
20744
  if (visibleViewportRef.current) {
20654
20745
  applyVisibleViewport(visibleViewportRef.current, parentScrollRef.current);
20655
20746
  }
20747
+ handleScroll();
20748
+ document.querySelectorAll("[data-ohw-hovered]").forEach((hovered) => {
20749
+ hovered.removeAttribute("data-ohw-hovered");
20750
+ });
20751
+ setHoveredTextRect(null);
20752
+ hoveredItemElRef.current = null;
20753
+ setHoveredItemRect(null);
20754
+ hoveredNavContainerRef.current = null;
20755
+ setHoveredNavContainerRect(null);
20656
20756
  };
20657
20757
  window.addEventListener("resize", handleViewportResize, { passive: true });
20658
20758
  document.addEventListener("click", handleClick, true);
@@ -20923,7 +21023,7 @@ function OhhwellsBridge() {
20923
21023
  postToParent2({
20924
21024
  type: "ow:ready",
20925
21025
  version: "1",
20926
- bridgeVersion: "0.1.71",
21026
+ bridgeVersion: "0.1.71-next.218",
20927
21027
  path: pathname,
20928
21028
  nodes: collectEditableNodes(editContentRef.current),
20929
21029
  sections