@ohhwells/bridge 0.1.144 → 0.1.146

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
@@ -2534,6 +2534,49 @@ function socialHrefKey(item) {
2534
2534
  function socialIconKey(item) {
2535
2535
  return item.querySelector(ICON_SELECTOR)?.dataset.ohwKey ?? null;
2536
2536
  }
2537
+ var SOCIAL_PLATFORM_LABELS = {
2538
+ instagram: "Instagram",
2539
+ facebook: "Facebook",
2540
+ x: "X",
2541
+ twitter: "X",
2542
+ tiktok: "TikTok",
2543
+ youtube: "YouTube",
2544
+ linkedin: "LinkedIn",
2545
+ pinterest: "Pinterest",
2546
+ whatsapp: "WhatsApp",
2547
+ telegram: "Telegram",
2548
+ spotify: "Spotify",
2549
+ threads: "Threads",
2550
+ vimeo: "Vimeo",
2551
+ discord: "Discord",
2552
+ medium: "Medium",
2553
+ website: "Website"
2554
+ };
2555
+ function socialPlatformLabel(item, content) {
2556
+ const iconKey = socialIconKey(item);
2557
+ const chosen = iconKey ? (content[socialPlatformKey(iconKey)] ?? "").trim().toLowerCase() : "";
2558
+ if (chosen) return SOCIAL_PLATFORM_LABELS[chosen] ?? chosen.charAt(0).toUpperCase() + chosen.slice(1);
2559
+ const label = socialLabelElement(item);
2560
+ const words = label && !label.hasAttribute(SOCIALS_EMPTY_LABEL_ATTR) ? (label.textContent ?? "").replace(/\s+/g, " ").trim() : "";
2561
+ if (words) return words;
2562
+ const link = item.matches("a") ? item : item.querySelector("a");
2563
+ const aria = (link?.getAttribute("aria-label") ?? item.getAttribute("aria-label") ?? "").trim();
2564
+ if (aria) return aria;
2565
+ const href = (link?.getAttribute("href") ?? "").trim();
2566
+ if (/^https?:\/\//i.test(href)) {
2567
+ let host = "";
2568
+ try {
2569
+ host = new URL(href).hostname.replace(/^www\./i, "").toLowerCase();
2570
+ } catch {
2571
+ host = "";
2572
+ }
2573
+ if (host.includes("x.com") || host.includes("twitter.com")) return "X";
2574
+ for (const [id, name] of Object.entries(SOCIAL_PLATFORM_LABELS)) {
2575
+ if (host.includes(id)) return name;
2576
+ }
2577
+ }
2578
+ return "Social";
2579
+ }
2537
2580
  var SOCIALS_ORDER_KEY = "__ohw_socials_order";
2538
2581
  function fromMarkup(markup) {
2539
2582
  const holder = document.createElement("div");
@@ -6215,7 +6258,9 @@ function applyTreeToState(state, payload) {
6215
6258
  ...selfEdit ? priorEntry?.beforeSection ? { beforeSection: priorEntry.beforeSection } : {} : insertBefore && payload.targetSectionId ? { beforeSection: payload.targetSectionId } : {},
6216
6259
  ...selfEdit ? priorEntry?.replaces ? { replaces: priorEntry.replaces } : {} : payload.mode === "replace" && payload.targetSectionId ? { replaces: payload.targetSectionId } : {},
6217
6260
  tree: payload.tree,
6218
- media: payload.media ?? {},
6261
+ // A column insert rebuilds the media map from the live DOM keys. The photo
6262
+ // still points at the ref it was generated with. Keep that ref.
6263
+ media: { ...priorEntry?.media ?? {}, ...payload.media ?? {} },
6219
6264
  ...payload.brand && typeof payload.brand === "object" ? { brand: payload.brand } : {}
6220
6265
  };
6221
6266
  const others = state.sections.filter((existing) => existing.id !== entry.id);
@@ -9716,6 +9761,46 @@ function FooterSocialRowBlock({
9716
9761
  }
9717
9762
  );
9718
9763
  }
9764
+ function EmptySlotPlaceholder({ name }) {
9765
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
9766
+ "div",
9767
+ {
9768
+ "data-ai-slot": name,
9769
+ "data-ai-slot-empty": "",
9770
+ "aria-hidden": "true",
9771
+ style: {
9772
+ minHeight: 88,
9773
+ borderRadius: 12,
9774
+ border: "1px dashed #D6D3D1",
9775
+ display: "flex",
9776
+ alignItems: "center",
9777
+ justifyContent: "center",
9778
+ padding: 16,
9779
+ color: "#A8A29E",
9780
+ fontSize: 13,
9781
+ lineHeight: 1.4,
9782
+ textAlign: "center",
9783
+ pointerEvents: "none"
9784
+ },
9785
+ children: "Ask AI to add something here"
9786
+ }
9787
+ );
9788
+ }
9789
+ function splitSides(node) {
9790
+ const named = /* @__PURE__ */ new Map();
9791
+ const unnamed = [];
9792
+ (node.children ?? []).forEach((child, index) => {
9793
+ if (child.slot === "left" || child.slot === "right") {
9794
+ named.set(child.slot, { node: child, index });
9795
+ } else {
9796
+ unnamed.push({ node: child, index });
9797
+ }
9798
+ });
9799
+ return {
9800
+ left: named.get("left") ?? unnamed.shift() ?? null,
9801
+ right: named.get("right") ?? unnamed.shift() ?? null
9802
+ };
9803
+ }
9719
9804
  function renderNode(node, ctx, path, ambientAlign) {
9720
9805
  const slots = node.slots ?? {};
9721
9806
  switch (node.type) {
@@ -9723,9 +9808,11 @@ function renderNode(node, ctx, path, ambientAlign) {
9723
9808
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TextBlock, { slots, ctx, path, align: node.align ?? ambientAlign });
9724
9809
  // Layout container: arranges child blocks, contributes no content of its own. `grid` is a
9725
9810
  // nested 12-column grid the children span (a collage is 3–5 media on it, bottom-aligned so
9726
- // mixed aspects read as a composition); `split` is exactly two children at a ratio; `stack`
9727
- // is a column. Children render through this same dispatcher, so edit markers, media
9728
- // resolution, and copy paths all work unchanged inside a group.
9811
+ // mixed aspects read as a composition); `split` is left and right (index order when `slot`
9812
+ // is omitted); `stack` is a column. A missing split side is a canvas placeholder only when
9813
+ // `emptySlots` names it. A one-child split used to keep a row at span 12 (a testimonial
9814
+ // under a heading) sits centered at half the row width. Children render through this same
9815
+ // dispatcher, so edit markers, media resolution, and copy paths all work unchanged.
9729
9816
  case "group": {
9730
9817
  const layout = str(slots.layout);
9731
9818
  const gap = slots.spacing === "tight" ? AI_TREE_TOKENS.spacing3 : slots.spacing === "airy" ? AI_TREE_TOKENS.spacing8 : AI_TREE_TOKENS.spacing6;
@@ -9823,24 +9910,38 @@ function renderNode(node, ctx, path, ambientAlign) {
9823
9910
  i
9824
9911
  ));
9825
9912
  if (layout === "grid") {
9826
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
9913
+ const holes = ctx.keyFor ? node.emptySlots ?? [] : [];
9914
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
9827
9915
  "div",
9828
9916
  {
9829
9917
  "data-ai-group": "grid",
9830
9918
  style: { display: "grid", gridTemplateColumns: "repeat(12, 1fr)", gap, alignItems: "end" },
9831
- children: kids
9919
+ children: [
9920
+ kids,
9921
+ holes.map((name) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { gridColumn: "span 6", minWidth: 0 }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EmptySlotPlaceholder, { name }) }, name))
9922
+ ]
9832
9923
  }
