@ohhwells/bridge 0.1.122 → 0.1.123

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
@@ -72,6 +72,21 @@ interface AiLayoutTree {
72
72
  align?: 'top' | 'center' | 'bottom' | 'stretch';
73
73
  }>;
74
74
  }
75
+ declare const BUTTON_FILL_TOKENS: readonly ["dark", "primary", "accent", "light"];
76
+ declare const BUTTON_STYLES: readonly ["solid", "outline", "ghost"];
77
+ declare const BUTTON_RADII: readonly ["square", "rounded", "pill"];
78
+ declare const BUTTON_SIZES: readonly ["s", "m", "l"];
79
+ interface ButtonStyle {
80
+ fill: (typeof BUTTON_FILL_TOKENS)[number];
81
+ textColor: (typeof BUTTON_FILL_TOKENS)[number];
82
+ style: (typeof BUTTON_STYLES)[number];
83
+ radius: (typeof BUTTON_RADII)[number];
84
+ size: (typeof BUTTON_SIZES)[number];
85
+ }
86
+ interface ButtonStyles {
87
+ primary: ButtonStyle;
88
+ secondary: ButtonStyle;
89
+ }
75
90
  /** One uploaded weight file for a custom font family. */
76
91
  interface AiCustomFontWeight {
77
92
  weight: number;
@@ -105,7 +120,20 @@ interface AiBrandKit {
105
120
  heading: string;
106
121
  body: string;
107
122
  };
123
+ /** Absent on a kit saved before OHH-791 — renderer falls back to today's hardcoded button look. */
124
+ buttons?: ButtonStyles;
108
125
  customFonts?: AiCustomFont[];
126
+ /** Which parts the user actually set. Absent (kits saved before this field) = both, as ever;
127
+ * an unset part is a carried-over template snapshot and must not be written onto the page. */
128
+ touched?: {
129
+ palette?: boolean;
130
+ fonts?: boolean;
131
+ /** Per variant, the button fields the user set (fill, textColor, style, radius, size). */
132
+ buttons?: {
133
+ primary?: (keyof ButtonStyle)[];
134
+ secondary?: (keyof ButtonStyle)[];
135
+ };
136
+ };
109
137
  }
110
138
  /**
111
139
  * Shape, padding and typography measured off a host-template CTA, so generated buttons read as the
package/dist/index.d.ts CHANGED
@@ -72,6 +72,21 @@ interface AiLayoutTree {
72
72
  align?: 'top' | 'center' | 'bottom' | 'stretch';
73
73
  }>;
74
74
  }
75
+ declare const BUTTON_FILL_TOKENS: readonly ["dark", "primary", "accent", "light"];
76
+ declare const BUTTON_STYLES: readonly ["solid", "outline", "ghost"];
77
+ declare const BUTTON_RADII: readonly ["square", "rounded", "pill"];
78
+ declare const BUTTON_SIZES: readonly ["s", "m", "l"];
79
+ interface ButtonStyle {
80
+ fill: (typeof BUTTON_FILL_TOKENS)[number];
81
+ textColor: (typeof BUTTON_FILL_TOKENS)[number];
82
+ style: (typeof BUTTON_STYLES)[number];
83
+ radius: (typeof BUTTON_RADII)[number];
84
+ size: (typeof BUTTON_SIZES)[number];
85
+ }
86
+ interface ButtonStyles {
87
+ primary: ButtonStyle;
88
+ secondary: ButtonStyle;
89
+ }
75
90
  /** One uploaded weight file for a custom font family. */
76
91
  interface AiCustomFontWeight {
77
92
  weight: number;
@@ -105,7 +120,20 @@ interface AiBrandKit {
105
120
  heading: string;
106
121
  body: string;
107
122
  };
123
+ /** Absent on a kit saved before OHH-791 — renderer falls back to today's hardcoded button look. */
124
+ buttons?: ButtonStyles;
108
125
  customFonts?: AiCustomFont[];
126
+ /** Which parts the user actually set. Absent (kits saved before this field) = both, as ever;
127
+ * an unset part is a carried-over template snapshot and must not be written onto the page. */
128
+ touched?: {
129
+ palette?: boolean;
130
+ fonts?: boolean;
131
+ /** Per variant, the button fields the user set (fill, textColor, style, radius, size). */
132
+ buttons?: {
133
+ primary?: (keyof ButtonStyle)[];
134
+ secondary?: (keyof ButtonStyle)[];
135
+ };
136
+ };
109
137
  }
