@ohhwells/bridge 0.1.145 → 0.1.147

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
@@ -87,6 +87,7 @@ __export(index_exports, {
87
87
  resolveFooterCopyright: () => resolveFooterCopyright,
88
88
  serializeAiFooterState: () => serializeAiFooterState,
89
89
  stepViableSlide: () => stepViableSlide,
90
+ stripTextAlignCss: () => stripTextAlignCss,
90
91
  toggleVariants: () => toggleVariants,
91
92
  useOhwCarousel: () => useOhwCarousel,
92
93
  validateUrlInput: () => validateUrlInput,
@@ -6258,12 +6259,34 @@ function applyTreeToState(state, payload) {
6258
6259
  ...selfEdit ? priorEntry?.beforeSection ? { beforeSection: priorEntry.beforeSection } : {} : insertBefore && payload.targetSectionId ? { beforeSection: payload.targetSectionId } : {},
6259
6260
  ...selfEdit ? priorEntry?.replaces ? { replaces: priorEntry.replaces } : {} : payload.mode === "replace" && payload.targetSectionId ? { replaces: payload.targetSectionId } : {},
6260
6261
  tree: payload.tree,
6261
- media: payload.media ?? {},
6262
+ // A column insert rebuilds the media map from the live DOM keys. The photo
6263
+ // still points at the ref it was generated with. Keep that ref.
6264
+ media: { ...priorEntry?.media ?? {}, ...payload.media ?? {} },
6262
6265
  ...payload.brand && typeof payload.brand === "object" ? { brand: payload.brand } : {}
6263
6266
  };
6264
6267
  const others = state.sections.filter((existing) => existing.id !== entry.id);
6265
6268
  return { ...state, v: 1, sections: [...others, entry] };
6266
6269
  }
6270
+ function stripTextAlignCss(html) {
6271
+ return html.replace(/\sstyle="([^"]*)"/gi, (whole, value) => {
6272
+ if (!/text-align\s*:/i.test(value)) return whole;
6273
+ const kept = value.split(";").map((decl) => decl.trim()).filter((decl) => decl && !/^text-align\s*:/i.test(decl)).join("; ");
6274
+ return kept ? ` style="${kept}"` : "";
6275
+ });
6276
+ }
6277
+ function stripTextAlignInValue(value) {
6278
+ if (typeof value === "string") return stripTextAlignCss(value);
6279
+ if (Array.isArray(value)) return value.map(stripTextAlignInValue);
6280
+ if (value && typeof value === "object") {
6281
+ return Object.fromEntries(
6282
+ Object.entries(value).map(([key, inner]) => [
6283
+ key,
6284
+ stripTextAlignInValue(inner)
6285
+ ])
6286
+ );
6287
+ }
6288
+ return value;
6289
+ }
6267
6290
  function foldAlignIntoTrees(state, store) {
6268
6291
  const byId = new Map(state.sections.map((entry) => [entry.id, entry]));
6269
6292
  const nextTrees = /* @__PURE__ */ new Map();
@@ -6286,6 +6309,12 @@ function foldAlignIntoTrees(state, store) {
6286
6309
  return stripChildAligns(rest);
6287
6310
  })
6288
6311
  } : node;
6312
+ const stripContentAligns = (node) => ({
6313
+ ...node,
6314
+ ...node.slots ? { slots: stripTextAlignInValue(node.slots) } : {},
6315
+ ...node.children ? { children: node.children.map(stripContentAligns) } : {}
6316
+ });
6317
+ const authoritative = (block) => stripChildAligns(stripContentAligns(block));
6289
6318
  const nodes = {};
6290
6319
  for (const [key, override] of Object.entries(store.nodes)) {
6291
6320
  const match = override.align !== void 0 && key.startsWith(AI_SLOT_KEY_PREFIX) ? key.match(/^ai\.(.+?)\.r(\d+)\.b(\d+)(?:\.|$)/) : null;
@@ -6297,7 +6326,7 @@ function foldAlignIntoTrees(state, store) {
6297
6326
  nodes[key] = override;
6298
6327
  continue;
6299
6328
  }
6300
- row.blocks[blockIdx] = { ...stripChildAligns(block), align: override.align };
6329
+ row.blocks[blockIdx] = { ...authoritative(block), align: override.align };
6301
6330
  const rest = { ...override };
6302
6331
  delete rest.align;
6303
6332
  if (Object.keys(rest).length > 0) nodes[key] = rest;
@@ -6310,7 +6339,7 @@ function foldAlignIntoTrees(state, store) {
6310
6339
  continue;
6311
6340
  }
6312
6341
  for (const row of tree.rows) {
6313
- row.blocks = row.blocks.map((block) => ({ ...stripChildAligns(block), align: override.align }));
6342
+ row.blocks = row.blocks.map((block) => ({ ...authoritative(block), align: override.align }));
6314
6343
  }
6315
6344
  const rest = { ...override };
6316
6345
  delete rest.align;
@@ -9759,6 +9788,46 @@ function FooterSocialRowBlock({
9759
9788
  }
9760
9789
  );
9761
9790
  }
