@ohhwells/bridge 0.1.117-next.324 → 0.1.117-next.327

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
@@ -572,6 +572,40 @@ function applyBrandToDom(kit) {
572
572
  }
573
573
  }
574
574
 
575
+ // src/lib/custom-code.ts
576
+ var ALLOWED_HEAD_TAGS = /* @__PURE__ */ new Set(["style", "meta", "link", "script", "noscript", "base"]);
577
+ var CUSTOM_CODE_AFTER_HEAD_KEY = "__ohw_custom_code_after_head";
578
+ var CUSTOM_CODE_BEFORE_HEAD_KEY = "__ohw_custom_code_before_head";
579
+ function clearExisting(slot) {
580
+ document.querySelectorAll(`[data-ohw-custom-code="${slot}"]`).forEach((el) => el.remove());
581
+ }
582
+ function buildNodes(raw, slot) {
583
+ if (!raw || !raw.trim()) return [];
584
+ const parsed = new DOMParser().parseFromString(raw, "text/html");
585
+ const source = [...parsed.head.childNodes, ...parsed.body.childNodes];
586
+ const nodes = [];
587
+ for (const node of source) {
588
+ if (node.nodeType !== Node.ELEMENT_NODE) continue;
589
+ const el = node;
590
+ const tag = el.tagName.toLowerCase();
591
+ if (!ALLOWED_HEAD_TAGS.has(tag)) continue;
592
+ const built = document.createElement(tag);
593
+ for (const attr of Array.from(el.attributes)) built.setAttribute(attr.name, attr.value);
594
+ if (tag === "script" || tag === "style" || tag === "noscript") built.textContent = el.textContent;
595
+ built.dataset.ohwCustomCode = slot;
596
+ nodes.push(built);
597
+ }
598
+ return nodes;
599
+ }
600
+ function applyCustomCodeToHead(content) {
601
+ clearExisting("after");
602
+ clearExisting("before");
603
+ const afterNodes = buildNodes(content[CUSTOM_CODE_AFTER_HEAD_KEY], "after");
604
+ if (afterNodes.length > 0) document.head.prepend(...afterNodes);
605
+ const beforeNodes = buildNodes(content[CUSTOM_CODE_BEFORE_HEAD_KEY], "before");
606
+ if (beforeNodes.length > 0) document.head.append(...beforeNodes);
607
+ }
608
+
575
609
  // src/lib/section-styles.ts
576
610
  var STYLE_STORE_KEY = "__ohw_styles";
577
611
  var STYLE_SHEET_ID = "ohw-section-styles";
@@ -2405,7 +2439,11 @@ function MediaStripBlock({ node, ctx, path }) {
2405
2439
  const justify = align === "left" ? "flex-start" : align === "right" ? "flex-end" : "center";
2406
2440
  const label = str(slots.label);
2407
2441
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { "data-ohw-media-strip": "", style: { display: "flex", flexDirection: "column", gap: AI_TREE_TOKENS.spacing6 }, children: [
2408
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: '[data-ohw-media-strip-item]{transition:transform 0.2s ease, opacity 0.2s ease}[data-ohw-media-strip-hover="lift"]:hover{transform:translateY(-4px)}[data-ohw-media-strip-hover="zoom"]:hover{transform:scale(1.05)}[data-ohw-media-strip-hover="fade"]:hover{opacity:0.75}' }),
2442
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", {
2443
+ // Host sites often style `a:hover{opacity}` globally — that would fade every linked item
2444
+ // whatever preset is chosen, so the anchors here opt out and only the preset applies.
2445
+ children: '[data-ohw-media-strip] a,[data-ohw-media-strip] a:hover{opacity:1}[data-ohw-media-strip-item]{transition:transform 0.2s ease, opacity 0.2s ease}[data-ohw-media-strip-hover="lift"]:hover{transform:translateY(-4px)}[data-ohw-media-strip-hover="zoom"]:hover{transform:scale(1.05)}[data-ohw-media-strip-hover="fade"]:hover{opacity:0.75}'
2446
+ }),
2409
2447
  label && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2410
2448
  "div",