110
138
  /**
111
139
  * Shape, padding and typography measured off a host-template CTA, so generated buttons read as the
package/dist/index.js CHANGED
@@ -31,6 +31,10 @@ function enforceLinkHrefs() {
31
31
 
32
32
  // src/lib/ai-tree.ts
33
33
  var AI_TREE_SCHEMA_VERSIONS = [1];
34
+ var BUTTON_FILL_TOKENS = ["dark", "primary", "accent", "light"];
35
+ var BUTTON_STYLES = ["solid", "outline", "ghost"];
36
+ var BUTTON_RADII = ["square", "rounded", "pill"];
37
+ var BUTTON_SIZES = ["s", "m", "l"];
34
38
  var AI_DEFAULT_BRAND = {
35
39
  palette: { dark: "#0C0A09", primary: "#0078E5", accent: "#747271", light: "#ECECEB" },
36
40
  fonts: {
@@ -290,6 +294,118 @@ var FONT_VARS = {
290
294
  var BRAND_FONT_LINK_ID = "ohw-brand-fonts";
291
295
  var CUSTOM_FONT_STYLE_ID = "ohw-brand-custom-fonts";
292
296
  var CUSTOM_FONT_FORMATS = /* @__PURE__ */ new Set(["woff2", "woff", "truetype", "opentype"]);
