@ohhwells/bridge 0.1.95 → 0.1.97

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
@@ -491,6 +491,10 @@ function styleSheetCss() {
491
491
  `[data-ohw-style-spacing="${spacing}"] { padding-top: ${px}px !important; padding-bottom: ${px}px !important; }`
492
492
  );
493
493
  }
494
+ rules.push(
495
+ `[data-ohw-style-corners="sharp"] :is(.card, [data-ohw-card], img, picture, figure, [data-ohw-editable="bg-image"]) { border-radius: 0 !important; }`,
496
+ `[data-ohw-style-corners="sharp"] :has(> img) { border-radius: 0 !important; }`
497
+ );
494
498
  for (const align of ["left", "center", "right"]) {
495
499
  rules.push(`[data-ohw-style-align="${align}"] { text-align: ${align} !important; }`);
496
500
  }
@@ -521,6 +525,7 @@ var SECTION_ATTRS = {
521
525
  headlineScale: "data-ohw-style-headline",
522
526
  imageAspect: "data-ohw-style-aspect",
523
527
  spacing: "data-ohw-style-spacing",
528
+ cornerStyle: "data-ohw-style-corners",
524
529
  align: "data-ohw-style-align"
525
530
  };
526
531
  var NODE_WROTE_ATTR = "data-ohw-style-node";