9833
9924
  );
9834
9925
  }
9835
9926
  if (layout === "split") {
9927
+ const { left, right } = splitSides(node);
9928
+ const open = new Set(ctx.keyFor ? node.emptySlots ?? [] : []);
9929
+ const side = (entry, name) => entry ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { minWidth: 0 }, children: renderNode(entry.node, ctx, `${path}.c${entry.index}`) }, name) : open.has(name) ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EmptySlotPlaceholder, { name }, name) : null;
9930
+ if (!left && !open.has("left") || !right && !open.has("right")) {
9931
+ const only = left ?? right;
9932
+ return only ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-ai-group": "split", style: { display: "flex", justifyContent: "center" }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { width: "50%", minWidth: 0 }, children: renderNode(only.node, ctx, `${path}.c${only.index}`) }) }) : null;
9933
+ }
9836
9934
  const ratio = str(slots.ratio);
9837
9935
  const cols = ratio === "3:5" ? "3fr 5fr" : ratio === "5:3" ? "5fr 3fr" : "1fr 1fr";
9838
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
9936
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
9839
9937
  "div",
9840
9938
  {
9841
9939
  "data-ai-group": "split",
9842
9940
  style: { display: "grid", gridTemplateColumns: cols, gap, alignItems: "center" },
9843
- children: kids
9941
+ children: [
9942
+ side(left, "left"),
9943
+ side(right, "right")
9944
+ ]
9844
9945
  }