297
+ var BUTTON_RADIUS_PX = {
298
+ square: 0,
299
+ rounded: AI_TREE_TOKENS.radiusButton,
300
+ pill: 999
301
+ };
302
+ var BUTTON_SIZE_SPEC = {
303
+ s: {
304
+ paddingX: `${AI_TREE_TOKENS.spacing4}px`,
305
+ paddingY: `${AI_TREE_TOKENS.spacing2}px`,
306
+ fontSize: "14px"
307
+ },
308
+ m: {
309
+ paddingX: `${AI_TREE_TOKENS.spacing6}px`,
310
+ paddingY: `${AI_TREE_TOKENS.spacing3}px`,
311
+ fontSize: `${AI_TREE_TOKENS.type.button.size}px`
312
+ },
313
+ l: {
314
+ paddingX: `${AI_TREE_TOKENS.spacing8}px`,
315
+ paddingY: `${AI_TREE_TOKENS.spacing4}px`,
316
+ fontSize: "18px"
317
+ }
318
+ };
319
+ var BUTTON_SIZE_SCALE = { s: 0.85, m: 1, l: 1.25 };
320
+ var roundHalf = (n) => Math.round(n * 2) / 2;
321
+ function measureTemplateButton(variant) {
322
+ const root = document.documentElement;
323
+ const el = document.querySelector(`.btn--${variant}, .btn-${variant}`);
324
+ if (!el) return null;
325
+ const sheet = document.getElementById(BUTTON_RULES_STYLE_ID);
326
+ const rules = sheet?.textContent ?? "";
327
+ const saved = BUTTON_VAR_NAMES.map((name) => [name, root.style.getPropertyValue(name)]);
328
+ if (sheet) sheet.textContent = "";
329
+ for (const [name] of saved) root.style.removeProperty(name);
330
+ const cs = getComputedStyle(el);
331
+ const base = {
332
+ fontSize: parseFloat(cs.fontSize),
333
+ top: parseFloat(cs.paddingTop),
334
+ right: parseFloat(cs.paddingRight),
335
+ bottom: parseFloat(cs.paddingBottom),
336
+ left: parseFloat(cs.paddingLeft)
337
+ };
338
+ if (sheet) sheet.textContent = rules;
339
+ for (const [name, value] of saved) if (value) root.style.setProperty(name, value);
340
+ return Object.values(base).every(Number.isFinite) && base.fontSize > 0 ? base : null;
341
+ }
342
+ function scaledButtonSize(size, base) {
343
+ if (!base) return BUTTON_SIZE_SPEC[size];
344
+ const k = BUTTON_SIZE_SCALE[size];
345
+ const px2 = (n) => `${roundHalf(n * k)}px`;
346
+ return {
347
+ // Each side scaled on its own, so a button with uneven padding (say, room for an icon on one
348
+ // side) keeps its shape; the x/y vars are the averages for templates that read those instead.
349
+ padding: `${px2(base.top)} ${px2(base.right)} ${px2(base.bottom)} ${px2(base.left)}`,
350
+ paddingX: px2((base.left + base.right) / 2),
351
+ paddingY: px2((base.top + base.bottom) / 2),
352
+ fontSize: `${roundHalf(base.fontSize * k)}px`
353
+ };
354
+ }
355
+ var BUTTON_VAR_ROLES = ["bg", "text", "border", "radius", "padding-x", "padding-y", "padding", "font-size"];
356
+ var BUTTON_VAR_NAMES = ["primary", "secondary"].flatMap(
357
+ (variant) => BUTTON_VAR_ROLES.map((role) => `--brand-button-${variant}-${role}`)
358
+ );
359
+ var ROLES_BY_FIELD = {
360
+ fill: ["bg"],
361
+ style: ["bg", "border"],
362
+ textColor: ["text"],
363
+ radius: ["radius"],
364
+ size: ["padding-x", "padding-y", "padding", "font-size"]
365
+ };
366
+ function touchedButtonRoles(variant, kit) {
367
+ if (!kit.touched) return new Set(BUTTON_VAR_ROLES);
368
+ const fields = kit.touched.buttons?.[variant] ?? [];
369
+ const outline = fields.includes("style") && kit.buttons?.[variant]?.style === "outline";
370
+ return new Set(
371
+ fields.flatMap((f) => {
372
+ if (outline && (f === "fill" || f === "style")) return ["border"];
373
+ return ROLES_BY_FIELD[f] ?? [];
374
+ })
375
+ );
376
+ }
377
+ function buttonPalette(kit) {
378
+ return kit.palette ?? {
379
+ dark: "var(--brand-text)",
380
+ primary: "var(--brand-primary)",
381
+ accent: "var(--brand-accent)",
382
+ light: "var(--brand-background)"
383
+ };
384
+ }
385
+ function buttonVariantVars(variant, style, roles, palette) {
386
+ const fill = palette[style.fill];
387
+ const text = palette[style.textColor];
388
+ const size = scaledButtonSize(style.size, roles.has("font-size") ? measureTemplateButton(variant) : null);
389
+ const { bg, border } = !roles.has("border") ? { bg: fill, border: "transparent" } : style.style === "outline" ? { bg: "transparent", border: fill } : style.style === "ghost" ? { bg: "transparent", border: "transparent" } : { bg: fill, border: "transparent" };
390
+ const all = {
391
+ [`--brand-button-${variant}-bg`]: bg,
392
+ [`--brand-button-${variant}-text`]: text,
393
+ [`--brand-button-${variant}-border`]: border,
394
+ [`--brand-button-${variant}-radius`]: `${BUTTON_RADIUS_PX[style.radius]}px`,
395
+ [`--brand-button-${variant}-padding-x`]: size.paddingX,
396
+ [`--brand-button-${variant}-padding-y`]: size.paddingY,
397
+ ...size.padding ? { [`--brand-button-${variant}-padding`]: size.padding } : {},
398
+ [`--brand-button-${variant}-font-size`]: size.fontSize
399
+ };
400
+ const prefix = `--brand-button-${variant}-`;
401
+ return Object.fromEntries(Object.entries(all).filter(([name]) => roles.has(name.slice(prefix.length))));
402
+ }
403
+ function buttonStyleVars(kit) {
404
+ return {
405
+ ...buttonVariantVars("primary", kit.buttons.primary, touchedButtonRoles("primary", kit), buttonPalette(kit)),
406
+ ...buttonVariantVars("secondary", kit.buttons.secondary, touchedButtonRoles("secondary", kit), buttonPalette(kit))
407
+ };
408
+ }
293
409
  function brandColorVars(palette) {
294
410
  const { dark, primary, accent, light } = palette;
295
411
  const mix = (a, aPct, b) => `color-mix(in srgb, ${a} ${aPct}%, ${b})`;
@@ -317,6 +433,11 @@ function brandColorVars(palette) {
317
433
  "--brand-on-primary": light
318
434
  };
319
435
  }
