@ohhwells/bridge 0.1.139 → 0.1.140

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
@@ -2919,14 +2919,22 @@ function isInactiveFooterChrome(el) {
2919
2919
  if (footer && footer.style.display === "none") return true;
2920
2920
  return false;
2921
2921
  }
2922
+ function isIncidentalFooter(el) {
2923
+ return Boolean(el.closest("blockquote, article, figure"));
2924
+ }
2922
2925
  function getFooterRoot() {
2923
2926
  const ai = document.querySelector("footer[data-ai-footer]");
2924
2927
  if (ai) return ai;
2925
2928
  for (const el of Array.from(
2926
- document.querySelectorAll('footer[data-ohw-section="footer"], footer')
2929
+ document.querySelectorAll('footer[data-ohw-section="footer"]')
2927
2930
  )) {
2931
+ if (isInactiveFooterChrome(el)) continue;
2932
+ return el;
2933
+ }
2934
+ for (const el of Array.from(document.querySelectorAll("footer"))) {
2928
2935
  if (el.hasAttribute("data-ai-footer")) continue;
2929
2936
  if (isInactiveFooterChrome(el)) continue;
2937
+ if (isIncidentalFooter(el)) continue;
2930
2938
  return el;
2931
2939
  }
2932
2940
  return null;
@@ -3337,8 +3345,8 @@ function buildFooterLink(colIndex, itemIndex, href, label, template) {
3337
3345
  return anchor;
3338
3346
  }
3339
3347
  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;
3348
+ const scope = getFooterLinksContainer() ?? getFooterRoot() ?? document;
3349
+ 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
3350
  }
