@ohhwells/bridge 0.1.51-next.119 → 0.1.51-next.120

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
@@ -344,11 +344,40 @@ import { LinkPopover } from "@ohhwells/bridge";
344
344
  The iframe bridge reports page sections on `ow:ready`:
345
345
 
346
346
  ```json
347
- { "type": "ow:ready", "version": "1", "nodes": [...], "sections": [{ "id": "hero", "label": "Hero" }] }
347
+ { "type": "ow:ready", "version": "1", "bridgeVersion": "0.1.50", "nodes": [...], "sections": [{ "id": "hero", "label": "Hero" }] }
348
348
  ```
349
349
 
350
+ `version` is the protocol version; `bridgeVersion` is the npm package version (inlined at build time), letting the editor detect sites running an older bridge and gate version-dependent features (e.g. AI prompting) gracefully.
351
+
350
352
  Templates must mark sections with `data-ohw-section` (and optional `data-ohw-section-label`).
351
353
 
354
+ ### Section-scoped content collection (AI prompt context)
355
+
356
+ The editor can request a single section's editable content (edit mode only):
357
+
358
+ ```json
359
+ // editor → iframe
360
+ { "type": "ow:collect-section", "sectionId": "hero" }
361
+
362
+ // iframe → editor
363
+ { "type": "ow:section-nodes", "sectionId": "hero", "found": true,
364
+ "nodes": [{ "key": "hero-headline", "type": "text/rich", "text": "..." }],
365
+ "rect": { "top": 0, "left": 0, "width": 1280, "height": 720 } }
366
+ ```
367
+
368
+ - `found` is `false` (with empty `nodes`, `rect: null`) when no `[data-ohw-section="<id>"]` exists on the current page.
369
+ - `rect` is the section element's document-relative bounding box — the editor uses it to draw the in-flight overlay over just the target section.
370
+ - The returned set contains pure section content only: structural keys (`__ohw_*`), carousel/video-setting/meta nodes, and keyless editables are never included.
371
+ - Node `type` is normalized to the set `image | bg-image | video | link | plain | text/rich | text` (unknown template-declared types become `text`).
372
+
373
+ `ow:enter-edit` (iframe → editor, sent when the user clicks into an editable) also carries the enclosing section so the editor can offer that section as the AI prompt target:
374
+
375
+ ```json
376
+ { "type": "ow:enter-edit", "key": "hero-headline", "sectionId": "hero", "sectionLabel": "Hero" }
377
+ ```
378
+
379
+ `sectionId`/`sectionLabel` are `null` for editables outside any `[data-ohw-section]` (label also when `data-ohw-section-label` is absent).
380
+
352
381
  ### Local link-editor testing (dev fixtures)
353
382
 
354
383
  Without the canvas editor parent, add `ohw-fixtures=1` to the edit URL. This preloads Re:Bound pages and sections into the link popover:
package/dist/index.cjs CHANGED
@@ -8717,10 +8717,13 @@ function getLinkHref3(el) {
8717
8717
  const anchor = el instanceof HTMLAnchorElement ? el : el.querySelector("a");
8718
8718
  return anchor?.getAttribute("href") ?? "";
8719
8719
  }
