@ohhwells/bridge 0.1.78 → 0.1.80

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
@@ -83,7 +83,12 @@ function parseAiSectionsState(raw) {
83
83
  media: entry.media && typeof entry.media === "object" ? entry.media : {}
84
84
  }));
85
85
  const removed = Array.isArray(parsed.removed) ? parsed.removed.filter((id) => typeof id === "string" && id.length > 0) : [];
86
- return { v: 1, sections, ...removed.length ? { removed } : {} };
86
+ return {
87
+ v: 1,
88
+ sections,
89
+ ...removed.length ? { removed } : {},
90
+ ...parsed.hideTemplate === true ? { hideTemplate: true } : {}
91
+ };
87
92
  } catch {
88
93
  return EMPTY_AI_SECTIONS;
89
94
  }
@@ -96,6 +101,7 @@ function applyTreeToState(state, payload) {
96
101
  const entry = {
97
102
  id: payload.id,
98
103
  label: payload.label ?? "Generated section",
104
+ ...typeof payload.path === "string" && payload.path ? { path: payload.path } : {},
99
105
  afterSection: payload.mode === "insert" && !insertBefore ? payload.targetSectionId ?? null : null,
100
106
  ...insertBefore && payload.targetSectionId ? { beforeSection: payload.targetSectionId } : {},
101
107
  ...payload.mode === "replace" && payload.targetSectionId ? { replaces: payload.targetSectionId } : {},
@@ -118,6 +124,317 @@ function deleteSectionFromState(state, sectionId) {
118
124
  return { ...state, v: 1, sections: state.sections, removed: [...removed, sectionId] };
119
125
  }
120
126
 
127
+ // src/lib/brand-chrome.ts
128
+ var BRAND_NAME_KEY = "__ohw_brand_name";
129
+ var BRAND_TITLE_KEY = "__ohw_site_title";
130
+ var BRAND_FAVICON_LETTER_KEY = "__ohw_favicon_letter";
131
+ var BRAND_CHROME_KEYS = /* @__PURE__ */ new Set([
132
+ BRAND_NAME_KEY,
133
+ BRAND_TITLE_KEY,
134
+ BRAND_FAVICON_LETTER_KEY
135
+ ]);
136
+ function upsertMeta(selector, attr, token, value) {
137
+ let el = document.head.querySelector(selector);
138
+ if (!el) {
139
+ el = document.createElement("meta");
140
+ el.setAttribute(attr, token);
141
+ document.head.appendChild(el);
142
+ }
143
+ if (el.getAttribute("content") !== value) el.setAttribute("content", value);
144
+ }
145
+ function escapeXml(value) {
146
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
147
+ }
148
+ function applyLetterFavicon(letter) {
149
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="12" fill="#111827"/><text x="32" y="46" font-family="system-ui,-apple-system,Segoe UI,sans-serif" font-size="40" font-weight="700" text-anchor="middle" fill="#ffffff">${escapeXml(letter)}</text></svg>`;
150
+ const href = `data:image/svg+xml,${encodeURIComponent(svg)}`;
151
+ let link = document.head.querySelector('link[rel="icon"]');
152
+ if (!link) {
153
+ link = document.createElement("link");
154
+ link.rel = "icon";
155
+ document.head.appendChild(link);
156
+ }
157
+ link.type = "image/svg+xml";
158
+ if (link.href !== href) link.href = href;
159
+ }
160
+ function applyBrandChrome(content) {
161
+ const name = content[BRAND_NAME_KEY];
162
+ if (typeof name === "string" && name.length > 0) {
163
+ document.querySelectorAll("[data-ohw-wordmark]").forEach((el) => {
164
+ if (el.textContent !== name) el.textContent = name;
165
+ if (el.getAttribute("title") !== name) el.setAttribute("title", name);
166
+ });
167
+ }
168
+ const title = content[BRAND_TITLE_KEY];
169
+ if (typeof title === "string" && title.length > 0) {
170
+ if (document.title !== title) document.title = title;
171
+ upsertMeta('meta[property="og:title"]', "property", "og:title", title);
172
+ upsertMeta('meta[name="twitter:title"]', "name", "twitter:title", title);
173
+ }
174
+ const letter = content[BRAND_FAVICON_LETTER_KEY];
175
+ if (typeof letter === "string" && letter.length > 0) {
176
+ applyLetterFavicon(letter);
177
+ }
178
+ }
179
+
180
+ // src/lib/brand-kit.ts
181
+ var BRAND_KIT_KEY = "__ohw_brand";
182
+ var BRAND_VAR_PREFIX = "--ohw-brand-";
183
+ var BRAND_VAR_NAMES = ["primary", "accent", "light", "dark", "surface", "border", "muted"].map(
184
+ (role) => `${BRAND_VAR_PREFIX}${role}`
185
+ );
186
+ var FONT_VARS = {
187
+ heading: ["--font-heading", "--font-display", "--brand-font-heading"],
188
+ body: ["--font-body", "--brand-font-body"]
189
+ };
190
+ var BRAND_FONT_LINK_ID = "ohw-brand-fonts";
191
+ function brandColorVars(kit) {
192
+ const { dark, primary, accent, light } = kit.palette;
193
+ const mix = (a, aPct, b) => `color-mix(in srgb, ${a} ${aPct}%, ${b})`;
194
+ return {
195
+ [`${BRAND_VAR_PREFIX}primary`]: primary,
196
+ [`${BRAND_VAR_PREFIX}accent`]: accent,
197
+ [`${BRAND_VAR_PREFIX}light`]: light,
198
+ [`${BRAND_VAR_PREFIX}dark`]: dark,
199
+ [`${BRAND_VAR_PREFIX}surface`]: mix(light, 95, dark),
200
+ [`${BRAND_VAR_PREFIX}border`]: mix(light, 85, dark),
201
+ [`${BRAND_VAR_PREFIX}muted`]: mix(dark, 62, light)
202
+ };
203
+ }
204
+ function parseBrandKit(raw) {
205
+ if (!raw) return null;
206
+ try {
207
+ const parsed = JSON.parse(raw);
208
+ const p = parsed?.palette;
209
+ const f = parsed?.fonts;
210
+ if (!p || !f || typeof p.dark !== "string" || typeof p.primary !== "string" || typeof p.accent !== "string" || typeof p.light !== "string" || typeof f.heading !== "string" || typeof f.body !== "string") {
211
+ return null;
212
+ }
213
+ return {
214
+ palette: { dark: p.dark, primary: p.primary, accent: p.accent, light: p.light },
215
+ fonts: { heading: f.heading, body: f.body }
216
+ };
217
+ } catch {
218
+ return null;
219
+ }
220
+ }
221
+ function familyOf(stack) {
222
+ const first = stack.split(",")[0]?.trim() ?? "";
223
+ return first.replace(/^['"]|['"]$/g, "");
224
+ }
225
+ function loadBrandFonts(families) {
226
+ const unique = [...new Set(families.map((f) => f.trim()).filter(Boolean))];
227
+ if (unique.length === 0) return;
228
+ const spec = unique.map((f) => `${f.replace(/ /g, "+")}:400,500,600,700`).join("|");
229
+ const href = `https://fonts.googleapis.com/css?family=${spec}&display=swap`;
230
+ let link = document.getElementById(BRAND_FONT_LINK_ID);
231
+ if (!link) {
232
+ link = document.createElement("link");
233
+ link.id = BRAND_FONT_LINK_ID;
234
+ link.rel = "stylesheet";
235
+ document.head.appendChild(link);
236
+ }
237
+ if (link.href !== href) link.href = href;
238
+ }
239
+ function applyBrandToDom(kit) {
240
+ const root = document.documentElement;
241
+ if (!kit) {
242
+ for (const name of BRAND_VAR_NAMES) root.style.removeProperty(name);
243
+ for (const name of [...FONT_VARS.heading, ...FONT_VARS.body]) root.style.removeProperty(name);
244
+ document.getElementById(BRAND_FONT_LINK_ID)?.remove();
245
+ return;
246
+ }
247
+ for (const [name, value] of Object.entries(brandColorVars(kit))) root.style.setProperty(name, value);
248
+ for (const name of FONT_VARS.heading) root.style.setProperty(name, kit.fonts.heading);
249
+ for (const name of FONT_VARS.body) root.style.setProperty(name, kit.fonts.body);
250
+ loadBrandFonts([familyOf(kit.fonts.heading), familyOf(kit.fonts.body)]);
251
+ }
252
+
253
+ // src/lib/section-styles.ts
254
+ var STYLE_STORE_KEY = "__ohw_styles";
255
+ var STYLE_SHEET_ID = "ohw-section-styles";
256
+ function parseStyleStore(raw) {
257
+ if (!raw) return null;
258
+ try {
259
+ const parsed = JSON.parse(raw);
260
+ if (parsed?.v !== 1) return null;
261
+ return {
262
+ v: 1,
263
+ sections: typeof parsed.sections === "object" && parsed.sections ? parsed.sections : {},
264
+ nodes: typeof parsed.nodes === "object" && parsed.nodes ? parsed.nodes : {}
265
+ };
266
+ } catch {
267
+ return null;
268
+ }
269
+ }
270
+ var BG_VALUES = {
271
+ surface: "color-mix(in srgb, var(--ohw-brand-light, var(--color-light, #ECECEB)) 94%, var(--ohw-brand-dark, var(--color-dark, #0C0A09)))",
272
+ accent: "var(--ohw-brand-primary, var(--color-primary, #0078E5))",
273
+ "accent-soft": "color-mix(in srgb, var(--ohw-brand-primary, var(--color-primary, #0078E5)) 12%, var(--ohw-brand-light, var(--color-light, #FFFFFF)))"
274
+ };
275
+ var HEADLINE_SIZES = { md: 24, lg: 32, xl: 40, display: 56 };
276
+ function styleSheetCss() {
277
+ const rules = [];
278
+ for (const [tone, value] of Object.entries(BG_VALUES)) {
279
+ rules.push(`[data-ohw-style-bg="${tone}"] { background: ${value} !important; }`);
280
+ }
281
+ rules.push(
282
+ `[data-ohw-style-bg="accent"] { color: var(--ohw-brand-light, var(--color-light, #FFFFFF)) !important; }`
283
+ );
284
+ rules.push(
285
+ `[data-ohw-style-distribution="space-between"] { display: flex !important; flex-direction: column; justify-content: space-between; }`,
286
+ `[data-ohw-style-distribution="center"] { display: flex !important; flex-direction: column; justify-content: center; }`
287
+ );
288
+ for (const [scale, size] of Object.entries(HEADLINE_SIZES)) {
289
+ rules.push(
290
+ `[data-ohw-style-headline="${scale}"] :is(h1, h2, h3) { font-size: ${size}px !important; line-height: 1.15 !important; }`
291
+ );
292
+ }
293
+ rules.push(
294
+ `[data-ohw-style-aspect="fill-height"] img { height: 100% !important; aspect-ratio: auto !important; object-fit: cover; }`
295
+ );
296
+ for (const aspect of ["1:1", "4:5", "3:4", "3:2", "16:9", "2:1", "3:1"]) {
297
+ rules.push(
298
+ `[data-ohw-style-aspect="${aspect.replace(":", "-")}"] img { aspect-ratio: ${aspect.replace(":", " / ")} !important; height: auto !important; object-fit: cover; }`
299
+ );
300
+ }
301
+ const pad = { tight: 40, balanced: 64, airy: 96 };
302
+ for (const [spacing, px] of Object.entries(pad)) {
303
+ rules.push(
304
+ `[data-ohw-style-spacing="${spacing}"] { padding-top: ${px}px !important; padding-bottom: ${px}px !important; }`
305
+ );
306
+ }
307
+ return rules.join("\n");
308
+ }
309
+ var STYLE_FONT_LINK_ID = "ohw-style-fonts";
310
+ function loadStyleFonts(families) {
311
+ const unique = [...new Set(families.map((f) => f.trim()).filter(Boolean))];
312
+ const existing = document.getElementById(STYLE_FONT_LINK_ID);
313
+ if (unique.length === 0) {
314
+ existing?.remove();
315
+ return;
316
+ }
317
+ const spec = unique.map((f) => `${f.replace(/ /g, "+")}:400,500,600,700`).join("|");
318
+ const href = `https://fonts.googleapis.com/css?family=${spec}&display=swap`;
319
+ let link = existing;
320
+ if (!link) {
321
+ link = document.createElement("link");
322
+ link.id = STYLE_FONT_LINK_ID;
323
+ link.rel = "stylesheet";
324
+ document.head.appendChild(link);
325
+ }
326
+ if (link.href !== href) link.href = href;
327
+ }
328
+ var SECTION_ATTRS = {
329
+ sectionBackground: "data-ohw-style-bg",
330
+ textDistribution: "data-ohw-style-distribution",
331
+ headlineScale: "data-ohw-style-headline",
332
+ imageAspect: "data-ohw-style-aspect",
333
+ spacing: "data-ohw-style-spacing"
334
+ };
335
+ var NODE_WROTE_ATTR = "data-ohw-style-node";
336
+ var NODE_PROPS = ["color", "font-family", "font-size", "background"];
337
+ function saveInline(el, prop) {
338
+ const attr = `data-ohw-style-prev-${prop}`;
339
+ if (!el.hasAttribute(attr)) el.setAttribute(attr, el.style.getPropertyValue(prop));
340
+ }
341
+ function restoreInline(el, prop) {
342
+ const attr = `data-ohw-style-prev-${prop}`;
343
+ if (!el.hasAttribute(attr)) return;
344
+ const prev = el.getAttribute(attr) ?? "";
345
+ if (prev) el.style.setProperty(prop, prev);
346
+ else el.style.removeProperty(prop);
347
+ el.removeAttribute(attr);
348
+ }
349
+ function ensureStyleSheet() {
350
+ let el = document.getElementById(STYLE_SHEET_ID);
351
+ if (!el) {
352
+ el = document.createElement("style");
353
+ el.id = STYLE_SHEET_ID;
354
+ document.head.appendChild(el);
355
+ }
356
+ const css = styleSheetCss();
357
+ if (el.textContent !== css) el.textContent = css;
358
+ }
359
+ function clearSectionAttrs(root) {
360
+ for (const attr of Object.values(SECTION_ATTRS)) {
361
+ for (const el of Array.from(root.querySelectorAll(`[${attr}]`))) el.removeAttribute(attr);
362
+ }
363
+ for (const el of Array.from(root.querySelectorAll("[data-ohw-style-bgcolor]"))) {
364
+ restoreInline(el, "background");
365
+ el.removeAttribute("data-ohw-style-bgcolor");
366
+ }
367
+ }
368
+ function clearNodeProps(root) {
369
+ for (const el of Array.from(root.querySelectorAll(`[${NODE_WROTE_ATTR}]`))) {
370
+ const h = el;
371
+ for (const prop of NODE_PROPS) restoreInline(h, prop);
372
+ h.removeAttribute(NODE_WROTE_ATTR);
373
+ }
374
+ }
375
+ function buttonSurfaceOf(el) {
376
+ return el.closest("a, button") ?? el;
377
+ }
378
+ function applyStylesToDom(store) {
379
+ ensureStyleSheet();
380
+ clearSectionAttrs(document);
381
+ clearNodeProps(document);
382
+ loadStyleFonts(
383
+ store ? Object.values(store.nodes).flatMap((n) => n.fontFamily ? [n.fontFamily] : []) : []
384
+ );
385
+ if (!store) return;
386
+ for (const [sectionId, override] of Object.entries(store.sections)) {
387
+ const sections = document.querySelectorAll(
388
+ `[data-ohw-section="${CSS.escape(sectionId)}"]`
389
+ );
390
+ for (const section of Array.from(sections)) {
391
+ for (const [prop, attr] of Object.entries(SECTION_ATTRS)) {
392
+ const value = override[prop];
393
+ if (value === void 0) continue;
394
+ if (prop === "sectionBackground" && override.sectionBackgroundColor !== void 0) continue;
395
+ section.setAttribute(attr, String(value).replace(":", "-"));
396
+ }
397
+ if (override.sectionBackgroundColor !== void 0) {
398
+ saveInline(section, "background");
399
+ section.style.setProperty("background", override.sectionBackgroundColor, "important");
400
+ section.setAttribute("data-ohw-style-bgcolor", "");
401
+ }
402
+ }
403
+ }
404
+ for (const [key, override] of Object.entries(store.nodes)) {
405
+ const nodes = document.querySelectorAll(`[data-ohw-key="${CSS.escape(key)}"]`);
406
+ for (const el of Array.from(nodes)) {
407
+ if (override.color !== void 0) {
408
+ saveInline(el, "color");
409
+ el.style.setProperty("color", override.color, "important");
410
+ el.setAttribute(NODE_WROTE_ATTR, "");
411
+ }
412
+ if (override.fontFamily !== void 0) {
413
+ saveInline(el, "font-family");
414
+ el.style.setProperty("font-family", `'${override.fontFamily}'`, "important");
415
+ el.setAttribute(NODE_WROTE_ATTR, "");
416
+ }
417
+ if (override.fontSize !== void 0) {
418
+ saveInline(el, "font-size");
419
+ el.style.setProperty("font-size", `${override.fontSize}px`, "important");
420
+ el.setAttribute(NODE_WROTE_ATTR, "");
421
+ }
422
+ if (override.buttonBackground !== void 0 || override.buttonText !== void 0) {
423
+ const surface = buttonSurfaceOf(el);
424
+ if (override.buttonBackground !== void 0) {
425
+ saveInline(surface, "background");
426
+ surface.style.setProperty("background", override.buttonBackground, "important");
427
+ }
428
+ if (override.buttonText !== void 0) {
429
+ saveInline(surface, "color");
430
+ surface.style.setProperty("color", override.buttonText, "important");
431
+ }
432
+ surface.setAttribute(NODE_WROTE_ATTR, "");
433
+ }
434
+ }
435
+ }
436
+ }
437
+
121
438
  // src/ui/ai-tree/aiSectionsManager.tsx
122
439
  import { flushSync } from "react-dom";
123
440
  import { createRoot } from "react-dom/client";
@@ -132,7 +449,8 @@ function lucideByName(name) {
132
449
  }
133
450
  var typeStyle = (spec, font) => ({
134
451
  fontFamily: font,
135
- fontSize: spec.size,
452
+ // Headings shrink with the viewport (reaching full size around ~900px wide); body copy stays put.
453
+ fontSize: spec.size >= 24 ? `clamp(${Math.max(18, Math.round(spec.size * 0.6))}px, ${(spec.size / 9).toFixed(2)}vw, ${spec.size}px)` : spec.size,
136
454
  lineHeight: spec.line,
137
455
  fontWeight: spec.weight
138
456
  });
@@ -141,26 +459,61 @@ var featureLines = (value) => value.split(/\n|<br\s*\/?>/i).map((line) => line.t
141
459
  var CHECK_MASK = `data:image/svg+xml,${encodeURIComponent(
142
460
  '<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>'
143
461
  )}`;
462
+ var AI_MOBILE_CSS = [
463
+ "@media (max-width: 768px){",
464
+ "[data-ai-section]{overflow-x:hidden}",
465
+ "[data-ai-container]{padding:0 20px !important}",
466
+ "[data-ai-row]{display:flex !important;flex-direction:column !important;align-items:stretch !important}",
467
+ "[data-ai-cell]{width:100%;min-width:0}",
468
+ "[data-ai-grid]{grid-template-columns:1fr !important}",
469
+ // Group containers flatten to a column on phones; span placements come along for free.
470
+ "[data-ai-group]{display:flex !important;flex-direction:column !important}",
471
+ "[data-ai-group] > *{grid-column:auto !important}",
472
+ "[data-ai-section] img{max-width:100%}",
473
+ "}",
474
+ "@media (min-width: 769px) and (max-width: 1024px){",
475
+ "[data-ai-grid]{grid-template-columns:repeat(2, 1fr) !important}",
476
+ "}"
477
+ ].join("");
144
478
  var FEATURE_LINE_CSS = [
145
479
  "[data-ai-features]>div{position:relative;padding-left:40px;min-height:24px}",
146
480
  '[data-ai-features]>div::before{content:"";position:absolute;left:0;top:1px;width:24px;height:24px;',
147
481
  `background-color:currentColor;-webkit-mask:url("${CHECK_MASK}") no-repeat 0 0/24px 24px;`,
148
482
  `mask:url("${CHECK_MASK}") no-repeat 0 0/24px 24px}`
149
483
  ].join("");
484
+ function hexLuminance(color) {
485
+ const m = /^#([0-9a-f]{6})$/i.exec(color.trim());
486
+ if (!m) return null;
487
+ const [r2, g, b] = [0, 2, 4].map((i) => {
488
+ const c = parseInt(m[1].slice(i, i + 2), 16) / 255;
489
+ return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
490
+ });
491
+ return 0.2126 * r2 + 0.7152 * g + 0.0722 * b;
492
+ }
493
+ function hexContrast(a, b) {
494
+ const la = hexLuminance(a);
495
+ const lb = hexLuminance(b);
496
+ if (la === null || lb === null) return null;
497
+ const [hi, lo] = la > lb ? [la, lb] : [lb, la];
498
+ return (hi + 0.05) / (lo + 0.05);
499
+ }
500
+ function accentBandContext(brand) {
501
+ const p = brand.palette;
502
+ const lightWins = (hexContrast(p.light, p.primary) ?? 99) >= (hexContrast(p.dark, p.primary) ?? 0);
503
+ if (lightWins) {
504
+ return {
505
+ brand: { ...brand, palette: { dark: p.light, primary: p.light, accent: p.light, light: p.primary } },
506
+ buttonLabel: p.primary
507
+ };
508
+ }
509
+ return {
510
+ brand: { ...brand, palette: { dark: p.dark, primary: p.dark, accent: p.dark, light: p.light } },
511
+ buttonLabel: p.light
512
+ };
513
+ }
150
514
  function textAttrs(ctx, path) {
151
515
  return ctx.keyFor ? { "data-ohw-key": ctx.keyFor(path), "data-ohw-editable": "text" } : {};
152
516
  }
153
- var AI_RESPONSIVE_CSS = [
154
- "@media (max-width: 960px) {",
155
- " [data-ai-responsive] [data-ai-grid] { grid-template-columns: repeat(2, 1fr) !important; }",
156
- ' [data-ai-responsive] [data-ai-grid="1"] { grid-template-columns: 1fr !important; }',
157
- "}",
158
- "@media (max-width: 640px) {",
159
- " [data-ai-responsive] [data-ai-grid] { grid-template-columns: 1fr !important; }",
160
- " [data-ai-responsive] [data-ai-columns] > * { grid-column: 1 / -1 !important; }",
161
- " [data-ai-responsive] [data-ai-section-inner] { padding-left: 20px !important; padding-right: 20px !important; }",
162
- "}"
163
- ].join("\n");
164
517
  var TRANSPARENT_PX = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
165
518
  function MediaBox({
166
519
  refValue,
@@ -173,17 +526,13 @@ function MediaBox({
173
526
  const url = refValue ? ctx.resolveMedia(refValue) : null;
174
527
  const isIcon = /^(lucide|simple):/.test(refValue);
175
528
  const aspectRatio = aspect && /^\d+:\d+$/.test(aspect) ? aspect.replace(":", " / ") : void 0;
176
- const editAttrs = ctx.keyFor && editPath ? {
177
- "data-ohw-key": ctx.keyFor(editPath),
178
- "data-ohw-editable": isIcon ? "icon" : "image"
179
- } : {};
529
+ const editAttrs = ctx.keyFor && editPath && !isIcon ? { "data-ohw-key": ctx.keyFor(editPath), "data-ohw-editable": "image" } : {};
180
530
  if (isIcon) {
181
531
  const Icon = refValue.startsWith("lucide:") ? lucideByName(refValue.slice(7)) : null;
182
532
  return /* @__PURE__ */ jsx(
183
533
  "span",
184
534
  {
185
535
  "data-ai-icon": refValue,
186
- ...editAttrs,
187
536
  style: {
188
537
  display: "inline-flex",
189
538
  width: 48,
@@ -236,7 +585,7 @@ function ButtonEl({
236
585
  }) {
237
586
  const secondary = slots.variant === "secondary";
238
587
  const href = str(slots.href);
239
- const linkAttrs = ctx.keyFor && path ? { "data-ohw-href-key": ctx.keyFor(`${path}.href`) } : {};
588
+ const linkAttrs = ctx.keyFor && path ? { "data-ohw-href-key": ctx.keyFor(`${path}.href`), "data-ohw-role": "button" } : {};
240
589
  return /* @__PURE__ */ jsx(
241
590
  "a",
242
591
  {
@@ -252,7 +601,7 @@ function ButtonEl({
252
601
  textDecoration: "none",
253
602
  cursor: "pointer",
254
603
  ...typeStyle(AI_TREE_TOKENS.type.button, ctx.brand.fonts.body),
255
- ...secondary ? { border: `1px solid ${ctx.brand.palette.dark}`, color: ctx.brand.palette.dark } : { background: ctx.brand.palette.primary, color: AI_TREE_TOKENS.textPrimaryForeground }
604
+ ...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 }
256
605
  },
257
606
  children: /* @__PURE__ */ jsx("span", { ...path ? textAttrs(ctx, `${path}.label`) : {}, children: str(slots.label) })
258
607
  }
@@ -758,7 +1107,24 @@ function CardBlock({ node, ctx, path }) {
758
1107
  minWidth: 0
759
1108
  },
760
1109
  children: [
761
- media && (horizontal ? /* @__PURE__ */ jsx("div", { style: { flex: 1, minWidth: 0, display: "flex", alignItems: "center", padding: mediaInset }, children: media }) : /* @__PURE__ */ jsx(
1110
+ media && (horizontal ? /* @__PURE__ */ jsx(
1111
+ "div",
1112
+ {
1113
+ style: (
1114
+ // An icon hugs its glyph — flex:1 gave a 48px icon half the card and pushed the
1115
+ // text to the far side. Photos keep the half-and-half split. The inset has no
1116
+ // inner padding (the photo split absorbed that), so the icon carries its own gap.
1117
+ /^(lucide|simple):/.test(mediaRef) ? {
1118
+ flexShrink: 0,
1119
+ display: "flex",
1120
+ alignItems: "center",
1121
+ padding: mediaInset,
1122
+ [mediaPosition === "right" ? "marginLeft" : "marginRight"]: 20
1123
+ } : { flex: 1, minWidth: 0, display: "flex", alignItems: "center", padding: mediaInset }
1124
+ ),
1125
+ children: media
1126
+ }
1127
+ ) : /* @__PURE__ */ jsx(
762
1128
  "div",
763
1129
  {
764
1130
  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" },
@@ -849,13 +1215,44 @@ function AccordionBlock({ node, ctx, path }) {
849
1215
  ) })
850
1216
  ] }, i)) });
851
1217
  }
1218
+ function useIsMobile() {
1219
+ const [mobile, setMobile] = React.useState(
1220
+ () => typeof window !== "undefined" && window.matchMedia("(max-width: 768px)").matches
1221
+ );
1222
+ React.useEffect(() => {
1223
+ const mq = window.matchMedia("(max-width: 768px)");
1224
+ const update = () => setMobile(mq.matches);
1225
+ update();
1226
+ mq.addEventListener("change", update);
1227
+ return () => mq.removeEventListener("change", update);
1228
+ }, []);
1229
+ return mobile;
1230
+ }
852
1231
  function Carousel({ items, itemsPerRow, ctx }) {
1232
+ const isMobile = useIsMobile();
1233
+ const perPage = isMobile ? 1 : itemsPerRow;
1234
+ const pages = Math.max(1, Math.ceil(items.length / perPage));
853
1235
  const [page, setPage] = React.useState(0);
854
- const pages = Math.max(1, Math.ceil(items.length / itemsPerRow));
855
1236
  const current = Math.min(page, pages - 1);
1237
+ if (pages <= 1) {
1238
+ const cols = Math.max(1, Math.min(items.length, itemsPerRow));
1239
+ return /* @__PURE__ */ jsx(
1240
+ "div",
1241
+ {
1242
+ "data-ai-grid": String(cols),
1243
+ style: {
1244
+ display: "grid",
1245
+ gridTemplateColumns: `repeat(${cols}, 1fr)`,
1246
+ gap: AI_TREE_TOKENS.spacing8,
1247
+ alignItems: "start"
1248
+ },
1249
+ children: items
1250
+ }
1251
+ );
1252
+ }
856
1253
  const pageGroups = Array.from(
857
1254
  { length: pages },
858
- (_, p) => items.slice(p * itemsPerRow, (p + 1) * itemsPerRow)
1255
+ (_, p) => items.slice(p * perPage, (p + 1) * perPage)
859
1256
  );
860
1257
  const chrome = (enabled) => ({
861
1258
  border: `1px solid ${ctx.brand.palette.dark}`,
@@ -880,55 +1277,69 @@ function Carousel({ items, itemsPerRow, ctx }) {
880
1277
  cursor: "pointer",
881
1278
  padding: 0
882
1279
  });
883
- return /* @__PURE__ */ jsxs("div", { "data-ai-carousel": "", style: { display: "flex", flexDirection: "column", gap: AI_TREE_TOKENS.spacing8, alignItems: "center" }, children: [
884
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: AI_TREE_TOKENS.spacing6, width: "100%" }, children: [
885
- /* @__PURE__ */ jsx(
886
- "button",
887
- {
888
- type: "button",
889
- "aria-label": "Previous",
890
- onClick: () => setPage((p) => Math.max(0, p - 1)),
891
- style: chrome(current > 0),
892
- children: /* @__PURE__ */ jsx(ArrowLeft, { size: 24, strokeWidth: 1.5, "aria-hidden": true })
893
- }
894
- ),
895
- /* @__PURE__ */ jsx("div", { style: { flex: 1, minWidth: 0, overflow: "hidden" }, children: /* @__PURE__ */ jsx(
1280
+ const viewport = /* @__PURE__ */ jsx("div", { style: { flex: isMobile ? "0 0 auto" : 1, minWidth: 0, width: "100%", overflow: "hidden" }, children: /* @__PURE__ */ jsx(
1281
+ "div",
1282
+ {
1283
+ style: {
1284
+ display: "flex",
1285
+ transform: `translateX(-${current * 100}%)`,
1286
+ transition: "transform 0.4s ease"
1287
+ },
1288
+ children: pageGroups.map((group, p) => /* @__PURE__ */ jsx(
896
1289
  "div",
897
1290
  {
1291
+ "data-ai-grid": String(perPage),
898
1292
  style: {
899
- display: "flex",
900
- transform: `translateX(-${current * 100}%)`,
901
- transition: "transform 0.4s ease"
1293
+ flex: "0 0 100%",
1294
+ display: "grid",
1295
+ gridTemplateColumns: `repeat(${perPage}, 1fr)`,
1296
+ gap: AI_TREE_TOKENS.spacing8,
1297
+ alignItems: "start"
902
1298
  },
903
- children: pageGroups.map((group, p) => /* @__PURE__ */ jsx(
904
- "div",
905
- {
906
- "data-ai-grid": String(itemsPerRow),
907
- style: {
908
- flex: "0 0 100%",
909
- display: "grid",
910
- gridTemplateColumns: `repeat(${itemsPerRow}, 1fr)`,
911
- gap: AI_TREE_TOKENS.spacing8,
912
- alignItems: "start"
913
- },
914
- children: group
915
- },
916
- p
917
- ))
918
- }
919
- ) }),
920
- /* @__PURE__ */ jsx(
921
- "button",
922
- {
923
- type: "button",
924
- "aria-label": "Next",
925
- onClick: () => setPage((p) => Math.min(pages - 1, p + 1)),
926
- style: chrome(current < pages - 1),
927
- children: /* @__PURE__ */ jsx(ArrowRight, { size: 24, strokeWidth: 1.5, "aria-hidden": true })
928
- }
929
- )
1299
+ children: group
1300
+ },
1301
+ p
1302
+ ))
1303
+ }
1304
+ ) });
1305
+ const prevBtn = /* @__PURE__ */ jsx(
1306
+ "button",
1307
+ {
1308
+ type: "button",
1309
+ "aria-label": "Previous",
1310
+ onClick: () => setPage((p) => Math.max(0, p - 1)),
1311
+ style: chrome(current > 0),
1312
+ children: /* @__PURE__ */ jsx(ArrowLeft, { size: 24, strokeWidth: 1.5, "aria-hidden": true })
1313
+ }
1314
+ );
1315
+ const nextBtn = /* @__PURE__ */ jsx(
1316
+ "button",
1317
+ {
1318
+ type: "button",
1319
+ "aria-label": "Next",
1320
+ onClick: () => setPage((p) => Math.min(pages - 1, p + 1)),
1321
+ style: chrome(current < pages - 1),
1322
+ children: /* @__PURE__ */ jsx(ArrowRight, { size: 24, strokeWidth: 1.5, "aria-hidden": true })
1323
+ }
1324
+ );
1325
+ const dots = /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: 9, justifyContent: "center" }, children: pageGroups.map((_, p) => /* @__PURE__ */ jsx("button", { type: "button", "aria-label": `Page ${p + 1}`, onClick: () => setPage(p), style: dot(p === current) }, p)) });
1326
+ if (isMobile) {
1327
+ return /* @__PURE__ */ jsxs("div", { "data-ai-carousel": "", style: { display: "flex", flexDirection: "column", gap: AI_TREE_TOKENS.spacing6, alignItems: "center" }, children: [
1328
+ viewport,
1329
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: AI_TREE_TOKENS.spacing6, justifyContent: "center" }, children: [
1330
+ prevBtn,
1331
+ nextBtn
1332
+ ] }),
1333
+ dots
1334
+ ] });
1335
+ }
1336
+ return /* @__PURE__ */ jsxs("div", { "data-ai-carousel": "", style: { display: "flex", flexDirection: "column", gap: AI_TREE_TOKENS.spacing8, alignItems: "center" }, children: [
1337
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: AI_TREE_TOKENS.spacing6, width: "100%" }, children: [
1338
+ prevBtn,
1339
+ viewport,
1340
+ nextBtn
930
1341
  ] }),
931
- pages > 1 && /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: 9, justifyContent: "center" }, children: pageGroups.map((_, p) => /* @__PURE__ */ jsx("button", { type: "button", "aria-label": `Page ${p + 1}`, onClick: () => setPage(p), style: dot(p === current) }, p)) })
1342
+ dots
932
1343
  ] });
933
1344
  }
934
1345
  function CollectionBlock({ node, ctx, path }) {
@@ -1007,7 +1418,7 @@ function CollectionBlock({ node, ctx, path }) {
1007
1418
  return /* @__PURE__ */ jsx(
1008
1419
  "div",
1009
1420
  {
1010
- "data-ai-grid": String(itemsPerRow),
1421
+ "data-ai-grid": "",
1011
1422
  style: {
1012
1423
  display: "grid",
1013
1424
  gridTemplateColumns: `repeat(${itemsPerRow}, 1fr)`,
@@ -1022,6 +1433,49 @@ function renderNode(node, ctx, path) {
1022
1433
  switch (node.type) {
1023
1434
  case "text":
1024
1435
  return /* @__PURE__ */ jsx(TextBlock, { slots, ctx, path });
1436
+ // Layout container: arranges child blocks, contributes no content of its own. `grid` is a
1437
+ // nested 12-column grid the children span (a collage is 3–5 media on it, bottom-aligned so
1438
+ // mixed aspects read as a composition); `split` is exactly two children at a ratio; `stack`
1439
+ // is a column. Children render through this same dispatcher, so edit markers, media
1440
+ // resolution, and copy paths all work unchanged inside a group.
1441
+ case "group": {
1442
+ const layout = str(slots.layout);
1443
+ const gap = slots.spacing === "tight" ? AI_TREE_TOKENS.spacing3 : slots.spacing === "airy" ? AI_TREE_TOKENS.spacing8 : AI_TREE_TOKENS.spacing6;
1444
+ const kids = (node.children ?? []).map((child, i) => /* @__PURE__ */ jsx(
1445
+ "div",
1446
+ {
1447
+ style: layout === "grid" ? {
1448
+ gridColumn: `span ${typeof child.span === "number" ? Math.min(12, Math.max(1, child.span)) : 12}`,
1449
+ minWidth: 0
1450
+ } : { minWidth: 0 },
1451
+ children: renderNode(child, ctx, `${path}.c${i}`)
1452
+ },
1453
+ i
1454
+ ));
1455
+ if (layout === "grid") {
1456
+ return /* @__PURE__ */ jsx(
1457
+ "div",
1458
+ {
1459
+ "data-ai-group": "grid",
1460
+ style: { display: "grid", gridTemplateColumns: "repeat(12, 1fr)", gap, alignItems: "end" },
1461
+ children: kids
1462
+ }
1463
+ );
1464
+ }
1465
+ if (layout === "split") {
1466
+ const ratio = str(slots.ratio);
1467
+ const cols = ratio === "3:5" ? "3fr 5fr" : ratio === "5:3" ? "5fr 3fr" : "1fr 1fr";
1468
+ return /* @__PURE__ */ jsx(
1469
+ "div",
1470
+ {
1471
+ "data-ai-group": "split",
1472
+ style: { display: "grid", gridTemplateColumns: cols, gap, alignItems: "center" },
1473
+ children: kids
1474
+ }
1475
+ );
1476
+ }
1477
+ return /* @__PURE__ */ jsx("div", { "data-ai-group": "stack", style: { display: "flex", flexDirection: "column", gap }, children: kids });
1478
+ }
1025
1479
  case "button":
1026
1480
  return /* @__PURE__ */ jsx(ButtonEl, { slots, ctx, path });
1027
1481
  case "button-row":
@@ -1102,33 +1556,111 @@ function renderNode(node, ctx, path) {
1102
1556
  }
1103
1557
  );
1104
1558
  }
1105
- case "form":
1106
- return /* @__PURE__ */ jsx("div", { style: { display: "flex", flexDirection: "column", gap: AI_TREE_TOKENS.spacing4 }, children: (node.children ?? []).map((child, i) => {
1107
- if (child.type === "input") {
1108
- const cs = child.slots ?? {};
1109
- return /* @__PURE__ */ jsxs("div", { children: [
1110
- /* @__PURE__ */ jsx(
1111
- "div",
1112
- {
1113
- ...textAttrs(ctx, `${path}.c${i}.label`),
1114
- style: { ...typeStyle(AI_TREE_TOKENS.type.bodyMBold, ctx.brand.fonts.body), color: ctx.brand.palette.dark, marginBottom: 6 },
1115
- children: str(cs.label)
1116
- }
1117
- ),
1118
- /* @__PURE__ */ jsx(
1119
- "div",
1120
- {
1121
- style: {
1122
- border: `1px solid ${ctx.brand.palette.accent}`,
1123
- borderRadius: AI_TREE_TOKENS.radiusButton,
1124
- height: cs.kind === "textarea" ? 96 : 42
1125
- }
1126
- }
1127
- )
1128
- ] }, i);
1129
- }
1130
- return /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(ButtonEl, { slots: child.slots ?? {}, ctx, path: `${path}.c${i}` }) }, i);
1131
- }) });
1559
+ case "form": {
1560
+ const formAttrs = ctx.keyFor ? {
1561
+ "data-ohw-editable": "form",
1562
+ "data-ohw-key": ctx.keyFor(`${path}.form`),
1563
+ "data-ohw-success-text": "Thanks \u2014 we'll be in touch shortly."
1564
+ } : {};
1565
+ const fieldStyle = {
1566
+ width: "100%",
1567
+ boxSizing: "border-box",
1568
+ border: `1px solid color-mix(in srgb, ${ctx.brand.palette.dark} 45%, #ffffff)`,
1569
+ borderRadius: 0,
1570
+ padding: 12,
1571
+ background: "#fff",
1572
+ color: ctx.brand.palette.dark,
1573
+ outline: "none",
1574
+ ...typeStyle(AI_TREE_TOKENS.type.bodyM, ctx.brand.fonts.body)
1575
+ };
1576
+ const labelStyle = {
1577
+ ...typeStyle(AI_TREE_TOKENS.type.bodyMBold, ctx.brand.fonts.body),
1578
+ color: ctx.brand.palette.dark,
1579
+ textAlign: "left",
1580
+ width: "100%"
1581
+ };
1582
+ const centered = ctx.sectionAlignment === "center";
1583
+ const submitAlign = centered ? "center" : "flex-start";
1584
+ const children = node.children ?? [];
1585
+ return (
1586
+ // 32px between the field group and the submit. In a stacked (centered) section the form is
1587
+ // capped at 780px and centered — the section's 12-col grid would otherwise leave it hugging
1588
+ // the left edge; a split section lets it fill its own column.
1589
+ /* @__PURE__ */ jsxs(
1590
+ "form",
1591
+ {
1592
+ ...formAttrs,
1593
+ "data-ai-form": "",
1594
+ style: {
1595
+ display: "flex",
1596
+ flexDirection: "column",
1597
+ gap: 32,
1598
+ width: "100%",
1599
+ ...centered ? { maxWidth: 780, marginLeft: "auto", marginRight: "auto" } : {}
1600
+ },
1601
+ children: [
1602
+ /* @__PURE__ */ jsx("div", { style: { display: "flex", flexDirection: "column", gap: 24, width: "100%", alignItems: "flex-start" }, children: children.map((child, i) => {
1603
+ if (child.type !== "input") return null;
1604
+ const cs = child.slots ?? {};
1605
+ const kind = str(cs.kind);
1606
+ const label = str(cs.label);
1607
+ const placeholder = str(cs.placeholder);
1608
+ const required = cs.required === true;
1609
+ const name = label.toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-+|-+$/gu, "") || `field-${i}`;
1610
+ const isTextarea = kind === "textarea";
1611
+ return /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 8, width: "100%" }, children: [
1612
+ /* @__PURE__ */ jsx("label", { ...textAttrs(ctx, `${path}.c${i}.label`), style: labelStyle, children: label }),
1613
+ isTextarea ? /* @__PURE__ */ jsx(
1614
+ "textarea",
1615
+ {
1616
+ name,
1617
+ placeholder,
1618
+ required,
1619
+ style: { ...fieldStyle, height: 180, resize: "vertical" }
1620
+ }
1621
+ ) : /* @__PURE__ */ jsx(
1622
+ "input",
1623
+ {
1624
+ name,
1625
+ type: kind === "email" ? "email" : "text",
1626
+ placeholder,
1627
+ required,
1628
+ style: { ...fieldStyle, height: 48 }
1629
+ }
1630
+ )
1631
+ ] }, i);
1632
+ }) }),
1633
+ children.map((child, i) => {
1634
+ if (child.type === "input") return null;
1635
+ const cs = child.slots ?? {};
1636
+ return /* @__PURE__ */ jsx(
1637
+ "button",
1638
+ {
1639
+ type: "submit",
1640
+ style: {
1641
+ alignSelf: submitAlign,
1642
+ border: "none",
1643
+ cursor: "pointer",
1644
+ padding: "12px 24px",
1645
+ // Corner radius follows the host template's own buttons (measured from a template
1646
+ // CTA); 8px only when the page has no template button to match.
1647
+ borderRadius: ctx.buttonRadius ?? 8,
1648
+ // Brand-styled: primary fill, brand-derived label colour (not a fixed token) so it
1649
+ // reads correctly on custom palettes.
1650
+ background: ctx.brand.palette.primary,
1651
+ color: ctx.buttonLabel ?? ctx.brand.palette.light,
1652
+ ...typeStyle(AI_TREE_TOKENS.type.button, ctx.brand.fonts.body)
1653
+ },
1654
+ children: /* @__PURE__ */ jsx("span", { ...textAttrs(ctx, `${path}.c${i}.label`), children: str(cs.label) })
1655
+ },
1656
+ i
1657
+ );
1658
+ })
1659
+ ]
1660
+ }
1661
+ )
1662
+ );
1663
+ }
1132
1664
  case "schedule-widget":