2411
2449
  {
@@ -2430,7 +2468,7 @@ function MediaStripBlock({ node, ctx, path }) {
2430
2468
  },
2431
2469
  children: items.map((item, i) => {
2432
2470
  const hasLink = typeof item.href === "string" && item.href.length > 0;
2433
- const hoverPreset = slots.hover === "none" || slots.hover === "lift" || slots.hover === "zoom" || slots.hover === "fade" ? slots.hover : hasLink ? "lift" : "none";
2471
+ const hoverPreset = !hasLink ? "none" : slots.hover === "none" || slots.hover === "lift" || slots.hover === "zoom" || slots.hover === "fade" ? slots.hover : "lift";
2434
2472
  const box = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2435
2473
  "div",
2436
2474
  {
@@ -2460,8 +2498,8 @@ function MediaStripBlock({ node, ctx, path }) {
2460
2498
  "a",
2461
2499
  {
2462
2500
  href: item.href,
2463
- target: item.linkTarget === "new-tab" ? "_blank" : void 0,
2464
- rel: item.linkTarget === "new-tab" ? "noopener noreferrer" : void 0,
2501
+ target: item.linkTarget === "same-tab" ? void 0 : "_blank",
2502
+ rel: item.linkTarget === "same-tab" ? void 0 : "noopener noreferrer",
2465
2503
  style: { display: "block", textDecoration: "none" },
2466
2504
  children: box
2467
2505
  },
@@ -10163,6 +10201,22 @@ function setMediaStripItemAlt(tree, index, alt) {
10163
10201
  return true;
10164
10202
  });
10165
10203
  }
10204
+ function setMediaStripItemLink(tree, index, link) {
10205
+ return withMediaStrip(tree, (block) => {
10206
+ const items = block.items;
10207
+ if (index < 0 || index >= items.length) return false;
10208
+ const item = { ...items[index] };
10209
+ if (link.href) {
10210
+ item.href = link.href;
10211
+ if (link.linkTarget) item.linkTarget = link.linkTarget;
10212
+ } else {
10213
+ delete item.href;
10214
+ delete item.linkTarget;
10215
+ }
10216
+ items[index] = item;
10217
+ return true;
10218
+ });
10219
+ }
10166
10220
 
10167
10221
  // src/ui/ai-section/AiMediaStripOverlay.tsx
10168
10222
  var import_jsx_runtime18 = require("react/jsx-runtime");
@@ -10368,6 +10422,88 @@ var TREATMENTS = [
10368
10422
  { value: "color", label: "Color" },
10369
10423
  { value: "grayscale", label: "Grayscale" }
10370
10424
  ];
10425
+ var HTTP_URL_RE = /^https?:\/\/\S+$/i;
10426
+ function LinkInput({ value, onCommit }) {
10427
+ const [draft, setDraft] = (0, import_react9.useState)(value);
10428
+ const [invalid, setInvalid] = (0, import_react9.useState)(false);
10429
+ (0, import_react9.useEffect)(() => {
10430
+ setDraft(value);
10431
+ setInvalid(false);
10432
+ }, [value]);
10433
+ const commit = () => {
10434
+ const next = draft.trim();
10435
+ if (next === value) return setInvalid(false);
10436
+ if (next && !HTTP_URL_RE.test(next)) return setInvalid(true);
10437
+ setInvalid(false);
10438
+ onCommit(next);
10439
+ };
10440
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
10441
+ "div",
10442
+ {
10443
+ style: {
10444
+ display: "flex",
10445
+ alignItems: "center",
10446
+ gap: 6,
10447
+ height: 28,
10448
+ padding: "0 12px",
10449
+ borderRadius: 999,
10450
+ background: "#ffffff",
10451
+ width: "100%",
10452
+ maxWidth: 260,
10453
+ boxSizing: "border-box",
10454
+ boxShadow: invalid ? "inset 0 0 0 1.5px #EF4444" : "none"
10455
+ },
10456
+ children: [
10457
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
10458
+ "span",
10459
+ {
10460
+ style: {
10461
+ flex: "0 0 auto",
10462
+ color: OHW_BLUE_DARK,
10463
+ fontSize: 10,
10464
+ fontWeight: 700,
10465
+ textTransform: "uppercase",
10466
+ letterSpacing: 0.4
10467
+ },
10468
+ children: "Link"
10469
+ }
10470
+ ),
10471
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
10472
+ "input",
10473
+ {
10474
+ type: "text",
10475
+ inputMode: "url",
10476
+ value: draft,
10477
+ onChange: (e) => setDraft(e.target.value),
10478
+ onBlur: commit,
10479
+ onKeyDown: (e) => {
10480
+ if (e.key === "Enter") e.currentTarget.blur();
10481
+ if (e.key === "Escape") {
10482
+ setDraft(value);
10483
+ setInvalid(false);
10484
+ e.currentTarget.blur();
10485
+ }
10486
+ },
10487
+ placeholder: "https://example.com",
10488
+ "aria-label": "Image link URL",
10489
+ "aria-invalid": invalid,
10490
+ style: {
10491
+ flex: 1,
10492
+ minWidth: 0,
10493
+ border: "none",
10494
+ outline: "none",
10495
+ background: "transparent",
10496
+ fontSize: 12,
10497
+ lineHeight: "16px",
10498
+ fontFamily: "Figtree, ui-sans-serif, system-ui, sans-serif",
10499
+ color: "#1f2937"
10500
+ }
10501
+ }
10502
+ )
10503
+ ]
10504
+ }
10505
+ );
10506
+ }
10371
10507
  function MediaStripToolbar({
10372
10508
  entry,
10373
10509
  index,
@@ -10379,6 +10515,7 @@ function MediaStripToolbar({
10379
10515
  onSetTreatment,
10380
10516
  onResize,
10381
10517
  onSetAlt,
10518
+ onSetLink,
10382
10519
  onAddItem,
10383
10520
  onRemoveItem,
10384
10521
  onChangeImage,
@@ -10388,10 +10525,12 @@ function MediaStripToolbar({
10388
10525
  const items = mediaStripItems(entry.tree);
10389
10526
  const itemCount = items.length;
10390
10527
  const currentAlt = items[index]?.alt ?? "";
10528
+ const currentHref = items[index]?.href ?? "";
10529
+ const currentTarget = items[index]?.linkTarget === "same-tab" ? "same-tab" : "new-tab";
10391
10530
  const slots = findMediaStripSlots(entry.tree);
10392
10531
  const variant = slots?.variant === "reel" ? "reel" : "logos";
10393
10532
  const align = slots?.align === "left" || slots?.align === "right" ? slots.align : "center";
10394
- const hover = slots?.hover === "none" || slots?.hover === "lift" || slots?.hover === "zoom" || slots?.hover === "fade" ? slots.hover : "none";
10533
+ const hover = slots?.hover === "none" || slots?.hover === "lift" || slots?.hover === "zoom" || slots?.hover === "fade" ? slots.hover : "lift";
10395
10534
  const treatment = slots?.treatment === "color" || slots?.treatment === "grayscale" ? slots.treatment : variant === "logos" ? "grayscale" : "color";
10396
10535
  const sizeValue = variant === "logos" ? typeof slots?.logoMaxHeight === "number" ? slots.logoMaxHeight : 48 : typeof slots?.cardWidth === "number" ? slots.cardWidth : 240;
10397
10536
  const isMobile = window.innerWidth <= 480;
@@ -10442,6 +10581,11 @@ function MediaStripToolbar({
10442
10581
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(Pill, { label: "+", onClick: () => onResize(8) })
10443
10582
  ] }),
10444
10583
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { style: { display: "flex", justifyContent: "center", width: "100%" }, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(AltInput, { value: currentAlt, onCommit: onSetAlt }) }),
10584
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { style: { display: "flex", justifyContent: "center", width: "100%" }, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(LinkInput, { value: currentHref, onCommit: (href) => onSetLink(href, currentTarget) }) }),
10585
+ currentHref && /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { style: rowStyle, children: [
10586
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(Pill, { label: "New tab", active: currentTarget === "new-tab", onClick: () => onSetLink(currentHref, "new-tab") }),
10587
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(Pill, { label: "Same tab", active: currentTarget === "same-tab", onClick: () => onSetLink(currentHref, "same-tab") })
10588
+ ] }),
10445
10589
  /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { style: rowStyle, children: [
10446
10590
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(Pill, { label: "Change Image", onClick: onChangeImage }),
10447
10591
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(Pill, { label: "+ Item", onClick: onAddItem }),
@@ -10637,6 +10781,12 @@ function AiMediaStripOverlay({
10637
10781
  onSetAlt: (alt) => {
10638
10782
  void runMutation(active.sectionId, (tree) => setMediaStripItemAlt(tree, active.index, alt));
10639
10783
  },
10784
+ onSetLink: (href, linkTarget) => {
10785
+ void runMutation(
10786
+ active.sectionId,
10787
+ (tree) => setMediaStripItemLink(tree, active.index, href ? { href, linkTarget } : {})
10788
+ );
10789
+ },
10640
10790
  onAddItem: () => {
10641
10791
  void requestAssetPick(postToParent2).then((picked) => {
10642
10792
  if (!picked) return;
@@ -22206,6 +22356,7 @@ function OhhwellsBridge() {
22206
22356
  applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
22207
22357
  }
22208
22358
  applyBrandChrome(content);
22359
+ applyCustomCodeToHead(content);
22209
22360
  initSectionInstancesFromContent(content, window.location.pathname);
22210
22361
  for (const [key, val] of Object.entries(content)) {
22211
22362
  if (key === "__ohw_sections") continue;
@@ -22215,6 +22366,7 @@ function OhhwellsBridge() {
22215
22366
  if (key === BRAND_KIT_KEY) continue;
22216
22367
  if (key === STYLE_STORE_KEY) continue;
22217
22368
  if (BRAND_CHROME_KEYS.has(key)) continue;
22369
+ if (key === CUSTOM_CODE_AFTER_HEAD_KEY || key === CUSTOM_CODE_BEFORE_HEAD_KEY) continue;
22218
22370
  if (applyVideoSettingNode(key, val)) continue;
22219
22371
  if (applyCarouselNode(key, val)) continue;
22220
22372
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
@@ -24626,6 +24778,7 @@ function OhhwellsBridge() {
24626
24778
  applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
24627
24779
  }
24628
24780
  applyBrandChrome(content);
24781
+ applyCustomCodeToHead(content);
24629
24782
  initSectionInstancesFromContent(content, window.location.pathname);
24630
24783
  let sectionsJson = null;
24631
24784
  for (const [key, val] of Object.entries(content)) {
@@ -24639,6 +24792,7 @@ function OhhwellsBridge() {
24639
24792
  if (key === BRAND_KIT_KEY) continue;
24640
24793
  if (key === STYLE_STORE_KEY) continue;
24641
24794
  if (BRAND_CHROME_KEYS.has(key)) continue;
24795
+ if (key === CUSTOM_CODE_AFTER_HEAD_KEY || key === CUSTOM_CODE_BEFORE_HEAD_KEY) continue;
24642
24796
  if (applyVideoSettingNode(key, val)) continue;
24643
24797
  if (applyCarouselNode(key, val)) continue;
24644
24798
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
@@ -25356,6 +25510,16 @@ function OhhwellsBridge() {
25356
25510
  if (brandKitRef.current) {
25357
25511
  nodes.push({ key: BRAND_KIT_KEY, type: "brand", text: brandKitRef.current });
25358
25512
  }
25513
+ nodes.push({
25514
+ key: CUSTOM_CODE_BEFORE_HEAD_KEY,
25515
+ type: "text",
25516
+ text: editContentRef.current[CUSTOM_CODE_BEFORE_HEAD_KEY] ?? ""
25517
+ });
25518
+ nodes.push({
25519
+ key: CUSTOM_CODE_AFTER_HEAD_KEY,
25520
+ type: "text",
25521
+ text: editContentRef.current[CUSTOM_CODE_AFTER_HEAD_KEY] ?? ""
25522
+ });
25359
25523
  document.querySelectorAll('[data-ohw-editable="form"]').forEach((form) => {
25360
25524
  const formKey = formKeyOf(form);
25361
25525
  if (!formKey) return;