8720
- function collectEditableNodes(extraContent) {
8721
- const nodes = Array.from(
8722
- document.querySelectorAll("[data-ohw-editable]")
8723
- ).map((el) => {
8720
+ function collectEditableNodes(extraContent, root = document) {
8721
+ const isScoped = root !== document;
8722
+ const editableEls = Array.from(root.querySelectorAll("[data-ohw-editable]"));
8723
+ if (isScoped && root instanceof HTMLElement && root.matches("[data-ohw-editable]")) {
8724
+ editableEls.unshift(root);
8725
+ }
8726
+ const nodes = editableEls.map((el) => {
8724
8727
  if (el.dataset.ohwEditable === "image") {
8725
8728
  const img = el instanceof HTMLImageElement ? el : el.querySelector("img");
8726
8729
  return { key: el.dataset.ohwKey ?? "", type: "image", text: img?.src ?? "" };
@@ -8742,12 +8745,16 @@ function collectEditableNodes(extraContent) {
8742
8745
  text: el.dataset.ohwEditable === "plain" ? el.innerText ?? "" : el.innerHTML
8743
8746
  };
8744
8747
  });
8745
- document.querySelectorAll("[data-ohw-href-key]").forEach((el) => {
8748
+ const hrefEls = Array.from(root.querySelectorAll("[data-ohw-href-key]"));
8749
+ if (isScoped && root instanceof HTMLElement && root.matches("[data-ohw-href-key]")) {
8750
+ hrefEls.unshift(root);
8751
+ }
8752
+ hrefEls.forEach((el) => {
8746
8753
  const key = el.getAttribute("data-ohw-href-key") ?? "";
8747
8754
  if (!key) return;
8748
8755
  nodes.push({ key, type: "link", text: getLinkHref3(el) });
8749
8756
  });
8750
- if (extraContent) {
8757
+ if (extraContent && !isScoped) {
8751
8758
  for (const key of [NAV_ORDER_KEY, FOOTER_ORDER_KEY, NAV_COUNT_KEY]) {
8752
8759
  const text = extraContent[key];
8753
8760
  if (typeof text === "string" && text.length > 0) {
@@ -8755,30 +8762,32 @@ function collectEditableNodes(extraContent) {
8755
8762
  }
8756
8763
  }
8757
8764
  }
8758
- document.querySelectorAll('[data-ohw-editable="video"]').forEach((el) => {
8759
- const key = el.dataset.ohwKey ?? "";
8760
- const video = getVideoEl(el);
8761
- if (!key || !video) return;
8762
- nodes.push({
8763
- key: `${key}${VIDEO_AUTOPLAY_SUFFIX}`,
8764
- type: "video-setting",
8765
- text: String(video.autoplay)
8766
- });
8767
- nodes.push({
8768
- key: `${key}${VIDEO_MUTED_SUFFIX}`,
8769
- type: "video-setting",
8770
- text: String(video.muted)
8765
+ if (!isScoped) {
8766
+ document.querySelectorAll('[data-ohw-editable="video"]').forEach((el) => {
8767
+ const key = el.dataset.ohwKey ?? "";
8768
+ const video = getVideoEl(el);
8769
+ if (!key || !video) return;
8770
+ nodes.push({
8771
+ key: `${key}${VIDEO_AUTOPLAY_SUFFIX}`,
8772
+ type: "video-setting",
8773
+ text: String(video.autoplay)
8774
+ });
8775
+ nodes.push({
8776
+ key: `${key}${VIDEO_MUTED_SUFFIX}`,
8777
+ type: "video-setting",
8778
+ text: String(video.muted)
8779
+ });
8771
8780
  });
8772
- });
8773
- for (const key of listCarouselKeys()) {
8774
- nodes.push({ key, type: "carousel", text: JSON.stringify(readCarouselValue(key)) });
8781
+ for (const key of listCarouselKeys()) {
8782
+ nodes.push({ key, type: "carousel", text: JSON.stringify(readCarouselValue(key)) });
8783
+ }
8775
8784
  }
8776
8785
  const byKey = /* @__PURE__ */ new Map();
8777
8786
  for (const node of nodes) {
8778
8787
  if (!node.key) continue;
8779
8788
  if (!byKey.has(node.key)) byKey.set(node.key, node);
8780
8789
  }
8781
- if (extraContent) {
8790
+ if (extraContent && !isScoped) {
8782
8791
  applyNavFooterDeleteOverrides(byKey, extraContent);
8783
8792
  }
8784
8793
  return Array.from(byKey.values());
@@ -9391,6 +9400,10 @@ function syncNavigationDragCursorAttrs() {
9391
9400
  el.setAttribute("data-ohw-can-drag", "");
9392
9401
  });
9393
9402
  }
9403
+ var AI_NODE_TYPES = /* @__PURE__ */ new Set(["image", "bg-image", "video", "link", "plain", "text/rich", "text"]);
9404
+ function normalizeAiNodeType(type) {
9405
+ return AI_NODE_TYPES.has(type) ? type : "text";
9406
+ }
9394
9407
  function isMediaEditable(el) {
9395
9408
  const t = el.dataset.ohwEditable;
9396
9409
  return t === "image" || t === "bg-image" || t === "video";
@@ -11110,7 +11123,14 @@ function OhhwellsBridge() {
11110
11123
  setReorderDragDisabled(reorderDisabled);
11111
11124
  }
11112
11125
  setToolbarRect(getEditMeasureEl(el).getBoundingClientRect());
11113
- postToParent2({ type: "ow:enter-edit", key: el.dataset.ohwKey });
11126
+ const sectionEl = el.closest("[data-ohw-section]");
11127
+ postToParent2({
11128
+ type: "ow:enter-edit",
11129
+ key: el.dataset.ohwKey,
11130
+ // Section the edited element belongs to, so the editor can scope AI prompts to it.
11131
+ sectionId: sectionEl?.dataset.ohwSection ?? null,
11132
+ sectionLabel: sectionEl?.dataset.ohwSectionLabel ?? null
11133
+ });
11114
11134
  requestAnimationFrame(() => refreshActiveCommandsRef.current());
11115
11135
  }, [clearSelectedAttr, deactivate, postToParent2]);
11116
11136
  activateRef.current = activate;
@@ -12786,6 +12806,25 @@ function OhhwellsBridge() {
12786
12806
  const h = document.documentElement.scrollHeight;
12787
12807
  if (h > 50) postToParentRef.current({ type: "ow:height", height: h });
12788
12808
  };
12809
+ const handleCollectSection = (e) => {
12810
+ if (e.data?.type !== "ow:collect-section") return;
12811
+ const sectionId = e.data.sectionId;
12812
+ if (typeof sectionId !== "string" || !sectionId) return;
12813
+ const sectionEl = document.querySelector(
12814
+ `[data-ohw-section="${CSS.escape(sectionId)}"]`
12815
+ );
12816
+ const nodes = sectionEl ? collectEditableNodes(void 0, sectionEl).filter(
12817
+ (n) => n.key && !n.key.startsWith("__ohw_") && !["meta", "sections", "carousel", "video-setting"].includes(n.type)
12818
+ ).map((n) => ({ ...n, text: n.text ?? "", type: normalizeAiNodeType(n.type) })) : [];
12819
+ const box = sectionEl?.getBoundingClientRect();
12820
+ const rect = box ? {
12821
+ top: box.top + window.scrollY,
12822
+ left: box.left + window.scrollX,
12823
+ width: box.width,
12824
+ height: box.height
12825
+ } : null;
12826
+ postToParentRef.current({ type: "ow:section-nodes", sectionId, found: !!sectionEl, nodes, rect });
12827
+ };
12789
12828
  let selectionChangeRaf = null;
12790
12829
  const runSelectionChange = () => {
12791
12830
  selectionChangeRaf = null;
@@ -13006,6 +13045,7 @@ function OhhwellsBridge() {
13006
13045
  window.addEventListener("message", handleScheduleLinked);
13007
13046
  window.addEventListener("message", handleClearSchedulingWidget);
13008
13047
  window.addEventListener("message", handleRemoveSchedulingSection);
13048
+ window.addEventListener("message", handleCollectSection);
13009
13049
  window.addEventListener("message", handleImageUrl);
13010
13050
  window.addEventListener("message", handleImageUploading);
13011
13051
  window.addEventListener("message", handleCarouselChange);
@@ -13056,6 +13096,7 @@ function OhhwellsBridge() {
13056
13096
  window.removeEventListener("message", handleScheduleLinked);
13057
13097
  window.removeEventListener("message", handleClearSchedulingWidget);
13058
13098
  window.removeEventListener("message", handleRemoveSchedulingSection);
13099
+ window.removeEventListener("message", handleCollectSection);
13059
13100
  window.removeEventListener("message", handleImageUrl);
13060
13101
  window.removeEventListener("message", handleImageUploading);
13061
13102
  window.removeEventListener("message", handleCarouselChange);
@@ -13268,6 +13309,7 @@ function OhhwellsBridge() {
13268
13309
  postToParent2({
13269
13310
  type: "ow:ready",
13270
13311
  version: "1",
13312
+ bridgeVersion: "0.1.51",
13271
13313
  path: pathname,
13272
13314
  nodes: collectEditableNodes(editContentRef.current),
13273
13315
  sections