9845
9946
  );
9846
9947
  }
@@ -25175,36 +25276,72 @@ function OhhwellsBridge() {
25175
25276
  const [selectedMedia, setSelectedMedia] = (0, import_react21.useState)(null);
25176
25277
  const selectedMediaElRef = (0, import_react21.useRef)(null);
25177
25278
  const mediaKeyDropTokenRef = (0, import_react21.useRef)(0);
25279
+ const selectedIconElRef = (0, import_react21.useRef)(null);
25280
+ const selectedSocialElRef = (0, import_react21.useRef)(null);
25281
+ const queueSectionKeyDrop = (0, import_react21.useCallback)((prev) => {
25282
+ const sectionEl = prev?.closest("[data-ohw-section]") ?? null;
25283
+ if (!sectionEl) return;
25284
+ const sectionInstanceId = instanceIdOf(movableUnit(sectionEl)) || null;
25285
+ if (!sectionInstanceId) return;
25286
+ const token = ++mediaKeyDropTokenRef.current;
25287
+ const onQueueDrained = (e) => {
25288
+ if (e.source !== window || e.data?.type !== "ow:media-key-drop" || e.data.token !== token) return;
25289
+ window.removeEventListener("message", onQueueDrained);
25290
+ if (aiSectionApiRef.current?.getSelectedId() !== sectionInstanceId) return;
25291
+ if (selectedMediaElRef.current || selectedIconElRef.current || selectedSocialElRef.current) return;
25292
+ postToParentRef.current({
25293
+ type: "ow:section-selected",
25294
+ sectionId: sectionEl.dataset.ohwSection ?? null,
25295
+ sectionLabel: sectionEl.dataset.ohwSectionLabel ?? titleCaseSectionId(sectionEl.dataset.ohwSection ?? ""),
25296
+ key: null
25297
+ });
25298
+ };
25299
+ window.addEventListener("message", onQueueDrained);
25300
+ window.postMessage({ type: "ow:media-key-drop", token }, "*");
25301
+ }, []);
25178
25302
  const clearMediaSelection = (0, import_react21.useCallback)(() => {
25179
25303
  const prev = selectedMediaElRef.current;
25180
25304
  selectedMediaElRef.current = null;
25181
25305
  setSelectedMedia(null);
25182
- const sectionEl = prev?.closest("[data-ohw-section]") ?? null;
25183
- if (sectionEl) {
25184
- const sectionInstanceId = instanceIdOf(movableUnit(sectionEl)) || null;
25185
- if (!sectionInstanceId) return;
25186
- const token = ++mediaKeyDropTokenRef.current;
25187
- const onQueueDrained = (e) => {
25188
- if (e.source !== window || e.data?.type !== "ow:media-key-drop" || e.data.token !== token) return;
25189
- window.removeEventListener("message", onQueueDrained);
25190
- if (aiSectionApiRef.current?.getSelectedId() !== sectionInstanceId) return;
25191
- if (selectedMediaElRef.current) return;
25192
- postToParentRef.current({
25193
- type: "ow:section-selected",
25194
- sectionId: sectionEl.dataset.ohwSection ?? null,
25195
- sectionLabel: sectionEl.dataset.ohwSectionLabel ?? titleCaseSectionId(sectionEl.dataset.ohwSection ?? ""),
25196
- key: null
25197
- });
25198
- };
25199
- window.addEventListener("message", onQueueDrained);
25200
- window.postMessage({ type: "ow:media-key-drop", token }, "*");
25201
- }
25202
- }, []);
25306
+ queueSectionKeyDrop(prev);
25307
+ }, [queueSectionKeyDrop]);
25203
25308
  const clearMediaSelectionRef = (0, import_react21.useRef)(clearMediaSelection);
