@ohhwells/bridge 0.1.95 → 0.1.96

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.d.cts CHANGED
@@ -76,6 +76,20 @@ interface AiBrandKit {
76
76
  body: string;
77
77
  };
78
78
  }
79
+ /**
80
+ * Shape, padding and typography measured off a host-template CTA, so generated buttons read as the
81
+ * site's own button rather than a default. Colour stays brand-driven (band-aware); everything here
82
+ * follows the template button. Null when the page has no template button to measure.
83
+ */
84
+ interface TemplateButtonStyle {
85
+ radius: string;
86
+ padding: string;
87
+ fontFamily: string;
88
+ fontSize: string;
89
+ fontWeight: string;
90
+ letterSpacing: string;
91
+ textTransform: string;
92
+ }
79
93
  /** Section System defaults — used when a site has no brand kit override. */
80
94
  declare const AI_DEFAULT_BRAND: AiBrandKit;
81
95
  /** Design tokens from the Section System Blocks page (px unless noted). */
@@ -143,8 +157,8 @@ declare function isRenderableTree(value: unknown): value is AiLayoutTree;
143
157
  interface AiTreeRendererProps {
144
158
  tree: AiLayoutTree;
145
159
  brand?: AiBrandKit;
146
- /** The host template's button corner radius (e.g. "0px", "8px"); a form's submit adopts it. */
147
- buttonRadius?: string | null;
160
+ /** The host template's button shape/padding/typography; generated CTAs adopt it. */
161
+ buttonStyle?: TemplateButtonStyle | null;
148
162
  /** Resolves a media ref to a URL; null renders a placeholder. */
149
163
  resolveMedia?: (ref: string) => string | null;
150
164
  /**
@@ -154,7 +168,7 @@ interface AiTreeRendererProps {
154
168
  */
155
169
  editKeyPrefix?: string;
156
170
  }
157
- declare function AiTreeRenderer({ tree, brand, buttonRadius, resolveMedia, editKeyPrefix, }: AiTreeRendererProps): react_jsx_runtime.JSX.Element | null;
171
+ declare function AiTreeRenderer({ tree, brand, buttonStyle, resolveMedia, editKeyPrefix, }: AiTreeRendererProps): react_jsx_runtime.JSX.Element | null;
158
172
 
159
173
  type LinkPage = {
160
174
  path: string;
package/dist/index.d.ts CHANGED
@@ -76,6 +76,20 @@ interface AiBrandKit {
76
76
  body: string;
77
77
  };
78
78
  }
79
+ /**
80
+ * Shape, padding and typography measured off a host-template CTA, so generated buttons read as the
81
+ * site's own button rather than a default. Colour stays brand-driven (band-aware); everything here
82
+ * follows the template button. Null when the page has no template button to measure.
83
+ */
84
+ interface TemplateButtonStyle {
85
+ radius: string;
86
+ padding: string;
87
+ fontFamily: string;
88
+ fontSize: string;
89
+ fontWeight: string;
90
+ letterSpacing: string;
91
+ textTransform: string;
92
+ }
79
93
  /** Section System defaults — used when a site has no brand kit override. */
80
94
  declare const AI_DEFAULT_BRAND: AiBrandKit;
81
95
  /** Design tokens from the Section System Blocks page (px unless noted). */
@@ -143,8 +157,8 @@ declare function isRenderableTree(value: unknown): value is AiLayoutTree;
143
157
  interface AiTreeRendererProps {
144
158
  tree: AiLayoutTree;
145
159
  brand?: AiBrandKit;
146
- /** The host template's button corner radius (e.g. "0px", "8px"); a form's submit adopts it. */
147
- buttonRadius?: string | null;
160
+ /** The host template's button shape/padding/typography; generated CTAs adopt it. */
161
+ buttonStyle?: TemplateButtonStyle | null;
148
162
  /** Resolves a media ref to a URL; null renders a placeholder. */
149
163
  resolveMedia?: (ref: string) => string | null;
150
164
  /**
@@ -154,7 +168,7 @@ interface AiTreeRendererProps {
154
168
  */
155
169
  editKeyPrefix?: string;
156
170
  }