9791
+ function EmptySlotPlaceholder({ name }) {
9792
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
9793
+ "div",
9794
+ {
9795
+ "data-ai-slot": name,
9796
+ "data-ai-slot-empty": "",
9797
+ "aria-hidden": "true",
9798
+ style: {
9799
+ minHeight: 88,
9800
+ borderRadius: 12,
9801
+ border: "1px dashed #D6D3D1",
9802
+ display: "flex",
9803
+ alignItems: "center",
9804
+ justifyContent: "center",
9805
+ padding: 16,
9806
+ color: "#A8A29E",
9807
+ fontSize: 13,
9808
+ lineHeight: 1.4,
9809
+ textAlign: "center",
9810
+ pointerEvents: "none"
9811
+ },
9812
+ children: "Ask AI to add something here"
9813
+ }
9814
+ );
9815
+ }
9816
+ function splitSides(node) {
9817
+ const named = /* @__PURE__ */ new Map();
9818
+ const unnamed = [];
9819
+ (node.children ?? []).forEach((child, index) => {
9820
+ if (child.slot === "left" || child.slot === "right") {
9821
+ named.set(child.slot, { node: child, index });
9822
+ } else {
9823
+ unnamed.push({ node: child, index });
9824
+ }
9825
+ });
9826
+ return {
9827
+ left: named.get("left") ?? unnamed.shift() ?? null,
9828
+ right: named.get("right") ?? unnamed.shift() ?? null
9829
+ };
9830
+ }
9762
9831
  function renderNode(node, ctx, path, ambientAlign) {
9763
9832
  const slots = node.slots ?? {};
9764
9833
  switch (node.type) {
@@ -9766,9 +9835,11 @@ function renderNode(node, ctx, path, ambientAlign) {
9766
9835
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TextBlock, { slots, ctx, path, align: node.align ?? ambientAlign });
9767
9836
  // Layout container: arranges child blocks, contributes no content of its own. `grid` is a
9768
9837
  // nested 12-column grid the children span (a collage is 3–5 media on it, bottom-aligned so
9769
- // mixed aspects read as a composition); `split` is exactly two children at a ratio; `stack`
9770
- // is a column. Children render through this same dispatcher, so edit markers, media
9771
- // resolution, and copy paths all work unchanged inside a group.
9838
+ // mixed aspects read as a composition); `split` is left and right (index order when `slot`
9839
+ // is omitted); `stack` is a column. A missing split side is a canvas placeholder only when
9840
+ // `emptySlots` names it. A one-child split used to keep a row at span 12 (a testimonial
9841
+ // under a heading) sits centered at half the row width. Children render through this same
9842
+ // dispatcher, so edit markers, media resolution, and copy paths all work unchanged.
9772
9843
  case "group": {
9773
9844
  const layout = str(slots.layout);
9774
9845
  const gap = slots.spacing === "tight" ? AI_TREE_TOKENS.spacing3 : slots.spacing === "airy" ? AI_TREE_TOKENS.spacing8 : AI_TREE_TOKENS.spacing6;
@@ -9866,24 +9937,38 @@ function renderNode(node, ctx, path, ambientAlign) {
9866
9937
  i
9867
9938
  ));
9868
9939
  if (layout === "grid") {
9869
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
9940
+ const holes = ctx.keyFor ? node.emptySlots ?? [] : [];
9941
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
9870
9942
  "div",
9871
9943
  {
9872
9944
  "data-ai-group": "grid",
9873
9945
  style: { display: "grid", gridTemplateColumns: "repeat(12, 1fr)", gap, alignItems: "end" },
9874
- children: kids
9946
+ children: [
9947
+ kids,
9948
+ 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))
9949
+ ]
9875
9950
  }
9876
9951
  );
9877
9952
  }
9878
9953
  if (layout === "split") {
9954
+ const { left, right } = splitSides(node);
9955
+ const open = new Set(ctx.keyFor ? node.emptySlots ?? [] : []);
9956
+ 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;
9957
+ if (!left && !open.has("left") || !right && !open.has("right")) {
9958
+ const only = left ?? right;
9959
+ 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;
9960
+ }
9879
9961
  const ratio = str(slots.ratio);
9880
9962
  const cols = ratio === "3:5" ? "3fr 5fr" : ratio === "5:3" ? "5fr 3fr" : "1fr 1fr";
9881
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
9963
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
9882
9964
  "div",
9883
9965
  {
9884
9966
  "data-ai-group": "split",
9885
9967
  style: { display: "grid", gridTemplateColumns: cols, gap, alignItems: "center" },
9886
- children: kids
9968
+ children: [
9969
+ side(left, "left"),
9970
+ side(right, "right")
9971
+ ]
9887
9972
  }
9888
9973
  );
9889
9974
  }
