@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.js CHANGED
@@ -2827,14 +2827,22 @@ function isInactiveFooterChrome(el) {
2827
2827
  if (footer && footer.style.display === "none") return true;
2828
2828
  return false;
2829
2829
  }
2830
+ function isIncidentalFooter(el) {
2831
+ return Boolean(el.closest("blockquote, article, figure"));
2832
+ }
2830
2833
  function getFooterRoot() {
2831
2834
  const ai = document.querySelector("footer[data-ai-footer]");
2832
2835
  if (ai) return ai;
2833
2836
  for (const el of Array.from(
2834
- document.querySelectorAll('footer[data-ohw-section="footer"], footer')
2837
+ document.querySelectorAll('footer[data-ohw-section="footer"]')
2835
2838
  )) {
2839
+ if (isInactiveFooterChrome(el)) continue;
2840
+ return el;
2841
+ }
2842
+ for (const el of Array.from(document.querySelectorAll("footer"))) {
2836
2843
  if (el.hasAttribute("data-ai-footer")) continue;
2837
2844
  if (isInactiveFooterChrome(el)) continue;
2845
+ if (isIncidentalFooter(el)) continue;
2838
2846
  return el;
2839
2847
  }
2840
2848
  return null;
@@ -3245,8 +3253,8 @@ function buildFooterLink(colIndex, itemIndex, href, label, template) {
3245
3253
  return anchor;
3246
3254
  }
3247
3255
  function footerColumnExistsInDom(colIndex) {
3248
- const root = getFooterRoot() ?? document;
3249
- 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;
3256
+ const scope = getFooterLinksContainer() ?? getFooterRoot() ?? document;
3257
+ 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;
3250
3258
  }
3251
3259
  function getFooterLinkTemplate() {
3252
3260
  for (const col of listFooterColumns()) {
@@ -5196,19 +5204,21 @@ function findIndexSegment(keys) {
5196
5204
  if (len < 2 || tokenLists.some((t) => t.length !== len)) return null;
5197
5205
  let position = -1;
5198
5206
  for (let p = 0; p < len; p++) {
5199
- const values = tokenLists.map((t) => t[p]);
5200
- const allSame = values.every((v) => v === values[0]);
5207
+ const values2 = tokenLists.map((t) => t[p]);
5208
+ const allSame = values2.every((v) => v === values2[0]);
5201
5209
  if (allSame) continue;
5202
- const allNumeric = values.every((v) => /^\d+$/.test(v));
5203
- if (!allNumeric || position !== -1) return null;
5210
+ if (position !== -1) return null;
5204
5211
  position = p;
5205
5212
  }
5206
5213
  if (position === -1) return null;
5214
+ const values = tokenLists.map((t) => t[position]);
5215
+ const numericCount = values.filter((v) => /^\d+$/.test(v)).length;
5216
+ if (numericCount !== 0 && numericCount !== values.length) return null;
5217
+ if (numericCount === 0 && position === len - 1) return null;
5207
5218
  const group = tokenLists[0].slice(0, position).join("-");
5208
5219
  if (!group) return null;
5209
- const indices = tokenLists.map((t) => parseInt(t[position], 10));
5210
- if (new Set(indices).size !== indices.length) return null;
5211
- return { position, group };
5220
+ if (new Set(values).size !== values.length) return null;
5221
+ return numericCount === values.length ? { position, group } : { position, group, slug: true };
5212
5222
  }
5213
5223
  function singleIndexSegment(key) {
5214
5224
  const tokens = baseKey(key).split("-");
@@ -5254,7 +5264,19 @@ function detectItemGroups(sectionEl) {
5254
5264
  list.push(m);
5255
5265
  byShape.set(shape, list);
5256
5266
  }
5257
- clusters.push(...Array.from(byShape.values()));
5267
+ const shaped = Array.from(byShape.values());
5268
+ clusters.push(...shaped.filter((c) => c.length >= 2));
5269
+ const singles = shaped.filter((c) => c.length === 1).map((c) => c[0]);
5270
+ const bySlugShape = /* @__PURE__ */ new Map();
5271
+ for (const m of singles) {
5272
+ const primary = primaryKey(keyedDescendants(m));
5273
+ const tokens = baseKey(primary ?? "").split("-");
5274
+ const slugShape = `${tokens.length}|${tokens[0]}|${tokens[tokens.length - 1]}`;
5275
+ const list = bySlugShape.get(slugShape) ?? [];
5276
+ list.push(m);
5277
+ bySlugShape.set(slugShape, list);
5278
+ }
5279
+ clusters.push(...Array.from(bySlugShape.values()));
5258
5280
  }
5259
5281
  for (const members of clusters) {
5260
5282
  const keyedPerMember = members.map((m) => keyedDescendants(m));
@@ -5265,11 +5287,28 @@ function detectItemGroups(sectionEl) {
5265
5287
  const dedupeKey = `${segment.group}`;
5266
5288
  if (seenParents.has(dedupeKey)) continue;
5267
5289
  seenParents.add(dedupeKey);
5268
- const items = members.map((el, i) => ({
5269
- el,
5270
- index: indexFromKey(primaries[i], segment.position),
5271
- keyed: keyedPerMember[i]
5272
- }));
5290
+ let items;
5291
+ if (segment.slug) {
5292
+ const tokens = members.map((_, i) => baseKey(primaries[i]).split("-")[segment.position]);
5293
+ let nextOriginal = 1;
5294
+ const indices = tokens.map((t) => {
5295
+ const cloneMatch = t.match(/^x(\d+)$/);
5296
+ return cloneMatch ? parseInt(cloneMatch[1], 10) : nextOriginal++;
5297
+ });
5298
+ if (new Set(indices).size !== indices.length) continue;
5299
+ items = members.map((el, i) => ({
5300
+ el,
5301
+ index: indices[i],
5302
+ keyed: keyedPerMember[i],
5303
+ slugToken: tokens[i]
5304
+ }));
5305
+ } else {
5306
+ items = members.map((el, i) => ({
5307
+ el,
5308
+ index: indexFromKey(primaries[i], segment.position),
5309
+ keyed: keyedPerMember[i]
5310
+ }));
5311
+ }
5273
5312
  groups.push({ group: segment.group, parent, nested: false, items });
5274
5313
  }
5275
5314
  }
@@ -5393,6 +5432,18 @@ function rekeyItemClone(clone, source, sibling, group, newIndex) {
5393
5432
  const delta = newIndex - source.index;
5394
5433
  const cloneKeyed = keyedDescendants(clone);
5395
5434
  const pairs = [];
5435
+ if (source.slugToken) {
5436
+ source.keyed.forEach((src, i) => {
5437
+ const target = cloneKeyed[i];
5438
+ if (!target || target.attr !== src.attr) return;
5439
+ const tokens = baseKey(src.key).split("-");
5440
+ if (!tokens.includes(source.slugToken)) return;
5441
+ const next = tokens.map((t) => t === source.slugToken ? `x${newIndex}` : t).join("-") + keySuffix(src.key);
5442
+ target.el.setAttribute(src.attr, next);
5443
+ pairs.push({ from: src.key, to: next });
5444
+ });
5445
+ return pairs;
5446
+ }
5396
5447
  source.keyed.forEach((src, i) => {
5397
5448
  const target = cloneKeyed[i];
5398
5449
  if (!target || target.attr !== src.attr) return;
@@ -5436,7 +5487,12 @@ function syncGroup(detected, state) {
5436
5487
  clone.setAttribute(ITEM_ATTR, `${detected.group}:${entry.index}`);
5437
5488
  rekeyItemClone(clone, source, sibling, detected.group, entry.index);
5438
5489
  source.el.insertAdjacentElement("afterend", clone);
5439
- byIndex.set(entry.index, { el: clone, index: entry.index, keyed: keyedDescendants(clone) });
5490
+ byIndex.set(entry.index, {
5491
+ el: clone,
5492
+ index: entry.index,
5493
+ keyed: keyedDescendants(clone),
5494
+ ...source.slugToken ? { slugToken: `x${entry.index}` } : {}
5495
+ });
5440
5496
  }
5441
5497
  const listed = state.items.map((e) => byIndex.get(e.index)?.el).filter((el) => !!el);
5442
5498
  let prev = null;
@@ -5538,9 +5594,16 @@ function applyItemOps(store, ops, currentContent) {
5538
5594
  const nodes = [];
5539
5595
  const added = [];
5540
5596
  let elements = syncGroup(detected, state);
5597
+ const sluggy = detected.items.some((item) => item.slugToken);
5598
+ const slugTokenOf = (index) => detected.items.find((item) => item.index === index)?.slugToken ?? (sluggy ? `x${index}` : void 0);
5541
5599
  for (const add of ops.add ?? []) {
5542
5600
  const sourceEl = elements.get(add.cloneOf);
5543
- const source = sourceEl ? { el: sourceEl, index: add.cloneOf, keyed: keyedDescendants(sourceEl) } : void 0;
5601
+ const source = sourceEl ? {
5602
+ el: sourceEl,
5603
+ index: add.cloneOf,
5604
+ keyed: keyedDescendants(sourceEl),
5605
+ ...slugTokenOf(add.cloneOf) ? { slugToken: slugTokenOf(add.cloneOf) } : {}
5606
+ } : void 0;
5544
5607
  if (!source) continue;
5545
5608
  const newIndex = Math.max(...state.items.map((e) => e.index), ...detected.items.map((i) => i.index)) + 1;
5546
5609
  const sibling = detected.items.find((item) => item.index !== add.cloneOf && !item.el.hasAttribute(ITEM_CLONE_ATTR)) ?? null;
@@ -5567,7 +5630,12 @@ function applyItemOps(store, ops, currentContent) {
5567
5630
  if (anchor) anchor.insertAdjacentElement("afterend", clone);
5568
5631
  else source.el.insertAdjacentElement("afterend", clone);
5569
5632
  elements.set(newIndex, clone);
5570
- detected.items.push({ el: clone, index: newIndex, keyed: cloneKeyed });
5633
+ detected.items.push({
5634
+ el: clone,
5635
+ index: newIndex,
5636
+ keyed: cloneKeyed,
5637
+ ...sluggy ? { slugToken: `x${newIndex}` } : {}
5638
+ });
5571
5639
  added.push(newIndex);
5572
5640
  elements = syncGroup(detected, state);
5573
5641
  }
@@ -6010,16 +6078,25 @@ function foldAlignIntoTrees(state, store) {
6010
6078
  nextTrees.set(id, fresh);
6011
6079
  return fresh;
6012
6080
  };
6081
+ const stripChildAligns = (node) => node.children ? {
6082
+ ...node,
6083
+ children: node.children.map((child) => {
6084
+ const { align: _dropped, ...rest } = child;
6085
+ return stripChildAligns(rest);
6086
+ })
6087
+ } : node;
6013
6088
  const nodes = {};
6014
6089
  for (const [key, override] of Object.entries(store.nodes)) {
6015
6090
  const match = override.align !== void 0 && key.startsWith(AI_SLOT_KEY_PREFIX) ? key.match(/^ai\.(.+?)\.r(\d+)\.b(\d+)(?:\.|$)/) : null;
6016
6091
  const tree = match ? treeFor(match[1]) : void 0;
6017
- const block = match && tree ? tree.rows[Number(match[2])]?.blocks[Number(match[3])] : void 0;
6018
- if (!block) {
6092
+ const row = match && tree ? tree.rows[Number(match[2])] : void 0;
6093
+ const blockIdx = match ? Number(match[3]) : -1;
6094
+ const block = row?.blocks[blockIdx];
6095
+ if (!row || !block) {
6019
6096
  nodes[key] = override;
6020
6097
  continue;
6021
6098
  }
6022
- block.align = override.align;
6099
+ row.blocks[blockIdx] = { ...stripChildAligns(block), align: override.align };
6023
6100
  const rest = { ...override };
6024
6101
  delete rest.align;
6025
6102
  if (Object.keys(rest).length > 0) nodes[key] = rest;
@@ -6032,7 +6109,7 @@ function foldAlignIntoTrees(state, store) {
6032
6109
  continue;
6033
6110
  }
6034
6111
  for (const row of tree.rows) {
6035
- for (const block of row.blocks) block.align = override.align;
6112
+ row.blocks = row.blocks.map((block) => ({ ...stripChildAligns(block), align: override.align }));
6036
6113
  }
6037
6114
  const rest = { ...override };
6038
6115
  delete rest.align;
@@ -6263,6 +6340,20 @@ function applyBrandChrome(content) {
6263
6340
 
6264
6341
  // src/lib/brand-kit.ts
6265
6342
  var BRAND_KIT_KEY = "__ohw_brand";
6343
+ var appliedPalette = null;
6344
+ var appliedFonts = null;
6345
+ function getAppliedBrand() {
6346
+ if (!appliedPalette) return null;
6347
+ return appliedFonts ? { palette: appliedPalette, fonts: appliedFonts } : { palette: appliedPalette };
6348
+ }
6349
+ function brandForGenerated(fallback) {
6350
+ const applied = getAppliedBrand();
6351
+ if (!applied) return fallback ?? void 0;
6352
+ return {
6353
+ palette: applied.palette,
6354
+ fonts: applied.fonts ?? fallback?.fonts ?? AI_DEFAULT_BRAND.fonts
6355
+ };
6356
+ }
6266
6357
  var BRAND_VAR_PREFIX = "--ohw-brand-";
6267
6358
  var BRAND_VAR_NAMES = ["primary", "accent", "light", "dark", "surface", "border", "muted"].map(
6268
6359
  (role) => `${BRAND_VAR_PREFIX}${role}`
@@ -6566,6 +6657,8 @@ function loadCustomFonts(fonts) {
6566
6657
  function applyBrandToDom(kit) {
6567
6658
  const root = document.documentElement;
6568
6659
  if (!kit) {
6660
+ appliedPalette = null;
6661
+ appliedFonts = null;
6569
6662
  for (const name of [...BRAND_VAR_NAMES, ...LEGACY_BRAND_VAR_NAMES, ...BUTTON_VAR_NAMES]) {
6570
6663
  root.style.removeProperty(name);
6571
6664
  }
@@ -6579,11 +6672,16 @@ function applyBrandToDom(kit) {
6579
6672
  const applyFonts = kit.touched ? kit.touched.fonts === true : true;
6580
6673
  if (kit.palette) {
6581
6674
  if (applyPalette) {
6675
+ appliedPalette = kit.palette;
6582
6676
  for (const [name, value] of Object.entries(brandColorVars(kit.palette))) root.style.setProperty(name, value);
6583
6677
  } else {
6678
+ appliedPalette = null;
6584
6679
  for (const name of [...BRAND_VAR_NAMES, ...LEGACY_BRAND_VAR_NAMES]) root.style.removeProperty(name);
6585
6680
  }
6586
6681
  }
6682
+ if (kit.fonts) {
6683
+ appliedFonts = applyFonts ? kit.fonts : null;
6684
+ }
6587
6685
  if (kit.buttons) {
6588
6686
  for (const name of BUTTON_VAR_NAMES) root.style.removeProperty(name);
6589
6687
  for (const [name, value] of Object.entries(buttonStyleVars({ ...kit, buttons: kit.buttons }))) root.style.setProperty(name, value);
@@ -7116,6 +7214,35 @@ function withThemeFonts(brand) {
7116
7214
  }
7117
7215
  };
7118
7216
  }
7217
+ function liveBrandColor(role, fallback) {
7218
+ if (role === "dark") {
7219
+ return `var(--brand-text, var(--ohw-brand-dark, var(--color-dark, ${fallback})))`;
7220
+ }
7221
+ if (role === "light") {
7222
+ return `var(--brand-background, var(--ohw-brand-light, var(--color-light, ${fallback})))`;
7223
+ }
7224
+ return `var(--brand-${role}, var(--ohw-brand-${role}, var(--color-${role}, ${fallback})))`;
7225
+ }
7226
+ function withLivePalette(brand, source) {
7227
+ const sourceOf = (color) => {
7228
+ const value = color.trim().toLowerCase();
7229
+ const entries = Object.entries(source.palette);
7230
+ return entries.find(([, hex]) => hex.trim().toLowerCase() === value)?.[0] ?? null;
7231
+ };
7232
+ const map = (color) => {
7233
+ const role = sourceOf(color);
7234
+ return role ? liveBrandColor(role, source.palette[role]) : color;
7235
+ };
7236
+ return {
7237
+ ...brand,
7238
+ palette: {
7239
+ dark: map(brand.palette.dark),
7240
+ primary: map(brand.palette.primary),
7241
+ accent: map(brand.palette.accent),
7242
+ light: map(brand.palette.light)
7243
+ }
7244
+ };
7245
+ }
7119
7246
  function footerLogoStyle(fallbackFont, fallbackColor) {
7120
7247
  return {
7121
7248
  fontFamily: `var(--brand-font-heading, var(--font-heading, var(--font-display, ${quotedFont(fallbackFont)})))`,
@@ -7143,6 +7270,7 @@ var blockAlignStyle = (blockAlign) => blockAlign ? {
7143
7270
  alignItems: blockAlign === "left" ? "flex-start" : blockAlign === "right" ? "flex-end" : "center",
7144
7271
  textAlign: blockAlign
7145
7272
  } : {};
7273
+ var fullBleedBlock = (block) => block.type === "carousel" || block.type === "group" && str(block.slots?.layout) === "overlap";
7146
7274
  var featureLines = (value) => value.split(/\n|<br\s*\/?>/i).map((line) => line.trim()).filter(Boolean);
7147
7275
  var AI_MOBILE_CSS = [
7148
7276
  // Unscoped (applies at every viewport, not just mobile): re-enables pointer-events for the
@@ -7333,7 +7461,7 @@ function hexContrast(a, b) {
7333
7461
  function primaryButtonLabel(brand) {
7334
7462
  const darkC = hexContrast(brand.palette.primary, brand.palette.dark);
7335
7463
  const lightC = hexContrast(brand.palette.primary, AI_TREE_TOKENS.textPrimaryForeground);
7336
- if (darkC === null || lightC === null) return AI_TREE_TOKENS.textPrimaryForeground;
7464
+ if (darkC === null || lightC === null) return `var(--brand-on-primary, ${AI_TREE_TOKENS.textPrimaryForeground})`;
7337
7465
  return darkC > lightC ? brand.palette.dark : AI_TREE_TOKENS.textPrimaryForeground;
7338
7466
  }
7339
7467
  function accentBandContext(brand) {
@@ -7357,7 +7485,11 @@ function readableForeground(bg, brand) {
7357
7485
  return darkC > lightC ? brand.palette.dark : AI_TREE_TOKENS.textPrimaryForeground;
7358
7486
  }
7359
7487
  function hexWithAlpha(hex, alpha) {
7360
- const m = /^#([0-9a-f]{6})$/i.exec(hex.trim());
7488
+ const value = hex.trim();
7489
+ if (value.startsWith("var(") || value.startsWith("color-mix(")) {
7490
+ return `color-mix(in srgb, ${value} ${Math.round(alpha * 100)}%, transparent)`;
7491
+ }
7492
+ const m = /^#([0-9a-f]{6})$/i.exec(value);
7361
7493
  if (!m) return hex;
7362
7494
  const r2 = parseInt(m[1].slice(0, 2), 16);
7363
7495
  const g = parseInt(m[1].slice(2, 4), 16);
@@ -7630,12 +7762,10 @@ function ButtonEl({
7630
7762
  cursor: "pointer",
7631
7763
  whiteSpace: "nowrap",
7632
7764
  ...buttonShellStyle(ctx, variant, fullWidth),
7633
- ...tokenColorStyle ?? (secondary ? { border: `1px solid ${secondaryColor ?? ctx.brand.palette.dark}`, color: secondaryColor ?? ctx.brand.palette.dark } : (
7634
- // Off-band, prefer the template button's measured fill/label pair — a template may
7635
- // fill its CTAs with any token (hvac: accent bg, primary text). On an accent band
7636
- // the flipped palette keeps contrast, so the brand-driven colours stay.
7637
- !ctx.buttonLabel && ctx.buttonStyle?.background ? { background: ctx.buttonStyle.background, color: ctx.buttonStyle.color } : { background: ctx.brand.palette.primary, color: ctx.buttonLabel ?? primaryButtonLabel(ctx.brand) }
7638
- ))
7765
+ ...tokenColorStyle ?? (secondary ? { border: `1px solid ${secondaryColor ?? ctx.brand.palette.dark}`, color: secondaryColor ?? ctx.brand.palette.dark } : {
7766
+ background: ctx.brand.palette.primary,
7767
+ color: ctx.buttonLabel ?? primaryButtonLabel(ctx.brand)
7768
+ })
7639
7769
  },
7640
7770
  children: /* @__PURE__ */ jsx("span", { ...path ? textAttrs(ctx, `${path}.label`) : {}, children: str(slots.label) })
7641
7771
  }
@@ -9206,8 +9336,8 @@ function renderNode(node, ctx, path, ambientAlign) {
9206
9336
  // layer (this group's i===0 sibling) can span edge-to-edge — reintroduced here,
9207
9337
  // scoped to just the overlay text, so a heading/copy/CTA stack doesn't stretch
9208
9338
  // the full section width instead of reading at the usual content width.
9209
- /* @__PURE__ */ 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") })
9210
- ) : renderNode(child, ctx, `${path}.c${i}`, i > 0 ? child.align ?? "center" : void 0),
9339
+ /* @__PURE__ */ 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") })
9340
+ ) : renderNode(child, ctx, `${path}.c${i}`, i > 0 ? child.align ?? node.align ?? "center" : void 0),
9211
9341
  i === 0 && needsBaseScrim && /* @__PURE__ */ jsx(
9212
9342
  "div",
9213
9343
  {
@@ -9240,7 +9370,7 @@ function renderNode(node, ctx, path, ambientAlign) {
9240
9370
  // child otherwise stretches full-width (unchanged pre-existing behaviour).
9241
9371
  ...blockAlignStyle(child.align ?? ambientAlign)
9242
9372
  },
9243
- children: renderNode(child, ctx, `${path}.c${i}`, ambientAlign)
9373
+ children: renderNode(child, ctx, `${path}.c${i}`, node.align ?? ambientAlign)
9244
9374
  },
9245
9375
  i
9246
9376
  ));
@@ -9270,8 +9400,20 @@ function renderNode(node, ctx, path, ambientAlign) {
9270
9400
  }
9271
9401
  case "button":
9272
9402
  return /* @__PURE__ */ jsx(ButtonEl, { slots, ctx, path });
9273
- case "button-row":
9274
- return /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: AI_TREE_TOKENS.spacing6, justifyContent: "center" }, children: (node.children ?? []).map((child, i) => /* @__PURE__ */ jsx(ButtonEl, { slots: child.slots ?? {}, ctx, path: `${path}.c${i}` }, i)) });
9403
+ case "button-row": {
9404
+ const rowAlign = node.align ?? ambientAlign;
9405
+ return /* @__PURE__ */ jsx(
9406
+ "div",
9407
+ {
9408
+ style: {
9409
+ display: "flex",
9410
+ gap: AI_TREE_TOKENS.spacing6,
9411
+ justifyContent: rowAlign === "left" ? "flex-start" : rowAlign === "right" ? "flex-end" : "center"
9412
+ },
9413
+ children: (node.children ?? []).map((child, i) => /* @__PURE__ */ jsx(ButtonEl, { slots: child.slots ?? {}, ctx, path: `${path}.c${i}` }, i))
9414
+ }
9415
+ );
9416
+ }
9275
9417
  case "media":
9276
9418
  return /* @__PURE__ */ jsx(
9277
9419
  MediaBox,
@@ -9480,10 +9622,10 @@ function renderNode(node, ctx, path, ambientAlign) {
9480
9622
  ...buttonShellStyle(ctx, "primary"),
9481
9623
  // Brand-styled: the template button's measured fill/label pair off-band, else
9482
9624
  // primary fill with a brand-derived label so it reads on custom palettes.
9483
- ...buttonColorStyle(ctx, "primary") ?? (!ctx.buttonLabel && ctx.buttonStyle?.background ? { background: ctx.buttonStyle.background, color: ctx.buttonStyle.color } : {
9625
+ ...buttonColorStyle(ctx, "primary") ?? {
9484
9626
  background: ctx.brand.palette.primary,
9485
9627
  color: ctx.buttonLabel ?? primaryButtonLabel(ctx.brand)
9486
- })
9628
+ }
9487
9629
  },
9488
9630
  children: /* @__PURE__ */ jsx("span", { ...textAttrs(ctx, `${path}.c${i}.label`), children: str(cs.label) })
9489
9631
  },
@@ -9527,7 +9669,7 @@ function AiTreeRenderer({
9527
9669
  }
9528
9670
  const resolvedBrand = withThemeFonts(brand ?? AI_DEFAULT_BRAND);
9529
9671
  const band = (tree.settings ?? {}).sectionBackground === "accent" ? accentBandContext(resolvedBrand) : null;
9530
- const blockBrand = band?.brand ?? resolvedBrand;
9672
+ const blockBrand = withLivePalette(band?.brand ?? resolvedBrand, resolvedBrand);
9531
9673
  const placeholderMap = buildPlaceholderMap(tree);
9532
9674
  const hasOverlapBackground = tree.scaffold === "overlay" && tree.rows.some(
9533
9675
  (row) => (row.blocks ?? []).some((b) => b.type === "group" && str(b.slots?.layout) === "overlap")
@@ -9543,7 +9685,7 @@ function AiTreeRenderer({
9543
9685
  // standalone resolve), so generated galleries, image rows, and overlay backgrounds arrive with
9544
9686
  // photos instead of grey boxes.
9545
9687
  resolveMedia: (ref) => resolveMedia?.(ref) ?? placeholderMap[ref] ?? resolvePlaceholderRef(ref),
9546
- cardSurface: blockBrand.palette.light.toUpperCase() === AI_DEFAULT_BRAND.palette.light.toUpperCase() ? "#ECECEB" : `color-mix(in srgb, ${blockBrand.palette.light} 90%, ${blockBrand.palette.dark})`,
9688
+ cardSurface: resolvedBrand.palette.light.toUpperCase() === AI_DEFAULT_BRAND.palette.light.toUpperCase() ? "#ECECEB" : `color-mix(in srgb, ${blockBrand.palette.light} 90%, ${blockBrand.palette.dark})`,
9547
9689
  keyFor: editKeyPrefix ? (path) => `${editKeyPrefix}.${path}` : null,
9548
9690
  sectionAlignment: (tree.settings ?? {}).alignment === "center" ? "center" : "left",
9549
9691
  buttonStyle,
@@ -9560,7 +9702,7 @@ function AiTreeRenderer({
9560
9702
  const backgroundUrl = isOverlay && settings.backgroundMedia ? ctx.resolveMedia(settings.backgroundMedia) : null;
9561
9703
  const bgAttrs = ctx.keyFor && isOverlay && settings.backgroundMedia ? { "data-ohw-key": ctx.keyFor("settings.backgroundMedia"), "data-ohw-editable": "bg-image" } : {};
9562
9704
  const toneBackground = (() => {
9563
- const { dark, primary, light } = resolvedBrand.palette;
9705
+ const { dark, primary, light } = blockBrand.palette;
9564
9706
  switch (settings.sectionBackground) {
9565
9707
  case "surface":
9566
9708
  return `color-mix(in srgb, ${light} 94%, ${dark})`;
@@ -9650,7 +9792,7 @@ function AiTreeRenderer({
9650
9792
  // its own horizontal `align`, which takes priority for that block.
9651
9793
  ...fillHeight ? { display: "grid" } : {},
9652
9794
  // Horizontal placement of the block's content within its column.
9653
- ...cellAlignStyle(block.align),
9795
+ ...fullBleedBlock(block) ? {} : cellAlignStyle(block.align),
9654
9796
  // space-between: each column becomes a flex column whose content spreads over
9655
9797
  // the full row height instead of clumping at the top.
9656
9798
  ...distributed === "space-between" ? { display: "flex", flexDirection: "column", justifyContent: "space-between" } : {}
@@ -9681,14 +9823,15 @@ function AiFooterRenderer({
9681
9823
  if (!isRenderableFooterTree(tree)) {
9682
9824
  return null;
9683
9825
  }
9684
- const resolvedBrand = withThemeFonts(brand ?? AI_DEFAULT_BRAND);
9826
+ const hexBrand = withThemeFonts(brand ?? AI_DEFAULT_BRAND);
9827
+ const resolvedBrand = withLivePalette(hexBrand, brand ?? AI_DEFAULT_BRAND);
9685
9828
  const bandBg = surface?.background ?? resolvedBrand.palette.dark;
9686
9829
  const bandText = surface?.text ?? resolvedBrand.palette.light;
9687
9830
  const onDark = surface ? isDarkColor(surface.background) : true;
9688
9831
  const ctx = {
9689
9832
  brand: resolvedBrand,
9690
9833
  resolveMedia: (ref) => resolveMedia?.(ref) ?? resolvePlaceholderRef(ref),
9691
- cardSurface: resolvedBrand.palette.light.toUpperCase() === AI_DEFAULT_BRAND.palette.light.toUpperCase() ? "#ECECEB" : `color-mix(in srgb, ${resolvedBrand.palette.light} 90%, ${resolvedBrand.palette.dark})`,
9834
+ cardSurface: hexBrand.palette.light.toUpperCase() === AI_DEFAULT_BRAND.palette.light.toUpperCase() ? "#ECECEB" : `color-mix(in srgb, ${resolvedBrand.palette.light} 90%, ${resolvedBrand.palette.dark})`,
9692
9835
  keyFor: editKeyPrefix ? (path) => `${editKeyPrefix}.${path}` : null,
9693
9836
  logoUrl: logoUrl ?? null,
9694
9837
  resolvePageHref
@@ -10264,10 +10407,9 @@ function syncSoftRemovedGenerated() {
10264
10407
  }
10265
10408
  function applyAiSectionsToDom(state, options) {
10266
10409
  if (typeof document === "undefined") return;
10267
- const brandOverride = deriveBrandOverride();
10268
10410
  const templateBrand = deriveTemplateBrand();
10269
10411
  const templateButtonStyle = deriveTemplateButtonStyle();
10270
- const brandKey = brandOverride ? JSON.stringify(brandOverride) : "";
10412
+ const brandKey = JSON.stringify(getAppliedBrand() ?? deriveBrandOverride() ?? null);
10271
10413
  const pagePath = window.location.pathname;
10272
10414
  const pageSections = state.sections.filter((entry) => (entry.path ?? "/") === pagePath);
10273
10415
  const activeIds = new Set(pageSections.map((entry) => entry.id));
@@ -10310,7 +10452,7 @@ function applyAiSectionsToDom(state, options) {
10310
10452
  AiTreeRenderer,
10311
10453
  {
10312
10454
  tree: entry.tree,
10313
- brand: brandOverride ?? entry.brand ?? options?.brand ?? templateBrand ?? void 0,
10455
+ brand: brandForGenerated(deriveBrandOverride() ?? entry.brand ?? options?.brand ?? templateBrand),
10314
10456
  buttonStyle: templateButtonStyle,
10315
10457
  resolveMedia,
10316
10458
  editKeyPrefix: `ai.${entry.id}`
@@ -10476,7 +10618,7 @@ function applyAiFooterToDom(state, content) {
10476
10618
  const templateFooter = findTemplateFooter();
10477
10619
  captureTemplateFooterSurface(templateFooter);
10478
10620
  const surface = capturedSurface ?? null;
10479
- const brand = mountOptions.brand ?? deriveTemplateBrand();
10621
+ const brand = brandForGenerated(mountOptions.brand ?? deriveTemplateBrand());
10480
10622
  const logoUrl = resolveLogoFromContent(content, templateFooter);
10481
10623
  const serialized = JSON.stringify(state) + JSON.stringify(brand ?? null) + (logoUrl ?? "") + JSON.stringify(surface) + JSON.stringify(defaultLegal());
10482
10624
  if (mounted2 && mounted2.serialized === serialized && mounted2.container.isConnected) {
@@ -18284,10 +18426,23 @@ function setPricingCardStyle(tree, index, style) {
18284
18426
  });
18285
18427
  }
18286
18428
  function setPricingCardFeaturedTag(tree, index, on) {
18287
- return withPricingCard(tree, index, (target) => {
18429
+ return withPricingCollection(tree, (collection) => {
18430
+ const children = collection.children;
18431
+ if (index < 0 || index >= children.length) return false;
18432
+ if (on) {
18433
+ children.forEach((card, i) => {
18434
+ if (i === index) return;
18435
+ const prev2 = featuredOf(card);
18436
+ if (prev2.enabled !== true || badgeOf(prev2).length === 0) return;
18437
+ card.slots = card.slots ?? {};
18438
+ card.slots.featured = composeFeatured(pricingCardStyleOf(prev2), false, badgeOf(prev2));
18439
+ });
18440
+ }
18441
+ const target = children[index];
18442
+ target.slots = target.slots ?? {};
18288
18443
  const prev = featuredOf(target);
18289
- const style = pricingCardStyleOf(prev);
18290
- target.slots.featured = composeFeatured(style, on, badgeOf(prev));
18444
+ target.slots.featured = composeFeatured(pricingCardStyleOf(prev), on, badgeOf(prev));
18445
+ return true;
18291
18446
  });
18292
18447
  }
18293
18448
  function pricingCardStyleOf(featured) {
@@ -18537,25 +18692,40 @@ function ToolbarSeparator() {
18537
18692
  }
18538
18693
  function CardStyleMenu({
18539
18694
  value,
18540
- onChange
18695
+ onChange,
18696
+ disabled = false
18541
18697
  }) {
18542
18698
  const current = PRICING_CARD_STYLES.find((s) => s.value === value) ?? PRICING_CARD_STYLES[0];
18699
+ const trigger = /* @__PURE__ */ jsxs11(
18700
+ "button",
18701
+ {
18702
+ type: "button",
18703
+ "aria-label": "Card style",
18704
+ disabled,
18705
+ className: cn(
18706
+ "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",
18707
+ disabled && "cursor-not-allowed opacity-40 hover:bg-transparent"
18708
+ ),
18709
+ children: [
18710
+ /* @__PURE__ */ jsx20(StyleSwatch, { style: current.value }),
18711
+ /* @__PURE__ */ jsx20("span", { className: "flex-1 text-left", children: current.label }),
18712
+ /* @__PURE__ */ jsx20(ChevronDown3, { size: 16, style: { pointerEvents: "none" } })
18713
+ ]
18714
+ }
18715
+ );
18716
+ if (disabled) {
18717
+ return /* @__PURE__ */ jsxs11("div", { className: "flex items-center gap-1.5 pl-1.5", children: [
18718
+ /* @__PURE__ */ jsx20("span", { className: "text-xs text-muted-foreground opacity-50", children: "Card style" }),
18719
+ /* @__PURE__ */ jsxs11(Tooltip, { children: [
18720
+ /* @__PURE__ */ jsx20(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx20("span", { className: "inline-flex", children: trigger }) }),
18721
+ /* @__PURE__ */ jsx20(TooltipContent, { side: "bottom", sideOffset: 8, children: "Mark as featured to edit card style" })
18722
+ ] })
18723
+ ] });
18724
+ }
18543
18725
  return /* @__PURE__ */ jsxs11("div", { className: "flex items-center gap-1.5 pl-1.5", children: [
18544
18726
  /* @__PURE__ */ jsx20("span", { className: "text-xs text-muted-foreground", children: "Card style" }),
18545
18727
  /* @__PURE__ */ jsxs11(DropdownMenu, { modal: false, children: [
18546
- /* @__PURE__ */ jsx20(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxs11(
18547
- "button",
18548
- {
18549
- type: "button",
18550
- "aria-label": "Card style",
18551
- 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",
18552
- children: [
18553
- /* @__PURE__ */ jsx20(StyleSwatch, { style: current.value }),
18554
- /* @__PURE__ */ jsx20("span", { className: "flex-1 text-left", children: current.label }),
18555
- /* @__PURE__ */ jsx20(ChevronDown3, { size: 16, style: { pointerEvents: "none" } })
18556
- ]
18557
- }
18558
- ) }),
18728
+ /* @__PURE__ */ jsx20(DropdownMenuTrigger, { asChild: true, children: trigger }),
18559
18729
  /* @__PURE__ */ jsx20(DropdownMenuContent, { align: "end", sideOffset: 8, className: "w-[152px] min-w-[152px] p-1", children: PRICING_CARD_STYLES.map((style) => {
18560
18730
  const selected = style.value === value;
18561
18731
  return /* @__PURE__ */ jsxs11(
@@ -18619,23 +18789,35 @@ function PricingCardToolbar({
18619
18789
  mobile ? "flex-wrap justify-center" : "flex-nowrap"
18620
18790
  ),
18621
18791
  children: [
18622
- /* @__PURE__ */ jsx20(CardStyleMenu, { value: style, onChange: onSetStyle }),
18623
18792
  /* @__PURE__ */ jsxs11(Tooltip, { children: [
18624
18793
  /* @__PURE__ */ jsx20(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx20(
18625
18794
  CustomToolbarButton,
18626
18795
  {
18627
18796
  type: "button",
18628
- "aria-label": featuredTag ? "Remove featured tag" : "Add featured tag",
18797
+ "aria-label": featuredTag ? "Unmark as featured" : "Mark as featured",
18629
18798
  "aria-pressed": featuredTag,
18630
- active: featuredTag,
18631
18799
  onClick: () => onSetFeaturedTag(!featuredTag),
18632
- className: "size-8",
18633
- children: /* @__PURE__ */ jsx20(Star2, { size: 16, fill: featuredTag ? "currentColor" : "none", style: { pointerEvents: "none" } })
18800
+ className: "group size-8",
18801
+ children: /* @__PURE__ */ jsx20(
18802
+ Star2,
18803
+ {
18804
+ size: 16,
18805
+ fill: "currentColor",
18806
+ strokeWidth: 0,
18807
+ className: cn(
18808
+ "transition-colors",
18809
+ featuredTag ? "text-[#f59e0b] group-hover:text-[#d97706]" : "text-[#d6d3d1] group-hover:text-[#fbbf24]"
18810
+ ),
18811
+ style: { pointerEvents: "none" }
18812
+ }
18813
+ )
18634
18814
  }
18635
18815
  ) }),
18636
- /* @__PURE__ */ jsx20(TooltipContent, { side: "bottom", sideOffset: 8, children: featuredTag ? "Remove featured tag" : "Add featured tag" })
18816
+ /* @__PURE__ */ jsx20(TooltipContent, { side: "bottom", sideOffset: 8, children: featuredTag ? "Unmark featured" : "Mark as featured" })
18637
18817
  ] }),
18638
18818
  /* @__PURE__ */ jsx20(ToolbarSeparator, {}),
18819
+ /* @__PURE__ */ jsx20(CardStyleMenu, { value: style, onChange: onSetStyle, disabled: !featuredTag }),
18820
+ /* @__PURE__ */ jsx20(ToolbarSeparator, {}),
18639
18821
  /* @__PURE__ */ jsx20(ToolbarAction, { label: "Add button", onClick: onAddButton, disabled: buttonCount >= 2, children: /* @__PURE__ */ jsx20(ButtonGlyph, {}) }),
18640
18822
  /* @__PURE__ */ jsx20(ToolbarAction, { label: "Remove button", onClick: onRemoveButton, disabled: buttonCount === 0, children: /* @__PURE__ */ jsxs11("span", { className: "relative inline-flex", children: [
18641
18823
  /* @__PURE__ */ jsx20(ButtonGlyph, {}),
@@ -18778,7 +18960,12 @@ function AiPricingCardOverlay({
18778
18960
  index: active.index,
18779
18961
  pending,
18780
18962
  error,
18781
- onAddTier: () => runMutation(active.sectionId, addPricingTier),
18963
+ onAddTier: () => {
18964
+ const newIndex = pricingCardCount(activeEntry.tree);
18965
+ void runMutation(active.sectionId, addPricingTier).then((applied) => {
18966
+ if (applied) setActive({ sectionId: active.sectionId, index: newIndex });
18967
+ });
18968
+ },
18782
18969
  onRemoveTier: () => {
18783
18970
  void runMutation(active.sectionId, (tree) => removePricingTier(tree, active.index)).then(
18784
18971
  (applied) => {
@@ -27195,7 +27382,7 @@ function OhhwellsBridge() {
27195
27382
  if (!subdomain) return {};
27196
27383
  return contentCache.get(subdomain) ?? {};
27197
27384
  };
27198
- const observeRoots = () => [document.querySelector("nav"), document.querySelector("footer")].filter(
27385
+ const observeRoots = () => [document.querySelector("nav"), getFooterRoot()].filter(
27199
27386
  (el) => Boolean(el)
27200
27387
  );
27201
27388
  const roots = observeRoots();
@@ -27666,7 +27853,7 @@ function OhhwellsBridge() {
27666
27853
  {
27667
27854
  const formEl = getFormElement(target);
27668
27855
  const onSuccessText = target.closest(`[${SUCCESS_TEXT_ATTR}]`);
27669
- if (formEl && formKeyOf(formEl) && !onSuccessText) {
27856
+ if (formEl && formKeyOf(formEl) && !onSuccessText && !isCtaButton(target)) {
27670
27857
  const fieldEl = getFieldWrapper(target);
27671
27858
  if (fieldEl) {
27672
27859
  const label = target.closest("label");
@@ -29559,6 +29746,7 @@ function OhhwellsBridge() {
29559
29746
  brandKitRef.current = value;
29560
29747
  applyBrandToDom(parseBrandKit(value));
29561
29748
  if (aiSectionsRef.current) applyAiSectionsToDom(parseAiSectionsState(aiSectionsRef.current));
29749
+ applyAiFooterFromContent(editContentRef.current);
29562
29750
  applyStylesToDom(parseStyleStore(stylesRef.current));
29563
29751
  postToParentRef.current({ type: "ow:change", nodes: [{ key: BRAND_KIT_KEY, text: value }] });
29564
29752
  postToParentRef.current({ type: "ow:ai-brand-applied", previous, value });
@@ -30190,9 +30378,10 @@ function OhhwellsBridge() {
30190
30378
  if (e.data?.type !== "ow:collect-section") return;
30191
30379
  const sectionId = e.data.sectionId;
30192
30380
  if (typeof sectionId !== "string" || !sectionId) return;
30193
- const sectionEl = document.querySelector(
30381
+ const foundEl = document.querySelector(
30194
30382
  `[data-ohw-section="${CSS.escape(sectionId)}"]`
30195
30383
  );
30384
+ 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;
30196
30385
  const nodes = sectionEl ? collectEditableNodes(void 0, sectionEl).filter(
30197
30386
  (n) => n.key && !n.key.startsWith("__ohw_") && !["meta", "sections", "video-setting"].includes(n.type)
30198
30387
  ).map((n) => ({ ...n, text: n.text ?? "", type: normalizeAiNodeType(n.type) })) : [];
@@ -30931,7 +31120,7 @@ function OhhwellsBridge() {
30931
31120
  postToParent2({
30932
31121
  type: "ow:ready",
30933
31122
  version: "1",
30934
- bridgeVersion: "0.1.138",
31123
+ bridgeVersion: "0.1.140",
30935
31124
  path: pathname,
30936
31125
  nodes: collectEditableNodes(editContentRef.current),
30937
31126
  sections
@@ -32146,6 +32335,7 @@ export {
32146
32335
  dropIndicatorVariants,
32147
32336
  ensureFooterTree,
32148
32337
  filterAvailablePages,
32338
+ foldAlignIntoTrees,
32149
32339
  getEditModeInitialState,
32150
32340
  insertFooterHours,
32151
32341
  isEditSessionActive,
@@ -32154,6 +32344,7 @@ export {
32154
32344
  isValidUrl,
32155
32345
  nextViableSlide,
32156
32346
  parseAiFooterState,
32347
+ parseAiSectionsState,
32157
32348
  parseTarget,
32158
32349
  removeFooterBlock,
32159
32350
  resolveFooterCopyright,