@@ -913,6 +918,284 @@ function initSectionInstancesFromContent(content, currentPath) {
913
918
  // src/ui/ai-tree/AiTreeRenderer.tsx
914
919
  var import_react = __toESM(require("react"), 1);
915
920
  var import_lucide_react = require("lucide-react");
921
+
922
+ // src/lib/placeholder-imagery.ts
923
+ var U = (id) => `https://images.unsplash.com/photo-${id}?auto=format&fit=crop&w=1600&q=80`;
924
+ var GENERIC = [
925
+ U("1441986300917-64674bd600d8"),
926
+ U("1486406146926-c627a92ad1ab"),
927
+ U("1497032628192-86f99bcd76bc"),
928
+ U("1521737604893-d14cc237f11d"),
929
+ U("1522071820081-009f0129c71c"),
930
+ U("1519389950473-47ba0277781c"),
931
+ U("1460925895917-afdab827c52f"),
932
+ U("1504384308090-c894fdcc538d")
933
+ ];
934
+ var PEOPLE = [
935
+ U("1500648767791-00dcc994a43e"),
936
+ U("1494790108377-be9c29b29330"),
937
+ U("1507003211169-0a1dd7228f2d"),
938
+ U("1438761681033-6461ffad8d80"),
939
+ U("1544005313-94ddf0286df2"),
940
+ U("1472099645785-5658abf4ff4e"),
941
+ U("1519085360753-af0119f7cbe7"),
942
+ U("1534528741775-53994a69daeb")
943
+ ];
944
+ var THEMED = [
945
+ {
946
+ keywords: ["portrait", "headshot", "person", "people", "team", "staff", "avatar", "founder", "face"],
947
+ pool: PEOPLE
948
+ },
949
+ {
950
+ keywords: ["pet", "dog", "cat", "puppy", "kitten", "vet", "animal"],
951
+ pool: [
952
+ U("1548199973-03cce0bbc87b"),
953
+ U("1450778869180-41d0601e046e"),
954
+ U("1583511655857-d19b40a7a54e"),
955
+ U("1587300003388-59208cc962cb"),
956
+ U("1517849845537-4d257902454a"),
957
+ U("1601758228041-f3b2795255f1")
958
+ ]
959
+ },
960
+ {
961
+ keywords: [
962
+ "baker",
963
+ "bakery",
964
+ "cafe",
965
+ "coffee",
966
+ "latte",
967
+ "restaurant",
968
+ "pastr",
969
+ "bread",
970
+ "cake",
971
+ "cater",
972
+ "chef",
973
+ "kitchen",
974
+ "food",
975
+ "pizza",
976
+ "dessert",
977
+ "brunch",
978
+ "bistro",
979
+ "deli",
980
+ "dish",
981
+ "menu"
982
+ ],
983
+ pool: [
984
+ U("1509440159596-0249088772ff"),
985
+ U("1555507036-ab1f4038808a"),
986
+ U("1517433670267-08bbd4be890f"),
987
+ U("1486427944299-d1955d23e34d"),
988
+ U("1504754524776-8f4f37790ca0"),
989
+ U("1495474472287-4d71bcdd2085"),
990
+ U("1521017432531-fbd92d768814"),
991
+ U("1556909114-f6e7ad7d3136")
992
+ ]
993
+ },
994
+ {
995
+ keywords: [
996
+ "shop",
997
+ "store",
998
+ "boutique",
999
+ "retail",
1000
+ "clothing",
1001
+ "fashion",
1002
+ "jewel",
1003
+ "gift",
1004
+ "florist",
1005
+ "market",
1006
+ "grocer",
1007
+ "product",
1008
+ "storefront"
1009
+ ],
1010
+ pool: [
1011
+ U("1441984904996-e0b6ba687e04"),
1012
+ U("1472851294608-062f824d29cc"),
1013
+ U("1523381210434-271e8be1f52b"),
1014
+ U("1534452203293-494d7ddbf7e0"),
1015
+ U("1445205170230-053b83016050"),
1016
+ U("1560243563-062bfc001d68")
1017
+ ]
1018
+ },
1019
+ {
1020
+ keywords: [
1021
+ "yoga",
1022
+ "pilates",
1023
+ "fitness",
1024
+ "gym",
1025
+ "workout",
1026
+ "trainer",
1027
+ "wellness",
1028
+ "meditat",
1029
+ "massage",
1030
+ "therap",
1031
+ "physio",
1032
+ "chiro",
1033
+ "nutrition",
1034
+ "spa",
1035
+ "studio"
1036
+ ],
1037
+ pool: [
1038
+ U("1544367567-0f2fcb009e0b"),
1039
+ U("1506126613408-eca07ce68773"),
1040
+ U("1545205597-3d9d02c29597"),
1041
+ U("1552196563-55cd4e45efb3"),
1042
+ U("1518611012118-696072aa579a"),
1043
+ U("1571019613454-1cb2f99b2d8b"),
1044
+ U("1540555700478-4be289fbecef"),
1045
+ U("1519824145371-296894a0daa9")
1046
+ ]
1047
+ },
1048
+ {
1049
+ keywords: [
1050
+ "salon",
1051
+ "hairdress",
1052
+ "haircut",
1053
+ "barber",
1054
+ "manicure",
1055
+ "pedicure",
1056
+ "nails",
1057
+ "beauty",
1058
+ "makeup",
1059
+ "cosmetic",
1060
+ "eyelash",
1061
+ "eyebrow",
1062
+ "skincare",
1063
+ "esthetic",
1064
+ "waxing",
1065
+ "hair"
1066
+ ],
1067
+ pool: [
1068
+ U("1560066984-138dadb4c035"),
1069
+ U("1522337660859-02fbefca4702"),
1070
+ U("1562322140-8baeececf3df"),
1071
+ U("1521590832167-7bcbfaa6381f"),
1072
+ U("1487412947147-5cebf100ffc2"),
1073
+ U("1526045478516-99145907023c")
1074
+ ]
1075
+ },
1076
+ {
1077
+ keywords: [
1078
+ "cleaning",
1079
+ "plumb",
1080
+ "electric",
1081
+ "landscap",
1082
+ "contractor",
1083
+ "handyman",
1084
+ "renov",
1085
+ "hvac",
1086
+ "roofing",
1087
+ "painting",
1088
+ "carpentry",
1089
+ "flooring",
1090
+ "movers",
1091
+ "construction",
1092
+ "tools"
1093
+ ],
1094
+ pool: [
1095
+ U("1581578731548-c64695cc6952"),
1096
+ U("1504307651254-35680f356dfd"),
1097
+ U("1581092160562-40aa08e78837"),
1098
+ U("1621905251189-08b45d6a269e"),
1099
+ U("1558618666-fcd25c85cd64"),
1100
+ U("1585128792020-803d29415281")
1101
+ ]
1102
+ },
1103
+ {
1104
+ keywords: [
1105
+ "legal",
1106
+ "attorney",
1107
+ "lawyer",
1108
+ "account",
1109
+ "bookkeep",
1110
+ "consult",
1111
+ "coaching",
1112
+ "financ",
1113
+ "insurance",
1114
+ "realtor",
1115
+ "estate",
1116
+ "marketing",
1117
+ "agency",
1118
+ "office",
1119
+ "business",
1120
+ "desk"
1121
+ ],
1122
+ pool: [
1123
+ U("1497366216548-37526070297c"),
1124
+ U("1497366811353-6870744d04b2"),
1125
+ U("1454165804606-c3d57bc86b40"),
1126
+ U("1521791136064-7986c2920216"),
1127
+ U("1556761175-b413da4baf72"),
1128
+ U("1542744173-8e7e53415bb0")
1129
+ ]
1130
+ },
1131
+ {
1132
+ keywords: [
1133
+ "wedding",
1134
+ "event",
1135
+ "party",
1136
+ "celebrat",
1137
+ "venue",
1138
+ "community",
1139
+ "nonprofit",
1140
+ "charity",
1141
+ "workshop",
1142
+ "photograph",
1143
+ "concert"
1144
+ ],
1145
+ pool: [
1146
+ U("1511578314322-379afb476865"),
1147
+ U("1501281668745-f7f57925c3b4"),
1148
+ U("1523580494863-6f3031224c94"),
1149
+ U("1540575467063-178a50c2df87"),
1150
+ U("1505236858219-8359eb29e329"),
1151
+ U("1528605248644-14dd04022da1")
1152
+ ]
1153
+ }
1154
+ ];
1155
+ function poolForSubject(subject) {
1156
+ for (const theme of THEMED) {
1157
+ if (theme.keywords.some((k) => subject.includes(k))) {
1158
+ return theme.pool;
1159
+ }
1160
+ }
1161
+ return GENERIC;
1162
+ }
1163
+ function mixedHash(text) {
1164
+ let hash = 2166136261;
1165
+ for (let i = 0; i < text.length; i++) {
1166
+ hash ^= text.charCodeAt(i);
1167
+ hash = Math.imul(hash, 16777619);
1168
+ }
1169
+ return hash >>> 16 & 65535;
1170
+ }
1171
+ function resolvePlaceholderRef(ref) {
1172
+ const match = /^placeholder:([a-z0-9-]+)$/.exec(ref);
1173
+ if (!match) return null;
1174
+ const subject = match[1];
1175
+ const pool = poolForSubject(subject.replace(/-\d+$/, ""));
1176
+ return pool[mixedHash(ref) % pool.length];
1177
+ }
1178
+ function collectPlaceholderRefs(tree) {
1179
+ const seen = /* @__PURE__ */ new Set();
1180
+ for (const match of JSON.stringify(tree ?? null).matchAll(/"(placeholder:[a-z0-9-]+)"/gu)) {
1181
+ seen.add(match[1]);
1182
+ }
1183
+ return [...seen];
1184
+ }
1185
+ function buildPlaceholderMap(tree) {
1186
+ const map = {};
1187
+ const cursor = /* @__PURE__ */ new Map();
1188
+ for (const ref of collectPlaceholderRefs(tree)) {
1189
+ const subject = ref.slice("placeholder:".length).replace(/-\d+$/, "");
1190
+ const pool = poolForSubject(subject);
1191
+ const start = cursor.get(pool) ?? mixedHash(ref) % pool.length;
1192
+ map[ref] = pool[start % pool.length];
1193
+ cursor.set(pool, start + 1);
1194
+ }
1195
+ return map;
1196
+ }
1197
+
1198
+ // src/ui/ai-tree/AiTreeRenderer.tsx
916
1199
  var import_jsx_runtime = require("react/jsx-runtime");
917
1200
  function lucideByName(name) {
918
1201
  const pascal = name.split("-").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
@@ -926,6 +1209,7 @@ var typeStyle = (spec, font) => ({
926
1209
  fontWeight: spec.weight
927
1210
  });
928
1211
  var str = (value) => typeof value === "string" ? value : "";
1212
+ var cardRadius = (slots) => slots.cornerStyle === "sharp" ? 0 : AI_TREE_TOKENS.radiusCard;
929
1213
  var featureLines = (value) => value.split(/\n|<br\s*\/?>/i).map((line) => line.trim()).filter(Boolean);
930
1214
  var CHECK_MASK = `data:image/svg+xml,${encodeURIComponent(
931
1215
  '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>'
@@ -952,6 +1236,25 @@ var FEATURE_LINE_CSS = [
952
1236
  `background-color:currentColor;-webkit-mask:url("${CHECK_MASK}") no-repeat 0 0/24px 24px;`,
953
1237
  `mask:url("${CHECK_MASK}") no-repeat 0 0/24px 24px}`
954
1238
  ].join("");
1239
+ function buttonShellStyle(ctx, fullWidth) {
1240
+ const bs = ctx.buttonStyle;
1241
+ if (bs) {
1242
+ return {
1243
+ borderRadius: bs.radius,
1244
+ ...bs.padding ? { padding: bs.padding } : {},
1245
+ ...bs.fontFamily ? { fontFamily: bs.fontFamily } : { fontFamily: ctx.brand.fonts.body },
1246
+ ...bs.fontSize ? { fontSize: bs.fontSize } : {},
1247
+ ...bs.fontWeight ? { fontWeight: bs.fontWeight } : {},
1248
+ ...bs.letterSpacing && bs.letterSpacing !== "normal" ? { letterSpacing: bs.letterSpacing } : {},
1249
+ ...bs.textTransform && bs.textTransform !== "none" ? { textTransform: bs.textTransform } : {}
1250
+ };
1251
+ }
1252
+ return {
1253
+ borderRadius: AI_TREE_TOKENS.radiusButton,
1254
+ padding: fullWidth ? `${AI_TREE_TOKENS.spacing2}px ${AI_TREE_TOKENS.spacing4}px` : `${AI_TREE_TOKENS.spacing3}px ${AI_TREE_TOKENS.spacing6}px`,
1255
+ ...typeStyle(AI_TREE_TOKENS.type.button, ctx.brand.fonts.body)
1256
+ };
1257
+ }
955
1258
  function hexLuminance(color) {
956
1259
  const m = /^#([0-9a-f]{6})$/i.exec(color.trim());
957
1260
  if (!m) return null;
@@ -968,6 +1271,12 @@ function hexContrast(a, b) {
968
1271
  const [hi, lo] = la > lb ? [la, lb] : [lb, la];
969
1272
  return (hi + 0.05) / (lo + 0.05);
970
1273
  }
1274
+ function primaryButtonLabel(brand) {
1275
+ const darkC = hexContrast(brand.palette.primary, brand.palette.dark);
1276
+ const lightC = hexContrast(brand.palette.primary, AI_TREE_TOKENS.textPrimaryForeground);
1277
+ if (darkC === null || lightC === null) return AI_TREE_TOKENS.textPrimaryForeground;
1278
+ return darkC > lightC ? brand.palette.dark : AI_TREE_TOKENS.textPrimaryForeground;
1279
+ }
971
1280
  function accentBandContext(brand) {
972
1281
  const p = brand.palette;
973
1282
  const lightWins = (hexContrast(p.light, p.primary) ?? 99) >= (hexContrast(p.dark, p.primary) ?? 0);
@@ -1067,12 +1376,10 @@ function ButtonEl({
1067
1376
  width: fullWidth ? "100%" : void 0,
1068
1377
  alignItems: "center",
1069
1378
  justifyContent: "center",
1070
- padding: fullWidth ? `${AI_TREE_TOKENS.spacing2}px ${AI_TREE_TOKENS.spacing4}px` : `${AI_TREE_TOKENS.spacing3}px ${AI_TREE_TOKENS.spacing6}px`,
1071
- borderRadius: AI_TREE_TOKENS.radiusButton,
1072
1379
  textDecoration: "none",
1073
1380
  cursor: "pointer",
1074
- ...typeStyle(AI_TREE_TOKENS.type.button, ctx.brand.fonts.body),
1075
- ...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 }
1381
+ ...buttonShellStyle(ctx, fullWidth),
1382
+ ...secondary ? { border: `1px solid ${ctx.brand.palette.dark}`, color: ctx.brand.palette.dark } : { background: ctx.brand.palette.primary, color: ctx.buttonLabel ?? primaryButtonLabel(ctx.brand) }
1076
1383
  },
1077
1384
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { ...path ? textAttrs(ctx, `${path}.label`) : {}, children: str(slots.label) })
1078
1385
  }
@@ -1248,10 +1555,11 @@ function PricingCard({ node, ctx, path }) {
1248
1555
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1249
1556
  "div",
1250
1557
  {
1558
+ "data-ohw-card": "",
1251
1559
  style: {
1252
1560
  background: hasBg ? ctx.brand.palette.light : "transparent",
1253
1561
  border: `1px solid ${dark}`,
1254
- borderRadius: AI_TREE_TOKENS.radiusCard,
1562
+ borderRadius: cardRadius(slots),
1255
1563
  padding: AI_TREE_TOKENS.paddingBlock,
1256
1564
  display: "flex",
1257
1565
  flexDirection: "column",
@@ -1356,10 +1664,11 @@ function TestimonialCard({ node, ctx, path }) {
1356
1664
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1357
1665
  "div",
1358
1666
  {
1667
+ "data-ohw-card": "",
1359
1668
  "data-ai-avatar-pos": avatarPos ?? void 0,
1360
1669
  style: {
1361
1670
  background: hasBg ? ctx.cardSurface : "transparent",
1362
- borderRadius: hasBg ? AI_TREE_TOKENS.radiusCard : 0,
1671
+ borderRadius: hasBg ? cardRadius(slots) : 0,
1363
1672
  overflow: "hidden",
1364
1673
  outline: slots.featured === true ? `2px solid ${ctx.brand.palette.primary}` : void 0,
1365
1674
  minWidth: 0
@@ -1394,10 +1703,11 @@ function TeamCard({ node, ctx, path }) {
1394
1703
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1395
1704
  "div",
1396
1705
  {
1706
+ "data-ohw-card": "",
1397
1707
  "data-ai-avatar-pos": avatarPos ?? void 0,
1398
1708
  style: {
1399
1709
  background: hasBg ? ctx.cardSurface : "transparent",
1400
- borderRadius: hasBg ? AI_TREE_TOKENS.radiusCard : 0,
1710
+ borderRadius: hasBg ? cardRadius(slots) : 0,
1401
1711
  overflow: "hidden",
1402
1712
  outline: slots.featured === true ? `2px solid ${ctx.brand.palette.primary}` : void 0,
1403
1713
  minWidth: 0,
@@ -1568,9 +1878,10 @@ function CardBlock({ node, ctx, path }) {
1568
1878
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1569
1879
  "div",
1570
1880
  {
1881
+ "data-ohw-card": "",
1571
1882
  style: {
1572
1883
  background: hasBg ? ctx.cardSurface : "transparent",
1573
- borderRadius: hasBg ? AI_TREE_TOKENS.radiusCard : 0,
1884
+ borderRadius: hasBg ? cardRadius(slots) : 0,
1574
1885
  overflow: "hidden",
1575
1886
  outline: slots.featured === true ? `2px solid ${ctx.brand.palette.primary}` : void 0,
1576
1887
  display: horizontal ? "flex" : "block",
@@ -1598,7 +1909,7 @@ function CardBlock({ node, ctx, path }) {
1598
1909
  ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1599
1910
  "div",
1600
1911
  {
1601
- style: /^(lucide|simple):/.test(mediaRef) ? { padding: `${AI_TREE_TOKENS.paddingBlock}px ${AI_TREE_TOKENS.paddingBlock}px 0` } : hasBg ? void 0 : { borderRadius: AI_TREE_TOKENS.radiusCard, overflow: "hidden" },
1912
+ style: /^(lucide|simple):/.test(mediaRef) ? { padding: `${AI_TREE_TOKENS.paddingBlock}px ${AI_TREE_TOKENS.paddingBlock}px 0` } : hasBg ? void 0 : { borderRadius: cardRadius(slots), overflow: "hidden" },
1602
1913
  children: media
1603
1914
  }
1604
1915
  )),
@@ -2009,6 +2320,32 @@ function renderNode(node, ctx, path) {
2009
2320
  if (child) {
2010
2321
  return renderNode(child, ctx, `${path}.c0`);
2011
2322
  }
2323
+ if (str(slots.provider) === "map" && str(slots.query)) {
2324
+ const query = str(slots.query);
2325
+ const mapAttrs = ctx.keyFor ? {
2326
+ "data-ohw-key": ctx.keyFor(`${path}.query`),
2327
+ "data-ohw-editable": "map",
2328
+ "data-ohw-map-query": query
2329
+ } : {};
2330
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2331
+ "iframe",
2332
+ {
2333
+ ...mapAttrs,
2334
+ "data-ai-embed": "map",
2335
+ title: str(slots.title) || "Map",
2336
+ src: `https://www.google.com/maps?q=${encodeURIComponent(query)}&output=embed`,
2337
+ loading: "lazy",
2338
+ referrerPolicy: "no-referrer-when-downgrade",
2339
+ style: {
2340
+ width: "100%",
2341
+ minHeight: 320,
2342
+ border: 0,
2343
+ borderRadius: AI_TREE_TOKENS.radiusCard,
2344
+ display: "block"
2345
+ }
2346
+ }
2347
+ );
2348
+ }
2012
2349
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2013
2350
  "div",