157
- declare function AiTreeRenderer({ tree, brand, buttonRadius, resolveMedia, editKeyPrefix, }: AiTreeRendererProps): react_jsx_runtime.JSX.Element | null;
171
+ declare function AiTreeRenderer({ tree, brand, buttonStyle, resolveMedia, editKeyPrefix, }: AiTreeRendererProps): react_jsx_runtime.JSX.Element | null;
158
172
 
159
173
  type LinkPage = {
160
174
  path: string;
package/dist/index.js CHANGED
@@ -879,6 +879,25 @@ var FEATURE_LINE_CSS = [
879
879
  `background-color:currentColor;-webkit-mask:url("${CHECK_MASK}") no-repeat 0 0/24px 24px;`,
880
880
  `mask:url("${CHECK_MASK}") no-repeat 0 0/24px 24px}`
881
881
  ].join("");
882
+ function buttonShellStyle(ctx, fullWidth) {
883
+ const bs = ctx.buttonStyle;
884
+ if (bs) {
885
+ return {
886
+ borderRadius: bs.radius,
887
+ ...bs.padding ? { padding: bs.padding } : {},
888
+ ...bs.fontFamily ? { fontFamily: bs.fontFamily } : { fontFamily: ctx.brand.fonts.body },
889
+ ...bs.fontSize ? { fontSize: bs.fontSize } : {},
890
+ ...bs.fontWeight ? { fontWeight: bs.fontWeight } : {},
891
+ ...bs.letterSpacing && bs.letterSpacing !== "normal" ? { letterSpacing: bs.letterSpacing } : {},
892
+ ...bs.textTransform && bs.textTransform !== "none" ? { textTransform: bs.textTransform } : {}
893
+ };
894
+ }
895
+ return {
896
+ borderRadius: AI_TREE_TOKENS.radiusButton,
897
+ padding: fullWidth ? `${AI_TREE_TOKENS.spacing2}px ${AI_TREE_TOKENS.spacing4}px` : `${AI_TREE_TOKENS.spacing3}px ${AI_TREE_TOKENS.spacing6}px`,
898
+ ...typeStyle(AI_TREE_TOKENS.type.button, ctx.brand.fonts.body)
899
+ };
900
+ }
882
901
  function hexLuminance(color) {
883
902
  const m = /^#([0-9a-f]{6})$/i.exec(color.trim());
884
903
  if (!m) return null;
@@ -895,6 +914,12 @@ function hexContrast(a, b) {
895
914
  const [hi, lo] = la > lb ? [la, lb] : [lb, la];
896
915
  return (hi + 0.05) / (lo + 0.05);
897
916
  }
917
+ function primaryButtonLabel(brand) {
918
+ const darkC = hexContrast(brand.palette.primary, brand.palette.dark);
919
+ const lightC = hexContrast(brand.palette.primary, AI_TREE_TOKENS.textPrimaryForeground);
920
+ if (darkC === null || lightC === null) return AI_TREE_TOKENS.textPrimaryForeground;
921
+ return darkC > lightC ? brand.palette.dark : AI_TREE_TOKENS.textPrimaryForeground;
922
+ }
898
923
  function accentBandContext(brand) {
899
924
  const p = brand.palette;
900
925
  const lightWins = (hexContrast(p.light, p.primary) ?? 99) >= (hexContrast(p.dark, p.primary) ?? 0);
@@ -994,12 +1019,10 @@ function ButtonEl({
994
1019
  width: fullWidth ? "100%" : void 0,
995
1020
  alignItems: "center",
996
1021
  justifyContent: "center",
997
- padding: fullWidth ? `${AI_TREE_TOKENS.spacing2}px ${AI_TREE_TOKENS.spacing4}px` : `${AI_TREE_TOKENS.spacing3}px ${AI_TREE_TOKENS.spacing6}px`,
998
- borderRadius: AI_TREE_TOKENS.radiusButton,
999
1022
  textDecoration: "none",
1000
1023
  cursor: "pointer",
1001
- ...typeStyle(AI_TREE_TOKENS.type.button, ctx.brand.fonts.body),
1002
- ...secondary ? { border: `1px solid ${ctx.brand.palette.dark}`, color: ctx.brand.palette.dark } : { background: ctx.brand.palette.primary, color: ctx.buttonLabel ?? AI_TREE_TOKENS.textPrimaryForeground }
1024
+ ...buttonShellStyle(ctx, fullWidth),
1025
+ ...secondary ? { border: `1px solid ${ctx.brand.palette.dark}`, color: ctx.brand.palette.dark } : { background: ctx.brand.palette.primary, color: ctx.buttonLabel ?? primaryButtonLabel(ctx.brand) }
1003
1026
  },
1004
1027
  children: /* @__PURE__ */ jsx("span", { ...path ? textAttrs(ctx, `${path}.label`) : {}, children: str(slots.label) })
1005
1028
  }
@@ -2039,15 +2062,12 @@ function renderNode(node, ctx, path) {
2039
2062
  alignSelf: submitAlign,
2040
2063
  border: "none",
2041
2064
  cursor: "pointer",
2042
- padding: "12px 24px",
2043
- // Corner radius follows the host template's own buttons (measured from a template
2044
- // CTA); 8px only when the page has no template button to match.
2045
- borderRadius: ctx.buttonRadius ?? 8,
2065
+ // Shape/padding/typography follow the host template's own buttons.
2066
+ ...buttonShellStyle(ctx),
2046
2067
  // Brand-styled: primary fill, brand-derived label colour (not a fixed token) so it
2047
2068
  // reads correctly on custom palettes.
2048
2069
  background: ctx.brand.palette.primary,
2049
- color: ctx.buttonLabel ?? ctx.brand.palette.light,
2050
- ...typeStyle(AI_TREE_TOKENS.type.button, ctx.brand.fonts.body)
2070
+ color: ctx.buttonLabel ?? primaryButtonLabel(ctx.brand)
2051
2071
  },
2052
2072
  children: /* @__PURE__ */ jsx("span", { ...textAttrs(ctx, `${path}.c${i}.label`), children: str(cs.label) })
2053
2073
  },
@@ -2082,7 +2102,7 @@ function renderNode(node, ctx, path) {
2082
2102
  function AiTreeRenderer({
2083
2103
  tree,
2084
2104
  brand,
2085
- buttonRadius,
2105
+ buttonStyle,
2086
2106
  resolveMedia,
2087
2107
  editKeyPrefix
2088
2108
  }) {
@@ -2098,7 +2118,7 @@ function AiTreeRenderer({
2098
2118
  cardSurface: blockBrand.palette.light.toUpperCase() === AI_DEFAULT_BRAND.palette.light.toUpperCase() ? "#ECECEB" : `color-mix(in srgb, ${blockBrand.palette.light} 90%, ${blockBrand.palette.dark})`,
2099
2119
  keyFor: editKeyPrefix ? (path) => `${editKeyPrefix}.${path}` : null,
2100
2120
  sectionAlignment: (tree.settings ?? {}).alignment === "center" ? "center" : "left",
2101
- buttonRadius,
2121
+ buttonStyle,
2102
2122
  ...band ? { buttonLabel: band.buttonLabel } : {}
2103
2123
  };
2104
2124
  const settings = tree.settings ?? {};
@@ -2228,13 +2248,13 @@ function deriveBrandOverride() {
2228
2248
  };
2229
2249
  }
2230
2250
  function deriveTemplateBrand() {
2231
- const dark = readRootVar("--color-dark");
2232
- const primary = readRootVar("--color-primary");
2233
- const light = readRootVar("--color-light");
2251
+ const primary = readRootVar("--brand-primary") || readRootVar("--color-primary");
2252
+ const dark = readRootVar("--brand-text") || readRootVar("--color-dark");
2253
+ const light = readRootVar("--brand-background") || readRootVar("--color-light");
2234
2254
  if (!dark || !primary || !light) return null;
2235
- const accent = readRootVar("--color-accent");
2236
- const heading = readRootVar("--font-heading") || readRootVar("--font-display");
2237
- const body = readRootVar("--font-body");
2255
+ const accent = readRootVar("--brand-accent") || readRootVar("--color-accent");
2256
+ const heading = readRootVar("--brand-font-heading") || readRootVar("--font-heading") || readRootVar("--font-display");
2257
+ const body = readRootVar("--brand-font-body") || readRootVar("--font-body");
2238
2258
  return {
2239
2259
  palette: { dark, primary, accent: accent || dark, light },
2240
2260
  fonts: {
@@ -2243,12 +2263,32 @@ function deriveTemplateBrand() {
2243
2263
  }
2244
2264
  };
2245
2265
  }
2246
- function deriveTemplateButtonRadius() {
2266
+ function deriveTemplateButtonStyle() {
2247
2267
  if (typeof document === "undefined") return null;
2248
- const btn = document.querySelector('[data-ohw-role="button"]');
2268
+ const btn = Array.from(document.querySelectorAll('[data-ohw-role="button"]')).find(
2269
+ (el) => !el.closest(`[${CONTAINER_ATTR}]`)
2270
+ );
2249
2271
  if (!btn) return null;
2250
- const radius = getComputedStyle(btn).borderTopLeftRadius;
2251
- return radius || null;
2272
+ const cs = getComputedStyle(btn);
2273
+ const corners = [
2274
+ cs.borderTopLeftRadius,
2275
+ cs.borderTopRightRadius,
2276
+ cs.borderBottomRightRadius,
2277
+ cs.borderBottomLeftRadius
2278
+ ].map((v) => v || "0px");
2279
+ const radius = corners.every((v) => v === corners[0]) ? corners[0] : corners.join(" ");
2280
+ const px = (v) => parseFloat(v) || 0;
2281
+ const padY = Math.max(px(cs.paddingTop), px(cs.paddingBottom));
2282
+ const padX = Math.max(px(cs.paddingLeft), px(cs.paddingRight));
2283
+ return {
2284
+ radius: radius || "10px",
2285
+ padding: `${padY}px ${padX}px`,
2286
+ fontFamily: cs.fontFamily || "",
2287
+ fontSize: cs.fontSize || "",
2288
+ fontWeight: cs.fontWeight || "",
2289
+ letterSpacing: cs.letterSpacing || "",
2290
+ textTransform: cs.textTransform || ""
2291
+ };
2252
2292
  }
2253
2293
  var mounted = /* @__PURE__ */ new Map();
2254
2294
  function findTemplateSection(id) {
@@ -2419,7 +2459,7 @@ function applyAiSectionsToDom(state, options) {
2419
2459
  if (typeof document === "undefined") return;
2420
2460
  const brandOverride = deriveBrandOverride();
2421
2461
  const templateBrand = deriveTemplateBrand();
2422
- const templateButtonRadius = deriveTemplateButtonRadius();
2462
+ const templateButtonStyle = deriveTemplateButtonStyle();
2423
2463
  const brandKey = brandOverride ? JSON.stringify(brandOverride) : "";
2424
2464
  const pagePath = window.location.pathname;
2425
2465
  const pageSections = state.sections.filter((entry) => (entry.path ?? "/") === pagePath);
@@ -2459,7 +2499,7 @@ function applyAiSectionsToDom(state, options) {
2459
2499
  {
2460
2500
  tree: entry.tree,
2461
2501
  brand: brandOverride ?? entry.brand ?? options?.brand ?? templateBrand ?? void 0,
2462
- buttonRadius: templateButtonRadius,
2502
+ buttonStyle: templateButtonStyle,
2463
2503
  resolveMedia,
2464
2504
  editKeyPrefix: `ai.${entry.id}`
2465
2505
  }
@@ -21602,7 +21642,7 @@ function OhhwellsBridge() {
21602
21642
  postToParent2({
21603
21643
  type: "ow:ready",
21604
21644
  version: "1",
21605
- bridgeVersion: "0.1.94",
21645
+ bridgeVersion: "0.1.95",
21606
21646
  path: pathname,
21607
21647
  nodes: collectEditableNodes(editContentRef.current),
21608
21648
  sections