1133
1665
  return /* @__PURE__ */ jsx(
1134
1666
  "div",
@@ -1149,16 +1681,27 @@ function renderNode(node, ctx, path) {
1149
1681
  return null;
1150
1682
  }
1151
1683
  }
1152
- function AiTreeRenderer({ tree, brand, resolveMedia, editKeyPrefix }) {
1684
+ function AiTreeRenderer({
1685
+ tree,
1686
+ brand,
1687
+ buttonRadius,
1688
+ resolveMedia,
1689
+ editKeyPrefix
1690
+ }) {
1153
1691
  if (!isRenderableTree(tree)) {
1154
1692
  return null;
1155
1693
  }
1156
1694
  const resolvedBrand = brand ?? AI_DEFAULT_BRAND;
1695
+ const band = (tree.settings ?? {}).sectionBackground === "accent" ? accentBandContext(resolvedBrand) : null;
1696
+ const blockBrand = band?.brand ?? resolvedBrand;
1157
1697
  const ctx = {
1158
- brand: resolvedBrand,
1698
+ brand: blockBrand,
1159
1699
  resolveMedia: resolveMedia ?? (() => null),
1160
- cardSurface: resolvedBrand.palette.light.toUpperCase() === AI_DEFAULT_BRAND.palette.light.toUpperCase() ? "#ECECEB" : `color-mix(in srgb, ${resolvedBrand.palette.light} 90%, ${resolvedBrand.palette.dark})`,
1161
- keyFor: editKeyPrefix ? (path) => `${editKeyPrefix}.${path}` : null
1700
+ cardSurface: blockBrand.palette.light.toUpperCase() === AI_DEFAULT_BRAND.palette.light.toUpperCase() ? "#ECECEB" : `color-mix(in srgb, ${blockBrand.palette.light} 90%, ${blockBrand.palette.dark})`,
1701
+ keyFor: editKeyPrefix ? (path) => `${editKeyPrefix}.${path}` : null,
1702
+ sectionAlignment: (tree.settings ?? {}).alignment === "center" ? "center" : "left",
1703
+ buttonRadius,
1704
+ ...band ? { buttonLabel: band.buttonLabel } : {}
1162
1705
  };
1163
1706
  const settings = tree.settings ?? {};
1164
1707
  const pad = AI_TREE_TOKENS.sectionPadding[settings.spacing ?? "balanced"];
@@ -1166,27 +1709,41 @@ function AiTreeRenderer({ tree, brand, resolveMedia, editKeyPrefix }) {
1166
1709
  const overlayAlpha = { low: 0.25, medium: 0.45, high: 0.65 }[settings.overlayIntensity ?? "medium"];
1167
1710
  const backgroundUrl = isOverlay && settings.backgroundMedia ? ctx.resolveMedia(settings.backgroundMedia) : null;
1168
1711
  const bgAttrs = ctx.keyFor && isOverlay && settings.backgroundMedia ? { "data-ohw-key": ctx.keyFor("settings.backgroundMedia"), "data-ohw-editable": "bg-image" } : {};
1712
+ const toneBackground = (() => {
1713
+ const { dark, primary, light } = resolvedBrand.palette;
1714
+ switch (settings.sectionBackground) {
1715
+ case "surface":
1716
+ return `color-mix(in srgb, ${light} 94%, ${dark})`;
1717
+ case "accent":
1718
+ return primary;
1719
+ case "accent-soft":
1720
+ return `color-mix(in srgb, ${primary} 12%, ${light})`;
1721
+ default:
1722
+ return void 0;
1723
+ }
1724
+ })();
1725
+ const distributed = !isOverlay && settings.textDistribution;
1169
1726
  return /* @__PURE__ */ jsxs(
1170
1727
  "section",
1171
1728
  {
1172
1729
  "data-ai-section": tree.tag ?? "",
1173
1730
  ...bgAttrs,
1174
- "data-ai-responsive": "",
1175
1731
  style: {
1176
1732
  position: "relative",
1177
1733
  padding: `${pad}px 0`,
1178
- background: isOverlay && !backgroundUrl ? "#DBEAFE" : void 0,
1734
+ background: toneBackground ?? (isOverlay && !backgroundUrl ? "#DBEAFE" : void 0),
1179
1735
  backgroundImage: backgroundUrl ? `url(${backgroundUrl})` : void 0,
1180
1736
  backgroundSize: "cover",
1181
- backgroundPosition: "center"
1737
+ backgroundPosition: "center",
1738
+ color: settings.sectionBackground === "accent" ? blockBrand.palette.dark : void 0
1182
1739
  },
1183
1740
  children: [
1184
- /* @__PURE__ */ jsx("style", { children: AI_RESPONSIVE_CSS }),
1185
1741
  isOverlay && backgroundUrl && /* @__PURE__ */ jsx("div", { style: { position: "absolute", inset: 0, background: `rgba(255,255,255,${overlayAlpha})` } }),
1742
+ /* @__PURE__ */ jsx("style", { children: AI_MOBILE_CSS }),
1186
1743
  /* @__PURE__ */ jsx(
1187
1744
  "div",
1188
1745
  {
1189
- "data-ai-section-inner": "",
1746
+ "data-ai-container": "",
1190
1747
  style: {
1191
1748
  position: "relative",
1192
1749
  maxWidth: AI_TREE_TOKENS.sectionMaxWidth,
@@ -1197,15 +1754,29 @@ function AiTreeRenderer({ tree, brand, resolveMedia, editKeyPrefix }) {
1197
1754
  children: tree.rows.map((row, r2) => /* @__PURE__ */ jsx(
1198
1755
  "div",
1199
1756
  {
1200
- "data-ai-columns": "",
1757
+ "data-ai-row": "",
1201
1758
  style: {
1202
1759
  display: "grid",
1203
1760
  gridTemplateColumns: "repeat(12, 1fr)",
1204
1761
  gap: AI_TREE_TOKENS.spacing6,
1205
- alignItems: settings.verticalPosition === "top" ? "start" : "center",
1762
+ alignItems: distributed === "space-between" ? "stretch" : (distributed ?? settings.verticalPosition) === "top" ? "start" : "center",
1206
1763
  marginTop: r2 > 0 ? AI_TREE_TOKENS.spacing8 : 0
1207
1764
  },
1208
- children: (row.blocks ?? []).map((block, b) => /* @__PURE__ */ jsx("div", { style: { gridColumn: `span ${Math.min(12, Math.max(1, block.span))}`, minWidth: 0 }, children: renderNode(block, ctx, `r${r2}.b${b}`) }, b))
1765
+ children: (row.blocks ?? []).map((block, b) => /* @__PURE__ */ jsx(
1766
+ "div",
1767
+ {
1768
+ "data-ai-cell": "",
1769
+ style: {
1770
+ gridColumn: `span ${Math.min(12, Math.max(1, block.span))}`,
1771
+ minWidth: 0,
1772
+ // space-between: each column becomes a flex column whose content spreads over
1773
+ // the full row height instead of clumping at the top.
1774
+ ...distributed === "space-between" ? { display: "flex", flexDirection: "column", justifyContent: "space-between" } : {}
1775
+ },
1776
+ children: renderNode(block, ctx, `r${r2}.b${b}`)
1777
+ },
1778
+ b
1779
+ ))
1209
1780
  },
1210
1781
  r2
1211
1782
  ))
@@ -1221,17 +1792,36 @@ import { jsx as jsx2 } from "react/jsx-runtime";
1221
1792
  var CONTAINER_ATTR = "data-ohw-ai-generated";
1222
1793
  var REPLACED_ATTR = "data-ohw-ai-replaced-by";
1223
1794
  var REMOVED_ATTR = "data-ohw-ai-removed";
1795
+ var TEMPLATE_HIDDEN_ATTR = "data-ohw-ai-template-hidden";
1796
+ var CHROME_SECTION_IDS = /* @__PURE__ */ new Set(["navbar", "footer"]);
1797
+ function readRootVar(name) {
1798
+ if (typeof document === "undefined") return "";
1799
+ return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
1800
+ }
1801
+ function deriveBrandOverride() {
1802
+ const dark = readRootVar("--ohw-brand-dark");
1803
+ const primary = readRootVar("--ohw-brand-primary");
1804
+ const light = readRootVar("--ohw-brand-light");
1805
+ if (!dark || !primary || !light) return null;
1806
+ const accent = readRootVar("--ohw-brand-accent");
1807
+ const heading = readRootVar("--font-heading") || readRootVar("--font-display");
1808
+ const body = readRootVar("--font-body");
1809
+ return {
1810
+ palette: { dark, primary, accent: accent || dark, light },
1811
+ fonts: {
1812
+ heading: heading || AI_DEFAULT_BRAND.fonts.heading,
1813
+ body: body || AI_DEFAULT_BRAND.fonts.body
1814
+ }
1815
+ };
1816
+ }
1224
1817
  function deriveTemplateBrand() {
1225
- if (typeof document === "undefined") return null;
1226
- const cs = getComputedStyle(document.documentElement);
1227
- const read = (name) => cs.getPropertyValue(name).trim();
1228
- const dark = read("--color-dark");
1229
- const primary = read("--color-primary");
1230
- const light = read("--color-light");
1818
+ const dark = readRootVar("--color-dark");
1819
+ const primary = readRootVar("--color-primary");
1820
+ const light = readRootVar("--color-light");
1231
1821
  if (!dark || !primary || !light) return null;
1232
- const accent = read("--color-accent");
1233
- const heading = read("--font-heading") || read("--font-display");
1234
- const body = read("--font-body");
1822
+ const accent = readRootVar("--color-accent");
1823
+ const heading = readRootVar("--font-heading") || readRootVar("--font-display");
1824
+ const body = readRootVar("--font-body");
1235
1825
  return {
1236
1826
  palette: { dark, primary, accent: accent || dark, light },
1237
1827
  fonts: {
@@ -1240,6 +1830,13 @@ function deriveTemplateBrand() {
1240
1830
  }
1241
1831
  };
1242
1832
  }
1833
+ function deriveTemplateButtonRadius() {
1834
+ if (typeof document === "undefined") return null;
1835
+ const btn = document.querySelector('[data-ohw-role="button"]');
1836
+ if (!btn) return null;
1837
+ const radius = getComputedStyle(btn).borderTopLeftRadius;
1838
+ return radius || null;
1839
+ }
1243
1840
  var mounted = /* @__PURE__ */ new Map();
1244
1841
  function findTemplateSection(id) {
1245
1842
  for (const el of document.querySelectorAll(`[data-ohw-section="${CSS.escape(id)}"]`)) {
@@ -1303,6 +1900,24 @@ function syncRemovedSections(state) {
1303
1900
  }
1304
1901
  }
1305
1902
  }
1903
+ function syncTemplateHidden(state, pageHasSections) {
1904
+ const hide = state.hideTemplate === true && pageHasSections;
1905
+ for (const el of Array.from(document.querySelectorAll(`[${TEMPLATE_HIDDEN_ATTR}]`))) {
1906
+ if (!hide) {
1907
+ el.style.removeProperty("display");
1908
+ el.removeAttribute(TEMPLATE_HIDDEN_ATTR);
1909
+ }
1910
+ }
1911
+ if (!hide) return;
1912
+ for (const el of Array.from(document.querySelectorAll("[data-ohw-section]"))) {
1913
+ if (el.hasAttribute(CONTAINER_ATTR)) continue;
1914
+ if (CHROME_SECTION_IDS.has(el.getAttribute("data-ohw-section") ?? "")) continue;
1915
+ if (el.parentElement?.closest("[data-ohw-section]")) continue;
1916
+ if (el.hasAttribute(REPLACED_ATTR) || el.hasAttribute(REMOVED_ATTR)) continue;
1917
+ el.style.display = "none";
1918
+ el.setAttribute(TEMPLATE_HIDDEN_ATTR, "");
1919
+ }
1920
+ }
1306
1921
  function syncReplacedOriginals(state) {
1307
1922
  for (const el of document.querySelectorAll(`[${REPLACED_ATTR}]`)) {
1308
1923
  const byId = el.getAttribute(REPLACED_ATTR) ?? "";
@@ -1321,10 +1936,64 @@ function syncReplacedOriginals(state) {
1321
1936
  }
1322
1937
  }
1323
1938
  }
1939
+ var sectionOrderIndex = /* @__PURE__ */ new Map();
1940
+ function setAiSectionOrder(raw, currentPath) {
1941
+ const next = /* @__PURE__ */ new Map();
1942
+ if (raw) {
1943
+ try {
1944
+ const entries = JSON.parse(raw);
1945
+ for (const entry of entries) {
1946
+ if (!entry.pagePath || entry.pagePath === currentPath) next.set(entry.instanceId, entry.order);
1947
+ }
1948
+ } catch {
1949
+ }
1950
+ }
1951
+ sectionOrderIndex = next;
1952
+ }
1953
+ function applyExplicitOrder(entries) {
1954
+ if (sectionOrderIndex.size === 0) return entries;
1955
+ return entries.map((entry, index) => ({ entry, index, order: sectionOrderIndex.get(entry.id) })).sort((a, b) => {
1956
+ if (a.order === void 0 && b.order === void 0) return a.index - b.index;
1957
+ if (a.order === void 0) return 1;
1958
+ if (b.order === void 0) return -1;
1959
+ return a.order - b.order;
1960
+ }).map((item) => item.entry);
1961
+ }
1962
+ function orderByChain(sections) {
1963
+ const ids = new Set(sections.map((entry) => entry.id));
1964
+ const after = /* @__PURE__ */ new Map();
1965
+ const roots = [];
1966
+ for (const entry of sections) {
1967
+ const anchor = entry.replaces ?? entry.beforeSection ?? entry.afterSection ?? null;
1968
+ if (anchor && ids.has(anchor)) {
1969
+ const bucket = after.get(anchor);
1970
+ if (bucket) bucket.push(entry);
1971
+ else after.set(anchor, [entry]);
1972
+ } else {
1973
+ roots.push(entry);
1974
+ }
1975
+ }
1976
+ const out = [];
1977
+ const seen = /* @__PURE__ */ new Set();
1978
+ const visit = (entry) => {
1979
+ if (seen.has(entry.id)) return;
1980
+ seen.add(entry.id);
1981
+ out.push(entry);
1982
+ for (const child of after.get(entry.id) ?? []) visit(child);
1983
+ };
1984
+ for (const root of roots) visit(root);
1985
+ return out.length === sections.length ? out : sections;
1986
+ }
1324
1987
  function applyAiSectionsToDom(state, options) {
1325
1988
  if (typeof document === "undefined") return;
1989
+ const brandOverride = deriveBrandOverride();
1326
1990
  const templateBrand = deriveTemplateBrand();
1327
- const activeIds = new Set(state.sections.map((entry) => entry.id));
1991
+ const templateButtonRadius = deriveTemplateButtonRadius();
1992
+ const brandKey = brandOverride ? JSON.stringify(brandOverride) : "";
1993
+ const pagePath = window.location.pathname;
1994
+ const pageSections = state.sections.filter((entry) => (entry.path ?? "/") === pagePath);
1995
+ const activeIds = new Set(pageSections.map((entry) => entry.id));
1996
+ const ordered = state.hideTemplate === true ? applyExplicitOrder(orderByChain(pageSections)) : pageSections;
1328
1997
  for (const [id, section] of mounted) {
1329
1998
  if (!activeIds.has(id)) {
1330
1999
  section.root.unmount();
@@ -1332,8 +2001,8 @@ function applyAiSectionsToDom(state, options) {
1332
2001
  mounted.delete(id);
1333
2002
  }
1334
2003
  }
1335
- for (const entry of state.sections) {
1336
- const serialized = JSON.stringify(entry);
2004
+ for (const entry of ordered) {
2005
+ const serialized = JSON.stringify(entry) + brandKey;
1337
2006
  const existing = mounted.get(entry.id);
1338
2007
  if (existing && existing.serialized === serialized && existing.container.isConnected) {
1339
2008
  continue;
@@ -1358,7 +2027,8 @@ function applyAiSectionsToDom(state, options) {
1358
2027
  AiTreeRenderer,
1359
2028
  {
1360
2029
  tree: entry.tree,
1361
- brand: entry.brand ?? options?.brand ?? templateBrand ?? void 0,
2030
+ brand: brandOverride ?? entry.brand ?? options?.brand ?? templateBrand ?? void 0,
2031
+ buttonRadius: templateButtonRadius,
1362
2032
  resolveMedia,
1363
2033
  editKeyPrefix: `ai.${entry.id}`
1364
2034
  }
@@ -1367,8 +2037,20 @@ function applyAiSectionsToDom(state, options) {
1367
2037
  });
1368
2038
  mounted.set(entry.id, { root, container, serialized });
1369
2039
  }
2040
+ if (state.hideTemplate === true) {
2041
+ let prev = null;
2042
+ for (const entry of ordered) {
2043
+ const el = mounted.get(entry.id)?.container;
2044
+ if (!el) continue;
2045
+ if (prev && !(prev.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_FOLLOWING)) {
2046
+ prev.insertAdjacentElement("afterend", el);
2047
+ }
2048
+ prev = el;
2049
+ }
2050
+ }
1370
2051
  syncReplacedOriginals(state);
1371
2052
  syncRemovedSections(state);
2053
+ syncTemplateHidden(state, pageSections.length > 0);
1372
2054
  }
1373
2055
 
1374
2056
  // src/useLinkHrefGuardian.ts
@@ -7067,13 +7749,17 @@ function MediaOverlay({
7067
7749
  hover,
7068
7750
  isUploading,
7069
7751
  fadingOut = false,
7752
+ selected = false,
7753
+ hovered = false,
7070
7754
  onFadeOutComplete,
7071
7755
  onReplace,
7756
+ onSelect,
7072
7757
  onVideoSettingsChange
7073
7758
  }) {
7074
7759
  const { rect } = hover;
7075
7760
  const skeletonRef = React8.useRef(null);
7076
7761
  const isVideo = hover.elementType === "video";
7762
+ const showChrome = !selected || hovered;
7077
7763
  const autoplay = hover.videoAutoplay ?? true;
7078
7764
  const muted = hover.videoMuted ?? true;
7079
7765
  const probeRef = React8.useRef(null);
@@ -7120,7 +7806,7 @@ function MediaOverlay({
7120
7806
  }
7121
7807
  );
7122
7808
  }
7123
- const settingsBar = isVideo && !hover.isDragOver ? /* @__PURE__ */ jsxs7(
7809
+ const settingsBar = isVideo && !hover.isDragOver && showChrome ? /* @__PURE__ */ jsxs7(
7124
7810
  "div",
7125
7811
  {
7126
7812
  "data-ohw-bridge": "",
@@ -7190,10 +7876,12 @@ function MediaOverlay({
7190
7876
  // in-document, pointer-events does it natively. The button below opts back in, so
7191
7877
  // Replace still works.
7192
7878
  pointerEvents: hover.hasTextOverlap ? "none" : "auto",
7193
- boxShadow: "inset 0 0 0 1.5px var(--color-primary)",
7194
- background: "color-mix(in srgb, var(--color-primary) 20%, transparent)"
7879
+ // Selected: a firm component ring with no wash, so the image reads as chosen rather
7880
+ // than hovered. Hover keeps the existing tinted preview.
7881
+ boxShadow: selected ? "inset 0 0 0 2px var(--color-primary)" : "inset 0 0 0 1.5px var(--color-primary)",
7882
+ background: selected ? "transparent" : "color-mix(in srgb, var(--color-primary) 20%, transparent)"
7195
7883
  },
7196
- onClick: () => onReplace(hover.key),
7884
+ onClick: () => onSelect && !selected ? onSelect(hover.key) : onReplace(hover.key),
7197
7885
  children: [
7198
7886
  /* @__PURE__ */ jsxs7(
7199
7887
  Button,
@@ -7247,7 +7935,7 @@ function MediaOverlay({
7247
7935
  },
7248
7936
  children: [
7249
7937
  isVideo ? /* @__PURE__ */ jsx15(Film, { size: 14 }) : /* @__PURE__ */ jsx15(ImageIcon, { size: 14 }),
7250
- replaceMode === "full" ? isVideo ? "Replace video" : "Replace image" : null
7938
+ replaceMode === "full" ? isVideo ? "Replace video" : hover.elementType === "bg-image" ? "Replace background" : "Replace image" : null
7251
7939
  ]
7252
7940
  }
7253
7941
  )
@@ -7331,6 +8019,8 @@ function parseSectionsFromRoot(root) {
7331
8019
  const id = el.getAttribute("data-ohw-section") ?? "";
7332
8020
  if (!id || seen.has(id) || LINK_PICKER_EXCLUDED_IDS.has(id)) continue;
7333
8021
  if (el.parentElement?.closest("[data-ohw-section]")) continue;
8022
+ if (el.hasAttribute("data-ohw-ai-template-hidden") || el.hasAttribute("data-ohw-ai-removed") || el.hasAttribute("data-ohw-ai-replaced-by"))
8023
+ continue;
7334
8024
  seen.add(id);
7335
8025
  const label = el.getAttribute("data-ohw-section-label") ?? titleCaseSectionId(id);
7336
8026
  sections.push({ id, label });
@@ -7616,6 +8306,7 @@ function AiSectionOverlay({
7616
8306
  }) {
7617
8307
  const [selectedId, setSelectedId] = useState5(null);
7618
8308
  const [reviewId, setReviewId] = useState5(null);
8309
+ const [reviewButtonsHidden, setReviewButtonsHidden] = useState5(false);
7619
8310
  const reviewIdRef = useRef4(null);
7620
8311
  reviewIdRef.current = reviewId;
7621
8312
  const selectedIdRef = useRef4(null);
@@ -7677,6 +8368,7 @@ function AiSectionOverlay({
7677
8368
  }
7678
8369
  const found = readRect(sectionId) != null;
7679
8370
  setReviewId(found ? sectionId : null);
8371
+ setReviewButtonsHidden(e.data.hideButtons === true);
7680
8372
  postToParent2({ type: "ow:ai-review-started", sectionId, found });
7681
8373
  if (found) {
7682
8374
  document.querySelector(`[data-ohw-instance="${CSS.escape(sectionId)}"]`)?.scrollIntoView({ behavior: "smooth", block: "nearest" });
@@ -7804,13 +8496,16 @@ function AiSectionOverlay({
7804
8496
  border: `2px solid ${PRIMARY2}`,
7805
8497
  borderRadius: edgeAwareRadius(reviewRect),
7806
8498
  zIndex: 2147483200,
7807
- // The veil itself: swallows clicks so the section stays locked until decided.
8499
+ // The veil itself: swallows clicks so the section stays locked until decided. This
8500
+ // stopPropagation only guards the bubble phase; the bridge's capture-phase click
8501
+ // handlers additionally bail on `[data-ohw-ai-review]`, without which a click on
8502
+ // Accept/Discard resolves to the media beneath and opens the file picker.
7808
8503
  background: "rgba(8, 133, 254, 0.04)",
7809
8504
  pointerEvents: "auto",
7810
8505
  cursor: "default"
7811
8506
  },
7812
8507
  onClick: (e) => e.stopPropagation(),
7813
- children: /* @__PURE__ */ jsxs9(
8508
+ children: !reviewButtonsHidden && /* @__PURE__ */ jsxs9(
7814
8509
  "div",
7815
8510
  {
7816
8511
  style: {
@@ -10377,8 +11072,13 @@ function referenceBox(slot) {
10377
11072
  const neighbour = row ? Array.from(row.querySelectorAll('[data-ohw-editable="icon"]')).find(
10378
11073
  (el) => el !== slot && !el.hasAttribute("data-ohw-social-icon-placeholder")
10379
11074
  ) : null;
10380
- const source = (neighbour ?? slot).querySelector(GLYPH_SELECTOR);
10381
- const box = source?.getBoundingClientRect() ?? null;
11075
+ if (neighbour) {
11076
+ const box2 = neighbour.querySelector(GLYPH_SELECTOR)?.getBoundingClientRect();
11077
+ if (box2?.width && box2.height) return box2;
11078
+ }
11079
+ const own = slot.getBoundingClientRect();
11080
+ if (own.width && own.height) return own;
11081
+ const box = slot.querySelector(GLYPH_SELECTOR)?.getBoundingClientRect() ?? null;
10382
11082
  return box?.width && box.height ? box : null;
10383
11083
  }
10384
11084
  function iconMarkupSizedFor(slot, markup) {
@@ -13253,6 +13953,7 @@ function useSectionDrag({
13253
13953
  }
13254
13954
  const orderJson = JSON.stringify(entries);
13255
13955
  editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: orderJson };
13956
+ setAiSectionOrder(orderJson, window.location.pathname);
13256
13957
  postToParentRef.current({ type: "ow:change", nodes: [{ key: SECTION_ORDER_KEY, text: orderJson }] });
13257
13958
  applyPersistedOrder(entries);
13258
13959
  clearSectionDragVisuals();
@@ -15372,6 +16073,70 @@ function OhhwellsBridge() {
15372
16073
  const hoveredImageHasTextOverlapRef = useRef10(false);
15373
16074
  const dragOverElRef = useRef10(null);
15374
16075
  const [mediaHover, setMediaHover] = useState13(null);
16076
+ const [selectedMedia, setSelectedMedia] = useState13(null);
16077
+ const selectedMediaElRef = useRef10(null);
16078
+ const clearMediaSelection = useCallback8(() => {
16079
+ const prev = selectedMediaElRef.current;
16080
+ selectedMediaElRef.current = null;
16081
+ setSelectedMedia(null);
16082
+ const sectionEl = prev?.closest("[data-ohw-section]") ?? null;
16083
+ if (sectionEl) {
16084
+ postToParentRef.current({
16085
+ type: "ow:section-selected",
16086
+ sectionId: sectionEl.dataset.ohwSection ?? null,
16087
+ sectionLabel: sectionEl.dataset.ohwSectionLabel ?? titleCaseSectionId(sectionEl.dataset.ohwSection ?? ""),
16088
+ key: null
16089
+ });
16090
+ }
16091
+ }, []);
16092
+ const clearMediaSelectionRef = useRef10(clearMediaSelection);
16093
+ clearMediaSelectionRef.current = clearMediaSelection;
16094
+ const selectMediaElement = useCallback8((el) => {
16095
+ const r2 = el.getBoundingClientRect();
16096
+ const video = el.dataset.ohwEditable === "video" ? el.querySelector("video") : null;
16097
+ selectedMediaElRef.current = el;
16098
+ setSelectedMedia({
16099
+ key: el.dataset.ohwKey ?? "",
16100
+ rect: { top: r2.top, left: r2.left, width: r2.width, height: r2.height },
16101
+ elementType: el.dataset.ohwEditable ?? "image",
16102
+ hasTextOverlap: false,
16103
+ isDragOver: false,
16104
+ ...video ? { videoAutoplay: video.autoplay, videoMuted: video.muted } : {}
16105
+ });
16106
+ const sectionEl = el.closest("[data-ohw-section]");
16107
+ aiSectionApiRef.current?.selectFromElement(el, { report: false });
16108
+ postToParentRef.current({
16109
+ type: "ow:section-selected",
16110
+ sectionId: sectionEl?.dataset.ohwSection ?? null,
16111
+ sectionLabel: sectionEl ? sectionEl.dataset.ohwSectionLabel ?? titleCaseSectionId(sectionEl.dataset.ohwSection ?? "") : null,
16112
+ key: el.dataset.ohwKey ?? null,
16113
+ // Display name for the pill — the raw key prettifies into fragments ("Img"); the
16114
+ // bridge knows what the node IS, so it names it.
16115
+ keyLabel: el.dataset.ohwEditable === "video" ? "Video" : el.dataset.ohwEditable === "bg-image" ? "Background" : "Image"
16116
+ });
16117
+ }, []);
16118
+ const selectMediaElementRef = useRef10(selectMediaElement);
16119
+ selectMediaElementRef.current = selectMediaElement;
16120
+ useEffect13(() => {
16121
+ if (!selectedMedia) return;
16122
+ const update = () => {
16123
+ const el = selectedMediaElRef.current;
16124
+ if (!el || !el.isConnected) {
16125
+ clearMediaSelection();
16126
+ return;
16127
+ }
16128
+ const r2 = el.getBoundingClientRect();
16129
+ setSelectedMedia(
16130
+ (prev) => prev ? { ...prev, rect: { top: r2.top, left: r2.left, width: r2.width, height: r2.height } } : prev
16131
+ );
16132
+ };
16133
+ window.addEventListener("scroll", update, true);
16134
+ window.addEventListener("resize", update);
16135
+ return () => {
16136
+ window.removeEventListener("scroll", update, true);
16137
+ window.removeEventListener("resize", update);
16138
+ };
16139
+ }, [selectedMedia !== null]);
15375
16140
  const [carouselHover, setCarouselHover] = useState13(null);
15376
16141
  const [uploadingRects, setUploadingRects] = useState13({});
15377
16142
  const hoveredGapRef = useRef10(null);
@@ -15655,6 +16420,8 @@ function OhhwellsBridge() {
15655
16420
  const addNavAfterAnchorRef = useRef10(null);
15656
16421
  const editContentRef = useRef10({});
15657
16422
  const aiSectionsRef = useRef10("");
16423
+ const brandKitRef = useRef10("");
16424
+ const stylesRef = useRef10("");
15658
16425
  const pendingDeleteUndoRef = useRef10(null);
15659
16426
  const [sitePages, setSitePages] = useState13([]);
15660
16427
  const [sectionsByPath, setSectionsByPath] = useState13({});
@@ -16975,13 +17742,29 @@ function OhhwellsBridge() {
16975
17742
  }
16976
17743
  const applyContent = (content) => {
16977
17744
  const imageLoads = [];
17745
+ if (typeof content[BRAND_KIT_KEY] === "string") {
17746
+ brandKitRef.current = content[BRAND_KIT_KEY];
17747
+ applyBrandToDom(parseBrandKit(content[BRAND_KIT_KEY]));
17748
+ } else {
17749
+ brandKitRef.current = "";
17750
+ applyBrandToDom(null);
17751
+ }
16978
17752
  if (typeof content[AI_SECTIONS_KEY] === "string") {
16979
17753
  aiSectionsRef.current = content[AI_SECTIONS_KEY];
17754
+ setAiSectionOrder(content[SECTION_ORDER_KEY], window.location.pathname);
16980
17755
  applyAiSectionsToDom(parseAiSectionsState(content[AI_SECTIONS_KEY]));
16981
17756
  }
17757
+ if (typeof content[STYLE_STORE_KEY] === "string") {
17758
+ stylesRef.current = content[STYLE_STORE_KEY];
17759
+ applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
17760
+ }
17761
+ applyBrandChrome(content);
16982
17762
  for (const [key, val] of Object.entries(content)) {
16983
17763
  if (key === "__ohw_sections") continue;
16984
17764
  if (key === AI_SECTIONS_KEY) continue;
17765
+ if (key === BRAND_KIT_KEY) continue;
17766
+ if (key === STYLE_STORE_KEY) continue;
17767
+ if (BRAND_CHROME_KEYS.has(key)) continue;
16985
17768
  if (key === LOGO_PLACEHOLDER_KEY) continue;
16986
17769
  if (LOGO_IMAGE_KEYS.includes(key)) continue;
16987
17770
  if (applyVideoSettingNode(key, val)) continue;
@@ -17169,8 +17952,25 @@ function OhhwellsBridge() {
17169
17952
  initSectionInstancesFromContent(content, window.location.pathname);
17170
17953
  observer?.disconnect();
17171
17954
  try {
17955
+ applyBrandChrome(content);
17956
+ if (typeof content[BRAND_KIT_KEY] === "string") {
17957
+ applyBrandToDom(parseBrandKit(content[BRAND_KIT_KEY]));
17958
+ } else {
17959
+ applyBrandToDom(null);
17960
+ }
17961
+ if (typeof content[AI_SECTIONS_KEY] === "string") {
17962
+ setAiSectionOrder(content[SECTION_ORDER_KEY], window.location.pathname);
17963
+ applyAiSectionsToDom(parseAiSectionsState(content[AI_SECTIONS_KEY]));
17964
+ }
17965
+ if (typeof content[STYLE_STORE_KEY] === "string") {
17966
+ applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
17967
+ }
17172
17968
  for (const [key, val] of Object.entries(content)) {
17173
17969
  if (key === "__ohw_sections") continue;
17970
+ if (key === AI_SECTIONS_KEY) continue;
17971
+ if (key === BRAND_KIT_KEY) continue;
17972
+ if (key === STYLE_STORE_KEY) continue;
17973
+ if (BRAND_CHROME_KEYS.has(key)) continue;
17174
17974
  if (key === LOGO_PLACEHOLDER_KEY) continue;
17175
17975
  if (LOGO_IMAGE_KEYS.includes(key)) continue;
17176
17976
  if (applyVideoSettingNode(key, val)) continue;
@@ -17313,9 +18113,21 @@ function OhhwellsBridge() {
17313
18113
  }, [isEditMode, pathname, subdomain, fetchState, resyncSelectedNavigationItem]);
17314
18114
  useEffect13(() => {
17315
18115
  if (!isEditMode) return;
18116
+ let lastPosted = 0;
17316
18117
  const measure = () => {
17317
18118
  const h = document.body.scrollHeight;
17318
- if (h > 50) postToParent2({ type: "ow:height", height: h });
18119
+ if (h > 50 && Math.abs(h - lastPosted) > 1) {
18120
+ lastPosted = h;
18121
+ postToParent2({ type: "ow:height", height: h });
18122
+ }
18123
+ };
18124
+ let raf = null;
18125
+ const schedule = () => {
18126
+ if (raf != null) return;
18127
+ raf = requestAnimationFrame(() => {
18128
+ raf = null;
18129
+ measure();
18130
+ });
17319
18131
  };
17320
18132
  const t1 = setTimeout(measure, 50);
17321
18133
  const t2 = setTimeout(measure, 500);
@@ -17335,6 +18147,7 @@ function OhhwellsBridge() {
17335
18147
  return () => {
17336
18148
  clearTimeout(t1);
17337
18149
  clearTimeout(t2);
18150
+ if (raf != null) cancelAnimationFrame(raf);
17338
18151
  clearResizeTimers();
17339
18152
  window.removeEventListener("resize", handleResize);
17340
18153
  };
@@ -17577,10 +18390,14 @@ function OhhwellsBridge() {
17577
18390
  return;
17578
18391
  }
17579
18392
  const target = e.target;
18393
+ if (target.closest("[data-ohw-ai-review]")) return;
17580
18394
  if (target.closest("[data-ohw-toolbar]")) return;
17581
18395
  if (target.closest("[data-ohw-state-toggle]")) return;
17582
18396
  if (target.closest("[data-ohw-max-badge]")) return;
17583
18397
  if (isInsideLinkEditor(target)) return;
18398
+ if (selectedMediaElRef.current && !selectedMediaElRef.current.contains(target) && !target.closest("[data-ohw-media-overlay], [data-ohw-edit-chrome], [data-ohw-item-interaction]")) {
18399
+ clearMediaSelectionRef.current();
18400
+ }
17584
18401
  if (isInsideFloatingPanel(target)) return;
17585
18402
  if (target.closest("[data-ohw-form-toolbar]")) return;
17586
18403
  if (target.closest(
@@ -17750,8 +18567,11 @@ function OhhwellsBridge() {
17750
18567
  if (isMediaEditable(editable) && !buttonOnMedia) {
17751
18568
  e.preventDefault();
17752
18569
  e.stopPropagation();
17753
- aiSectionApiRef.current?.selectFromElement(editable);
17754
- postToParentRef.current({ type: "ow:image-pick", key: editable.dataset.ohwKey ?? "", elementType: editable.dataset.ohwEditable ?? "image" });
18570
+ if (selectedMediaElRef.current === editable) {
18571
+ postToParentRef.current({ type: "ow:image-pick", key: editable.dataset.ohwKey ?? "", elementType: editable.dataset.ohwEditable ?? "image" });
18572
+ } else {
18573
+ selectMediaElementRef.current(editable);
18574
+ }
17755
18575
  return;
17756
18576
  }
17757
18577
  const socialItem = getSocialItem(editable);
@@ -17892,6 +18712,7 @@ function OhhwellsBridge() {
17892
18712
  };
17893
18713
  const handleDblClick = (e) => {
17894
18714
  const target = e.target;
18715
+ if (target.closest("[data-ohw-ai-review]")) return;
17895
18716
  if (target.closest("[data-ohw-toolbar]")) return;
17896
18717
  if (target.closest("[data-ohw-state-toggle]")) return;
17897
18718
  if (target.closest("[data-ohw-max-badge]")) return;
@@ -18692,7 +19513,9 @@ function OhhwellsBridge() {
18692
19513
  return;
18693
19514
  }
18694
19515
  const { y } = toProbeCoords(clientX, clientY, fromParentViewport);
18695
- const sections = Array.from(document.querySelectorAll("[data-ohw-section]")).sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top);
19516
+ const sections = Array.from(document.querySelectorAll("[data-ohw-section]")).filter(
19517
+ (el) => !el.hasAttribute("data-ohw-ai-template-hidden") && !el.hasAttribute("data-ohw-ai-removed") && !el.hasAttribute("data-ohw-ai-replaced-by") && el.getBoundingClientRect().height > 0
19518
+ ).sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top);
18696
19519
  const ZONE = 20;
18697
19520
  for (let i = 0; i < sections.length; i++) {
18698
19521
  const a = sections[i];
@@ -19024,10 +19847,23 @@ function OhhwellsBridge() {
19024
19847
  if (e.data?.type !== "ow:hydrate") return;
19025
19848
  const content = e.data.content;
19026
19849
  if (!content) return;
19850
+ if (typeof content[BRAND_KIT_KEY] === "string") {
19851
+ brandKitRef.current = content[BRAND_KIT_KEY];
19852
+ applyBrandToDom(parseBrandKit(content[BRAND_KIT_KEY]));
19853
+ } else {
19854
+ brandKitRef.current = "";
19855
+ applyBrandToDom(null);
19856
+ }
19027
19857
  if (typeof content[AI_SECTIONS_KEY] === "string") {
19028
19858
  aiSectionsRef.current = content[AI_SECTIONS_KEY];
19859
+ setAiSectionOrder(content[SECTION_ORDER_KEY], window.location.pathname);
19029
19860
  applyAiSectionsToDom(parseAiSectionsState(content[AI_SECTIONS_KEY]));
19030
19861
  }
19862
+ if (typeof content[STYLE_STORE_KEY] === "string") {
19863
+ stylesRef.current = content[STYLE_STORE_KEY];
19864
+ applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
19865
+ }
19866
+ applyBrandChrome(content);
19031
19867
  let sectionsJson = null;
19032
19868
  for (const [key, val] of Object.entries(content)) {
19033
19869
  if (key === "__ohw_sections") {
@@ -19035,6 +19871,9 @@ function OhhwellsBridge() {
19035
19871
  continue;
19036
19872
  }
19037
19873
  if (key === AI_SECTIONS_KEY) continue;
19874
+ if (key === BRAND_KIT_KEY) continue;
19875
+ if (key === STYLE_STORE_KEY) continue;
19876
+ if (BRAND_CHROME_KEYS.has(key)) continue;
19038
19877
  if (key === LOGO_PLACEHOLDER_KEY) continue;
19039
19878
  if (LOGO_IMAGE_KEYS.includes(key) && !String(val ?? "").trim()) continue;
19040
19879
  if (applyVideoSettingNode(key, val)) continue;
@@ -19050,6 +19889,8 @@ function OhhwellsBridge() {
19050
19889
  if (video && video.src !== val) applyVideoSrc(video, val);
19051
19890
  } else if (el.dataset.ohwEditable === "link") {
19052
19891
  applyLinkHref(el, val);
19892
+ } else if (el.dataset.ohwEditable === "icon") {
19893
+ applyIconMarkup(el, val);
19053
19894
  } else if (isIconMarkupValue(val)) {
19054
19895
  } else {
19055
19896
  el.innerHTML = val;
@@ -19134,12 +19975,21 @@ function OhhwellsBridge() {
19134
19975
  nodes: collectEditableNodes(editContentRef.current)
19135
19976
  });
19136
19977
  };
19978
+ const clearInteractionChrome = () => {
19979
+ deactivateRef.current();
19980
+ deselectRef.current();
19981
+ clearMediaSelectionRef.current();
19982
+ };
19137
19983
  const handleAiApplyTree = (e) => {
19138
19984
  if (e.data?.type !== "ow:ai-apply-tree") return;
19139
19985
  const payload = e.data.payload;
19140
19986
  if (!payload || typeof payload.id !== "string" || !isRenderableTree(payload.tree)) return;
19987
+ clearInteractionChrome();
19141
19988
  const previous = aiSectionsRef.current;
19142
- const nextState = applyTreeToState(parseAiSectionsState(previous), payload);
19989
+ const nextState = applyTreeToState(parseAiSectionsState(previous), {
19990
+ ...payload,
19991
+ path: payload.path ?? window.location.pathname
19992
+ });
19143
19993
  const nextValue = serializeAiSectionsState(nextState);
19144
19994
  aiSectionsRef.current = nextValue;
19145
19995
  applyAiSectionsToDom(nextState);
@@ -19160,6 +20010,7 @@ function OhhwellsBridge() {
19160
20010
  const exists = document.querySelector(`[data-ohw-section="${CSS.escape(sectionId)}"]`);
19161
20011
  if (!exists) return;
19162
20012
  if (isPageFrameSection(exists)) return;
20013
+ clearInteractionChrome();
19163
20014
  const previous = aiSectionsRef.current;
19164
20015
  const nextState = deleteSectionFromState(parseAiSectionsState(previous), sectionId);
19165
20016
  const nextValue = serializeAiSectionsState(nextState);
@@ -19175,14 +20026,45 @@ function OhhwellsBridge() {
19175
20026
  const handleAiSetSections = (e) => {
19176
20027
  if (e.data?.type !== "ow:ai-set-sections") return;
19177
20028
  const value = typeof e.data.value === "string" ? e.data.value : "";
20029
+ clearInteractionChrome();
19178
20030
  aiSectionsRef.current = value;
19179
20031
  applyAiSectionsToDom(parseAiSectionsState(value));
20032
+ applyStylesToDom(parseStyleStore(stylesRef.current));
19180
20033
  const restoredHeight = document.body.scrollHeight;
19181
20034
  if (restoredHeight > 50) postToParentRef.current({ type: "ow:height", height: restoredHeight });
19182
20035
  postToParentRef.current({ type: "ow:change", nodes: [{ key: AI_SECTIONS_KEY, text: value }] });
19183
20036
  postAiSectionsChanged();
19184
20037
  };
19185
20038
  window.addEventListener("message", handleAiSetSections);
20039
+ const handleAiSetBrand = (e) => {
20040
+ if (e.data?.type !== "ow:ai-set-brand") return;
20041
+ const value = typeof e.data.value === "string" ? e.data.value : "";
20042
+ const previous = brandKitRef.current;
20043
+ brandKitRef.current = value;
20044
+ applyBrandToDom(parseBrandKit(value));
20045
+ if (aiSectionsRef.current) applyAiSectionsToDom(parseAiSectionsState(aiSectionsRef.current));
20046
+ applyStylesToDom(parseStyleStore(stylesRef.current));
20047
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: BRAND_KIT_KEY, text: value }] });
20048
+ postToParentRef.current({ type: "ow:ai-brand-applied", previous, value });
20049
+ };
20050
+ window.addEventListener("message", handleAiSetBrand);
20051
+ const handleAiSetStyles = (e) => {
20052
+ if (e.data?.type !== "ow:ai-set-styles") return;
20053
+ const value = typeof e.data.value === "string" ? e.data.value : "";
20054
+ const previous = stylesRef.current;
20055
+ stylesRef.current = value;
20056
+ applyStylesToDom(parseStyleStore(value));
20057
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: STYLE_STORE_KEY, text: value }] });
20058
+ postToParentRef.current({ type: "ow:ai-styles-applied", previous, value });
20059
+ };
20060
+ window.addEventListener("message", handleAiSetStyles);
20061
+ const handleGetBrand = (e) => {
20062
+ if (e.data?.type !== "ow:get-brand") return;
20063
+ const template = deriveTemplateBrand();
20064
+ const value = brandKitRef.current || (template ? JSON.stringify(template) : "");
20065
+ postToParentRef.current({ type: "ow:brand-value", value });
20066
+ };
20067
+ window.addEventListener("message", handleGetBrand);
19186
20068
  const handleMoveSection = (e) => {
19187
20069
  if (e.data?.type !== "ow:move-section") return;
19188
20070
  const instanceId = typeof e.data.instanceId === "string" ? e.data.instanceId : "";
@@ -19192,6 +20074,7 @@ function OhhwellsBridge() {
19192
20074
  if (!entries) return;
19193
20075
  const orderJson = JSON.stringify(entries);
19194
20076
  editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: orderJson };
20077
+ setAiSectionOrder(orderJson, window.location.pathname);
19195
20078
  postToParentRef.current({ type: "ow:change", nodes: [{ key: SECTION_ORDER_KEY, text: orderJson }] });
19196
20079
  window.dispatchEvent(new Event("resize"));
19197
20080
  };
@@ -19255,6 +20138,7 @@ function OhhwellsBridge() {
19255
20138
  }
19256
20139
  deselectRef.current();
19257
20140
  deactivateRef.current();
20141
+ clearMediaSelectionRef.current();
19258
20142
  };
19259
20143
  window.addEventListener("message", handleDeactivate);
19260
20144
  const handleToastAction = (e) => {
@@ -19340,6 +20224,10 @@ function OhhwellsBridge() {
19340
20224
  const handleKeyDown = (e) => {
19341
20225
  if (e.key === "Escape" && document.querySelector("[data-ohw-section-picker]")) return;
19342
20226
  if (e.key === "Escape" && document.querySelector("[data-ohw-more-menu]")) return;
20227
+ if (e.key === "Escape" && selectedMediaElRef.current) {
20228
+ clearMediaSelectionRef.current();
20229
+ return;
20230
+ }
19343
20231
  if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "a" && activeElRef.current) {
19344
20232
  e.preventDefault();
19345
20233
  selectAllTextInEditable(activeElRef.current);
@@ -19499,6 +20387,12 @@ function OhhwellsBridge() {
19499
20387
  if (aiSectionsRef.current) {
19500
20388
  nodes.push({ key: AI_SECTIONS_KEY, type: "sections", text: aiSectionsRef.current });
19501
20389
  }
20390
+ if (stylesRef.current) {
20391
+ nodes.push({ key: STYLE_STORE_KEY, type: "styles", text: stylesRef.current });
20392
+ }
20393
+ if (brandKitRef.current) {
20394
+ nodes.push({ key: BRAND_KIT_KEY, type: "brand", text: brandKitRef.current });
20395
+ }
19502
20396
  document.querySelectorAll('[data-ohw-editable="form"]').forEach((form) => {
19503
20397
  const formKey = formKeyOf(form);
19504
20398
  if (!formKey) return;
@@ -19913,6 +20807,9 @@ function OhhwellsBridge() {
19913
20807
  window.removeEventListener("message", handleAiApplyTree);
19914
20808
  window.removeEventListener("message", handleAiDeleteSection);
19915
20809
  window.removeEventListener("message", handleAiSetSections);
20810
+ window.removeEventListener("message", handleAiSetBrand);
20811
+ window.removeEventListener("message", handleAiSetStyles);
20812
+ window.removeEventListener("message", handleGetBrand);
19916
20813
  window.removeEventListener("message", handleMoveSection);
19917
20814
  window.removeEventListener("message", handlePanelDragging);
19918
20815
  window.removeEventListener("message", handleDeleteSection);
@@ -20123,7 +21020,7 @@ function OhhwellsBridge() {
20123
21020
  postToParent2({
20124
21021
  type: "ow:ready",
20125
21022
  version: "1",
20126
- bridgeVersion: "0.1.77",
21023
+ bridgeVersion: "0.1.79",
20127
21024
  path: pathname,
20128
21025
  nodes: collectEditableNodes(editContentRef.current),
20129
21026
  sections
@@ -20530,11 +21427,22 @@ function OhhwellsBridge() {
20530
21427
  const showEditLink = toolbarShowEditLink;
20531
21428
  const currentSections = sectionsByPath[pathname] ?? [];
20532
21429
  linkPopoverOpenRef.current = linkPopover !== null;
21430
+ const handleMediaSelect = useCallback8((key) => {
21431
+ const el = hoveredImageRef.current?.dataset.ohwKey === key ? hoveredImageRef.current : Array.from(document.querySelectorAll(MEDIA_SELECTOR)).find(
21432
+ (m) => (m.dataset.ohwKey ?? "") === key
21433
+ ) ?? null;
21434
+ if (!el) return;
21435
+ selectMediaElementRef.current(el);
21436
+ }, []);
20533
21437
  const handleMediaReplace = useCallback8(
20534
21438
  (key) => {
20535
- postToParent2({ type: "ow:image-pick", key, elementType: mediaHover?.elementType ?? "image" });
21439
+ postToParent2({
21440
+ type: "ow:image-pick",
21441
+ key,
21442
+ elementType: mediaHover?.elementType ?? selectedMedia?.elementType ?? "image"
21443
+ });
20536
21444
  },
20537
- [postToParent2, mediaHover?.elementType]
21445
+ [postToParent2, mediaHover?.elementType, selectedMedia?.elementType]
20538
21446
  );
20539
21447
  const handleEditCarousel = useCallback8(
20540
21448
  (key) => {
@@ -20606,12 +21514,25 @@ function OhhwellsBridge() {
20606
21514
  },
20607
21515
  `uploading-${key}`
20608
21516
  )),
20609
- mediaHover && !(mediaHover.key in uploadingRects) && /* @__PURE__ */ jsx33(
21517
+ mediaHover && !(mediaHover.key in uploadingRects) && mediaHover.key !== selectedMedia?.key && /* @__PURE__ */ jsx33(
20610
21518
  MediaOverlay,
20611
21519
  {
20612
21520
  hover: mediaHover,
20613
21521
  isUploading: false,
20614
21522
  onReplace: handleMediaReplace,
21523
+ onSelect: handleMediaSelect,
21524
+ onVideoSettingsChange: handleVideoSettingsChange
21525
+ }
21526
+ ),
21527
+ selectedMedia && !(selectedMedia.key in uploadingRects) && !linkPopover && /* @__PURE__ */ jsx33(
21528
+ MediaOverlay,
21529
+ {
21530
+ hover: selectedMedia,
21531
+ selected: true,
21532
+ hovered: mediaHover?.key === selectedMedia.key,
21533
+ isUploading: false,
21534
+ onReplace: handleMediaReplace,
21535
+ onSelect: handleMediaSelect,
20615
21536
  onVideoSettingsChange: handleVideoSettingsChange
20616
21537
  }
20617
21538
  ),