@ohhwells/bridge 0.1.139 → 0.1.141

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
@@ -72,6 +72,7 @@ __export(index_exports, {
72
72
  dropIndicatorVariants: () => dropIndicatorVariants,
73
73
  ensureFooterTree: () => ensureFooterTree,
74
74
  filterAvailablePages: () => filterAvailablePages,
75
+ foldAlignIntoTrees: () => foldAlignIntoTrees,
75
76
  getEditModeInitialState: () => getEditModeInitialState,
76
77
  insertFooterHours: () => insertFooterHours,
77
78
  isEditSessionActive: () => isEditSessionActive,
@@ -80,6 +81,7 @@ __export(index_exports, {
80
81
  isValidUrl: () => isValidUrl,
81
82
  nextViableSlide: () => nextViableSlide,
82
83
  parseAiFooterState: () => parseAiFooterState,
84
+ parseAiSectionsState: () => parseAiSectionsState,
83
85
  parseTarget: () => parseTarget,
84
86
  removeFooterBlock: () => removeFooterBlock,
85
87
  resolveFooterCopyright: () => resolveFooterCopyright,
@@ -2919,14 +2921,22 @@ function isInactiveFooterChrome(el) {
2919
2921
  if (footer && footer.style.display === "none") return true;
2920
2922
  return false;
2921
2923
  }
2924
+ function isIncidentalFooter(el) {
2925
+ return Boolean(el.closest("blockquote, article, figure"));
2926
+ }
2922
2927
  function getFooterRoot() {
2923
2928
  const ai = document.querySelector("footer[data-ai-footer]");
2924
2929
  if (ai) return ai;
2925
2930
  for (const el of Array.from(
2926
- document.querySelectorAll('footer[data-ohw-section="footer"], footer')
2931
+ document.querySelectorAll('footer[data-ohw-section="footer"]')
2927
2932
  )) {
2933
+ if (isInactiveFooterChrome(el)) continue;
2934
+ return el;
2935
+ }
2936
+ for (const el of Array.from(document.querySelectorAll("footer"))) {
2928
2937
  if (el.hasAttribute("data-ai-footer")) continue;
2929
2938
  if (isInactiveFooterChrome(el)) continue;
2939
+ if (isIncidentalFooter(el)) continue;
2930
2940
  return el;
2931
2941
  }
2932
2942
  return null;
@@ -3337,8 +3347,8 @@ function buildFooterLink(colIndex, itemIndex, href, label, template) {
3337
3347
  return anchor;
3338
3348
  }
3339
3349
  function footerColumnExistsInDom(colIndex) {
3340
- const root = getFooterRoot() ?? document;
3341
- return root.querySelector(`[data-ohw-footer-col="${colIndex}"]`) !== null || root.querySelector(`[data-ohw-href-key^="footer-${colIndex}-"]`) !== null || root.querySelector(`[data-ohw-key="footer-${colIndex}-heading"]`) !== null;
3350
+ const scope = getFooterLinksContainer() ?? getFooterRoot() ?? document;
3351
+ return scope.querySelector(`[data-ohw-footer-col="${colIndex}"]`) !== null || scope.querySelector(`[data-ohw-href-key^="footer-${colIndex}-"]`) !== null || scope.querySelector(`[data-ohw-key="footer-${colIndex}-heading"]`) !== null;
3342
3352
  }
3343
3353
  function getFooterLinkTemplate() {
3344
3354
  for (const col of listFooterColumns()) {
@@ -5288,19 +5298,21 @@ function findIndexSegment(keys) {
5288
5298
  if (len < 2 || tokenLists.some((t) => t.length !== len)) return null;
5289
5299
  let position = -1;
5290
5300
  for (let p = 0; p < len; p++) {
5291
- const values = tokenLists.map((t) => t[p]);
5292
- const allSame = values.every((v) => v === values[0]);
5301
+ const values2 = tokenLists.map((t) => t[p]);
5302
+ const allSame = values2.every((v) => v === values2[0]);
5293
5303
  if (allSame) continue;
5294
- const allNumeric = values.every((v) => /^\d+$/.test(v));
5295
- if (!allNumeric || position !== -1) return null;
5304
+ if (position !== -1) return null;
5296
5305
  position = p;
5297
5306
  }
5298
5307
  if (position === -1) return null;
5308
+ const values = tokenLists.map((t) => t[position]);
5309
+ const numericCount = values.filter((v) => /^\d+$/.test(v)).length;
5310
+ if (numericCount !== 0 && numericCount !== values.length) return null;
5311
+ if (numericCount === 0 && position === len - 1) return null;
5299
5312
  const group = tokenLists[0].slice(0, position).join("-");
5300
5313
  if (!group) return null;
5301
- const indices = tokenLists.map((t) => parseInt(t[position], 10));
5302
- if (new Set(indices).size !== indices.length) return null;
5303
- return { position, group };
5314
+ if (new Set(values).size !== values.length) return null;
5315
+ return numericCount === values.length ? { position, group } : { position, group, slug: true };
5304
5316
  }
5305
5317
  function singleIndexSegment(key) {
5306
5318
  const tokens = baseKey(key).split("-");
@@ -5346,7 +5358,19 @@ function detectItemGroups(sectionEl) {
5346
5358
  list.push(m);
5347
5359
  byShape.set(shape, list);
5348
5360
  }
5349
- clusters.push(...Array.from(byShape.values()));
5361
+ const shaped = Array.from(byShape.values());
5362
+ clusters.push(...shaped.filter((c) => c.length >= 2));
5363
+ const singles = shaped.filter((c) => c.length === 1).map((c) => c[0]);
5364
+ const bySlugShape = /* @__PURE__ */ new Map();
5365
+ for (const m of singles) {
5366
+ const primary = primaryKey(keyedDescendants(m));
5367
+ const tokens = baseKey(primary ?? "").split("-");
5368
+ const slugShape = `${tokens.length}|${tokens[0]}|${tokens[tokens.length - 1]}`;
5369
+ const list = bySlugShape.get(slugShape) ?? [];
5370
+ list.push(m);
5371
+ bySlugShape.set(slugShape, list);
5372
+ }
5373
+ clusters.push(...Array.from(bySlugShape.values()));
5350
5374
  }
5351
5375
  for (const members of clusters) {
5352
5376
  const keyedPerMember = members.map((m) => keyedDescendants(m));
@@ -5357,11 +5381,28 @@ function detectItemGroups(sectionEl) {
5357
5381
  const dedupeKey = `${segment.group}`;
5358
5382
  if (seenParents.has(dedupeKey)) continue;
5359
5383
  seenParents.add(dedupeKey);
5360
- const items = members.map((el, i) => ({
5361
- el,
5362
- index: indexFromKey(primaries[i], segment.position),
5363
- keyed: keyedPerMember[i]
5364
- }));
5384
+ let items;
5385
+ if (segment.slug) {
5386
+ const tokens = members.map((_, i) => baseKey(primaries[i]).split("-")[segment.position]);
5387
+ let nextOriginal = 1;
5388
+ const indices = tokens.map((t) => {
5389
+ const cloneMatch = t.match(/^x(\d+)$/);
5390
+ return cloneMatch ? parseInt(cloneMatch[1], 10) : nextOriginal++;
5391
+ });
5392
+ if (new Set(indices).size !== indices.length) continue;
5393
+ items = members.map((el, i) => ({
5394
+ el,
5395
+ index: indices[i],
5396
+ keyed: keyedPerMember[i],
5397
+ slugToken: tokens[i]
5398
+ }));
5399
+ } else {
5400
+ items = members.map((el, i) => ({
5401
+ el,
5402
+ index: indexFromKey(primaries[i], segment.position),
5403
+ keyed: keyedPerMember[i]
5404
+ }));
5405
+ }
5365
5406
  groups.push({ group: segment.group, parent, nested: false, items });
5366
5407
  }
5367
5408
  }
@@ -5485,6 +5526,18 @@ function rekeyItemClone(clone, source, sibling, group, newIndex) {
5485
5526
  const delta = newIndex - source.index;
5486
5527
  const cloneKeyed = keyedDescendants(clone);
5487
5528
  const pairs = [];
5529
+ if (source.slugToken) {
5530
+ source.keyed.forEach((src, i) => {
5531
+ const target = cloneKeyed[i];
5532
+ if (!target || target.attr !== src.attr) return;
5533
+ const tokens = baseKey(src.key).split("-");
5534
+ if (!tokens.includes(source.slugToken)) return;
5535
+ const next = tokens.map((t) => t === source.slugToken ? `x${newIndex}` : t).join("-") + keySuffix(src.key);
5536
+ target.el.setAttribute(src.attr, next);
5537
+ pairs.push({ from: src.key, to: next });
5538
+ });
5539
+ return pairs;
5540
+ }
5488
5541
  source.keyed.forEach((src, i) => {
5489
5542
  const target = cloneKeyed[i];
5490
5543
  if (!target || target.attr !== src.attr) return;
@@ -5528,7 +5581,12 @@ function syncGroup(detected, state) {
5528
5581
  clone.setAttribute(ITEM_ATTR, `${detected.group}:${entry.index}`);
5529
5582
  rekeyItemClone(clone, source, sibling, detected.group, entry.index);
5530
5583
  source.el.insertAdjacentElement("afterend", clone);
5531
- byIndex.set(entry.index, { el: clone, index: entry.index, keyed: keyedDescendants(clone) });
5584
+ byIndex.set(entry.index, {
5585
+ el: clone,
5586
+ index: entry.index,
5587
+ keyed: keyedDescendants(clone),
5588
+ ...source.slugToken ? { slugToken: `x${entry.index}` } : {}
5589
+ });
5532
5590
  }
5533
5591
  const listed = state.items.map((e) => byIndex.get(e.index)?.el).filter((el) => !!el);
5534
5592
  let prev = null;
@@ -5630,9 +5688,16 @@ function applyItemOps(store, ops, currentContent) {
5630
5688
  const nodes = [];
5631
5689
  const added = [];
5632
5690
  let elements = syncGroup(detected, state);
5691
+ const sluggy = detected.items.some((item) => item.slugToken);
5692
+ const slugTokenOf = (index) => detected.items.find((item) => item.index === index)?.slugToken ?? (sluggy ? `x${index}` : void 0);
5633
5693
  for (const add of ops.add ?? []) {
5634
5694
  const sourceEl = elements.get(add.cloneOf);
5635
- const source = sourceEl ? { el: sourceEl, index: add.cloneOf, keyed: keyedDescendants(sourceEl) } : void 0;
5695
+ const source = sourceEl ? {
5696
+ el: sourceEl,
5697
+ index: add.cloneOf,
5698
+ keyed: keyedDescendants(sourceEl),
5699
+ ...slugTokenOf(add.cloneOf) ? { slugToken: slugTokenOf(add.cloneOf) } : {}
5700
+ } : void 0;
5636
5701
  if (!source) continue;
5637
5702
  const newIndex = Math.max(...state.items.map((e) => e.index), ...detected.items.map((i) => i.index)) + 1;
5638
5703
  const sibling = detected.items.find((item) => item.index !== add.cloneOf && !item.el.hasAttribute(ITEM_CLONE_ATTR)) ?? null;
@@ -5659,7 +5724,12 @@ function applyItemOps(store, ops, currentContent) {
5659
5724
  if (anchor) anchor.insertAdjacentElement("afterend", clone);
5660
5725
  else source.el.insertAdjacentElement("afterend", clone);
5661
5726
  elements.set(newIndex, clone);
5662
- detected.items.push({ el: clone, index: newIndex, keyed: cloneKeyed });
5727
+ detected.items.push({
5728
+ el: clone,
5729
+ index: newIndex,
5730
+ keyed: cloneKeyed,
5731
+ ...sluggy ? { slugToken: `x${newIndex}` } : {}
5732
+ });
5663
5733
  added.push(newIndex);
5664
5734
  elements = syncGroup(detected, state);
5665
5735
  }
@@ -6102,16 +6172,25 @@ function foldAlignIntoTrees(state, store) {
6102
6172
  nextTrees.set(id, fresh);
6103
6173
  return fresh;
6104
6174
  };
6175
+ const stripChildAligns = (node) => node.children ? {
6176
+ ...node,
6177
+ children: node.children.map((child) => {
6178
+ const { align: _dropped, ...rest } = child;
6179
+ return stripChildAligns(rest);
6180
+ })
6181
+ } : node;
6105
6182
  const nodes = {};
6106
6183
  for (const [key, override] of Object.entries(store.nodes)) {
6107
6184
  const match = override.align !== void 0 && key.startsWith(AI_SLOT_KEY_PREFIX) ? key.match(/^ai\.(.+?)\.r(\d+)\.b(\d+)(?:\.|$)/) : null;
6108
6185
  const tree = match ? treeFor(match[1]) : void 0;
6109
- const block = match && tree ? tree.rows[Number(match[2])]?.blocks[Number(match[3])] : void 0;
6110
- if (!block) {
6186
+ const row = match && tree ? tree.rows[Number(match[2])] : void 0;
6187
+ const blockIdx = match ? Number(match[3]) : -1;
6188
+ const block = row?.blocks[blockIdx];
6189
+ if (!row || !block) {
6111
6190
  nodes[key] = override;
6112
6191
  continue;
6113
6192
  }
6114
- block.align = override.align;
6193
+ row.blocks[blockIdx] = { ...stripChildAligns(block), align: override.align };
6115
6194
  const rest = { ...override };
6116
6195
  delete rest.align;
6117
6196
  if (Object.keys(rest).length > 0) nodes[key] = rest;
@@ -6124,7 +6203,7 @@ function foldAlignIntoTrees(state, store) {
6124
6203
  continue;
6125
6204
  }
6126
6205
  for (const row of tree.rows) {
6127
- for (const block of row.blocks) block.align = override.align;
6206
+ row.blocks = row.blocks.map((block) => ({ ...stripChildAligns(block), align: override.align }));
6128
6207
  }
6129
6208
  const rest = { ...override };
6130
6209
  delete rest.align;
@@ -6355,6 +6434,20 @@ function applyBrandChrome(content) {
6355
6434
 
6356
6435
  // src/lib/brand-kit.ts
6357
6436
  var BRAND_KIT_KEY = "__ohw_brand";
6437
+ var appliedPalette = null;
6438
+ var appliedFonts = null;
6439
+ function getAppliedBrand() {
6440
+ if (!appliedPalette) return null;
6441
+ return appliedFonts ? { palette: appliedPalette, fonts: appliedFonts } : { palette: appliedPalette };
6442
+ }
6443
+ function brandForGenerated(fallback) {
6444
+ const applied = getAppliedBrand();
6445
+ if (!applied) return fallback ?? void 0;
6446
+ return {
6447
+ palette: applied.palette,
6448
+ fonts: applied.fonts ?? fallback?.fonts ?? AI_DEFAULT_BRAND.fonts
6449
+ };
6450
+ }
6358
6451
  var BRAND_VAR_PREFIX = "--ohw-brand-";
6359
6452
  var BRAND_VAR_NAMES = ["primary", "accent", "light", "dark", "surface", "border", "muted"].map(
6360
6453
  (role) => `${BRAND_VAR_PREFIX}${role}`
@@ -6658,6 +6751,8 @@ function loadCustomFonts(fonts) {
6658
6751
  function applyBrandToDom(kit) {
6659
6752
  const root = document.documentElement;
6660
6753
  if (!kit) {
6754
+ appliedPalette = null;
6755
+ appliedFonts = null;
6661
6756
  for (const name of [...BRAND_VAR_NAMES, ...LEGACY_BRAND_VAR_NAMES, ...BUTTON_VAR_NAMES]) {
6662
6757
  root.style.removeProperty(name);
6663
6758
  }
@@ -6671,11 +6766,16 @@ function applyBrandToDom(kit) {
6671
6766
  const applyFonts = kit.touched ? kit.touched.fonts === true : true;
6672
6767
  if (kit.palette) {
6673
6768
  if (applyPalette) {
6769
+ appliedPalette = kit.palette;
6674
6770
  for (const [name, value] of Object.entries(brandColorVars(kit.palette))) root.style.setProperty(name, value);
6675
6771
  } else {
6772
+ appliedPalette = null;
6676
6773
  for (const name of [...BRAND_VAR_NAMES, ...LEGACY_BRAND_VAR_NAMES]) root.style.removeProperty(name);
6677
6774
  }
6678
6775
  }
6776
+ if (kit.fonts) {
6777
+ appliedFonts = applyFonts ? kit.fonts : null;
6778
+ }
6679
6779
  if (kit.buttons) {
6680
6780
  for (const name of BUTTON_VAR_NAMES) root.style.removeProperty(name);
6681
6781
  for (const [name, value] of Object.entries(buttonStyleVars({ ...kit, buttons: kit.buttons }))) root.style.setProperty(name, value);
@@ -7208,6 +7308,35 @@ function withThemeFonts(brand) {
7208
7308
  }
7209
7309
  };
7210
7310
  }
7311
+ function liveBrandColor(role, fallback) {
7312
+ if (role === "dark") {
7313
+ return `var(--brand-text, var(--ohw-brand-dark, var(--color-dark, ${fallback})))`;
7314
+ }
7315
+ if (role === "light") {
7316
+ return `var(--brand-background, var(--ohw-brand-light, var(--color-light, ${fallback})))`;
7317
+ }
7318
+ return `var(--brand-${role}, var(--ohw-brand-${role}, var(--color-${role}, ${fallback})))`;
7319
+ }
7320
+ function withLivePalette(brand, source) {
7321
+ const sourceOf = (color) => {
7322
+ const value = color.trim().toLowerCase();
7323
+ const entries = Object.entries(source.palette);
7324
+ return entries.find(([, hex]) => hex.trim().toLowerCase() === value)?.[0] ?? null;
7325
+ };
7326
+ const map = (color) => {
7327
+ const role = sourceOf(color);
7328
+ return role ? liveBrandColor(role, source.palette[role]) : color;
7329
+ };
7330
+ return {
7331
+ ...brand,
7332
+ palette: {
7333
+ dark: map(brand.palette.dark),
7334
+ primary: map(brand.palette.primary),
7335
+ accent: map(brand.palette.accent),
7336
+ light: map(brand.palette.light)
7337
+ }
7338
+ };
7339
+ }
7211
7340
  function footerLogoStyle(fallbackFont, fallbackColor) {
7212
7341
  return {
7213
7342
  fontFamily: `var(--brand-font-heading, var(--font-heading, var(--font-display, ${quotedFont(fallbackFont)})))`,
@@ -7235,6 +7364,7 @@ var blockAlignStyle = (blockAlign) => blockAlign ? {
7235
7364
  alignItems: blockAlign === "left" ? "flex-start" : blockAlign === "right" ? "flex-end" : "center",
7236
7365
  textAlign: blockAlign
7237
7366
  } : {};
7367
+ var fullBleedBlock = (block) => block.type === "carousel" || block.type === "group" && str(block.slots?.layout) === "overlap";
7238
7368
  var featureLines = (value) => value.split(/\n|<br\s*\/?>/i).map((line) => line.trim()).filter(Boolean);
7239
7369
  var AI_MOBILE_CSS = [
7240
7370
  // Unscoped (applies at every viewport, not just mobile): re-enables pointer-events for the
@@ -7425,7 +7555,7 @@ function hexContrast(a, b) {
7425
7555
  function primaryButtonLabel(brand) {
7426
7556
  const darkC = hexContrast(brand.palette.primary, brand.palette.dark);
7427
7557
  const lightC = hexContrast(brand.palette.primary, AI_TREE_TOKENS.textPrimaryForeground);
7428
- if (darkC === null || lightC === null) return AI_TREE_TOKENS.textPrimaryForeground;
7558
+ if (darkC === null || lightC === null) return `var(--brand-on-primary, ${AI_TREE_TOKENS.textPrimaryForeground})`;
7429
7559
  return darkC > lightC ? brand.palette.dark : AI_TREE_TOKENS.textPrimaryForeground;
7430
7560
  }
7431
7561
  function accentBandContext(brand) {
@@ -7449,7 +7579,11 @@ function readableForeground(bg, brand) {
7449
7579
  return darkC > lightC ? brand.palette.dark : AI_TREE_TOKENS.textPrimaryForeground;
7450
7580
  }
7451
7581
  function hexWithAlpha(hex, alpha) {
7452
- const m = /^#([0-9a-f]{6})$/i.exec(hex.trim());
7582
+ const value = hex.trim();
7583
+ if (value.startsWith("var(") || value.startsWith("color-mix(")) {
7584
+ return `color-mix(in srgb, ${value} ${Math.round(alpha * 100)}%, transparent)`;
7585
+ }
7586
+ const m = /^#([0-9a-f]{6})$/i.exec(value);
7453
7587
  if (!m) return hex;
7454
7588
  const r2 = parseInt(m[1].slice(0, 2), 16);
7455
7589
  const g = parseInt(m[1].slice(2, 4), 16);
@@ -7722,12 +7856,10 @@ function ButtonEl({
7722
7856
  cursor: "pointer",
7723
7857
  whiteSpace: "nowrap",
7724
7858
  ...buttonShellStyle(ctx, variant, fullWidth),
7725
- ...tokenColorStyle ?? (secondary ? { border: `1px solid ${secondaryColor ?? ctx.brand.palette.dark}`, color: secondaryColor ?? ctx.brand.palette.dark } : (
7726
- // Off-band, prefer the template button's measured fill/label pair — a template may
7727
- // fill its CTAs with any token (hvac: accent bg, primary text). On an accent band
7728
- // the flipped palette keeps contrast, so the brand-driven colours stay.
7729
- !ctx.buttonLabel && ctx.buttonStyle?.background ? { background: ctx.buttonStyle.background, color: ctx.buttonStyle.color } : { background: ctx.brand.palette.primary, color: ctx.buttonLabel ?? primaryButtonLabel(ctx.brand) }
7730
- ))
7859
+ ...tokenColorStyle ?? (secondary ? { border: `1px solid ${secondaryColor ?? ctx.brand.palette.dark}`, color: secondaryColor ?? ctx.brand.palette.dark } : {
7860
+ background: ctx.brand.palette.primary,
7861
+ color: ctx.buttonLabel ?? primaryButtonLabel(ctx.brand)
7862
+ })
7731
7863
  },
7732
7864
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { ...path ? textAttrs(ctx, `${path}.label`) : {}, children: str(slots.label) })
7733
7865
  }
@@ -9298,8 +9430,8 @@ function renderNode(node, ctx, path, ambientAlign) {
9298
9430
  // layer (this group's i===0 sibling) can span edge-to-edge — reintroduced here,
9299
9431
  // scoped to just the overlay text, so a heading/copy/CTA stack doesn't stretch
9300
9432
  // the full section width instead of reading at the usual content width.
9301
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { maxWidth: AI_TREE_TOKENS.sectionMaxWidth, margin: "0 auto", padding: "0 40px", width: "100%", boxSizing: "border-box" }, children: renderNode(child, ctx, `${path}.c${i}`, child.align ?? "center") })
9302
- ) : renderNode(child, ctx, `${path}.c${i}`, i > 0 ? child.align ?? "center" : void 0),
9433
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { maxWidth: AI_TREE_TOKENS.sectionMaxWidth, margin: "0 auto", padding: "0 40px", width: "100%", boxSizing: "border-box" }, children: renderNode(child, ctx, `${path}.c${i}`, child.align ?? node.align ?? "center") })
9434
+ ) : renderNode(child, ctx, `${path}.c${i}`, i > 0 ? child.align ?? node.align ?? "center" : void 0),
9303
9435
  i === 0 && needsBaseScrim && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
9304
9436
  "div",
9305
9437
  {
@@ -9332,7 +9464,7 @@ function renderNode(node, ctx, path, ambientAlign) {
9332
9464
  // child otherwise stretches full-width (unchanged pre-existing behaviour).
9333
9465
  ...blockAlignStyle(child.align ?? ambientAlign)
9334
9466
  },
9335
- children: renderNode(child, ctx, `${path}.c${i}`, ambientAlign)
9467
+ children: renderNode(child, ctx, `${path}.c${i}`, node.align ?? ambientAlign)
9336
9468
  },
9337
9469
  i
9338
9470
  ));
@@ -9362,8 +9494,20 @@ function renderNode(node, ctx, path, ambientAlign) {
9362
9494
  }
9363
9495
  case "button":
9364
9496
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ButtonEl, { slots, ctx, path });
9365
- case "button-row":
9366
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { display: "flex", gap: AI_TREE_TOKENS.spacing6, justifyContent: "center" }, children: (node.children ?? []).map((child, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ButtonEl, { slots: child.slots ?? {}, ctx, path: `${path}.c${i}` }, i)) });
9497
+ case "button-row": {
9498
+ const rowAlign = node.align ?? ambientAlign;
9499
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
9500
+ "div",
9501
+ {
9502
+ style: {
9503
+ display: "flex",
9504
+ gap: AI_TREE_TOKENS.spacing6,
9505
+ justifyContent: rowAlign === "left" ? "flex-start" : rowAlign === "right" ? "flex-end" : "center"
9506
+ },
9507
+ children: (node.children ?? []).map((child, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ButtonEl, { slots: child.slots ?? {}, ctx, path: `${path}.c${i}` }, i))
9508
+ }
9509
+ );
9510
+ }
9367
9511
  case "media":
9368
9512
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
9369
9513
  MediaBox,
@@ -9572,10 +9716,10 @@ function renderNode(node, ctx, path, ambientAlign) {
9572
9716
  ...buttonShellStyle(ctx, "primary"),
9573
9717
  // Brand-styled: the template button's measured fill/label pair off-band, else
9574
9718
  // primary fill with a brand-derived label so it reads on custom palettes.
9575
- ...buttonColorStyle(ctx, "primary") ?? (!ctx.buttonLabel && ctx.buttonStyle?.background ? { background: ctx.buttonStyle.background, color: ctx.buttonStyle.color } : {
9719
+ ...buttonColorStyle(ctx, "primary") ?? {
9576
9720
  background: ctx.brand.palette.primary,
9577
9721
  color: ctx.buttonLabel ?? primaryButtonLabel(ctx.brand)
9578
- })
9722
+ }
9579
9723
  },
9580
9724
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { ...textAttrs(ctx, `${path}.c${i}.label`), children: str(cs.label) })
9581
9725
  },
@@ -9619,7 +9763,7 @@ function AiTreeRenderer({
9619
9763
  }
9620
9764
  const resolvedBrand = withThemeFonts(brand ?? AI_DEFAULT_BRAND);
9621
9765
  const band = (tree.settings ?? {}).sectionBackground === "accent" ? accentBandContext(resolvedBrand) : null;
9622
- const blockBrand = band?.brand ?? resolvedBrand;
9766
+ const blockBrand = withLivePalette(band?.brand ?? resolvedBrand, resolvedBrand);
9623
9767
  const placeholderMap = buildPlaceholderMap(tree);
9624
9768
  const hasOverlapBackground = tree.scaffold === "overlay" && tree.rows.some(
9625
9769
  (row) => (row.blocks ?? []).some((b) => b.type === "group" && str(b.slots?.layout) === "overlap")
@@ -9635,7 +9779,7 @@ function AiTreeRenderer({
9635
9779
  // standalone resolve), so generated galleries, image rows, and overlay backgrounds arrive with
9636
9780
  // photos instead of grey boxes.
9637
9781
  resolveMedia: (ref) => resolveMedia?.(ref) ?? placeholderMap[ref] ?? resolvePlaceholderRef(ref),
9638
- cardSurface: blockBrand.palette.light.toUpperCase() === AI_DEFAULT_BRAND.palette.light.toUpperCase() ? "#ECECEB" : `color-mix(in srgb, ${blockBrand.palette.light} 90%, ${blockBrand.palette.dark})`,
9782
+ cardSurface: resolvedBrand.palette.light.toUpperCase() === AI_DEFAULT_BRAND.palette.light.toUpperCase() ? "#ECECEB" : `color-mix(in srgb, ${blockBrand.palette.light} 90%, ${blockBrand.palette.dark})`,
9639
9783
  keyFor: editKeyPrefix ? (path) => `${editKeyPrefix}.${path}` : null,
9640
9784
  sectionAlignment: (tree.settings ?? {}).alignment === "center" ? "center" : "left",
9641
9785
  buttonStyle,
@@ -9652,7 +9796,7 @@ function AiTreeRenderer({
9652
9796
  const backgroundUrl = isOverlay && settings.backgroundMedia ? ctx.resolveMedia(settings.backgroundMedia) : null;
9653
9797
  const bgAttrs = ctx.keyFor && isOverlay && settings.backgroundMedia ? { "data-ohw-key": ctx.keyFor("settings.backgroundMedia"), "data-ohw-editable": "bg-image" } : {};
9654
9798
  const toneBackground = (() => {
9655
- const { dark, primary, light } = resolvedBrand.palette;
9799
+ const { dark, primary, light } = blockBrand.palette;
9656
9800
  switch (settings.sectionBackground) {
9657
9801
  case "surface":
9658
9802
  return `color-mix(in srgb, ${light} 94%, ${dark})`;
@@ -9742,7 +9886,7 @@ function AiTreeRenderer({
9742
9886
  // its own horizontal `align`, which takes priority for that block.
9743
9887
  ...fillHeight ? { display: "grid" } : {},
9744
9888
  // Horizontal placement of the block's content within its column.
9745
- ...cellAlignStyle(block.align),
9889
+ ...fullBleedBlock(block) ? {} : cellAlignStyle(block.align),
9746
9890
  // space-between: each column becomes a flex column whose content spreads over
9747
9891
  // the full row height instead of clumping at the top.
9748
9892
  ...distributed === "space-between" ? { display: "flex", flexDirection: "column", justifyContent: "space-between" } : {}
@@ -9773,14 +9917,15 @@ function AiFooterRenderer({
9773
9917
  if (!isRenderableFooterTree(tree)) {
9774
9918
  return null;
9775
9919
  }
9776
- const resolvedBrand = withThemeFonts(brand ?? AI_DEFAULT_BRAND);
9920
+ const hexBrand = withThemeFonts(brand ?? AI_DEFAULT_BRAND);
9921
+ const resolvedBrand = withLivePalette(hexBrand, brand ?? AI_DEFAULT_BRAND);
9777
9922
  const bandBg = surface?.background ?? resolvedBrand.palette.dark;
9778
9923
  const bandText = surface?.text ?? resolvedBrand.palette.light;
9779
9924
  const onDark = surface ? isDarkColor(surface.background) : true;
9780
9925
  const ctx = {
9781
9926
  brand: resolvedBrand,
9782
9927
  resolveMedia: (ref) => resolveMedia?.(ref) ?? resolvePlaceholderRef(ref),
9783
- cardSurface: resolvedBrand.palette.light.toUpperCase() === AI_DEFAULT_BRAND.palette.light.toUpperCase() ? "#ECECEB" : `color-mix(in srgb, ${resolvedBrand.palette.light} 90%, ${resolvedBrand.palette.dark})`,
9928
+ cardSurface: hexBrand.palette.light.toUpperCase() === AI_DEFAULT_BRAND.palette.light.toUpperCase() ? "#ECECEB" : `color-mix(in srgb, ${resolvedBrand.palette.light} 90%, ${resolvedBrand.palette.dark})`,
9784
9929
  keyFor: editKeyPrefix ? (path) => `${editKeyPrefix}.${path}` : null,
9785
9930
  logoUrl: logoUrl ?? null,
9786
9931
  resolvePageHref
@@ -10356,10 +10501,9 @@ function syncSoftRemovedGenerated() {
10356
10501
  }
10357
10502
  function applyAiSectionsToDom(state, options) {
10358
10503
  if (typeof document === "undefined") return;
10359
- const brandOverride = deriveBrandOverride();
10360
10504
  const templateBrand = deriveTemplateBrand();
10361
10505
  const templateButtonStyle = deriveTemplateButtonStyle();
10362
- const brandKey = brandOverride ? JSON.stringify(brandOverride) : "";
10506
+ const brandKey = JSON.stringify(getAppliedBrand() ?? deriveBrandOverride() ?? null);
10363
10507
  const pagePath = window.location.pathname;
10364
10508
  const pageSections = state.sections.filter((entry) => (entry.path ?? "/") === pagePath);
10365
10509
  const activeIds = new Set(pageSections.map((entry) => entry.id));
@@ -10402,7 +10546,7 @@ function applyAiSectionsToDom(state, options) {
10402
10546
  AiTreeRenderer,
10403
10547
  {
10404
10548
  tree: entry.tree,
10405
- brand: brandOverride ?? entry.brand ?? options?.brand ?? templateBrand ?? void 0,
10549
+ brand: brandForGenerated(deriveBrandOverride() ?? entry.brand ?? options?.brand ?? templateBrand),
10406
10550
  buttonStyle: templateButtonStyle,
10407
10551
  resolveMedia,
10408
10552
  editKeyPrefix: `ai.${entry.id}`
@@ -10568,7 +10712,7 @@ function applyAiFooterToDom(state, content) {
10568
10712
  const templateFooter = findTemplateFooter();
10569
10713
  captureTemplateFooterSurface(templateFooter);
10570
10714
  const surface = capturedSurface ?? null;
10571
- const brand = mountOptions.brand ?? deriveTemplateBrand();
10715
+ const brand = brandForGenerated(mountOptions.brand ?? deriveTemplateBrand());
10572
10716
  const logoUrl = resolveLogoFromContent(content, templateFooter);
10573
10717
  const serialized = JSON.stringify(state) + JSON.stringify(brand ?? null) + (logoUrl ?? "") + JSON.stringify(surface) + JSON.stringify(defaultLegal());
10574
10718
  if (mounted2 && mounted2.serialized === serialized && mounted2.container.isConnected) {
@@ -18376,10 +18520,23 @@ function setPricingCardStyle(tree, index, style) {
18376
18520
  });
18377
18521
  }
18378
18522
  function setPricingCardFeaturedTag(tree, index, on) {
18379
- return withPricingCard(tree, index, (target) => {
18523
+ return withPricingCollection(tree, (collection) => {
18524
+ const children = collection.children;
18525
+ if (index < 0 || index >= children.length) return false;
18526
+ if (on) {
18527
+ children.forEach((card, i) => {
18528
+ if (i === index) return;
18529
+ const prev2 = featuredOf(card);
18530
+ if (prev2.enabled !== true || badgeOf(prev2).length === 0) return;
18531
+ card.slots = card.slots ?? {};
18532
+ card.slots.featured = composeFeatured(pricingCardStyleOf(prev2), false, badgeOf(prev2));
18533
+ });
18534
+ }
18535
+ const target = children[index];
18536
+ target.slots = target.slots ?? {};
18380
18537
  const prev = featuredOf(target);
18381
- const style = pricingCardStyleOf(prev);
18382
- target.slots.featured = composeFeatured(style, on, badgeOf(prev));
18538
+ target.slots.featured = composeFeatured(pricingCardStyleOf(prev), on, badgeOf(prev));
18539
+ return true;
18383
18540
  });
18384
18541
  }
18385
18542
  function pricingCardStyleOf(featured) {
@@ -18629,25 +18786,40 @@ function ToolbarSeparator() {
18629
18786
  }
18630
18787
  function CardStyleMenu({
18631
18788
  value,
18632
- onChange
18789
+ onChange,
18790
+ disabled = false
18633
18791
  }) {
18634
18792
  const current = PRICING_CARD_STYLES.find((s) => s.value === value) ?? PRICING_CARD_STYLES[0];
18793
+ const trigger = /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
18794
+ "button",
18795
+ {
18796
+ type: "button",
18797
+ "aria-label": "Card style",
18798
+ disabled,
18799
+ className: cn(
18800
+ "inline-flex h-8 min-w-[110px] items-center gap-[7px] rounded-[6px] bg-transparent px-2 text-[13px] font-medium text-foreground transition-colors hover:bg-muted data-[state=open]:bg-muted",
18801
+ disabled && "cursor-not-allowed opacity-40 hover:bg-transparent"
18802
+ ),
18803
+ children: [
18804
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(StyleSwatch, { style: current.value }),
18805
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "flex-1 text-left", children: current.label }),
18806
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_lucide_react9.ChevronDown, { size: 16, style: { pointerEvents: "none" } })
18807
+ ]
18808
+ }
18809
+ );
18810
+ if (disabled) {
18811
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "flex items-center gap-1.5 pl-1.5", children: [
18812
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "text-xs text-muted-foreground opacity-50", children: "Card style" }),
18813
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(Tooltip, { children: [
18814
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "inline-flex", children: trigger }) }),
18815
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(TooltipContent, { side: "bottom", sideOffset: 8, children: "Mark as featured to edit card style" })
18816
+ ] })
18817
+ ] });
18818
+ }
18635
18819
  return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "flex items-center gap-1.5 pl-1.5", children: [
18636
18820
  /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "text-xs text-muted-foreground", children: "Card style" }),
18637
18821
  /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(DropdownMenu, { modal: false, children: [
18638
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
18639
- "button",
18640
- {
18641
- type: "button",
18642
- "aria-label": "Card style",
18643
- className: "inline-flex h-8 min-w-[110px] items-center gap-[7px] rounded-[6px] bg-transparent px-2 text-[13px] font-medium text-foreground transition-colors hover:bg-muted data-[state=open]:bg-muted",
18644
- children: [
18645
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(StyleSwatch, { style: current.value }),
18646
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "flex-1 text-left", children: current.label }),
18647
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_lucide_react9.ChevronDown, { size: 16, style: { pointerEvents: "none" } })
18648
- ]
18649
- }
18650
- ) }),
18822
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(DropdownMenuTrigger, { asChild: true, children: trigger }),
18651
18823
  /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(DropdownMenuContent, { align: "end", sideOffset: 8, className: "w-[152px] min-w-[152px] p-1", children: PRICING_CARD_STYLES.map((style) => {
18652
18824
  const selected = style.value === value;
18653
18825
  return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
@@ -18711,23 +18883,35 @@ function PricingCardToolbar({
18711
18883
  mobile ? "flex-wrap justify-center" : "flex-nowrap"
18712
18884
  ),
18713
18885
  children: [
18714
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(CardStyleMenu, { value: style, onChange: onSetStyle }),
18715
18886
  /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(Tooltip, { children: [
18716
18887
  /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
18717
18888
  CustomToolbarButton,
18718
18889
  {
18719
18890
  type: "button",
18720
- "aria-label": featuredTag ? "Remove featured tag" : "Add featured tag",
18891
+ "aria-label": featuredTag ? "Unmark as featured" : "Mark as featured",
18721
18892
  "aria-pressed": featuredTag,
18722
- active: featuredTag,
18723
18893
  onClick: () => onSetFeaturedTag(!featuredTag),
18724
- className: "size-8",
18725
- children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_lucide_react9.Star, { size: 16, fill: featuredTag ? "currentColor" : "none", style: { pointerEvents: "none" } })
18894
+ className: "group size-8",
18895
+ children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
18896
+ import_lucide_react9.Star,
18897
+ {
18898
+ size: 16,
18899
+ fill: "currentColor",
18900
+ strokeWidth: 0,
18901
+ className: cn(
18902
+ "transition-colors",
18903
+ featuredTag ? "text-[#f59e0b] group-hover:text-[#d97706]" : "text-[#d6d3d1] group-hover:text-[#fbbf24]"
18904
+ ),
18905
+ style: { pointerEvents: "none" }
18906
+ }
18907
+ )
18726
18908
  }
18727
18909
  ) }),
18728
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(TooltipContent, { side: "bottom", sideOffset: 8, children: featuredTag ? "Remove featured tag" : "Add featured tag" })
18910
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(TooltipContent, { side: "bottom", sideOffset: 8, children: featuredTag ? "Unmark featured" : "Mark as featured" })
18729
18911
  ] }),
18730
18912
  /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(ToolbarSeparator, {}),
18913
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(CardStyleMenu, { value: style, onChange: onSetStyle, disabled: !featuredTag }),
18914
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(ToolbarSeparator, {}),
18731
18915
  /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(ToolbarAction, { label: "Add button", onClick: onAddButton, disabled: buttonCount >= 2, children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(ButtonGlyph, {}) }),
18732
18916
  /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(ToolbarAction, { label: "Remove button", onClick: onRemoveButton, disabled: buttonCount === 0, children: /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("span", { className: "relative inline-flex", children: [
18733
18917
  /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(ButtonGlyph, {}),
@@ -18870,7 +19054,12 @@ function AiPricingCardOverlay({
18870
19054
  index: active.index,
18871
19055
  pending,
18872
19056
  error,
18873
- onAddTier: () => runMutation(active.sectionId, addPricingTier),
19057
+ onAddTier: () => {
19058
+ const newIndex = pricingCardCount(activeEntry.tree);
19059
+ void runMutation(active.sectionId, addPricingTier).then((applied) => {
19060
+ if (applied) setActive({ sectionId: active.sectionId, index: newIndex });
19061
+ });
19062
+ },
18874
19063
  onRemoveTier: () => {
18875
19064
  void runMutation(active.sectionId, (tree) => removePricingTier(tree, active.index)).then(
18876
19065
  (applied) => {
@@ -27281,7 +27470,7 @@ function OhhwellsBridge() {
27281
27470
  if (!subdomain) return {};
27282
27471
  return contentCache.get(subdomain) ?? {};
27283
27472
  };
27284
- const observeRoots = () => [document.querySelector("nav"), document.querySelector("footer")].filter(
27473
+ const observeRoots = () => [document.querySelector("nav"), getFooterRoot()].filter(
27285
27474
  (el) => Boolean(el)
27286
27475
  );
27287
27476
  const roots = observeRoots();
@@ -27752,7 +27941,7 @@ function OhhwellsBridge() {
27752
27941
  {
27753
27942
  const formEl = getFormElement(target);
27754
27943
  const onSuccessText = target.closest(`[${SUCCESS_TEXT_ATTR}]`);
27755
- if (formEl && formKeyOf(formEl) && !onSuccessText) {
27944
+ if (formEl && formKeyOf(formEl) && !onSuccessText && !isCtaButton(target)) {
27756
27945
  const fieldEl = getFieldWrapper(target);
27757
27946
  if (fieldEl) {
27758
27947
  const label = target.closest("label");
@@ -29645,6 +29834,7 @@ function OhhwellsBridge() {
29645
29834
  brandKitRef.current = value;
29646
29835
  applyBrandToDom(parseBrandKit(value));
29647
29836
  if (aiSectionsRef.current) applyAiSectionsToDom(parseAiSectionsState(aiSectionsRef.current));
29837
+ applyAiFooterFromContent(editContentRef.current);
29648
29838
  applyStylesToDom(parseStyleStore(stylesRef.current));
29649
29839
  postToParentRef.current({ type: "ow:change", nodes: [{ key: BRAND_KIT_KEY, text: value }] });
29650
29840
  postToParentRef.current({ type: "ow:ai-brand-applied", previous, value });
@@ -30276,9 +30466,10 @@ function OhhwellsBridge() {
30276
30466
  if (e.data?.type !== "ow:collect-section") return;
30277
30467
  const sectionId = e.data.sectionId;
30278
30468
  if (typeof sectionId !== "string" || !sectionId) return;
30279
- const sectionEl = document.querySelector(
30469
+ const foundEl = document.querySelector(
30280
30470
  `[data-ohw-section="${CSS.escape(sectionId)}"]`
30281
30471
  );
30472
+ const sectionEl = foundEl && !foundEl.hasAttribute("data-ohw-ai-template-hidden") && !foundEl.hasAttribute("data-ohw-ai-removed") && !foundEl.hasAttribute("data-ohw-ai-replaced-by") ? foundEl : null;
30282
30473
  const nodes = sectionEl ? collectEditableNodes(void 0, sectionEl).filter(
30283
30474
  (n) => n.key && !n.key.startsWith("__ohw_") && !["meta", "sections", "video-setting"].includes(n.type)
30284
30475
  ).map((n) => ({ ...n, text: n.text ?? "", type: normalizeAiNodeType(n.type) })) : [];
@@ -31017,7 +31208,7 @@ function OhhwellsBridge() {
31017
31208
  postToParent2({
31018
31209
  type: "ow:ready",
31019
31210
  version: "1",
31020
- bridgeVersion: "0.1.138",
31211
+ bridgeVersion: "0.1.140",
31021
31212
  path: pathname,
31022
31213
  nodes: collectEditableNodes(editContentRef.current),
31023
31214
  sections
@@ -32233,6 +32424,7 @@ function EmptySection({ title, homeHref = "/", eyebrowKey, titleKey, subtitleKey
32233
32424
  dropIndicatorVariants,
32234
32425
  ensureFooterTree,
32235
32426
  filterAvailablePages,
32427
+ foldAlignIntoTrees,
32236
32428
  getEditModeInitialState,
32237
32429
  insertFooterHours,
32238
32430
  isEditSessionActive,
@@ -32241,6 +32433,7 @@ function EmptySection({ title, homeHref = "/", eyebrowKey, titleKey, subtitleKey
32241
32433
  isValidUrl,
32242
32434
  nextViableSlide,
32243
32435
  parseAiFooterState,
32436
+ parseAiSectionsState,
32244
32437
  parseTarget,
32245
32438
  removeFooterBlock,
32246
32439
  resolveFooterCopyright,