3343
3351
  function getFooterLinkTemplate() {
3344
3352
  for (const col of listFooterColumns()) {
@@ -6355,6 +6363,20 @@ function applyBrandChrome(content) {
6355
6363
 
6356
6364
  // src/lib/brand-kit.ts
6357
6365
  var BRAND_KIT_KEY = "__ohw_brand";
6366
+ var appliedPalette = null;
6367
+ var appliedFonts = null;
6368
+ function getAppliedBrand() {
6369
+ if (!appliedPalette) return null;
6370
+ return appliedFonts ? { palette: appliedPalette, fonts: appliedFonts } : { palette: appliedPalette };
6371
+ }
6372
+ function brandForGenerated(fallback) {
6373
+ const applied = getAppliedBrand();
6374
+ if (!applied) return fallback ?? void 0;
6375
+ return {
6376
+ palette: applied.palette,
6377
+ fonts: applied.fonts ?? fallback?.fonts ?? AI_DEFAULT_BRAND.fonts
6378
+ };
6379
+ }
6358
6380
  var BRAND_VAR_PREFIX = "--ohw-brand-";
6359
6381
  var BRAND_VAR_NAMES = ["primary", "accent", "light", "dark", "surface", "border", "muted"].map(
6360
6382
  (role) => `${BRAND_VAR_PREFIX}${role}`
@@ -6658,6 +6680,8 @@ function loadCustomFonts(fonts) {
6658
6680
  function applyBrandToDom(kit) {
6659
6681
  const root = document.documentElement;
6660
6682
  if (!kit) {
6683
+ appliedPalette = null;
6684
+ appliedFonts = null;
6661
6685
  for (const name of [...BRAND_VAR_NAMES, ...LEGACY_BRAND_VAR_NAMES, ...BUTTON_VAR_NAMES]) {
6662
6686
  root.style.removeProperty(name);
6663
6687
  }
@@ -6671,11 +6695,16 @@ function applyBrandToDom(kit) {
6671
6695
  const applyFonts = kit.touched ? kit.touched.fonts === true : true;
6672
6696
  if (kit.palette) {
6673
6697
  if (applyPalette) {
6698
+ appliedPalette = kit.palette;
6674
6699
  for (const [name, value] of Object.entries(brandColorVars(kit.palette))) root.style.setProperty(name, value);
6675
6700
  } else {
6701
+ appliedPalette = null;
6676
6702
  for (const name of [...BRAND_VAR_NAMES, ...LEGACY_BRAND_VAR_NAMES]) root.style.removeProperty(name);
6677
6703
  }
6678
6704
  }
6705
+ if (kit.fonts) {
6706
+ appliedFonts = applyFonts ? kit.fonts : null;
6707
+ }
6679
6708
  if (kit.buttons) {
6680
6709
  for (const name of BUTTON_VAR_NAMES) root.style.removeProperty(name);
6681
6710
  for (const [name, value] of Object.entries(buttonStyleVars({ ...kit, buttons: kit.buttons }))) root.style.setProperty(name, value);
@@ -7208,6 +7237,35 @@ function withThemeFonts(brand) {
7208
7237
  }
7209
7238
  };
7210
7239
  }
7240
+ function liveBrandColor(role, fallback) {
7241
+ if (role === "dark") {
7242
+ return `var(--brand-text, var(--ohw-brand-dark, var(--color-dark, ${fallback})))`;
7243
+ }
7244
+ if (role === "light") {
7245
+ return `var(--brand-background, var(--ohw-brand-light, var(--color-light, ${fallback})))`;
7246
+ }
7247
+ return `var(--brand-${role}, var(--ohw-brand-${role}, var(--color-${role}, ${fallback})))`;
7248
+ }
7249
+ function withLivePalette(brand, source) {
7250
+ const sourceOf = (color) => {
7251
+ const value = color.trim().toLowerCase();
7252
+ const entries = Object.entries(source.palette);
7253
+ return entries.find(([, hex]) => hex.trim().toLowerCase() === value)?.[0] ?? null;
7254
+ };
7255
+ const map = (color) => {
7256
+ const role = sourceOf(color);
7257
+ return role ? liveBrandColor(role, source.palette[role]) : color;
7258
+ };
7259
+ return {
7260
+ ...brand,
7261
+ palette: {
7262
+ dark: map(brand.palette.dark),
7263
+ primary: map(brand.palette.primary),
7264
+ accent: map(brand.palette.accent),
7265
+ light: map(brand.palette.light)
7266
+ }
7267
+ };
7268
+ }
7211
7269
  function footerLogoStyle(fallbackFont, fallbackColor) {
7212
7270
  return {
7213
7271
  fontFamily: `var(--brand-font-heading, var(--font-heading, var(--font-display, ${quotedFont(fallbackFont)})))`,
@@ -7425,7 +7483,7 @@ function hexContrast(a, b) {
7425
7483
  function primaryButtonLabel(brand) {
7426
7484
  const darkC = hexContrast(brand.palette.primary, brand.palette.dark);
7427
7485
  const lightC = hexContrast(brand.palette.primary, AI_TREE_TOKENS.textPrimaryForeground);
7428
- if (darkC === null || lightC === null) return AI_TREE_TOKENS.textPrimaryForeground;
7486
+ if (darkC === null || lightC === null) return `var(--brand-on-primary, ${AI_TREE_TOKENS.textPrimaryForeground})`;
7429
7487
  return darkC > lightC ? brand.palette.dark : AI_TREE_TOKENS.textPrimaryForeground;
7430
7488
  }
7431
7489
  function accentBandContext(brand) {
@@ -7449,7 +7507,11 @@ function readableForeground(bg, brand) {
7449
7507
  return darkC > lightC ? brand.palette.dark : AI_TREE_TOKENS.textPrimaryForeground;
7450
7508
  }
7451
7509
  function hexWithAlpha(hex, alpha) {
7452
- const m = /^#([0-9a-f]{6})$/i.exec(hex.trim());
7510
+ const value = hex.trim();
7511
+ if (value.startsWith("var(") || value.startsWith("color-mix(")) {
7512
+ return `color-mix(in srgb, ${value} ${Math.round(alpha * 100)}%, transparent)`;
7513
+ }
7514
+ const m = /^#([0-9a-f]{6})$/i.exec(value);
7453
7515
  if (!m) return hex;
7454
7516
  const r2 = parseInt(m[1].slice(0, 2), 16);
7455
7517
  const g = parseInt(m[1].slice(2, 4), 16);
@@ -7722,12 +7784,10 @@ function ButtonEl({
7722
7784
  cursor: "pointer",
7723
7785
  whiteSpace: "nowrap",
7724
7786
  ...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
- ))
7787
+ ...tokenColorStyle ?? (secondary ? { border: `1px solid ${secondaryColor ?? ctx.brand.palette.dark}`, color: secondaryColor ?? ctx.brand.palette.dark } : {
7788
+ background: ctx.brand.palette.primary,
7789
+ color: ctx.buttonLabel ?? primaryButtonLabel(ctx.brand)
7790
+ })
7731
7791
  },
7732
7792
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { ...path ? textAttrs(ctx, `${path}.label`) : {}, children: str(slots.label) })
7733
7793
  }
@@ -9572,10 +9632,10 @@ function renderNode(node, ctx, path, ambientAlign) {
9572
9632
  ...buttonShellStyle(ctx, "primary"),
9573
9633
  // Brand-styled: the template button's measured fill/label pair off-band, else
9574
9634
  // 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 } : {
9635
+ ...buttonColorStyle(ctx, "primary") ?? {
9576
9636
  background: ctx.brand.palette.primary,
9577
9637
  color: ctx.buttonLabel ?? primaryButtonLabel(ctx.brand)
9578
- })
9638
+ }
9579
9639
  },
9580
9640
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { ...textAttrs(ctx, `${path}.c${i}.label`), children: str(cs.label) })
9581
9641
  },
@@ -9619,7 +9679,7 @@ function AiTreeRenderer({
9619
9679
  }
9620
9680
  const resolvedBrand = withThemeFonts(brand ?? AI_DEFAULT_BRAND);
9621
9681
  const band = (tree.settings ?? {}).sectionBackground === "accent" ? accentBandContext(resolvedBrand) : null;
9622
- const blockBrand = band?.brand ?? resolvedBrand;
9682
+ const blockBrand = withLivePalette(band?.brand ?? resolvedBrand, resolvedBrand);
9623
9683
  const placeholderMap = buildPlaceholderMap(tree);
9624
9684
  const hasOverlapBackground = tree.scaffold === "overlay" && tree.rows.some(
9625
9685
  (row) => (row.blocks ?? []).some((b) => b.type === "group" && str(b.slots?.layout) === "overlap")
@@ -9635,7 +9695,7 @@ function AiTreeRenderer({
9635
9695
  // standalone resolve), so generated galleries, image rows, and overlay backgrounds arrive with
9636
9696
  // photos instead of grey boxes.
9637
9697
  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})`,
9698
+ cardSurface: resolvedBrand.palette.light.toUpperCase() === AI_DEFAULT_BRAND.palette.light.toUpperCase() ? "#ECECEB" : `color-mix(in srgb, ${blockBrand.palette.light} 90%, ${blockBrand.palette.dark})`,
9639
9699
  keyFor: editKeyPrefix ? (path) => `${editKeyPrefix}.${path}` : null,
9640
9700
  sectionAlignment: (tree.settings ?? {}).alignment === "center" ? "center" : "left",
9641
9701
  buttonStyle,
@@ -9652,7 +9712,7 @@ function AiTreeRenderer({
9652
9712
  const backgroundUrl = isOverlay && settings.backgroundMedia ? ctx.resolveMedia(settings.backgroundMedia) : null;
9653
9713
  const bgAttrs = ctx.keyFor && isOverlay && settings.backgroundMedia ? { "data-ohw-key": ctx.keyFor("settings.backgroundMedia"), "data-ohw-editable": "bg-image" } : {};
9654
9714
  const toneBackground = (() => {
9655
- const { dark, primary, light } = resolvedBrand.palette;
9715
+ const { dark, primary, light } = blockBrand.palette;
9656
9716
  switch (settings.sectionBackground) {
9657
9717
  case "surface":
9658
9718
  return `color-mix(in srgb, ${light} 94%, ${dark})`;
@@ -9773,14 +9833,15 @@ function AiFooterRenderer({
9773
9833
  if (!isRenderableFooterTree(tree)) {
9774
9834
  return null;
9775
9835
  }
9776
- const resolvedBrand = withThemeFonts(brand ?? AI_DEFAULT_BRAND);
9836
+ const hexBrand = withThemeFonts(brand ?? AI_DEFAULT_BRAND);
9837
+ const resolvedBrand = withLivePalette(hexBrand, brand ?? AI_DEFAULT_BRAND);
9777
9838
  const bandBg = surface?.background ?? resolvedBrand.palette.dark;
9778
9839
  const bandText = surface?.text ?? resolvedBrand.palette.light;
9779
9840
  const onDark = surface ? isDarkColor(surface.background) : true;
9780
9841
  const ctx = {
9781
9842
  brand: resolvedBrand,
9782
9843
  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})`,
9844
+ cardSurface: hexBrand.palette.light.toUpperCase() === AI_DEFAULT_BRAND.palette.light.toUpperCase() ? "#ECECEB" : `color-mix(in srgb, ${resolvedBrand.palette.light} 90%, ${resolvedBrand.palette.dark})`,
9784
9845
  keyFor: editKeyPrefix ? (path) => `${editKeyPrefix}.${path}` : null,
9785
9846
  logoUrl: logoUrl ?? null,
9786
9847
  resolvePageHref
@@ -10356,10 +10417,9 @@ function syncSoftRemovedGenerated() {
10356
10417
  }
10357
10418
  function applyAiSectionsToDom(state, options) {
10358
10419
  if (typeof document === "undefined") return;
10359
- const brandOverride = deriveBrandOverride();
10360
10420
  const templateBrand = deriveTemplateBrand();
10361
10421
  const templateButtonStyle = deriveTemplateButtonStyle();
10362
- const brandKey = brandOverride ? JSON.stringify(brandOverride) : "";
10422
+ const brandKey = JSON.stringify(getAppliedBrand() ?? deriveBrandOverride() ?? null);
10363
10423
  const pagePath = window.location.pathname;
10364
10424
  const pageSections = state.sections.filter((entry) => (entry.path ?? "/") === pagePath);
10365
10425
  const activeIds = new Set(pageSections.map((entry) => entry.id));
@@ -10402,7 +10462,7 @@ function applyAiSectionsToDom(state, options) {
10402
10462
  AiTreeRenderer,
10403
10463
  {
10404
10464
  tree: entry.tree,
10405
- brand: brandOverride ?? entry.brand ?? options?.brand ?? templateBrand ?? void 0,
10465
+ brand: brandForGenerated(deriveBrandOverride() ?? entry.brand ?? options?.brand ?? templateBrand),
10406
10466
  buttonStyle: templateButtonStyle,
10407
10467
  resolveMedia,
10408
10468
  editKeyPrefix: `ai.${entry.id}`
@@ -10568,7 +10628,7 @@ function applyAiFooterToDom(state, content) {
10568
10628
  const templateFooter = findTemplateFooter();
10569
10629
  captureTemplateFooterSurface(templateFooter);
10570
10630
  const surface = capturedSurface ?? null;
10571
- const brand = mountOptions.brand ?? deriveTemplateBrand();
10631
+ const brand = brandForGenerated(mountOptions.brand ?? deriveTemplateBrand());
10572
10632
  const logoUrl = resolveLogoFromContent(content, templateFooter);
10573
10633
  const serialized = JSON.stringify(state) + JSON.stringify(brand ?? null) + (logoUrl ?? "") + JSON.stringify(surface) + JSON.stringify(defaultLegal());
10574
10634
  if (mounted2 && mounted2.serialized === serialized && mounted2.container.isConnected) {
@@ -27281,7 +27341,7 @@ function OhhwellsBridge() {
27281
27341
  if (!subdomain) return {};
27282
27342
  return contentCache.get(subdomain) ?? {};
27283
27343
  };
27284
- const observeRoots = () => [document.querySelector("nav"), document.querySelector("footer")].filter(
27344
+ const observeRoots = () => [document.querySelector("nav"), getFooterRoot()].filter(
27285
27345
  (el) => Boolean(el)
27286
27346
  );
27287
27347
  const roots = observeRoots();
@@ -27752,7 +27812,7 @@ function OhhwellsBridge() {
27752
27812
  {
27753
27813
  const formEl = getFormElement(target);
27754
27814
  const onSuccessText = target.closest(`[${SUCCESS_TEXT_ATTR}]`);
27755
- if (formEl && formKeyOf(formEl) && !onSuccessText) {
27815
+ if (formEl && formKeyOf(formEl) && !onSuccessText && !isCtaButton(target)) {
27756
27816
  const fieldEl = getFieldWrapper(target);
27757
27817
  if (fieldEl) {
27758
27818
  const label = target.closest("label");
@@ -29645,6 +29705,7 @@ function OhhwellsBridge() {
29645
29705
  brandKitRef.current = value;
29646
29706
  applyBrandToDom(parseBrandKit(value));
29647
29707
  if (aiSectionsRef.current) applyAiSectionsToDom(parseAiSectionsState(aiSectionsRef.current));
29708
+ applyAiFooterFromContent(editContentRef.current);
29648
29709
  applyStylesToDom(parseStyleStore(stylesRef.current));
29649
29710
  postToParentRef.current({ type: "ow:change", nodes: [{ key: BRAND_KIT_KEY, text: value }] });
29650
29711
  postToParentRef.current({ type: "ow:ai-brand-applied", previous, value });
@@ -31017,7 +31078,7 @@ function OhhwellsBridge() {
31017
31078
  postToParent2({
31018
31079
  type: "ow:ready",
31019
31080
  version: "1",
31020
- bridgeVersion: "0.1.138",
31081
+ bridgeVersion: "0.1.139",
31021
31082
  path: pathname,
31022
31083
  nodes: collectEditableNodes(editContentRef.current),
31023
31084
  sections