436
+ function isButtonStyle(value) {
437
+ if (!value || typeof value !== "object") return false;
438
+ const v = value;
439
+ return BUTTON_FILL_TOKENS.includes(v.fill) && BUTTON_FILL_TOKENS.includes(v.textColor) && BUTTON_STYLES.includes(v.style) && BUTTON_RADII.includes(v.radius) && BUTTON_SIZES.includes(v.size);
440
+ }
320
441
  function parseCustomFontWeight(raw) {
321
442
  if (typeof raw !== "object" || raw === null) return null;
322
443
  const w = raw;
@@ -336,6 +457,15 @@ function parseCustomFont(raw) {
336
457
  if (weights.length === 0) return null;
337
458
  return { family: f.family, label: f.label, weights };
338
459
  }
460
+ var BUTTON_FIELDS = Object.keys(ROLES_BY_FIELD);
461
+ function parseTouchedButtons(raw) {
462
+ const out = {};
463
+ for (const variant of ["primary", "secondary"]) {
464
+ const list = raw?.[variant];
465
+ if (Array.isArray(list)) out[variant] = BUTTON_FIELDS.filter((f) => list.includes(f));
466
+ }
467
+ return out;
468
+ }
339
469
  function parseBrandKit(raw) {
340
470
  if (!raw) return null;
341
471
  try {
@@ -345,11 +475,21 @@ function parseBrandKit(raw) {
345
475
  const palette = p && typeof p.dark === "string" && typeof p.primary === "string" && typeof p.accent === "string" && typeof p.light === "string" ? { dark: p.dark, primary: p.primary, accent: p.accent, light: p.light } : void 0;
346
476
  const fonts = f && typeof f.heading === "string" && typeof f.body === "string" ? { heading: f.heading, body: f.body } : void 0;
347
477
  const customFonts = Array.isArray(parsed?.customFonts) ? parsed.customFonts.map(parseCustomFont).filter((c) => c !== null) : void 0;
348
- if (!palette && !fonts && !customFonts) return null;
478
+ const b = parsed?.buttons;
479
+ const buttons = b && isButtonStyle(b.primary) && isButtonStyle(b.secondary) ? { primary: b.primary, secondary: b.secondary } : void 0;
480
+ if (!palette && !fonts && !customFonts && !buttons) return null;
349
481
  return {
350
482
  ...palette ? { palette } : {},
351
483
  ...fonts ? { fonts } : {},
352
- ...customFonts ? { customFonts } : {}
484
+ ...customFonts ? { customFonts } : {},
485
+ ...buttons ? { buttons } : {},
486
+ ...parsed.touched && typeof parsed.touched === "object" ? {
487
+ touched: {
488
+ palette: parsed.touched.palette === true,
489
+ fonts: parsed.touched.fonts === true,
490
+ buttons: parseTouchedButtons(parsed.touched.buttons)
491
+ }
492
+ } : {}
353
493
  };
354
494
  } catch {
355
495
  return null;
@@ -378,6 +518,39 @@ function loadBrandFonts(families) {
378
518
  }
379
519
  if (link.href !== href) link.href = href;
380
520
  }
521
+ var BUTTON_RULES_STYLE_ID = "ohw-brand-button-rules";
522
+ function buttonRulesCss(kit) {
523
+ const rule = (variant) => {
524
+ const roles = touchedButtonRoles(variant, kit);
525
+ const v = (role) => `var(--brand-button-${variant}-${role})`;
526
+ const decls = [];
527
+ if (roles.has("bg")) decls.push(`background:${v("bg")} !important`);
528
+ if (roles.has("text")) decls.push(`color:${v("text")} !important`);
529
+ if (roles.has("border")) decls.push(`border:1.5px solid ${v("border")} !important`);
530
+ if (roles.has("radius")) decls.push(`border-radius:${v("radius")} !important`);
531
+ if (roles.has("padding-x")) decls.push(`padding:var(--brand-button-${variant}-padding, ${v("padding-y")} ${v("padding-x")}) !important`);
532
+ if (roles.has("font-size")) decls.push(`font-size:${v("font-size")} !important`);
533
+ if (decls.length === 0) return "";
534
+ const selectors = [`html .btn--${variant}`, `html .btn-${variant}`];
535
+ const labelRule = roles.has("text") ? `${selectors.map((x) => `${x} *`).join(",")}{color:inherit !important}` : "";
536
+ return `${selectors.join(",")}{${decls.join(";")}}` + labelRule + (roles.has("bg") ? `${selectors.map((x) => `${x}:hover`).join(",")}{opacity:0.88}` : "");
537
+ };
538
+ return rule("primary") + rule("secondary");
539
+ }
540
+ function syncButtonRules(kit) {
541
+ let el = document.getElementById(BUTTON_RULES_STYLE_ID);
542
+ const css = kit ? buttonRulesCss(kit) : "";
543
+ if (!css) {
544
+ el?.remove();
545
+ return;
546
+ }
547
+ if (!el) {
548
+ el = document.createElement("style");
549
+ el.id = BUTTON_RULES_STYLE_ID;
550
+ document.head.appendChild(el);
551
+ }
552
+ if (el.textContent !== css) el.textContent = css;
553
+ }
381
554
  function escapeCssString(value) {
382
555
  return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
383
556
  }
@@ -403,28 +576,47 @@ function loadCustomFonts(fonts) {
403
576
  function applyBrandToDom(kit) {
404
577
  const root = document.documentElement;
405
578
  if (!kit) {
406
- for (const name of [...BRAND_VAR_NAMES, ...LEGACY_BRAND_VAR_NAMES]) root.style.removeProperty(name);
579
+ for (const name of [...BRAND_VAR_NAMES, ...LEGACY_BRAND_VAR_NAMES, ...BUTTON_VAR_NAMES]) {
580
+ root.style.removeProperty(name);
581
+ }
407
582
  for (const name of [...FONT_VARS.heading, ...FONT_VARS.body]) root.style.removeProperty(name);
408
583
  document.getElementById(BRAND_FONT_LINK_ID)?.remove();
409
584
  document.getElementById(CUSTOM_FONT_STYLE_ID)?.remove();
585
+ syncButtonRules(null);
410
586
  return;
411
587
  }
588
+ const applyPalette = kit.touched ? kit.touched.palette === true : true;
589
+ const applyFonts = kit.touched ? kit.touched.fonts === true : true;
412
590
  if (kit.palette) {
413
- for (const [name, value] of Object.entries(brandColorVars(kit.palette))) root.style.setProperty(name, value);
591
+ if (applyPalette) {
592
+ for (const [name, value] of Object.entries(brandColorVars(kit.palette))) root.style.setProperty(name, value);
593
+ } else {
594
+ for (const name of [...BRAND_VAR_NAMES, ...LEGACY_BRAND_VAR_NAMES]) root.style.removeProperty(name);
595
+ }
414
596
  }
597
+ if (kit.buttons) {
598
+ for (const name of BUTTON_VAR_NAMES) root.style.removeProperty(name);
599
+ for (const [name, value] of Object.entries(buttonStyleVars({ ...kit, buttons: kit.buttons }))) root.style.setProperty(name, value);
600
+ }
601
+ syncButtonRules(kit.buttons ? kit : null);
415
602
  if (kit.customFonts) {
416
603
  loadCustomFonts(kit.customFonts);
417
604
  }
418
605
  if (kit.fonts) {
419
- const heading = quoteFamily(kit.fonts.heading);
420
- const body = quoteFamily(kit.fonts.body);
421
- for (const name of FONT_VARS.heading) root.style.setProperty(name, heading);
422
- for (const name of FONT_VARS.body) root.style.setProperty(name, body);
423
- const customFamilies = new Set((kit.customFonts ?? []).map((f) => f.family));
424
- const googleFamilies = [familyOf(kit.fonts.heading), familyOf(kit.fonts.body)].filter(
425
- (f) => !customFamilies.has(f)
426
- );
427
- loadBrandFonts(googleFamilies);
606
+ if (applyFonts) {
607
+ const heading = quoteFamily(kit.fonts.heading);
608
+ const body = quoteFamily(kit.fonts.body);
609
+ for (const name of FONT_VARS.heading) root.style.setProperty(name, heading);
610
+ for (const name of FONT_VARS.body) root.style.setProperty(name, body);
611
+ const customFamilies = new Set((kit.customFonts ?? []).map((f) => f.family));
612
+ const googleFamilies = [familyOf(kit.fonts.heading), familyOf(kit.fonts.body)].filter(
613
+ (f) => !customFamilies.has(f)
614
+ );
615
+ loadBrandFonts(googleFamilies);
616
+ } else {
617
+ for (const name of [...FONT_VARS.heading, ...FONT_VARS.body]) root.style.removeProperty(name);
618
+ document.getElementById(BRAND_FONT_LINK_ID)?.remove();
619
+ }
428
620
  }
429
621
  }
430
622
 
@@ -574,7 +766,6 @@ var NODE_PROPS = [
574
766
  "color",
575
767
  "font-family",
576
768
  "font-size",
577
- "background",
578
769
  "text-align",
579
770
  "justify-content",
580
771
  "align-items"
@@ -624,9 +815,6 @@ function clearNodeProps(root) {
624
815
  h.removeAttribute(NODE_WROTE_ATTR);
625
816
  }
626
817
  }
627
- function buttonSurfaceOf(el) {
628
- return el.closest("a, button") ?? el;
629
- }
630
818
  function alignSubjectOf(el) {
631
819
  const button = el.closest('[data-ohw-role="button"]');
632
820
  if (button?.parentElement) return button.parentElement;
@@ -744,18 +932,6 @@ function applyStylesToDom(store) {
744
932
  );
745
933
  subject.setAttribute(NODE_WROTE_ATTR, "");
746
934
  }
747
- if (override.buttonBackground !== void 0 || override.buttonText !== void 0) {
748
- const surface = buttonSurfaceOf(el);
749
- if (override.buttonBackground !== void 0) {
750
- saveInline(surface, "background");
751
- surface.style.setProperty("background", override.buttonBackground, "important");
752
- }
753
- if (override.buttonText !== void 0) {
754
- saveInline(surface, "color");
755
- surface.style.setProperty("color", override.buttonText, "important");
756
- }
757
- surface.setAttribute(NODE_WROTE_ATTR, "");
758
- }
759
935
  }
760
936
  }
761
937
  for (const [sectionId, override] of Object.entries(store.sections)) {
@@ -1341,7 +1517,59 @@ var AI_MOBILE_CSS = [
1341
1517
  "[data-ai-grid]{grid-template-columns:repeat(2, 1fr) !important}",
1342
1518
  "}"
1343
1519
  ].join("");
1344
- function buttonShellStyle(ctx, fullWidth) {
1520
+ function buttonSizeSpec(size) {
1521
+ switch (size) {
1522
+ case "s":
1523
+ return { padding: `${AI_TREE_TOKENS.spacing2}px ${AI_TREE_TOKENS.spacing4}px`, fontSize: 14 };
1524
+ case "l":
1525
+ return { padding: `${AI_TREE_TOKENS.spacing4}px ${AI_TREE_TOKENS.spacing8}px`, fontSize: 18 };
1526
+ case "m":
1527
+ default:
1528
+ return { padding: `${AI_TREE_TOKENS.spacing3}px ${AI_TREE_TOKENS.spacing6}px`, fontSize: AI_TREE_TOKENS.type.button.size };
1529
+ }
1530
+ }
1531
+ function buttonRadiusPx(radius) {
1532
+ switch (radius) {
1533
+ case "square":
1534
+ return 0;
1535
+ case "pill":
1536
+ return 999;
1537
+ case "rounded":
1538
+ default:
1539
+ return AI_TREE_TOKENS.radiusButton;
1540
+ }
1541
+ }
1542
+ function resolveButtonToken(brand, token) {
1543
+ return brand.palette[token];
1544
+ }
1545
+ function buttonColorStyle(ctx, variant) {
1546
+ const token = ctx.brand.buttons?.[variant];
1547
+ if (!token) return null;
1548
+ const fill = resolveButtonToken(ctx.brand, token.fill);
1549
+ const text = resolveButtonToken(ctx.brand, token.textColor);
1550
+ switch (token.style) {
1551
+ case "outline":
1552
+ return { background: "transparent", border: `1px solid ${fill}`, color: text };
1553
+ case "ghost":
1554
+ return { background: "transparent", border: "1px solid transparent", color: text };
1555
+ case "solid":
1556
+ default:
1557
+ return { background: fill, color: text };
1558
+ }
1559
+ }
1560
+ function buttonShellStyle(ctx, variant, fullWidth) {
1561
+ const buttonToken = ctx.brand.buttons?.[variant];
1562
+ if (buttonToken) {
1563
+ const { padding, fontSize } = buttonSizeSpec(buttonToken.size);
1564
+ return {
1565
+ borderRadius: buttonRadiusPx(buttonToken.radius),
1566
+ padding,
1567
+ fontFamily: ctx.brand.fonts.body,
1568
+ fontSize,
1569
+ lineHeight: AI_TREE_TOKENS.type.button.line,
1570
+ fontWeight: AI_TREE_TOKENS.type.button.weight
1571
+ };
1572
+ }
1345
1573
  const bs = ctx.buttonStyle;
1346
1574
  if (bs) {
1347
1575
  return {
@@ -1537,12 +1765,16 @@ function ButtonEl({
1537
1765
  secondaryColor
1538
1766
  }) {
1539
1767
  const secondary = slots.variant === "secondary";
1768
+ const variant = secondary ? "secondary" : "primary";
1769
+ const tokenColorStyle = buttonColorStyle(ctx, variant);
1540
1770
  const href = str(slots.href);
1541
1771
  const linkAttrs = ctx.keyFor && path ? { "data-ohw-href-key": ctx.keyFor(`${path}.href`), "data-ohw-role": "button" } : {};
1542
1772
  return /* @__PURE__ */ jsx(
1543
1773
  "a",
1544
1774
  {
1545
1775
  href: href || void 0,
1776
+ className: `btn btn--${variant}`,
1777
+ "data-ohw-btn-variant": variant,
1546
1778
  ...linkAttrs,
1547
1779
  style: {
1548
1780
  display: fullWidth ? "flex" : "inline-flex",
@@ -1552,13 +1784,13 @@ function ButtonEl({
1552
1784
  textDecoration: "none",
1553
1785
  cursor: "pointer",
1554
1786
  whiteSpace: "nowrap",
1555
- ...buttonShellStyle(ctx, fullWidth),
1556
- ...secondary ? { border: `1px solid ${secondaryColor ?? ctx.brand.palette.dark}`, color: secondaryColor ?? ctx.brand.palette.dark } : (
1787
+ ...buttonShellStyle(ctx, variant, fullWidth),
1788
+ ...tokenColorStyle ?? (secondary ? { border: `1px solid ${secondaryColor ?? ctx.brand.palette.dark}`, color: secondaryColor ?? ctx.brand.palette.dark } : (
1557
1789
  // Off-band, prefer the template button's measured fill/label pair — a template may
1558
1790
  // fill its CTAs with any token (hvac: accent bg, primary text). On an accent band
1559
1791
  // the flipped palette keeps contrast, so the brand-driven colours stay.
1560
1792
  !ctx.buttonLabel && ctx.buttonStyle?.background ? { background: ctx.buttonStyle.background, color: ctx.buttonStyle.color } : { background: ctx.brand.palette.primary, color: ctx.buttonLabel ?? primaryButtonLabel(ctx.brand) }
1561
- )
1793
+ ))
1562
1794
  },
1563
1795
  children: /* @__PURE__ */ jsx("span", { ...path ? textAttrs(ctx, `${path}.label`) : {}, children: str(slots.label) })
1564
1796
  }
@@ -2780,18 +3012,21 @@ function renderNode(node, ctx, path) {
2780
3012
  "button",
2781
3013
  {
2782
3014
  type: "submit",
3015
+ className: "btn btn--primary",
3016
+ "data-ohw-btn-variant": "primary",
3017
+ "data-ohw-role": "button",
2783
3018
  style: {
2784
3019
  alignSelf: submitAlign,
2785
3020
  border: "none",
2786
3021
  cursor: "pointer",
2787
3022
  // Shape/padding/typography follow the host template's own buttons.
2788
- ...buttonShellStyle(ctx),
3023
+ ...buttonShellStyle(ctx, "primary"),
2789
3024
  // Brand-styled: the template button's measured fill/label pair off-band, else
2790
3025
  // primary fill with a brand-derived label so it reads on custom palettes.
2791
- ...!ctx.buttonLabel && ctx.buttonStyle?.background ? { background: ctx.buttonStyle.background, color: ctx.buttonStyle.color } : {
3026
+ ...buttonColorStyle(ctx, "primary") ?? (!ctx.buttonLabel && ctx.buttonStyle?.background ? { background: ctx.buttonStyle.background, color: ctx.buttonStyle.color } : {
2792
3027
  background: ctx.brand.palette.primary,
2793
3028
  color: ctx.buttonLabel ?? primaryButtonLabel(ctx.brand)
2794
- }
3029
+ })
2795
3030
  },
2796
3031
  children: /* @__PURE__ */ jsx("span", { ...textAttrs(ctx, `${path}.c${i}.label`), children: str(cs.label) })
2797
3032
  },
@@ -16775,6 +17010,9 @@ function guardTemplateHover(el) {
16775
17010
  observer.observe(el, { attributes: true, attributeFilter: ["style", "class"] });
16776
17011
  paintObserverByEl.set(el, observer);
16777
17012
  }
17013
+ function paintedTokensOf(el) {
17014
+ return paintLockByEl.get(el)?.tokens ?? null;
17015
+ }
16778
17016
  function applyButtonStyle(el, assignment, tokens) {
16779
17017
  paintLockByEl.set(el, { assignment, tokens });
16780
17018
  paintingButton = true;
@@ -17113,12 +17351,9 @@ function duplicateButton(source, content) {
17113
17351
  const label = buttonLabel(source);
17114
17352
  if (href) copy.setAttribute("href", href);
17115
17353
  const sourceAssignment = readAssignments(content)[sourceId] ?? assignmentFor(source, content, tokens);
17116
- const copyAssignment = {
17117
- variant: "secondary",
17118
- size: sourceAssignment.size
17119
- };
17120
- stampVariantClass(copy, "secondary");
17121
- applyButtonStyle(copy, copyAssignment, tokens);
17354
+ const copyAssignment = { ...sourceAssignment };
17355
+ stampVariantClass(copy, copyAssignment.variant);
17356
+ applyButtonStyle(copy, copyAssignment, paintedTokensOf(source) ?? tokens);
17122
17357
  const copyLabelEl = labelElement2(copy);
17123
17358
  if (copyLabelEl) copyLabelEl.textContent = label;
17124
17359
  let collection = buttonCollectionOf(source);
@@ -26569,7 +26804,7 @@ function OhhwellsBridge() {
26569
26804
  postToParent2({
26570
26805
  type: "ow:ready",
26571
26806
  version: "1",
26572
- bridgeVersion: "0.1.121",
26807
+ bridgeVersion: "0.1.122",
26573
26808
  path: pathname,
26574
26809
  nodes: collectEditableNodes(editContentRef.current),
26575
26810
  sections