@ohhwells/bridge 0.1.139-next.373 → 0.1.139-next.375

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
@@ -7427,7 +7557,7 @@ function hexContrast(a, b) {
7427
7557
  function primaryButtonLabel(brand) {
7428
7558
  const darkC = hexContrast(brand.palette.primary, brand.palette.dark);
7429
7559
  const lightC = hexContrast(brand.palette.primary, AI_TREE_TOKENS.textPrimaryForeground);
7430
- if (darkC === null || lightC === null) return AI_TREE_TOKENS.textPrimaryForeground;
7560
+ if (darkC === null || lightC === null) return `var(--brand-on-primary, ${AI_TREE_TOKENS.textPrimaryForeground})`;
7431
7561
  return darkC > lightC ? brand.palette.dark : AI_TREE_TOKENS.textPrimaryForeground;
7432
7562
  }
7433
7563
  function accentBandContext(brand) {
@@ -7451,7 +7581,11 @@ function readableForeground(bg, brand) {
7451
7581
  return darkC > lightC ? brand.palette.dark : AI_TREE_TOKENS.textPrimaryForeground;
7452
7582
  }
7453
7583
  function hexWithAlpha(hex, alpha) {
7454
- const m = /^#([0-9a-f]{6})$/i.exec(hex.trim());
7584
+ const value = hex.trim();
7585
+ if (value.startsWith("var(") || value.startsWith("color-mix(")) {
7586
+ return `color-mix(in srgb, ${value} ${Math.round(alpha * 100)}%, transparent)`;
7587
+ }
7588
+ const m = /^#([0-9a-f]{6})$/i.exec(value);
7455
7589
  if (!m) return hex;
7456
7590
  const r2 = parseInt(m[1].slice(0, 2), 16);
7457
7591
  const g = parseInt(m[1].slice(2, 4), 16);
@@ -7750,12 +7884,10 @@ function ButtonEl({
7750
7884
  cursor: "pointer",
7751
7885
  whiteSpace: "nowrap",
7752
7886
  ...buttonShellStyle(ctx, variant, fullWidth),
7753
- ...tokenColorStyle ?? (secondary ? { border: `1px solid ${secondaryColor ?? ctx.brand.palette.dark}`, color: secondaryColor ?? ctx.brand.palette.dark } : (
7754
- // Off-band, prefer the template button's measured fill/label pair — a template may
7755
- // fill its CTAs with any token (hvac: accent bg, primary text). On an accent band
7756
- // the flipped palette keeps contrast, so the brand-driven colours stay.
7757
- !ctx.buttonLabel && ctx.buttonStyle?.background ? { background: ctx.buttonStyle.background, color: ctx.buttonStyle.color } : { background: ctx.brand.palette.primary, color: ctx.buttonLabel ?? primaryButtonLabel(ctx.brand) }
7758
- ))
7887
+ ...tokenColorStyle ?? (secondary ? { border: `1px solid ${secondaryColor ?? ctx.brand.palette.dark}`, color: secondaryColor ?? ctx.brand.palette.dark } : {
7888
+ background: ctx.brand.palette.primary,
7889
+ color: ctx.buttonLabel ?? primaryButtonLabel(ctx.brand)
7890
+ })
7759
7891
  },
7760
7892
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { ...path ? textAttrs(ctx, `${path}.label`) : {}, children: str(slots.label) })
7761
7893
  }
@@ -9326,8 +9458,8 @@ function renderNode(node, ctx, path, ambientAlign) {
9326
9458
  // layer (this group's i===0 sibling) can span edge-to-edge — reintroduced here,
9327
9459
  // scoped to just the overlay text, so a heading/copy/CTA stack doesn't stretch
9328
9460
  // the full section width instead of reading at the usual content width.
9329
- /* @__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") })
9330
- ) : renderNode(child, ctx, `${path}.c${i}`, i > 0 ? child.align ?? "center" : void 0),
9461
+ /* @__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") })
9462
+ ) : renderNode(child, ctx, `${path}.c${i}`, i > 0 ? child.align ?? node.align ?? "center" : void 0),
9331
9463
  i === 0 && needsBaseScrim && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
9332
9464
  "div",
9333
9465
  {
@@ -9360,7 +9492,7 @@ function renderNode(node, ctx, path, ambientAlign) {
9360
9492
  // child otherwise stretches full-width (unchanged pre-existing behaviour).
9361
9493
  ...blockAlignStyle(child.align ?? ambientAlign)
9362
9494
  },
9363
- children: renderNode(child, ctx, `${path}.c${i}`, ambientAlign)
9495
+ children: renderNode(child, ctx, `${path}.c${i}`, node.align ?? ambientAlign)
9364
9496
  },
9365
9497
  i
9366
9498
  ));
@@ -9390,8 +9522,20 @@ function renderNode(node, ctx, path, ambientAlign) {
9390
9522
  }
9391
9523
  case "button":
9392
9524
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ButtonEl, { slots, ctx, path });
9393
- case "button-row":
9394
- 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)) });
9525
+ case "button-row": {
9526
+ const rowAlign = node.align ?? ambientAlign;
9527
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
9528
+ "div",
9529
+ {
9530
+ style: {
9531
+ display: "flex",
9532
+ gap: AI_TREE_TOKENS.spacing6,
9533
+ justifyContent: rowAlign === "left" ? "flex-start" : rowAlign === "right" ? "flex-end" : "center"
9534
+ },
9535
+ children: (node.children ?? []).map((child, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ButtonEl, { slots: child.slots ?? {}, ctx, path: `${path}.c${i}` }, i))
9536
+ }
9537
+ );
9538
+ }
9395
9539
  case "media":
9396
9540
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
9397
9541
  MediaBox,
@@ -9600,10 +9744,10 @@ function renderNode(node, ctx, path, ambientAlign) {
9600
9744
  ...buttonShellStyle(ctx, "primary"),
9601
9745
  // Brand-styled: the template button's measured fill/label pair off-band, else
9602
9746
  // primary fill with a brand-derived label so it reads on custom palettes.
9603
- ...buttonColorStyle(ctx, "primary") ?? (!ctx.buttonLabel && ctx.buttonStyle?.background ? { background: ctx.buttonStyle.background, color: ctx.buttonStyle.color } : {
9747
+ ...buttonColorStyle(ctx, "primary") ?? {
9604
9748
  background: ctx.brand.palette.primary,
9605
9749
  color: ctx.buttonLabel ?? primaryButtonLabel(ctx.brand)
9606
- })
9750
+ }
9607
9751
  },
9608
9752
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { ...textAttrs(ctx, `${path}.c${i}.label`), children: str(cs.label) })
9609
9753
  },
@@ -9647,7 +9791,7 @@ function AiTreeRenderer({
9647
9791
  }
9648
9792
  const resolvedBrand = withThemeFonts(brand ?? AI_DEFAULT_BRAND);
9649
9793
  const band = (tree.settings ?? {}).sectionBackground === "accent" ? accentBandContext(resolvedBrand) : null;
9650
- const blockBrand = band?.brand ?? resolvedBrand;
9794
+ const blockBrand = withLivePalette(band?.brand ?? resolvedBrand, resolvedBrand);
9651
9795
  const placeholderMap = buildPlaceholderMap(tree);
9652
9796
  const hasOverlapBackground = tree.scaffold === "overlay" && tree.rows.some(
9653
9797
  (row) => (row.blocks ?? []).some((b) => b.type === "group" && str(b.slots?.layout) === "overlap")
@@ -9663,7 +9807,7 @@ function AiTreeRenderer({
9663
9807
  // standalone resolve), so generated galleries, image rows, and overlay backgrounds arrive with
9664
9808
  // photos instead of grey boxes.
9665
9809
  resolveMedia: (ref) => resolveMedia?.(ref) ?? placeholderMap[ref] ?? resolvePlaceholderRef(ref),
9666
- cardSurface: blockBrand.palette.light.toUpperCase() === AI_DEFAULT_BRAND.palette.light.toUpperCase() ? "#ECECEB" : `color-mix(in srgb, ${blockBrand.palette.light} 90%, ${blockBrand.palette.dark})`,
9810
+ cardSurface: resolvedBrand.palette.light.toUpperCase() === AI_DEFAULT_BRAND.palette.light.toUpperCase() ? "#ECECEB" : `color-mix(in srgb, ${blockBrand.palette.light} 90%, ${blockBrand.palette.dark})`,
9667
9811
  keyFor: editKeyPrefix ? (path) => `${editKeyPrefix}.${path}` : null,
9668
9812
  sectionAlignment: (tree.settings ?? {}).alignment === "center" ? "center" : "left",
9669
9813
  buttonStyle,
@@ -9680,7 +9824,7 @@ function AiTreeRenderer({
9680
9824
  const backgroundUrl = isOverlay && settings.backgroundMedia ? ctx.resolveMedia(settings.backgroundMedia) : null;
9681
9825
  const bgAttrs = ctx.keyFor && isOverlay && settings.backgroundMedia ? { "data-ohw-key": ctx.keyFor("settings.backgroundMedia"), "data-ohw-editable": "bg-image" } : {};
9682
9826
  const toneBackground = (() => {
9683
- const { dark, primary, light } = resolvedBrand.palette;
9827
+ const { dark, primary, light } = blockBrand.palette;
9684
9828
  switch (settings.sectionBackground) {
9685
9829
  case "surface":
9686
9830
  return `color-mix(in srgb, ${light} 94%, ${dark})`;
@@ -9772,7 +9916,7 @@ function AiTreeRenderer({
9772
9916
  // its own horizontal `align`, which takes priority for that block.
9773
9917
  ...fillHeight ? { display: "grid" } : {},
9774
9918
  // Horizontal placement of the block's content within its column.
9775
- ...cellAlignStyle(block.align),
9919
+ ...fullBleedBlock(block) ? {} : cellAlignStyle(block.align),
9776
9920
  // space-between: each column becomes a flex column whose content spreads over
9777
9921
  // the full row height instead of clumping at the top.
9778
9922
  ...distributed === "space-between" ? { display: "flex", flexDirection: "column", justifyContent: "space-between" } : {}
@@ -9803,14 +9947,15 @@ function AiFooterRenderer({
9803
9947
  if (!isRenderableFooterTree(tree)) {
9804
9948
  return null;
9805
9949
  }
9806
- const resolvedBrand = withThemeFonts(brand ?? AI_DEFAULT_BRAND);
9950
+ const hexBrand = withThemeFonts(brand ?? AI_DEFAULT_BRAND);
9951
+ const resolvedBrand = withLivePalette(hexBrand, brand ?? AI_DEFAULT_BRAND);
9807
9952
  const bandBg = surface?.background ?? resolvedBrand.palette.dark;
9808
9953
  const bandText = surface?.text ?? resolvedBrand.palette.light;
9809
9954
  const onDark = surface ? isDarkColor(surface.background) : true;
9810
9955
  const ctx = {
9811
9956
  brand: resolvedBrand,
9812
9957
  resolveMedia: (ref) => resolveMedia?.(ref) ?? resolvePlaceholderRef(ref),
9813
- cardSurface: resolvedBrand.palette.light.toUpperCase() === AI_DEFAULT_BRAND.palette.light.toUpperCase() ? "#ECECEB" : `color-mix(in srgb, ${resolvedBrand.palette.light} 90%, ${resolvedBrand.palette.dark})`,
9958
+ cardSurface: hexBrand.palette.light.toUpperCase() === AI_DEFAULT_BRAND.palette.light.toUpperCase() ? "#ECECEB" : `color-mix(in srgb, ${resolvedBrand.palette.light} 90%, ${resolvedBrand.palette.dark})`,
9814
9959
  keyFor: editKeyPrefix ? (path) => `${editKeyPrefix}.${path}` : null,
9815
9960
  logoUrl: logoUrl ?? null,
9816
9961
  resolvePageHref
@@ -10386,10 +10531,9 @@ function syncSoftRemovedGenerated() {
10386
10531
  }
10387
10532
  function applyAiSectionsToDom(state, options) {
10388
10533
  if (typeof document === "undefined") return;
10389
- const brandOverride = deriveBrandOverride();
10390
10534
  const templateBrand = deriveTemplateBrand();
10391
10535
  const templateButtonStyle = deriveTemplateButtonStyle();
10392
- const brandKey = brandOverride ? JSON.stringify(brandOverride) : "";
10536
+ const brandKey = JSON.stringify(getAppliedBrand() ?? deriveBrandOverride() ?? null);
10393
10537
  const pagePath = window.location.pathname;
10394
10538
  const pageSections = state.sections.filter((entry) => (entry.path ?? "/") === pagePath);
10395
10539
  const activeIds = new Set(pageSections.map((entry) => entry.id));
@@ -10433,7 +10577,7 @@ function applyAiSectionsToDom(state, options) {
10433
10577
  AiTreeRenderer,
10434
10578
  {
10435
10579
  tree: entry.tree,
10436
- brand: brandOverride ?? entry.brand ?? options?.brand ?? templateBrand ?? void 0,
10580
+ brand: brandForGenerated(deriveBrandOverride() ?? entry.brand ?? options?.brand ?? templateBrand),
10437
10581
  buttonStyle: templateButtonStyle,
10438
10582
  resolveMedia,
10439
10583
  editKeyPrefix: `ai.${entry.id}`
@@ -10599,7 +10743,7 @@ function applyAiFooterToDom(state, content) {
10599
10743
  const templateFooter = findTemplateFooter();
10600
10744
  captureTemplateFooterSurface(templateFooter);
10601
10745
  const surface = capturedSurface ?? null;
10602
- const brand = mountOptions.brand ?? deriveTemplateBrand();
10746
+ const brand = brandForGenerated(mountOptions.brand ?? deriveTemplateBrand());
10603
10747
  const logoUrl = resolveLogoFromContent(content, templateFooter);
10604
10748
  const serialized = JSON.stringify(state) + JSON.stringify(brand ?? null) + (logoUrl ?? "") + JSON.stringify(surface) + JSON.stringify(defaultLegal());
10605
10749
  if (mounted2 && mounted2.serialized === serialized && mounted2.container.isConnected) {
@@ -18407,10 +18551,23 @@ function setPricingCardStyle(tree, index, style) {
18407
18551
  });
18408
18552
  }
18409
18553
  function setPricingCardFeaturedTag(tree, index, on) {
18410
- return withPricingCard(tree, index, (target) => {
18554
+ return withPricingCollection(tree, (collection) => {
18555
+ const children = collection.children;
18556
+ if (index < 0 || index >= children.length) return false;
18557
+ if (on) {
18558
+ children.forEach((card, i) => {
18559
+ if (i === index) return;
18560
+ const prev2 = featuredOf(card);
18561
+ if (prev2.enabled !== true || badgeOf(prev2).length === 0) return;
18562
+ card.slots = card.slots ?? {};
18563
+ card.slots.featured = composeFeatured(pricingCardStyleOf(prev2), false, badgeOf(prev2));
18564
+ });
18565
+ }
18566
+ const target = children[index];
18567
+ target.slots = target.slots ?? {};
18411
18568
  const prev = featuredOf(target);
18412
- const style = pricingCardStyleOf(prev);
18413
- target.slots.featured = composeFeatured(style, on, badgeOf(prev));
18569
+ target.slots.featured = composeFeatured(pricingCardStyleOf(prev), on, badgeOf(prev));
18570
+ return true;
18414
18571
  });
18415
18572
  }
18416
18573
  function pricingCardStyleOf(featured) {
@@ -18660,25 +18817,40 @@ function ToolbarSeparator() {
18660
18817
  }
18661
18818
  function CardStyleMenu({
18662
18819
  value,
18663
- onChange
18820
+ onChange,
18821
+ disabled = false
18664
18822
  }) {
18665
18823
  const current = PRICING_CARD_STYLES.find((s) => s.value === value) ?? PRICING_CARD_STYLES[0];
18824
+ const trigger = /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
18825
+ "button",
18826
+ {
18827
+ type: "button",
18828
+ "aria-label": "Card style",
18829
+ disabled,
18830
+ className: cn(
18831
+ "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",
18832
+ disabled && "cursor-not-allowed opacity-40 hover:bg-transparent"
18833
+ ),
18834
+ children: [
18835
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(StyleSwatch, { style: current.value }),
18836
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "flex-1 text-left", children: current.label }),
18837
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_lucide_react9.ChevronDown, { size: 16, style: { pointerEvents: "none" } })
18838
+ ]
18839
+ }
18840
+ );
18841
+ if (disabled) {
18842
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "flex items-center gap-1.5 pl-1.5", children: [
18843
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "text-xs text-muted-foreground opacity-50", children: "Card style" }),
18844
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(Tooltip, { children: [
18845
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "inline-flex", children: trigger }) }),
18846
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(TooltipContent, { side: "bottom", sideOffset: 8, children: "Mark as featured to edit card style" })
18847
+ ] })
18848
+ ] });
18849
+ }
18666
18850
  return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "flex items-center gap-1.5 pl-1.5", children: [
18667
18851
  /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "text-xs text-muted-foreground", children: "Card style" }),
18668
18852
  /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(DropdownMenu, { modal: false, children: [
18669
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
18670
- "button",
18671
- {
18672
- type: "button",
18673
- "aria-label": "Card style",
18674
- 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",
18675
- children: [
18676
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(StyleSwatch, { style: current.value }),
18677
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "flex-1 text-left", children: current.label }),
18678
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_lucide_react9.ChevronDown, { size: 16, style: { pointerEvents: "none" } })
18679
- ]
18680
- }
18681
- ) }),
18853
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(DropdownMenuTrigger, { asChild: true, children: trigger }),
18682
18854
  /* @__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) => {
18683
18855
  const selected = style.value === value;
18684
18856
  return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
@@ -18742,23 +18914,35 @@ function PricingCardToolbar({
18742
18914
  mobile ? "flex-wrap justify-center" : "flex-nowrap"
18743
18915
  ),
18744
18916
  children: [
18745
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(CardStyleMenu, { value: style, onChange: onSetStyle }),
18746
18917
  /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(Tooltip, { children: [
18747
18918
  /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
18748
18919
  CustomToolbarButton,
18749
18920
  {
18750
18921
  type: "button",
18751
- "aria-label": featuredTag ? "Remove featured tag" : "Add featured tag",
18922
+ "aria-label": featuredTag ? "Unmark as featured" : "Mark as featured",
18752
18923
  "aria-pressed": featuredTag,
18753
- active: featuredTag,
18754
18924
  onClick: () => onSetFeaturedTag(!featuredTag),
18755
- className: "size-8",
18756
- children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_lucide_react9.Star, { size: 16, fill: featuredTag ? "currentColor" : "none", style: { pointerEvents: "none" } })
18925
+ className: "group size-8",
18926
+ children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
18927
+ import_lucide_react9.Star,
18928
+ {
18929
+ size: 16,
18930
+ fill: "currentColor",
18931
+ strokeWidth: 0,
18932
+ className: cn(
18933
+ "transition-colors",
18934
+ featuredTag ? "text-[#f59e0b] group-hover:text-[#d97706]" : "text-[#d6d3d1] group-hover:text-[#fbbf24]"
18935
+ ),
18936
+ style: { pointerEvents: "none" }
18937
+ }
18938
+ )
18757
18939
  }
18758
18940
  ) }),
18759
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(TooltipContent, { side: "bottom", sideOffset: 8, children: featuredTag ? "Remove featured tag" : "Add featured tag" })
18941
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(TooltipContent, { side: "bottom", sideOffset: 8, children: featuredTag ? "Unmark featured" : "Mark as featured" })
18760
18942
  ] }),
18761
18943
  /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(ToolbarSeparator, {}),
18944
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(CardStyleMenu, { value: style, onChange: onSetStyle, disabled: !featuredTag }),
18945
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(ToolbarSeparator, {}),
18762
18946
  /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(ToolbarAction, { label: "Add button", onClick: onAddButton, disabled: buttonCount >= 2, children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(ButtonGlyph, {}) }),
18763
18947
  /* @__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: [
18764
18948
  /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(ButtonGlyph, {}),
@@ -27325,7 +27509,7 @@ function OhhwellsBridge() {
27325
27509
  if (!subdomain) return {};
27326
27510
  return contentCache.get(subdomain) ?? {};
27327
27511
  };
27328
- const observeRoots = () => [document.querySelector("nav"), document.querySelector("footer")].filter(
27512
+ const observeRoots = () => [document.querySelector("nav"), getFooterRoot()].filter(
27329
27513
  (el) => Boolean(el)
27330
27514
  );
27331
27515
  const roots = observeRoots();
@@ -27781,7 +27965,7 @@ function OhhwellsBridge() {
27781
27965
  {
27782
27966
  const formEl = getFormElement(target);
27783
27967
  const onSuccessText = target.closest(`[${SUCCESS_TEXT_ATTR}]`);
27784
- if (formEl && formKeyOf(formEl) && !onSuccessText) {
27968
+ if (formEl && formKeyOf(formEl) && !onSuccessText && !isCtaButton(target)) {
27785
27969
  const fieldEl = getFieldWrapper(target);
27786
27970
  if (fieldEl) {
27787
27971
  const label = target.closest("label");
@@ -29690,6 +29874,7 @@ function OhhwellsBridge() {
29690
29874
  brandKitRef.current = value;
29691
29875
  applyBrandToDom(parseBrandKit(value));
29692
29876
  if (aiSectionsRef.current) applyAiSectionsToDom(parseAiSectionsState(aiSectionsRef.current));
29877
+ applyAiFooterFromContent(editContentRef.current);
29693
29878
  applyStylesToDom(parseStyleStore(stylesRef.current));
29694
29879
  postToParentRef.current({ type: "ow:change", nodes: [{ key: BRAND_KIT_KEY, text: value }] });
29695
29880
  postToParentRef.current({ type: "ow:ai-brand-applied", previous, value });
@@ -30327,9 +30512,10 @@ function OhhwellsBridge() {
30327
30512
  if (e.data?.type !== "ow:collect-section") return;
30328
30513
  const sectionId = e.data.sectionId;
30329
30514
  if (typeof sectionId !== "string" || !sectionId) return;
30330
- const sectionEl = document.querySelector(
30515
+ const foundEl = document.querySelector(
30331
30516
  `[data-ohw-section="${CSS.escape(sectionId)}"]`
30332
30517
  );
30518
+ 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;
30333
30519
  const nodes = sectionEl ? collectEditableNodes(void 0, sectionEl).filter(
30334
30520
  (n) => n.key && !n.key.startsWith("__ohw_") && !["meta", "sections", "video-setting"].includes(n.type)
30335
30521
  ).map((n) => ({ ...n, text: n.text ?? "", type: normalizeAiNodeType(n.type) })) : [];
@@ -32284,6 +32470,7 @@ function EmptySection({ title, homeHref = "/", eyebrowKey, titleKey, subtitleKey
32284
32470
  dropIndicatorVariants,
32285
32471
  ensureFooterTree,
32286
32472
  filterAvailablePages,
32473
+ foldAlignIntoTrees,
32287
32474
  getEditModeInitialState,
32288
32475
  insertFooterHours,
32289
32476
  isEditSessionActive,
@@ -32292,6 +32479,7 @@ function EmptySection({ title, homeHref = "/", eyebrowKey, titleKey, subtitleKey
32292
32479
  isValidUrl,
32293
32480
  nextViableSlide,
32294
32481
  parseAiFooterState,
32482
+ parseAiSectionsState,
32295
32483
  parseTarget,
32296
32484
  removeFooterBlock,
32297
32485
  resolveFooterCopyright,