@@ -30154,14 +30239,73 @@ function OhhwellsBridge() {
30154
30239
  value = JSON.stringify(foldedHeight.store);
30155
30240
  }
30156
30241
  }
30242
+ const restoreNodes = e.data.restoreNodes;
30243
+ const restored = [];
30244
+ if (restoreNodes && typeof restoreNodes === "object") {
30245
+ for (const [key, html] of Object.entries(restoreNodes)) {
30246
+ if (typeof html !== "string") continue;
30247
+ const els = document.querySelectorAll(`[data-ohw-key="${CSS.escape(key)}"]`);
30248
+ if (els.length === 0) continue;
30249
+ for (const el of Array.from(els)) el.innerHTML = html;
30250
+ editContentRef.current = { ...editContentRef.current, [key]: html };
30251
+ restored.push({ key, text: html });
30252
+ }
30253
+ }
30254
+ const previousNodes = {};
30255
+ const strippedNodes = [];
30256
+ if (!restoreNodes) {
30257
+ const aligned = parseStyleStore(value);
30258
+ const prior = parseStyleStore(previous);
30259
+ const keys = /* @__PURE__ */ new Set();
30260
+ if (aligned) {
30261
+ for (const [key, override] of Object.entries(aligned.nodes)) {
30262
+ if (override.align === void 0) continue;
30263
+ if (prior?.nodes[key]?.align === override.align) continue;
30264
+ keys.add(key);
30265
+ }
30266
+ for (const [sectionId, override] of Object.entries(aligned.sections)) {
30267
+ if (override.align === void 0) continue;
30268
+ if (prior?.sections[sectionId]?.align === override.align) continue;
30269
+ const escaped = CSS.escape(sectionId);
30270
+ const markers = document.querySelectorAll(
30271
+ `[data-ohw-instance="${escaped}"], [data-ohw-section="${escaped}"]`
30272
+ );
30273
+ for (const marker of Array.from(markers)) {
30274
+ const editables = marker.querySelectorAll(
30275
+ '[data-ohw-key][data-ohw-editable="text"], [data-ohw-key][data-ohw-editable="plain"]'
30276
+ );
30277
+ for (const el of Array.from(editables)) {
30278
+ const key = el.getAttribute("data-ohw-key");
30279
+ if (key) keys.add(key);
30280
+ }
30281
+ }
30282
+ }
30283
+ }
30284
+ for (const key of keys) {
30285
+ if (key.startsWith(AI_SLOT_KEY_PREFIX) || key.startsWith("__ohw_")) continue;
30286
+ const els = document.querySelectorAll(`[data-ohw-key="${CSS.escape(key)}"]`);
30287
+ if (els.length === 0) continue;
30288
+ const before = sanitizeHtml(els[0].innerHTML);
30289
+ const after = stripTextAlignCss(before);
30290
+ if (after === before) continue;
30291
+ for (const el of Array.from(els)) el.innerHTML = after;
30292
+ previousNodes[key] = before;
30293
+ editContentRef.current = { ...editContentRef.current, [key]: after };
30294
+ strippedNodes.push({ key, text: after });
30295
+ }
30296
+ }
30157
30297
  stylesRef.current = value;
30158
30298
  applyStylesToDom(parseStyleStore(value));
30159
- postToParentRef.current({ type: "ow:change", nodes: [{ key: STYLE_STORE_KEY, text: value }] });
30299
+ postToParentRef.current({
30300
+ type: "ow:change",
30301
+ nodes: [{ key: STYLE_STORE_KEY, text: value }, ...strippedNodes, ...restored]
30302
+ });
30160
30303
  postToParentRef.current({
30161
30304
  type: "ow:ai-styles-applied",
30162
30305
  previous,
30163
30306
  value,
30164
- ...previousSections !== void 0 ? { previousSections } : {}
30307
+ ...previousSections !== void 0 ? { previousSections } : {},
30308
+ ...Object.keys(previousNodes).length > 0 ? { previousNodes } : {}
30165
30309
  });
30166
30310
  };
30167
30311
  window.addEventListener("message", handleAiSetStyles);
@@ -31506,7 +31650,7 @@ function OhhwellsBridge() {
31506
31650
  postToParent2({
31507
31651
  type: "ow:ready",
31508
31652
  version: "1",
31509
- bridgeVersion: "0.1.144",
31653
+ bridgeVersion: "0.1.146",
31510
31654
  path: pathname,
31511
31655
  nodes: collectEditableNodes(editContentRef.current),
31512
31656
  sections
@@ -32737,6 +32881,7 @@ function EmptySection({ title, homeHref = "/", eyebrowKey, titleKey, subtitleKey
32737
32881
  resolveFooterCopyright,
32738
32882
  serializeAiFooterState,
32739
32883
  stepViableSlide,
32884
+ stripTextAlignCss,
32740
32885
  toggleVariants,
32741
32886
  useOhwCarousel,
32742
32887
  validateUrlInput,