2014
2351
  {
@@ -2112,15 +2449,12 @@ function renderNode(node, ctx, path) {
2112
2449
  alignSelf: submitAlign,
2113
2450
  border: "none",
2114
2451
  cursor: "pointer",
2115
- padding: "12px 24px",
2116
- // Corner radius follows the host template's own buttons (measured from a template
2117
- // CTA); 8px only when the page has no template button to match.
2118
- borderRadius: ctx.buttonRadius ?? 8,
2452
+ // Shape/padding/typography follow the host template's own buttons.
2453
+ ...buttonShellStyle(ctx),
2119
2454
  // Brand-styled: primary fill, brand-derived label colour (not a fixed token) so it
2120
2455
  // reads correctly on custom palettes.
2121
2456
  background: ctx.brand.palette.primary,
2122
- color: ctx.buttonLabel ?? ctx.brand.palette.light,
2123
- ...typeStyle(AI_TREE_TOKENS.type.button, ctx.brand.fonts.body)
2457
+ color: ctx.buttonLabel ?? primaryButtonLabel(ctx.brand)
2124
2458
  },
2125
2459
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { ...textAttrs(ctx, `${path}.c${i}.label`), children: str(cs.label) })
2126
2460
  },
@@ -2155,7 +2489,7 @@ function renderNode(node, ctx, path) {
2155
2489
  function AiTreeRenderer({
2156
2490
  tree,
2157
2491
  brand,
2158
- buttonRadius,
2492
+ buttonStyle,
2159
2493
  resolveMedia,
2160
2494
  editKeyPrefix
2161
2495
  }) {
@@ -2165,13 +2499,18 @@ function AiTreeRenderer({
2165
2499
  const resolvedBrand = brand ?? AI_DEFAULT_BRAND;
2166
2500
  const band = (tree.settings ?? {}).sectionBackground === "accent" ? accentBandContext(resolvedBrand) : null;
2167
2501
  const blockBrand = band?.brand ?? resolvedBrand;
2502
+ const placeholderMap = buildPlaceholderMap(tree);
2168
2503
  const ctx = {
2169
2504
  brand: blockBrand,
2170
- resolveMedia: resolveMedia ?? (() => null),
2505
+ // An owner/library ref resolves through the host resolver; a `placeholder:<subject>` ref the
2506
+ // host cannot resolve falls back to real stock photography (the per-section map first, then a
2507
+ // standalone resolve), so generated galleries, image rows, and overlay backgrounds arrive with
2508
+ // photos instead of grey boxes.
2509
+ resolveMedia: (ref) => resolveMedia?.(ref) ?? placeholderMap[ref] ?? resolvePlaceholderRef(ref),
2171
2510
  cardSurface: blockBrand.palette.light.toUpperCase() === AI_DEFAULT_BRAND.palette.light.toUpperCase() ? "#ECECEB" : `color-mix(in srgb, ${blockBrand.palette.light} 90%, ${blockBrand.palette.dark})`,
2172
2511
  keyFor: editKeyPrefix ? (path) => `${editKeyPrefix}.${path}` : null,
2173
2512
  sectionAlignment: (tree.settings ?? {}).alignment === "center" ? "center" : "left",
2174
- buttonRadius,
2513
+ buttonStyle,
2175
2514
  ...band ? { buttonLabel: band.buttonLabel } : {}
2176
2515
  };
2177
2516
  const settings = tree.settings ?? {};
@@ -2301,13 +2640,13 @@ function deriveBrandOverride() {
2301
2640
  };
2302
2641
  }
2303
2642
  function deriveTemplateBrand() {
2304
- const dark = readRootVar("--color-dark");
2305
- const primary = readRootVar("--color-primary");
2306
- const light = readRootVar("--color-light");
2643
+ const primary = readRootVar("--brand-primary") || readRootVar("--color-primary");
2644
+ const dark = readRootVar("--brand-text") || readRootVar("--color-dark");
2645
+ const light = readRootVar("--brand-background") || readRootVar("--color-light");
2307
2646
  if (!dark || !primary || !light) return null;
2308
- const accent = readRootVar("--color-accent");
2309
- const heading = readRootVar("--font-heading") || readRootVar("--font-display");
2310
- const body = readRootVar("--font-body");
2647
+ const accent = readRootVar("--brand-accent") || readRootVar("--color-accent");
2648
+ const heading = readRootVar("--brand-font-heading") || readRootVar("--font-heading") || readRootVar("--font-display");
2649
+ const body = readRootVar("--brand-font-body") || readRootVar("--font-body");
2311
2650
  return {
2312
2651
  palette: { dark, primary, accent: accent || dark, light },
2313
2652
  fonts: {
@@ -2316,12 +2655,32 @@ function deriveTemplateBrand() {
2316
2655
  }
2317
2656
  };
2318
2657
  }
2319
- function deriveTemplateButtonRadius() {
2658
+ function deriveTemplateButtonStyle() {
2320
2659
  if (typeof document === "undefined") return null;
2321
- const btn = document.querySelector('[data-ohw-role="button"]');
2660
+ const btn = Array.from(document.querySelectorAll('[data-ohw-role="button"]')).find(
2661
+ (el) => !el.closest(`[${CONTAINER_ATTR}]`)
2662
+ );
2322
2663
  if (!btn) return null;
2323
- const radius = getComputedStyle(btn).borderTopLeftRadius;
2324
- return radius || null;
2664
+ const cs = getComputedStyle(btn);
2665
+ const corners = [
2666
+ cs.borderTopLeftRadius,
2667
+ cs.borderTopRightRadius,
2668
+ cs.borderBottomRightRadius,
2669
+ cs.borderBottomLeftRadius
2670
+ ].map((v) => v || "0px");
2671
+ const radius = corners.every((v) => v === corners[0]) ? corners[0] : corners.join(" ");
2672
+ const px = (v) => parseFloat(v) || 0;
2673
+ const padY = Math.max(px(cs.paddingTop), px(cs.paddingBottom));
2674
+ const padX = Math.max(px(cs.paddingLeft), px(cs.paddingRight));
2675
+ return {
2676
+ radius: radius || "10px",
2677
+ padding: `${padY}px ${padX}px`,
2678
+ fontFamily: cs.fontFamily || "",
2679
+ fontSize: cs.fontSize || "",
2680
+ fontWeight: cs.fontWeight || "",
2681
+ letterSpacing: cs.letterSpacing || "",
2682
+ textTransform: cs.textTransform || ""
2683
+ };
2325
2684
  }
2326
2685
  var mounted = /* @__PURE__ */ new Map();
2327
2686
  function findTemplateSection(id) {
@@ -2492,7 +2851,7 @@ function applyAiSectionsToDom(state, options) {
2492
2851
  if (typeof document === "undefined") return;
2493
2852
  const brandOverride = deriveBrandOverride();
2494
2853
  const templateBrand = deriveTemplateBrand();
2495
- const templateButtonRadius = deriveTemplateButtonRadius();
2854
+ const templateButtonStyle = deriveTemplateButtonStyle();
2496
2855
  const brandKey = brandOverride ? JSON.stringify(brandOverride) : "";
2497
2856
  const pagePath = window.location.pathname;
2498
2857
  const pageSections = state.sections.filter((entry) => (entry.path ?? "/") === pagePath);
@@ -2532,7 +2891,7 @@ function applyAiSectionsToDom(state, options) {
2532
2891
  {
2533
2892
  tree: entry.tree,
2534
2893
  brand: brandOverride ?? entry.brand ?? options?.brand ?? templateBrand ?? void 0,
2535
- buttonRadius: templateButtonRadius,
2894
+ buttonStyle: templateButtonStyle,
2536
2895
  resolveMedia,
2537
2896
  editKeyPrefix: `ai.${entry.id}`
2538
2897
  }
@@ -14625,6 +14984,9 @@ function collectEditableNodes(extraContent, root = document) {
14625
14984
  if (el.dataset.ohwEditable === "link") {
14626
14985
  return { key: el.dataset.ohwKey ?? "", type: "link", text: getLinkHref3(el) };
14627
14986
  }
14987
+ if (el.dataset.ohwEditable === "map") {
14988
+ return { key: el.dataset.ohwKey ?? "", type: "map", text: el.dataset.ohwMapQuery ?? "" };
14989
+ }
14628
14990
  return {
14629
14991
  key: el.dataset.ohwKey ?? "",
14630
14992
  type: el.dataset.ohwEditable ?? "text",
@@ -15408,7 +15770,7 @@ var EDITOR_CHROME_SELECTOR = '[data-ohw-toolbar], [data-ohw-form-toolbar], [data
15408
15770
  function isOverEditorChrome(x, y) {
15409
15771
  return document.elementsFromPoint(x, y).some((el) => el instanceof HTMLElement && el.matches(EDITOR_CHROME_SELECTOR));
15410
15772
  }
15411
- var NON_MEDIA_SELECTOR = '[data-ohw-editable]:not([data-ohw-editable="image"]):not([data-ohw-editable="bg-image"]):not([data-ohw-editable="video"]):not([data-ohw-editable="icon"]):not([data-ohw-editable="form"])';
15773
+ var NON_MEDIA_SELECTOR = '[data-ohw-editable]:not([data-ohw-editable="image"]):not([data-ohw-editable="bg-image"]):not([data-ohw-editable="video"]):not([data-ohw-editable="icon"]):not([data-ohw-editable="form"]):not([data-ohw-editable="map"])';
15412
15774
  function getVideoEl2(el) {
15413
15775
  return el instanceof HTMLVideoElement ? el : el.querySelector("video");
15414
15776
  }
@@ -15464,6 +15826,12 @@ function applyVideoSettingNode(key, val) {
15464
15826
  });
15465
15827
  return true;
15466
15828
  }
15829
+ function applyMapQuery(el, val) {
15830
+ if (!(el instanceof HTMLIFrameElement)) return;
15831
+ const nextSrc = `https://www.google.com/maps?q=${encodeURIComponent(val)}&output=embed`;
15832
+ if (el.src !== nextSrc) el.src = nextSrc;
15833
+ el.setAttribute("data-ohw-map-query", val);
15834
+ }
15467
15835
  function applyLinkByKey(key, val) {
15468
15836
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
15469
15837
  if (el.dataset.ohwEditable === "link") applyLinkHref(el, val);
@@ -18264,6 +18632,8 @@ function OhhwellsBridge() {
18264
18632
  }
18265
18633
  } else if (el.dataset.ohwEditable === "link") {
18266
18634
  applyLinkHref(el, val);
18635
+ } else if (el.dataset.ohwEditable === "map") {
18636
+ applyMapQuery(el, val);
18267
18637
  } else if (el.dataset.ohwEditable === "icon") {
18268
18638
  applyIconMarkup(el, val);
18269
18639
  } else if (el.dataset.ohwEditable === "form") {
@@ -18460,6 +18830,8 @@ function OhhwellsBridge() {
18460
18830
  if (video && video.src !== val) applyVideoSrc(video, val);
18461
18831
  } else if (el.dataset.ohwEditable === "link") {
18462
18832
  applyLinkHref(el, val);
18833
+ } else if (el.dataset.ohwEditable === "map") {
18834
+ applyMapQuery(el, val);
18463
18835
  } else if (el.dataset.ohwEditable === "form") {
18464
18836
  } else if (isIconMarkupValue(val)) {
18465
18837
  } else if (el.innerHTML !== val) {
@@ -20433,6 +20805,8 @@ function OhhwellsBridge() {
20433
20805
  if (video && video.src !== val) applyVideoSrc(video, val);
20434
20806
  } else if (el.dataset.ohwEditable === "link") {
20435
20807
  applyLinkHref(el, val);
20808
+ } else if (el.dataset.ohwEditable === "map") {
20809
+ applyMapQuery(el, val);
20436
20810
  } else if (el.dataset.ohwEditable === "icon") {
20437
20811
  applyIconMarkup(el, val);
20438
20812
  } else if (isIconMarkupValue(val)) {
@@ -21669,7 +22043,7 @@ function OhhwellsBridge() {
21669
22043
  postToParent2({
21670
22044
  type: "ow:ready",
21671
22045
  version: "1",
21672
- bridgeVersion: "0.1.94",
22046
+ bridgeVersion: "0.1.96",
21673
22047
  path: pathname,
21674
22048
  nodes: collectEditableNodes(editContentRef.current),
21675
22049
  sections