25204
25309
  clearMediaSelectionRef.current = clearMediaSelection;
25310
+ const clearIconSelection = (0, import_react21.useCallback)(() => {
25311
+ const prev = selectedIconElRef.current;
25312
+ if (!prev) return;
25313
+ selectedIconElRef.current = null;
25314
+ queueSectionKeyDrop(prev);
25315
+ }, [queueSectionKeyDrop]);
25316
+ const clearIconSelectionRef = (0, import_react21.useRef)(clearIconSelection);
25317
+ clearIconSelectionRef.current = clearIconSelection;
25318
+ const clearSocialSelection = (0, import_react21.useCallback)(() => {
25319
+ const prev = selectedSocialElRef.current;
25320
+ if (!prev) return;
25321
+ selectedSocialElRef.current = null;
25322
+ queueSectionKeyDrop(prev);
25323
+ }, [queueSectionKeyDrop]);
25324
+ const clearSocialSelectionRef = (0, import_react21.useRef)(clearSocialSelection);
25325
+ clearSocialSelectionRef.current = clearSocialSelection;
25326
+ const selectIconElement = (0, import_react21.useCallback)((el) => {
25327
+ if (selectedMediaElRef.current) clearMediaSelectionRef.current();
25328
+ selectedIconElRef.current = el;
25329
+ const sectionEl = el.closest("[data-ohw-section]");
25330
+ aiSectionApiRef.current?.selectFromElement(el, { report: false });
25331
+ postToParentRef.current({
25332
+ type: "ow:section-selected",
25333
+ sectionId: sectionEl?.dataset.ohwSection ?? null,
25334
+ sectionLabel: sectionEl ? sectionEl.dataset.ohwSectionLabel ?? titleCaseSectionId(sectionEl.dataset.ohwSection ?? "") : null,
25335
+ key: el.dataset.ohwKey ?? null,
25336
+ keyLabel: "Icon"
25337
+ });
25338
+ }, []);
25339
+ const selectIconElementRef = (0, import_react21.useRef)(selectIconElement);
25340
+ selectIconElementRef.current = selectIconElement;
25205
25341
  const selectMediaElement = (0, import_react21.useCallback)((el) => {
25206
25342
  const r2 = el.getBoundingClientRect();
25207
25343
  const video = el.dataset.ohwEditable === "video" ? el.querySelector("video") : null;
25344
+ selectedIconElRef.current = null;
25208
25345
  selectedMediaElRef.current = el;
25209
25346
  setSelectedMedia({
25210
25347
  key: el.dataset.ohwKey ?? "",
@@ -25858,6 +25995,7 @@ function OhhwellsBridge() {
25858
25995
  selectedElRef.current = null;
25859
25996
  selectedHrefKeyRef.current = null;
25860
25997
  selectedFooterColAttrRef.current = null;
25998
+ clearSocialSelectionRef.current();
25861
25999
  setSelectedIsCta(false);
25862
26000
  setSelectedIsSocial(false);
25863
26001
  setSelectedIsSocialsRow(false);
@@ -26759,7 +26897,22 @@ function OhhwellsBridge() {
26759
26897
  const select = (0, import_react21.useCallback)((anchor) => {
26760
26898
  if (!isNavigationItem2(anchor)) return;
26761
26899
  if (activeElRef.current) deactivate();
26762
- aiSectionApiRef.current?.selectFromElement(anchor);
26900
+ clearSocialSelectionRef.current();
26901
+ const socialItem = getSocialItem(anchor);
26902
+ if (socialItem) {
26903
+ selectedSocialElRef.current = socialItem;
26904
+ const sectionEl = socialItem.closest("[data-ohw-section]");
26905
+ aiSectionApiRef.current?.selectFromElement(anchor, { report: false });
26906
+ postToParentRef.current({
26907
+ type: "ow:section-selected",
26908
+ sectionId: sectionEl?.dataset.ohwSection ?? null,
26909
+ sectionLabel: sectionEl ? sectionEl.dataset.ohwSectionLabel ?? titleCaseSectionId(sectionEl.dataset.ohwSection ?? "") : null,
26910
+ key: socialIconKey(socialItem) ?? socialItem.getAttribute("data-ohw-href-key"),
26911
+ keyLabel: socialPlatformLabel(socialItem, editContentRef.current)
26912
+ });
26913
+ } else {
26914
+ aiSectionApiRef.current?.selectFromElement(anchor);
26915
+ }
26763
26916
  selectedElRef.current = anchor;
26764
26917
  selectedHrefKeyRef.current = anchor.getAttribute("data-ohw-href-key");
26765
26918
  selectedFooterColAttrRef.current = null;
@@ -27897,8 +28050,11 @@ function OhhwellsBridge() {
27897
28050
  display: none !important;
27898
28051
  }
27899
28052
  /* Body text (no item-action toolbar) \u2014 first click enters text edit \u2192 I-beam.
27900
- Exclude select-first nav/footer labels so grab/default on [data-ohw-href-key] can win. */
27901
- [data-ohw-editable]:not([contenteditable]):not([data-ohw-editable="image"]):not([data-ohw-editable="bg-image"]):not([data-ohw-editable="video"]):not([data-ohw-editable="link"]):not(:is([data-ohw-href-key] *)) {
28053
+ Exclude select-first nav/footer labels so grab/default on [data-ohw-href-key] can win.
28054
+ Icons are excluded too: a glyph is replaced by a click, never typed into (OHH-623),
28055
+ so it takes the picker's pointer below, not an I-beam \u2014 this blanket !important used
28056
+ to beat the stylesheet's own non-!important icon rule (OHH-718). */
28057
+ [data-ohw-editable]:not([contenteditable]):not([data-ohw-editable="image"]):not([data-ohw-editable="bg-image"]):not([data-ohw-editable="video"]):not([data-ohw-editable="link"]):not([data-ohw-editable="icon"]):not(:is([data-ohw-href-key] *)) {
27902
28058
  cursor: text !important;
27903
28059
  }
27904
28060
  /* Select-first items: default arrow; grab only when drag-start is allowed (data-ohw-can-drag). */
@@ -27919,6 +28075,7 @@ function OhhwellsBridge() {
27919
28075
  [data-ohw-editable="video"], [data-ohw-editable="video"] *,
27920
28076
  [data-ohw-editable="bg-image"], [data-ohw-editable="bg-image"] * { cursor: pointer !important; }
27921
28077
  [data-ohw-editable="link"], [data-ohw-editable="link"] * { cursor: pointer !important; }
28078
+ [data-ohw-editable="icon"], [data-ohw-editable="icon"] * { cursor: pointer !important; }
27922
28079
  /* A text/plain editable nested inside a media editable (e.g. a placeholder's fallback
27923
28080
  label inside its bg-image box) is its own click target, not the media picker \u2014 the
27924
28081
  blanket media cursor rule above would otherwise show pointer where a click actually
@@ -28070,6 +28227,9 @@ function OhhwellsBridge() {
28070
28227
  if (selectedMediaElRef.current && !selectedMediaElRef.current.contains(target) && !target.closest("[data-ohw-media-overlay], [data-ohw-edit-chrome], [data-ohw-item-interaction]")) {
28071
28228
  clearMediaSelectionRef.current();
28072
28229
  }
28230
+ if (selectedIconElRef.current && !selectedIconElRef.current.contains(target)) {
28231
+ clearIconSelectionRef.current();
28232
+ }
28073
28233
  if (selectedMediaElRef.current && target.closest("[data-ohw-media-overlay]") && !findClosestButtonLike(target)) {
28074
28234
  const hit = findImageAtPoint(e.clientX, e.clientY, false);
28075
28235
  if (hit && hit !== selectedMediaElRef.current) {
@@ -28247,7 +28407,7 @@ function OhhwellsBridge() {
28247
28407
  if (isIconEditable(editable) && !getSocialItem(editable)) {
28248
28408
  e.preventDefault();
28249
28409
  e.stopPropagation();
28250
- aiSectionApiRef.current?.selectFromElement(editable);
28410
+ selectIconElementRef.current(editable);
28251
28411
  const aiIcon = editable.hasAttribute("data-ai-icon");
28252
28412
  const featureIcon = aiIcon && /\.features\.\d+\.icon$/.test(editable.dataset.ohwKey ?? "");
28253
28413
  postToParentRef.current({
@@ -29871,6 +30031,7 @@ function OhhwellsBridge() {
29871
30031
  deactivateRef.current();
29872
30032
  deselectRef.current();
29873
30033
  clearMediaSelectionRef.current();
30034
+ clearIconSelectionRef.current();
29874
30035
  };
29875
30036
  clearInteractionChromeRef.current = clearInteractionChrome;
29876
30037
  const applyTreeToLocalState = (payload) => {
@@ -30288,6 +30449,7 @@ function OhhwellsBridge() {
30288
30449
  deselectRef.current();
30289
30450
  deactivateRef.current();
30290
30451
  clearMediaSelectionRef.current();
30452
+ clearIconSelectionRef.current();
30291
30453
  };
30292
30454
  window.addEventListener("message", handleDeactivate);
30293
30455
  const handleToastAction = (e) => {
@@ -30377,6 +30539,10 @@ function OhhwellsBridge() {
30377
30539
  clearMediaSelectionRef.current();
30378
30540
  return;
30379
30541
  }
30542
+ if (e.key === "Escape" && selectedIconElRef.current) {
30543
+ clearIconSelectionRef.current();
30544
+ return;
30545
+ }
30380
30546
  if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "a" && activeElRef.current) {
30381
30547
  e.preventDefault();
30382
30548
  selectAllTextInEditable(activeElRef.current);
@@ -31398,7 +31564,7 @@ function OhhwellsBridge() {
31398
31564
  postToParent2({
31399
31565
  type: "ow:ready",
31400
31566
  version: "1",
31401
- bridgeVersion: "0.1.143",
31567
+ bridgeVersion: "0.1.145",
31402
31568
  path: pathname,
31403
31569
  nodes: collectEditableNodes(editContentRef.current),
31404
31570
  sections