@ohhwells/bridge 0.1.78 → 0.1.79

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
@@ -156,7 +156,12 @@ function parseAiSectionsState(raw) {
156
156
  media: entry.media && typeof entry.media === "object" ? entry.media : {}
157
157
  }));
158
158
  const removed = Array.isArray(parsed.removed) ? parsed.removed.filter((id) => typeof id === "string" && id.length > 0) : [];
159
- return { v: 1, sections, ...removed.length ? { removed } : {} };
159
+ return {
160
+ v: 1,
161
+ sections,
162
+ ...removed.length ? { removed } : {},
163
+ ...parsed.hideTemplate === true ? { hideTemplate: true } : {}
164
+ };
160
165
  } catch {
161
166
  return EMPTY_AI_SECTIONS;
162
167
  }
@@ -169,6 +174,7 @@ function applyTreeToState(state, payload) {
169
174
  const entry = {
170
175
  id: payload.id,
171
176
  label: payload.label ?? "Generated section",
177
+ ...typeof payload.path === "string" && payload.path ? { path: payload.path } : {},
172
178
  afterSection: payload.mode === "insert" && !insertBefore ? payload.targetSectionId ?? null : null,
173
179
  ...insertBefore && payload.targetSectionId ? { beforeSection: payload.targetSectionId } : {},
174
180
  ...payload.mode === "replace" && payload.targetSectionId ? { replaces: payload.targetSectionId } : {},
@@ -191,6 +197,317 @@ function deleteSectionFromState(state, sectionId) {
191
197
  return { ...state, v: 1, sections: state.sections, removed: [...removed, sectionId] };
192
198
  }
193
199
 
200
+ // src/lib/brand-chrome.ts
201
+ var BRAND_NAME_KEY = "__ohw_brand_name";
202
+ var BRAND_TITLE_KEY = "__ohw_site_title";
203
+ var BRAND_FAVICON_LETTER_KEY = "__ohw_favicon_letter";
204
+ var BRAND_CHROME_KEYS = /* @__PURE__ */ new Set([
205
+ BRAND_NAME_KEY,
206
+ BRAND_TITLE_KEY,
207
+ BRAND_FAVICON_LETTER_KEY
208
+ ]);
209
+ function upsertMeta(selector, attr, token, value) {
210
+ let el = document.head.querySelector(selector);
211
+ if (!el) {
212
+ el = document.createElement("meta");
213
+ el.setAttribute(attr, token);
214
+ document.head.appendChild(el);
215
+ }
216
+ if (el.getAttribute("content") !== value) el.setAttribute("content", value);
217
+ }
218
+ function escapeXml(value) {
219
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
220
+ }
221
+ function applyLetterFavicon(letter) {
222
+ 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>`;
223
+ const href = `data:image/svg+xml,${encodeURIComponent(svg)}`;
224
+ let link = document.head.querySelector('link[rel="icon"]');
225
+ if (!link) {
226
+ link = document.createElement("link");
227
+ link.rel = "icon";
228
+ document.head.appendChild(link);
229
+ }
230
+ link.type = "image/svg+xml";
231
+ if (link.href !== href) link.href = href;
232
+ }
233
+ function applyBrandChrome(content) {
234
+ const name = content[BRAND_NAME_KEY];
235
+ if (typeof name === "string" && name.length > 0) {
236
+ document.querySelectorAll("[data-ohw-wordmark]").forEach((el) => {
237
+ if (el.textContent !== name) el.textContent = name;
238
+ if (el.getAttribute("title") !== name) el.setAttribute("title", name);
239
+ });
240
+ }
241
+ const title = content[BRAND_TITLE_KEY];
242
+ if (typeof title === "string" && title.length > 0) {
243
+ if (document.title !== title) document.title = title;
244
+ upsertMeta('meta[property="og:title"]', "property", "og:title", title);
245
+ upsertMeta('meta[name="twitter:title"]', "name", "twitter:title", title);
246
+ }
247
+ const letter = content[BRAND_FAVICON_LETTER_KEY];
248
+ if (typeof letter === "string" && letter.length > 0) {
249
+ applyLetterFavicon(letter);
250
+ }
251
+ }
252
+
253
+ // src/lib/brand-kit.ts
254
+ var BRAND_KIT_KEY = "__ohw_brand";
255
+ var BRAND_VAR_PREFIX = "--ohw-brand-";
256
+ var BRAND_VAR_NAMES = ["primary", "accent", "light", "dark", "surface", "border", "muted"].map(
257
+ (role) => `${BRAND_VAR_PREFIX}${role}`
258
+ );
259
+ var FONT_VARS = {
260
+ heading: ["--font-heading", "--font-display", "--brand-font-heading"],
261
+ body: ["--font-body", "--brand-font-body"]
262
+ };
263
+ var BRAND_FONT_LINK_ID = "ohw-brand-fonts";
264
+ function brandColorVars(kit) {
265
+ const { dark, primary, accent, light } = kit.palette;
266
+ const mix = (a, aPct, b) => `color-mix(in srgb, ${a} ${aPct}%, ${b})`;
267
+ return {
268
+ [`${BRAND_VAR_PREFIX}primary`]: primary,
269
+ [`${BRAND_VAR_PREFIX}accent`]: accent,
270
+ [`${BRAND_VAR_PREFIX}light`]: light,
271
+ [`${BRAND_VAR_PREFIX}dark`]: dark,
272
+ [`${BRAND_VAR_PREFIX}surface`]: mix(light, 95, dark),
273
+ [`${BRAND_VAR_PREFIX}border`]: mix(light, 85, dark),
274
+ [`${BRAND_VAR_PREFIX}muted`]: mix(dark, 62, light)
275
+ };
276
+ }
277
+ function parseBrandKit(raw) {
278
+ if (!raw) return null;
279
+ try {
280
+ const parsed = JSON.parse(raw);
281
+ const p = parsed?.palette;
282
+ const f = parsed?.fonts;
283
+ 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") {
284
+ return null;
285
+ }
286
+ return {
287
+ palette: { dark: p.dark, primary: p.primary, accent: p.accent, light: p.light },
288
+ fonts: { heading: f.heading, body: f.body }
289
+ };
290
+ } catch {
291
+ return null;
292
+ }
293
+ }
294
+ function familyOf(stack) {
295
+ const first = stack.split(",")[0]?.trim() ?? "";
296
+ return first.replace(/^['"]|['"]$/g, "");
297
+ }
298
+ function loadBrandFonts(families) {
299
+ const unique = [...new Set(families.map((f) => f.trim()).filter(Boolean))];
300
+ if (unique.length === 0) return;
301
+ const spec = unique.map((f) => `${f.replace(/ /g, "+")}:400,500,600,700`).join("|");
302
+ const href = `https://fonts.googleapis.com/css?family=${spec}&display=swap`;
303
+ let link = document.getElementById(BRAND_FONT_LINK_ID);
304
+ if (!link) {
305
+ link = document.createElement("link");
306
+ link.id = BRAND_FONT_LINK_ID;
307
+ link.rel = "stylesheet";
308
+ document.head.appendChild(link);
309
+ }
310
+ if (link.href !== href) link.href = href;
311
+ }
312
+ function applyBrandToDom(kit) {
313
+ const root = document.documentElement;
314
+ if (!kit) {
315
+ for (const name of BRAND_VAR_NAMES) root.style.removeProperty(name);
316
+ for (const name of [...FONT_VARS.heading, ...FONT_VARS.body]) root.style.removeProperty(name);
317
+ document.getElementById(BRAND_FONT_LINK_ID)?.remove();
318
+ return;
319
+ }
320
+ for (const [name, value] of Object.entries(brandColorVars(kit))) root.style.setProperty(name, value);
321
+ for (const name of FONT_VARS.heading) root.style.setProperty(name, kit.fonts.heading);
322
+ for (const name of FONT_VARS.body) root.style.setProperty(name, kit.fonts.body);
323
+ loadBrandFonts([familyOf(kit.fonts.heading), familyOf(kit.fonts.body)]);
324
+ }
325
+
326
+ // src/lib/section-styles.ts
327
+ var STYLE_STORE_KEY = "__ohw_styles";
328
+ var STYLE_SHEET_ID = "ohw-section-styles";
329
+ function parseStyleStore(raw) {
330
+ if (!raw) return null;
331
+ try {
332
+ const parsed = JSON.parse(raw);
333
+ if (parsed?.v !== 1) return null;
334
+ return {
335
+ v: 1,
336
+ sections: typeof parsed.sections === "object" && parsed.sections ? parsed.sections : {},
337
+ nodes: typeof parsed.nodes === "object" && parsed.nodes ? parsed.nodes : {}
338
+ };
339
+ } catch {
340
+ return null;
341
+ }
342
+ }
343
+ var BG_VALUES = {
344
+ surface: "color-mix(in srgb, var(--ohw-brand-light, var(--color-light, #ECECEB)) 94%, var(--ohw-brand-dark, var(--color-dark, #0C0A09)))",
345
+ accent: "var(--ohw-brand-primary, var(--color-primary, #0078E5))",
346
+ "accent-soft": "color-mix(in srgb, var(--ohw-brand-primary, var(--color-primary, #0078E5)) 12%, var(--ohw-brand-light, var(--color-light, #FFFFFF)))"
347
+ };
348
+ var HEADLINE_SIZES = { md: 24, lg: 32, xl: 40, display: 56 };
349
+ function styleSheetCss() {
350
+ const rules = [];
351
+ for (const [tone, value] of Object.entries(BG_VALUES)) {
352
+ rules.push(`[data-ohw-style-bg="${tone}"] { background: ${value} !important; }`);
353
+ }
354
+ rules.push(
355
+ `[data-ohw-style-bg="accent"] { color: var(--ohw-brand-light, var(--color-light, #FFFFFF)) !important; }`
356
+ );
357
+ rules.push(
358
+ `[data-ohw-style-distribution="space-between"] { display: flex !important; flex-direction: column; justify-content: space-between; }`,
359
+ `[data-ohw-style-distribution="center"] { display: flex !important; flex-direction: column; justify-content: center; }`
360
+ );
361
+ for (const [scale, size] of Object.entries(HEADLINE_SIZES)) {
362
+ rules.push(
363
+ `[data-ohw-style-headline="${scale}"] :is(h1, h2, h3) { font-size: ${size}px !important; line-height: 1.15 !important; }`
364
+ );
365
+ }
366
+ rules.push(
367
+ `[data-ohw-style-aspect="fill-height"] img { height: 100% !important; aspect-ratio: auto !important; object-fit: cover; }`
368
+ );
369
+ for (const aspect of ["1:1", "4:5", "3:4", "3:2", "16:9", "2:1", "3:1"]) {
370
+ rules.push(
371
+ `[data-ohw-style-aspect="${aspect.replace(":", "-")}"] img { aspect-ratio: ${aspect.replace(":", " / ")} !important; height: auto !important; object-fit: cover; }`
372
+ );
373
+ }
374
+ const pad = { tight: 40, balanced: 64, airy: 96 };
375
+ for (const [spacing, px] of Object.entries(pad)) {
376
+ rules.push(
377
+ `[data-ohw-style-spacing="${spacing}"] { padding-top: ${px}px !important; padding-bottom: ${px}px !important; }`
378
+ );
379
+ }
380
+ return rules.join("\n");
381
+ }
382
+ var STYLE_FONT_LINK_ID = "ohw-style-fonts";
383
+ function loadStyleFonts(families) {
384
+ const unique = [...new Set(families.map((f) => f.trim()).filter(Boolean))];
385
+ const existing = document.getElementById(STYLE_FONT_LINK_ID);
386
+ if (unique.length === 0) {
387
+ existing?.remove();
388
+ return;
389
+ }
390
+ const spec = unique.map((f) => `${f.replace(/ /g, "+")}:400,500,600,700`).join("|");
391
+ const href = `https://fonts.googleapis.com/css?family=${spec}&display=swap`;
392
+ let link = existing;
393
+ if (!link) {
394
+ link = document.createElement("link");
395
+ link.id = STYLE_FONT_LINK_ID;
396
+ link.rel = "stylesheet";
397
+ document.head.appendChild(link);
398
+ }
399
+ if (link.href !== href) link.href = href;
400
+ }
401
+ var SECTION_ATTRS = {
402
+ sectionBackground: "data-ohw-style-bg",
403
+ textDistribution: "data-ohw-style-distribution",
404
+ headlineScale: "data-ohw-style-headline",
405
+ imageAspect: "data-ohw-style-aspect",
406
+ spacing: "data-ohw-style-spacing"
407
+ };
408
+ var NODE_WROTE_ATTR = "data-ohw-style-node";
409
+ var NODE_PROPS = ["color", "font-family", "font-size", "background"];
410
+ function saveInline(el, prop) {
411
+ const attr = `data-ohw-style-prev-${prop}`;
412
+ if (!el.hasAttribute(attr)) el.setAttribute(attr, el.style.getPropertyValue(prop));
413
+ }
414
+ function restoreInline(el, prop) {
415
+ const attr = `data-ohw-style-prev-${prop}`;
416
+ if (!el.hasAttribute(attr)) return;
417
+ const prev = el.getAttribute(attr) ?? "";
418
+ if (prev) el.style.setProperty(prop, prev);
419
+ else el.style.removeProperty(prop);
420
+ el.removeAttribute(attr);
421
+ }
422
+ function ensureStyleSheet() {
423
+ let el = document.getElementById(STYLE_SHEET_ID);
424
+ if (!el) {
425
+ el = document.createElement("style");
426
+ el.id = STYLE_SHEET_ID;
427
+ document.head.appendChild(el);
428
+ }
429
+ const css = styleSheetCss();
430
+ if (el.textContent !== css) el.textContent = css;
431
+ }
432
+ function clearSectionAttrs(root) {
433
+ for (const attr of Object.values(SECTION_ATTRS)) {
434
+ for (const el of Array.from(root.querySelectorAll(`[${attr}]`))) el.removeAttribute(attr);
435
+ }
436
+ for (const el of Array.from(root.querySelectorAll("[data-ohw-style-bgcolor]"))) {
437
+ restoreInline(el, "background");
438
+ el.removeAttribute("data-ohw-style-bgcolor");
439
+ }
440
+ }
441
+ function clearNodeProps(root) {
442
+ for (const el of Array.from(root.querySelectorAll(`[${NODE_WROTE_ATTR}]`))) {
443
+ const h = el;
444
+ for (const prop of NODE_PROPS) restoreInline(h, prop);
445
+ h.removeAttribute(NODE_WROTE_ATTR);
446
+ }
447
+ }
448
+ function buttonSurfaceOf(el) {
449
+ return el.closest("a, button") ?? el;
450
+ }
451
+ function applyStylesToDom(store) {
452
+ ensureStyleSheet();
453
+ clearSectionAttrs(document);
454
+ clearNodeProps(document);
455
+ loadStyleFonts(
456
+ store ? Object.values(store.nodes).flatMap((n) => n.fontFamily ? [n.fontFamily] : []) : []
457
+ );
458
+ if (!store) return;
459
+ for (const [sectionId, override] of Object.entries(store.sections)) {
460
+ const sections = document.querySelectorAll(
461
+ `[data-ohw-section="${CSS.escape(sectionId)}"]`
462
+ );
463
+ for (const section of Array.from(sections)) {
464
+ for (const [prop, attr] of Object.entries(SECTION_ATTRS)) {
465
+ const value = override[prop];
466
+ if (value === void 0) continue;
467
+ if (prop === "sectionBackground" && override.sectionBackgroundColor !== void 0) continue;
468
+ section.setAttribute(attr, String(value).replace(":", "-"));
469
+ }
470
+ if (override.sectionBackgroundColor !== void 0) {
471
+ saveInline(section, "background");
472
+ section.style.setProperty("background", override.sectionBackgroundColor, "important");
473
+ section.setAttribute("data-ohw-style-bgcolor", "");
474
+ }
475
+ }
476
+ }
477
+ for (const [key, override] of Object.entries(store.nodes)) {
478
+ const nodes = document.querySelectorAll(`[data-ohw-key="${CSS.escape(key)}"]`);
479
+ for (const el of Array.from(nodes)) {
480
+ if (override.color !== void 0) {
481
+ saveInline(el, "color");
482
+ el.style.setProperty("color", override.color, "important");
483
+ el.setAttribute(NODE_WROTE_ATTR, "");
484
+ }
485
+ if (override.fontFamily !== void 0) {
486
+ saveInline(el, "font-family");
487
+ el.style.setProperty("font-family", `'${override.fontFamily}'`, "important");
488
+ el.setAttribute(NODE_WROTE_ATTR, "");
489
+ }
490
+ if (override.fontSize !== void 0) {
491
+ saveInline(el, "font-size");
492
+ el.style.setProperty("font-size", `${override.fontSize}px`, "important");
493
+ el.setAttribute(NODE_WROTE_ATTR, "");
494
+ }
495
+ if (override.buttonBackground !== void 0 || override.buttonText !== void 0) {
496
+ const surface = buttonSurfaceOf(el);
497
+ if (override.buttonBackground !== void 0) {
498
+ saveInline(surface, "background");
499
+ surface.style.setProperty("background", override.buttonBackground, "important");
500
+ }
501
+ if (override.buttonText !== void 0) {
502
+ saveInline(surface, "color");
503
+ surface.style.setProperty("color", override.buttonText, "important");
504
+ }
505
+ surface.setAttribute(NODE_WROTE_ATTR, "");
506
+ }
507
+ }
508
+ }
509
+ }
510
+
194
511
  // src/ui/ai-tree/aiSectionsManager.tsx
195
512
  var import_react_dom = require("react-dom");
196
513
  var import_client = require("react-dom/client");
@@ -205,7 +522,8 @@ function lucideByName(name) {
205
522
  }
206
523
  var typeStyle = (spec, font) => ({
207
524
  fontFamily: font,
208
- fontSize: spec.size,
525
+ // Headings shrink with the viewport (reaching full size around ~900px wide); body copy stays put.
526
+ 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,
209
527
  lineHeight: spec.line,
210
528
  fontWeight: spec.weight
211
529
  });
@@ -214,26 +532,63 @@ var featureLines = (value) => value.split(/\n|<br\s*\/?>/i).map((line) => line.t
214
532
  var CHECK_MASK = `data:image/svg+xml,${encodeURIComponent(
215
533
  '<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>'
216
534
  )}`;
535
+ var AI_MOBILE_CSS = [
536
+ "@media (max-width: 768px){",
537
+ "[data-ai-section]{overflow-x:hidden}",
538
+ "[data-ai-container]{padding:0 20px !important}",
539
+ "[data-ai-row]{display:flex !important;flex-direction:column !important;align-items:stretch !important}",
540
+ "[data-ai-cell]{width:100%;min-width:0}",
541
+ "[data-ai-grid]{grid-template-columns:1fr !important}",
542
+ // Group containers flatten to a column on phones; span placements come along for free.
543
+ "[data-ai-group]{display:flex !important;flex-direction:column !important}",
544
+ "[data-ai-group] > *{grid-column:auto !important}",
545
+ // The 50:50 form collapses to a single stacked column on phones.
546
+ "[data-ai-form]{grid-template-columns:1fr !important}",
547
+ "[data-ai-section] img{max-width:100%}",
548
+ "}",
549
+ "@media (min-width: 769px) and (max-width: 1024px){",
550
+ "[data-ai-grid]{grid-template-columns:repeat(2, 1fr) !important}",
551
+ "}"
552
+ ].join("");
217
553
  var FEATURE_LINE_CSS = [
218
554
  "[data-ai-features]>div{position:relative;padding-left:40px;min-height:24px}",
219
555
  '[data-ai-features]>div::before{content:"";position:absolute;left:0;top:1px;width:24px;height:24px;',
220
556
  `background-color:currentColor;-webkit-mask:url("${CHECK_MASK}") no-repeat 0 0/24px 24px;`,
221
557
  `mask:url("${CHECK_MASK}") no-repeat 0 0/24px 24px}`
222
558
  ].join("");
559
+ function hexLuminance(color) {
560
+ const m = /^#([0-9a-f]{6})$/i.exec(color.trim());
561
+ if (!m) return null;
562
+ const [r2, g, b] = [0, 2, 4].map((i) => {
563
+ const c = parseInt(m[1].slice(i, i + 2), 16) / 255;
564
+ return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
565
+ });
566
+ return 0.2126 * r2 + 0.7152 * g + 0.0722 * b;
567
+ }
568
+ function hexContrast(a, b) {
569
+ const la = hexLuminance(a);
570
+ const lb = hexLuminance(b);
571
+ if (la === null || lb === null) return null;
572
+ const [hi, lo] = la > lb ? [la, lb] : [lb, la];
573
+ return (hi + 0.05) / (lo + 0.05);
574
+ }
575
+ function accentBandContext(brand) {
576
+ const p = brand.palette;
577
+ const lightWins = (hexContrast(p.light, p.primary) ?? 99) >= (hexContrast(p.dark, p.primary) ?? 0);
578
+ if (lightWins) {
579
+ return {
580
+ brand: { ...brand, palette: { dark: p.light, primary: p.light, accent: p.light, light: p.primary } },
581
+ buttonLabel: p.primary
582
+ };
583
+ }
584
+ return {
585
+ brand: { ...brand, palette: { dark: p.dark, primary: p.dark, accent: p.dark, light: p.light } },
586
+ buttonLabel: p.light
587
+ };
588
+ }
223
589
  function textAttrs(ctx, path) {
224
590
  return ctx.keyFor ? { "data-ohw-key": ctx.keyFor(path), "data-ohw-editable": "text" } : {};
225
591
  }
226
- var AI_RESPONSIVE_CSS = [
227
- "@media (max-width: 960px) {",
228
- " [data-ai-responsive] [data-ai-grid] { grid-template-columns: repeat(2, 1fr) !important; }",
229
- ' [data-ai-responsive] [data-ai-grid="1"] { grid-template-columns: 1fr !important; }',
230
- "}",
231
- "@media (max-width: 640px) {",
232
- " [data-ai-responsive] [data-ai-grid] { grid-template-columns: 1fr !important; }",
233
- " [data-ai-responsive] [data-ai-columns] > * { grid-column: 1 / -1 !important; }",
234
- " [data-ai-responsive] [data-ai-section-inner] { padding-left: 20px !important; padding-right: 20px !important; }",
235
- "}"
236
- ].join("\n");
237
592
  var TRANSPARENT_PX = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
238
593
  function MediaBox({
239
594
  refValue,
@@ -246,17 +601,13 @@ function MediaBox({
246
601
  const url = refValue ? ctx.resolveMedia(refValue) : null;
247
602
  const isIcon = /^(lucide|simple):/.test(refValue);
248
603
  const aspectRatio = aspect && /^\d+:\d+$/.test(aspect) ? aspect.replace(":", " / ") : void 0;
249
- const editAttrs = ctx.keyFor && editPath ? {
250
- "data-ohw-key": ctx.keyFor(editPath),
251
- "data-ohw-editable": isIcon ? "icon" : "image"
252
- } : {};
604
+ const editAttrs = ctx.keyFor && editPath && !isIcon ? { "data-ohw-key": ctx.keyFor(editPath), "data-ohw-editable": "image" } : {};
253
605
  if (isIcon) {
254
606
  const Icon = refValue.startsWith("lucide:") ? lucideByName(refValue.slice(7)) : null;
255
607
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
256
608
  "span",
257
609
  {
258
610
  "data-ai-icon": refValue,
259
- ...editAttrs,
260
611
  style: {
261
612
  display: "inline-flex",
262
613
  width: 48,
@@ -309,7 +660,7 @@ function ButtonEl({
309
660
  }) {
310
661
  const secondary = slots.variant === "secondary";
311
662
  const href = str(slots.href);
312
- const linkAttrs = ctx.keyFor && path ? { "data-ohw-href-key": ctx.keyFor(`${path}.href`) } : {};
663
+ const linkAttrs = ctx.keyFor && path ? { "data-ohw-href-key": ctx.keyFor(`${path}.href`), "data-ohw-role": "button" } : {};
313
664
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
314
665
  "a",
315
666
  {
@@ -325,7 +676,7 @@ function ButtonEl({
325
676
  textDecoration: "none",
326
677
  cursor: "pointer",
327
678
  ...typeStyle(AI_TREE_TOKENS.type.button, ctx.brand.fonts.body),
328
- ...secondary ? { border: `1px solid ${ctx.brand.palette.dark}`, color: ctx.brand.palette.dark } : { background: ctx.brand.palette.primary, color: AI_TREE_TOKENS.textPrimaryForeground }
679
+ ...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 }
329
680
  },
330
681
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { ...path ? textAttrs(ctx, `${path}.label`) : {}, children: str(slots.label) })
331
682
  }
@@ -831,7 +1182,24 @@ function CardBlock({ node, ctx, path }) {
831
1182
  minWidth: 0
832
1183
  },
833
1184
  children: [
834
- media && (horizontal ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { flex: 1, minWidth: 0, display: "flex", alignItems: "center", padding: mediaInset }, children: media }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1185
+ media && (horizontal ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1186
+ "div",
1187
+ {
1188
+ style: (
1189
+ // An icon hugs its glyph — flex:1 gave a 48px icon half the card and pushed the
1190
+ // text to the far side. Photos keep the half-and-half split. The inset has no
1191
+ // inner padding (the photo split absorbed that), so the icon carries its own gap.
1192
+ /^(lucide|simple):/.test(mediaRef) ? {
1193
+ flexShrink: 0,
1194
+ display: "flex",
1195
+ alignItems: "center",
1196
+ padding: mediaInset,
1197
+ [mediaPosition === "right" ? "marginLeft" : "marginRight"]: 20
1198
+ } : { flex: 1, minWidth: 0, display: "flex", alignItems: "center", padding: mediaInset }
1199
+ ),
1200
+ children: media
1201
+ }
1202
+ ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
835
1203
  "div",
836
1204
  {
837
1205
  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" },
@@ -922,13 +1290,44 @@ function AccordionBlock({ node, ctx, path }) {
922
1290
  ) })
923
1291
  ] }, i)) });
924
1292
  }
1293
+ function useIsMobile() {
1294
+ const [mobile, setMobile] = import_react.default.useState(
1295
+ () => typeof window !== "undefined" && window.matchMedia("(max-width: 768px)").matches
1296
+ );
1297
+ import_react.default.useEffect(() => {
1298
+ const mq = window.matchMedia("(max-width: 768px)");
1299
+ const update = () => setMobile(mq.matches);
1300
+ update();
1301
+ mq.addEventListener("change", update);
1302
+ return () => mq.removeEventListener("change", update);
1303
+ }, []);
1304
+ return mobile;
1305
+ }
925
1306
  function Carousel({ items, itemsPerRow, ctx }) {
1307
+ const isMobile = useIsMobile();
1308
+ const perPage = isMobile ? 1 : itemsPerRow;
1309
+ const pages = Math.max(1, Math.ceil(items.length / perPage));
926
1310
  const [page, setPage] = import_react.default.useState(0);
927
- const pages = Math.max(1, Math.ceil(items.length / itemsPerRow));
928
1311
  const current = Math.min(page, pages - 1);
1312
+ if (pages <= 1) {
1313
+ const cols = Math.max(1, Math.min(items.length, itemsPerRow));
1314
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1315
+ "div",
1316
+ {
1317
+ "data-ai-grid": String(cols),
1318
+ style: {
1319
+ display: "grid",
1320
+ gridTemplateColumns: `repeat(${cols}, 1fr)`,
1321
+ gap: AI_TREE_TOKENS.spacing8,
1322
+ alignItems: "start"
1323
+ },
1324
+ children: items
1325
+ }
1326
+ );
1327
+ }
929
1328
  const pageGroups = Array.from(
930
1329
  { length: pages },
931
- (_, p) => items.slice(p * itemsPerRow, (p + 1) * itemsPerRow)
1330
+ (_, p) => items.slice(p * perPage, (p + 1) * perPage)
932
1331
  );
933
1332
  const chrome = (enabled) => ({
934
1333
  border: `1px solid ${ctx.brand.palette.dark}`,
@@ -953,55 +1352,69 @@ function Carousel({ items, itemsPerRow, ctx }) {
953
1352
  cursor: "pointer",
954
1353
  padding: 0
955
1354
  });
956
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { "data-ai-carousel": "", style: { display: "flex", flexDirection: "column", gap: AI_TREE_TOKENS.spacing8, alignItems: "center" }, children: [
957
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: AI_TREE_TOKENS.spacing6, width: "100%" }, children: [
958
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
959
- "button",
960
- {
961
- type: "button",
962
- "aria-label": "Previous",
963
- onClick: () => setPage((p) => Math.max(0, p - 1)),
964
- style: chrome(current > 0),
965
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.ArrowLeft, { size: 24, strokeWidth: 1.5, "aria-hidden": true })
966
- }
967
- ),
968
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { flex: 1, minWidth: 0, overflow: "hidden" }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1355
+ const viewport = /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { flex: isMobile ? "0 0 auto" : 1, minWidth: 0, width: "100%", overflow: "hidden" }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1356
+ "div",
1357
+ {
1358
+ style: {
1359
+ display: "flex",
1360
+ transform: `translateX(-${current * 100}%)`,
1361
+ transition: "transform 0.4s ease"
1362
+ },
1363
+ children: pageGroups.map((group, p) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
969
1364
  "div",
970
1365
  {
1366
+ "data-ai-grid": String(perPage),
971
1367
  style: {
972
- display: "flex",
973
- transform: `translateX(-${current * 100}%)`,
974
- transition: "transform 0.4s ease"
1368
+ flex: "0 0 100%",
1369
+ display: "grid",
1370
+ gridTemplateColumns: `repeat(${perPage}, 1fr)`,
1371
+ gap: AI_TREE_TOKENS.spacing8,
1372
+ alignItems: "start"
975
1373
  },
976
- children: pageGroups.map((group, p) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
977
- "div",
978
- {
979
- "data-ai-grid": String(itemsPerRow),
980
- style: {
981
- flex: "0 0 100%",
982
- display: "grid",
983
- gridTemplateColumns: `repeat(${itemsPerRow}, 1fr)`,
984
- gap: AI_TREE_TOKENS.spacing8,
985
- alignItems: "start"
986
- },
987
- children: group
988
- },
989
- p
990
- ))
991
- }
992
- ) }),
993
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
994
- "button",
995
- {
996
- type: "button",
997
- "aria-label": "Next",
998
- onClick: () => setPage((p) => Math.min(pages - 1, p + 1)),
999
- style: chrome(current < pages - 1),
1000
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.ArrowRight, { size: 24, strokeWidth: 1.5, "aria-hidden": true })
1001
- }
1002
- )
1374
+ children: group
1375
+ },
1376
+ p
1377
+ ))
1378
+ }
1379
+ ) });
1380
+ const prevBtn = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1381
+ "button",
1382
+ {
1383
+ type: "button",
1384
+ "aria-label": "Previous",
1385
+ onClick: () => setPage((p) => Math.max(0, p - 1)),
1386
+ style: chrome(current > 0),
1387
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.ArrowLeft, { size: 24, strokeWidth: 1.5, "aria-hidden": true })
1388
+ }
1389
+ );
1390
+ const nextBtn = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1391
+ "button",
1392
+ {
1393
+ type: "button",
1394
+ "aria-label": "Next",
1395
+ onClick: () => setPage((p) => Math.min(pages - 1, p + 1)),
1396
+ style: chrome(current < pages - 1),
1397
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.ArrowRight, { size: 24, strokeWidth: 1.5, "aria-hidden": true })
1398
+ }
1399
+ );
1400
+ const dots = /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { display: "flex", gap: 9, justifyContent: "center" }, children: pageGroups.map((_, p) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", "aria-label": `Page ${p + 1}`, onClick: () => setPage(p), style: dot(p === current) }, p)) });
1401
+ if (isMobile) {
1402
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { "data-ai-carousel": "", style: { display: "flex", flexDirection: "column", gap: AI_TREE_TOKENS.spacing6, alignItems: "center" }, children: [
1403
+ viewport,
1404
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: AI_TREE_TOKENS.spacing6, justifyContent: "center" }, children: [
1405
+ prevBtn,
1406
+ nextBtn
1407
+ ] }),
1408
+ dots
1409
+ ] });
1410
+ }
1411
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { "data-ai-carousel": "", style: { display: "flex", flexDirection: "column", gap: AI_TREE_TOKENS.spacing8, alignItems: "center" }, children: [
1412
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: AI_TREE_TOKENS.spacing6, width: "100%" }, children: [
1413
+ prevBtn,
1414
+ viewport,
1415
+ nextBtn
1003
1416
  ] }),
1004
- pages > 1 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { display: "flex", gap: 9, justifyContent: "center" }, children: pageGroups.map((_, p) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", "aria-label": `Page ${p + 1}`, onClick: () => setPage(p), style: dot(p === current) }, p)) })
1417
+ dots
1005
1418
  ] });
1006
1419
  }
1007
1420
  function CollectionBlock({ node, ctx, path }) {
@@ -1080,7 +1493,7 @@ function CollectionBlock({ node, ctx, path }) {
1080
1493
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1081
1494
  "div",
1082
1495
  {
1083
- "data-ai-grid": String(itemsPerRow),
1496
+ "data-ai-grid": "",
1084
1497
  style: {
1085
1498
  display: "grid",
1086
1499
  gridTemplateColumns: `repeat(${itemsPerRow}, 1fr)`,
@@ -1095,6 +1508,49 @@ function renderNode(node, ctx, path) {
1095
1508
  switch (node.type) {
1096
1509
  case "text":
1097
1510
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TextBlock, { slots, ctx, path });
1511
+ // Layout container: arranges child blocks, contributes no content of its own. `grid` is a
1512
+ // nested 12-column grid the children span (a collage is 3–5 media on it, bottom-aligned so
1513
+ // mixed aspects read as a composition); `split` is exactly two children at a ratio; `stack`
1514
+ // is a column. Children render through this same dispatcher, so edit markers, media
1515
+ // resolution, and copy paths all work unchanged inside a group.
1516
+ case "group": {
1517
+ const layout = str(slots.layout);
1518
+ const gap = slots.spacing === "tight" ? AI_TREE_TOKENS.spacing3 : slots.spacing === "airy" ? AI_TREE_TOKENS.spacing8 : AI_TREE_TOKENS.spacing6;
1519
+ const kids = (node.children ?? []).map((child, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1520
+ "div",
1521
+ {
1522
+ style: layout === "grid" ? {
1523
+ gridColumn: `span ${typeof child.span === "number" ? Math.min(12, Math.max(1, child.span)) : 12}`,
1524
+ minWidth: 0
1525
+ } : { minWidth: 0 },
1526
+ children: renderNode(child, ctx, `${path}.c${i}`)
1527
+ },
1528
+ i
1529
+ ));
1530
+ if (layout === "grid") {
1531
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1532
+ "div",
1533
+ {
1534
+ "data-ai-group": "grid",
1535
+ style: { display: "grid", gridTemplateColumns: "repeat(12, 1fr)", gap, alignItems: "end" },
1536
+ children: kids
1537
+ }
1538
+ );
1539
+ }
1540
+ if (layout === "split") {
1541
+ const ratio = str(slots.ratio);
1542
+ const cols = ratio === "3:5" ? "3fr 5fr" : ratio === "5:3" ? "5fr 3fr" : "1fr 1fr";
1543
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1544
+ "div",
1545
+ {
1546
+ "data-ai-group": "split",
1547
+ style: { display: "grid", gridTemplateColumns: cols, gap, alignItems: "center" },
1548
+ children: kids
1549
+ }
1550
+ );
1551
+ }
1552
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-ai-group": "stack", style: { display: "flex", flexDirection: "column", gap }, children: kids });
1553
+ }
1098
1554
  case "button":
1099
1555
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ButtonEl, { slots, ctx, path });
1100
1556
  case "button-row":
@@ -1175,33 +1631,102 @@ function renderNode(node, ctx, path) {
1175
1631
  }
1176
1632
  );
1177
1633
  }
1178
- case "form":
1179
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { display: "flex", flexDirection: "column", gap: AI_TREE_TOKENS.spacing4 }, children: (node.children ?? []).map((child, i) => {
1180
- if (child.type === "input") {
1181
- const cs = child.slots ?? {};
1182
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
1183
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1184
- "div",
1185
- {
1186
- ...textAttrs(ctx, `${path}.c${i}.label`),
1187
- style: { ...typeStyle(AI_TREE_TOKENS.type.bodyMBold, ctx.brand.fonts.body), color: ctx.brand.palette.dark, marginBottom: 6 },
1188
- children: str(cs.label)
1189
- }
1190
- ),
1191
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1192
- "div",
1634
+ case "form": {
1635
+ const formAttrs = ctx.keyFor ? {
1636
+ "data-ohw-editable": "form",
1637
+ "data-ohw-key": ctx.keyFor(`${path}.form`),
1638
+ "data-ohw-success-text": "Thanks \u2014 we'll be in touch shortly."
1639
+ } : {};
1640
+ const fieldStyle = {
1641
+ width: "100%",
1642
+ boxSizing: "border-box",
1643
+ border: `1px solid ${ctx.brand.palette.accent}`,
1644
+ borderRadius: AI_TREE_TOKENS.radiusButton,
1645
+ padding: "12px 14px",
1646
+ background: "#fff",
1647
+ color: ctx.brand.palette.dark,
1648
+ outline: "none",
1649
+ ...typeStyle(AI_TREE_TOKENS.type.bodyM, ctx.brand.fonts.body)
1650
+ };
1651
+ const labelStyle = {
1652
+ ...typeStyle(AI_TREE_TOKENS.type.bodyMBold, ctx.brand.fonts.body),
1653
+ color: ctx.brand.palette.dark
1654
+ };
1655
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1656
+ "form",
1657
+ {
1658
+ ...formAttrs,
1659
+ "data-ai-form": "",
1660
+ style: {
1661
+ display: "grid",
1662
+ gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
1663
+ columnGap: 64,
1664
+ rowGap: AI_TREE_TOKENS.spacing4
1665
+ },
1666
+ children: (node.children ?? []).map((child, i) => {
1667
+ if (child.type === "input") {
1668
+ const cs2 = child.slots ?? {};
1669
+ const kind = str(cs2.kind);
1670
+ const label = str(cs2.label);
1671
+ const placeholder = str(cs2.placeholder);
1672
+ const name = label.toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-+|-+$/gu, "") || `field-${i}`;
1673
+ const isTextarea = kind === "textarea";
1674
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1675
+ "div",
1676
+ {
1677
+ style: {
1678
+ display: "flex",
1679
+ flexDirection: "column",
1680
+ gap: 8,
1681
+ ...isTextarea ? { gridColumn: "1 / -1", maxWidth: 780 } : {}
1682
+ },
1683
+ children: [
1684
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { ...textAttrs(ctx, `${path}.c${i}.label`), style: labelStyle, children: label }),
1685
+ isTextarea ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1686
+ "textarea",
1687
+ {
1688
+ name,
1689
+ placeholder,
1690
+ style: { ...fieldStyle, height: 140, resize: "vertical" }
1691
+ }
1692
+ ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1693
+ "input",
1694
+ {
1695
+ name,
1696
+ type: kind === "email" ? "email" : "text",
1697
+ placeholder,
1698
+ style: { ...fieldStyle, height: 48 }
1699
+ }
1700
+ )
1701
+ ]
1702
+ },
1703
+ i
1704
+ );
1705
+ }
1706
+ const cs = child.slots ?? {};
1707
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1708
+ "button",
1193
1709
  {
1710
+ type: "submit",
1194
1711
  style: {
1195
- border: `1px solid ${ctx.brand.palette.accent}`,
1712
+ gridColumn: "1 / -1",
1713
+ justifySelf: "start",
1714
+ border: "none",
1715
+ cursor: "pointer",
1716
+ padding: `${AI_TREE_TOKENS.spacing3}px ${AI_TREE_TOKENS.spacing6}px`,
1196
1717
  borderRadius: AI_TREE_TOKENS.radiusButton,
1197
- height: cs.kind === "textarea" ? 96 : 42
1198
- }
1199
- }
1200
- )
1201
- ] }, i);
1718
+ background: ctx.brand.palette.primary,
1719
+ color: AI_TREE_TOKENS.textPrimaryForeground,
1720
+ ...typeStyle(AI_TREE_TOKENS.type.button, ctx.brand.fonts.body)
1721
+ },
1722
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { ...textAttrs(ctx, `${path}.c${i}.label`), children: str(cs.label) })
1723
+ },
1724
+ i
1725
+ );
1726
+ })
1202
1727
  }
1203
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ButtonEl, { slots: child.slots ?? {}, ctx, path: `${path}.c${i}` }) }, i);
1204
- }) });
1728
+ );
1729
+ }
1205
1730
  case "schedule-widget":
1206
1731
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1207
1732
  "div",
@@ -1227,11 +1752,14 @@ function AiTreeRenderer({ tree, brand, resolveMedia, editKeyPrefix }) {
1227
1752
  return null;
1228
1753
  }
1229
1754
  const resolvedBrand = brand ?? AI_DEFAULT_BRAND;
1755
+ const band = (tree.settings ?? {}).sectionBackground === "accent" ? accentBandContext(resolvedBrand) : null;
1756
+ const blockBrand = band?.brand ?? resolvedBrand;
1230
1757
  const ctx = {
1231
- brand: resolvedBrand,
1758
+ brand: blockBrand,
1232
1759
  resolveMedia: resolveMedia ?? (() => null),
1233
- cardSurface: resolvedBrand.palette.light.toUpperCase() === AI_DEFAULT_BRAND.palette.light.toUpperCase() ? "#ECECEB" : `color-mix(in srgb, ${resolvedBrand.palette.light} 90%, ${resolvedBrand.palette.dark})`,
1234
- keyFor: editKeyPrefix ? (path) => `${editKeyPrefix}.${path}` : null
1760
+ cardSurface: blockBrand.palette.light.toUpperCase() === AI_DEFAULT_BRAND.palette.light.toUpperCase() ? "#ECECEB" : `color-mix(in srgb, ${blockBrand.palette.light} 90%, ${blockBrand.palette.dark})`,
1761
+ keyFor: editKeyPrefix ? (path) => `${editKeyPrefix}.${path}` : null,
1762
+ ...band ? { buttonLabel: band.buttonLabel } : {}
1235
1763
  };
1236
1764
  const settings = tree.settings ?? {};
1237
1765
  const pad = AI_TREE_TOKENS.sectionPadding[settings.spacing ?? "balanced"];
@@ -1239,27 +1767,41 @@ function AiTreeRenderer({ tree, brand, resolveMedia, editKeyPrefix }) {
1239
1767
  const overlayAlpha = { low: 0.25, medium: 0.45, high: 0.65 }[settings.overlayIntensity ?? "medium"];
1240
1768
  const backgroundUrl = isOverlay && settings.backgroundMedia ? ctx.resolveMedia(settings.backgroundMedia) : null;
1241
1769
  const bgAttrs = ctx.keyFor && isOverlay && settings.backgroundMedia ? { "data-ohw-key": ctx.keyFor("settings.backgroundMedia"), "data-ohw-editable": "bg-image" } : {};
1770
+ const toneBackground = (() => {
1771
+ const { dark, primary, light } = resolvedBrand.palette;
1772
+ switch (settings.sectionBackground) {
1773
+ case "surface":
1774
+ return `color-mix(in srgb, ${light} 94%, ${dark})`;
1775
+ case "accent":
1776
+ return primary;
1777
+ case "accent-soft":
1778
+ return `color-mix(in srgb, ${primary} 12%, ${light})`;
1779
+ default:
1780
+ return void 0;
1781
+ }
1782
+ })();
1783
+ const distributed = !isOverlay && settings.textDistribution;
1242
1784
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1243
1785
  "section",
1244
1786
  {
1245
1787
  "data-ai-section": tree.tag ?? "",
1246
1788
  ...bgAttrs,
1247
- "data-ai-responsive": "",
1248
1789
  style: {
1249
1790
  position: "relative",
1250
1791
  padding: `${pad}px 0`,
1251
- background: isOverlay && !backgroundUrl ? "#DBEAFE" : void 0,
1792
+ background: toneBackground ?? (isOverlay && !backgroundUrl ? "#DBEAFE" : void 0),
1252
1793
  backgroundImage: backgroundUrl ? `url(${backgroundUrl})` : void 0,
1253
1794
  backgroundSize: "cover",
1254
- backgroundPosition: "center"
1795
+ backgroundPosition: "center",
1796
+ color: settings.sectionBackground === "accent" ? blockBrand.palette.dark : void 0
1255
1797
  },
1256
1798
  children: [
1257
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: AI_RESPONSIVE_CSS }),
1258
1799
  isOverlay && backgroundUrl && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { position: "absolute", inset: 0, background: `rgba(255,255,255,${overlayAlpha})` } }),
1800
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: AI_MOBILE_CSS }),
1259
1801
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1260
1802
  "div",
1261
1803
  {
1262
- "data-ai-section-inner": "",
1804
+ "data-ai-container": "",
1263
1805
  style: {
1264
1806
  position: "relative",
1265
1807
  maxWidth: AI_TREE_TOKENS.sectionMaxWidth,
@@ -1270,15 +1812,29 @@ function AiTreeRenderer({ tree, brand, resolveMedia, editKeyPrefix }) {
1270
1812
  children: tree.rows.map((row, r2) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1271
1813
  "div",
1272
1814
  {
1273
- "data-ai-columns": "",
1815
+ "data-ai-row": "",
1274
1816
  style: {
1275
1817
  display: "grid",
1276
1818
  gridTemplateColumns: "repeat(12, 1fr)",
1277
1819
  gap: AI_TREE_TOKENS.spacing6,
1278
- alignItems: settings.verticalPosition === "top" ? "start" : "center",
1820
+ alignItems: distributed === "space-between" ? "stretch" : (distributed ?? settings.verticalPosition) === "top" ? "start" : "center",
1279
1821
  marginTop: r2 > 0 ? AI_TREE_TOKENS.spacing8 : 0
1280
1822
  },
1281
- children: (row.blocks ?? []).map((block, b) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { gridColumn: `span ${Math.min(12, Math.max(1, block.span))}`, minWidth: 0 }, children: renderNode(block, ctx, `r${r2}.b${b}`) }, b))
1823
+ children: (row.blocks ?? []).map((block, b) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1824
+ "div",
1825
+ {
1826
+ "data-ai-cell": "",
1827
+ style: {
1828
+ gridColumn: `span ${Math.min(12, Math.max(1, block.span))}`,
1829
+ minWidth: 0,
1830
+ // space-between: each column becomes a flex column whose content spreads over
1831
+ // the full row height instead of clumping at the top.
1832
+ ...distributed === "space-between" ? { display: "flex", flexDirection: "column", justifyContent: "space-between" } : {}
1833
+ },
1834
+ children: renderNode(block, ctx, `r${r2}.b${b}`)
1835
+ },
1836
+ b
1837
+ ))
1282
1838
  },
1283
1839
  r2
1284
1840
  ))
@@ -1294,17 +1850,36 @@ var import_jsx_runtime2 = require("react/jsx-runtime");
1294
1850
  var CONTAINER_ATTR = "data-ohw-ai-generated";
1295
1851
  var REPLACED_ATTR = "data-ohw-ai-replaced-by";
1296
1852
  var REMOVED_ATTR = "data-ohw-ai-removed";
1853
+ var TEMPLATE_HIDDEN_ATTR = "data-ohw-ai-template-hidden";
1854
+ var CHROME_SECTION_IDS = /* @__PURE__ */ new Set(["navbar", "footer"]);
1855
+ function readRootVar(name) {
1856
+ if (typeof document === "undefined") return "";
1857
+ return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
1858
+ }
1859
+ function deriveBrandOverride() {
1860
+ const dark = readRootVar("--ohw-brand-dark");
1861
+ const primary = readRootVar("--ohw-brand-primary");
1862
+ const light = readRootVar("--ohw-brand-light");
1863
+ if (!dark || !primary || !light) return null;
1864
+ const accent = readRootVar("--ohw-brand-accent");
1865
+ const heading = readRootVar("--font-heading") || readRootVar("--font-display");
1866
+ const body = readRootVar("--font-body");
1867
+ return {
1868
+ palette: { dark, primary, accent: accent || dark, light },
1869
+ fonts: {
1870
+ heading: heading || AI_DEFAULT_BRAND.fonts.heading,
1871
+ body: body || AI_DEFAULT_BRAND.fonts.body
1872
+ }
1873
+ };
1874
+ }
1297
1875
  function deriveTemplateBrand() {
1298
- if (typeof document === "undefined") return null;
1299
- const cs = getComputedStyle(document.documentElement);
1300
- const read = (name) => cs.getPropertyValue(name).trim();
1301
- const dark = read("--color-dark");
1302
- const primary = read("--color-primary");
1303
- const light = read("--color-light");
1876
+ const dark = readRootVar("--color-dark");
1877
+ const primary = readRootVar("--color-primary");
1878
+ const light = readRootVar("--color-light");
1304
1879
  if (!dark || !primary || !light) return null;
1305
- const accent = read("--color-accent");
1306
- const heading = read("--font-heading") || read("--font-display");
1307
- const body = read("--font-body");
1880
+ const accent = readRootVar("--color-accent");
1881
+ const heading = readRootVar("--font-heading") || readRootVar("--font-display");
1882
+ const body = readRootVar("--font-body");
1308
1883
  return {
1309
1884
  palette: { dark, primary, accent: accent || dark, light },
1310
1885
  fonts: {
@@ -1376,6 +1951,24 @@ function syncRemovedSections(state) {
1376
1951
  }
1377
1952
  }
1378
1953
  }
1954
+ function syncTemplateHidden(state, pageHasSections) {
1955
+ const hide = state.hideTemplate === true && pageHasSections;
1956
+ for (const el of Array.from(document.querySelectorAll(`[${TEMPLATE_HIDDEN_ATTR}]`))) {
1957
+ if (!hide) {
1958
+ el.style.removeProperty("display");
1959
+ el.removeAttribute(TEMPLATE_HIDDEN_ATTR);
1960
+ }
1961
+ }
1962
+ if (!hide) return;
1963
+ for (const el of Array.from(document.querySelectorAll("[data-ohw-section]"))) {
1964
+ if (el.hasAttribute(CONTAINER_ATTR)) continue;
1965
+ if (CHROME_SECTION_IDS.has(el.getAttribute("data-ohw-section") ?? "")) continue;
1966
+ if (el.parentElement?.closest("[data-ohw-section]")) continue;
1967
+ if (el.hasAttribute(REPLACED_ATTR) || el.hasAttribute(REMOVED_ATTR)) continue;
1968
+ el.style.display = "none";
1969
+ el.setAttribute(TEMPLATE_HIDDEN_ATTR, "");
1970
+ }
1971
+ }
1379
1972
  function syncReplacedOriginals(state) {
1380
1973
  for (const el of document.querySelectorAll(`[${REPLACED_ATTR}]`)) {
1381
1974
  const byId = el.getAttribute(REPLACED_ATTR) ?? "";
@@ -1394,10 +1987,63 @@ function syncReplacedOriginals(state) {
1394
1987
  }
1395
1988
  }
1396
1989
  }
1990
+ var sectionOrderIndex = /* @__PURE__ */ new Map();
1991
+ function setAiSectionOrder(raw, currentPath) {
1992
+ const next = /* @__PURE__ */ new Map();
1993
+ if (raw) {
1994
+ try {
1995
+ const entries = JSON.parse(raw);
1996
+ for (const entry of entries) {
1997
+ if (!entry.pagePath || entry.pagePath === currentPath) next.set(entry.instanceId, entry.order);
1998
+ }
1999
+ } catch {
2000
+ }
2001
+ }
2002
+ sectionOrderIndex = next;
2003
+ }
2004
+ function applyExplicitOrder(entries) {
2005
+ if (sectionOrderIndex.size === 0) return entries;
2006
+ return entries.map((entry, index) => ({ entry, index, order: sectionOrderIndex.get(entry.id) })).sort((a, b) => {
2007
+ if (a.order === void 0 && b.order === void 0) return a.index - b.index;
2008
+ if (a.order === void 0) return 1;
2009
+ if (b.order === void 0) return -1;
2010
+ return a.order - b.order;
2011
+ }).map((item) => item.entry);
2012
+ }
2013
+ function orderByChain(sections) {
2014
+ const ids = new Set(sections.map((entry) => entry.id));
2015
+ const after = /* @__PURE__ */ new Map();
2016
+ const roots = [];
2017
+ for (const entry of sections) {
2018
+ const anchor = entry.replaces ?? entry.beforeSection ?? entry.afterSection ?? null;
2019
+ if (anchor && ids.has(anchor)) {
2020
+ const bucket = after.get(anchor);
2021
+ if (bucket) bucket.push(entry);
2022
+ else after.set(anchor, [entry]);
2023
+ } else {
2024
+ roots.push(entry);
2025
+ }
2026
+ }
2027
+ const out = [];
2028
+ const seen = /* @__PURE__ */ new Set();
2029
+ const visit = (entry) => {
2030
+ if (seen.has(entry.id)) return;
2031
+ seen.add(entry.id);
2032
+ out.push(entry);
2033
+ for (const child of after.get(entry.id) ?? []) visit(child);
2034
+ };
2035
+ for (const root of roots) visit(root);
2036
+ return out.length === sections.length ? out : sections;
2037
+ }
1397
2038
  function applyAiSectionsToDom(state, options) {
1398
2039
  if (typeof document === "undefined") return;
2040
+ const brandOverride = deriveBrandOverride();
1399
2041
  const templateBrand = deriveTemplateBrand();
1400
- const activeIds = new Set(state.sections.map((entry) => entry.id));
2042
+ const brandKey = brandOverride ? JSON.stringify(brandOverride) : "";
2043
+ const pagePath = window.location.pathname;
2044
+ const pageSections = state.sections.filter((entry) => (entry.path ?? "/") === pagePath);
2045
+ const activeIds = new Set(pageSections.map((entry) => entry.id));
2046
+ const ordered = state.hideTemplate === true ? applyExplicitOrder(orderByChain(pageSections)) : pageSections;
1401
2047
  for (const [id, section] of mounted) {
1402
2048
  if (!activeIds.has(id)) {
1403
2049
  section.root.unmount();
@@ -1405,8 +2051,8 @@ function applyAiSectionsToDom(state, options) {
1405
2051
  mounted.delete(id);
1406
2052
  }
1407
2053
  }
1408
- for (const entry of state.sections) {
1409
- const serialized = JSON.stringify(entry);
2054
+ for (const entry of ordered) {
2055
+ const serialized = JSON.stringify(entry) + brandKey;
1410
2056
  const existing = mounted.get(entry.id);
1411
2057
  if (existing && existing.serialized === serialized && existing.container.isConnected) {
1412
2058
  continue;
@@ -1431,7 +2077,7 @@ function applyAiSectionsToDom(state, options) {
1431
2077
  AiTreeRenderer,
1432
2078
  {
1433
2079
  tree: entry.tree,
1434
- brand: entry.brand ?? options?.brand ?? templateBrand ?? void 0,
2080
+ brand: brandOverride ?? entry.brand ?? options?.brand ?? templateBrand ?? void 0,
1435
2081
  resolveMedia,
1436
2082
  editKeyPrefix: `ai.${entry.id}`
1437
2083
  }
@@ -1440,8 +2086,20 @@ function applyAiSectionsToDom(state, options) {
1440
2086
  });
1441
2087
  mounted.set(entry.id, { root, container, serialized });
1442
2088
  }
2089
+ if (state.hideTemplate === true) {
2090
+ let prev = null;
2091
+ for (const entry of ordered) {
2092
+ const el = mounted.get(entry.id)?.container;
2093
+ if (!el) continue;
2094
+ if (prev && !(prev.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_FOLLOWING)) {
2095
+ prev.insertAdjacentElement("afterend", el);
2096
+ }
2097
+ prev = el;
2098
+ }
2099
+ }
1443
2100
  syncReplacedOriginals(state);
1444
2101
  syncRemovedSections(state);
2102
+ syncTemplateHidden(state, pageSections.length > 0);
1445
2103
  }
1446
2104
 
1447
2105
  // src/useLinkHrefGuardian.ts
@@ -7140,13 +7798,17 @@ function MediaOverlay({
7140
7798
  hover,
7141
7799
  isUploading,
7142
7800
  fadingOut = false,
7801
+ selected = false,
7802
+ hovered = false,
7143
7803
  onFadeOutComplete,
7144
7804
  onReplace,
7805
+ onSelect,
7145
7806
  onVideoSettingsChange
7146
7807
  }) {
7147
7808
  const { rect } = hover;
7148
7809
  const skeletonRef = React8.useRef(null);
7149
7810
  const isVideo = hover.elementType === "video";
7811
+ const showChrome = !selected || hovered;
7150
7812
  const autoplay = hover.videoAutoplay ?? true;
7151
7813
  const muted = hover.videoMuted ?? true;
7152
7814
  const probeRef = React8.useRef(null);
@@ -7193,7 +7855,7 @@ function MediaOverlay({
7193
7855
  }
7194
7856
  );
7195
7857
  }
7196
- const settingsBar = isVideo && !hover.isDragOver ? /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
7858
+ const settingsBar = isVideo && !hover.isDragOver && showChrome ? /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
7197
7859
  "div",
7198
7860
  {
7199
7861
  "data-ohw-bridge": "",
@@ -7263,10 +7925,12 @@ function MediaOverlay({
7263
7925
  // in-document, pointer-events does it natively. The button below opts back in, so
7264
7926
  // Replace still works.
7265
7927
  pointerEvents: hover.hasTextOverlap ? "none" : "auto",
7266
- boxShadow: "inset 0 0 0 1.5px var(--color-primary)",
7267
- background: "color-mix(in srgb, var(--color-primary) 20%, transparent)"
7928
+ // Selected: a firm component ring with no wash, so the image reads as chosen rather
7929
+ // than hovered. Hover keeps the existing tinted preview.
7930
+ boxShadow: selected ? "inset 0 0 0 2px var(--color-primary)" : "inset 0 0 0 1.5px var(--color-primary)",
7931
+ background: selected ? "transparent" : "color-mix(in srgb, var(--color-primary) 20%, transparent)"
7268
7932
  },
7269
- onClick: () => onReplace(hover.key),
7933
+ onClick: () => onSelect && !selected ? onSelect(hover.key) : onReplace(hover.key),
7270
7934
  children: [
7271
7935
  /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
7272
7936
  Button,
@@ -7320,7 +7984,7 @@ function MediaOverlay({
7320
7984
  },
7321
7985
  children: [
7322
7986
  isVideo ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_lucide_react5.Film, { size: 14 }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_lucide_react5.ImageIcon, { size: 14 }),
7323
- replaceMode === "full" ? isVideo ? "Replace video" : "Replace image" : null
7987
+ replaceMode === "full" ? isVideo ? "Replace video" : hover.elementType === "bg-image" ? "Replace background" : "Replace image" : null
7324
7988
  ]
7325
7989
  }
7326
7990
  )
@@ -7404,6 +8068,8 @@ function parseSectionsFromRoot(root) {
7404
8068
  const id = el.getAttribute("data-ohw-section") ?? "";
7405
8069
  if (!id || seen.has(id) || LINK_PICKER_EXCLUDED_IDS.has(id)) continue;
7406
8070
  if (el.parentElement?.closest("[data-ohw-section]")) continue;
8071
+ if (el.hasAttribute("data-ohw-ai-template-hidden") || el.hasAttribute("data-ohw-ai-removed") || el.hasAttribute("data-ohw-ai-replaced-by"))
8072
+ continue;
7407
8073
  seen.add(id);
7408
8074
  const label = el.getAttribute("data-ohw-section-label") ?? titleCaseSectionId(id);
7409
8075
  sections.push({ id, label });
@@ -7689,6 +8355,7 @@ function AiSectionOverlay({
7689
8355
  }) {
7690
8356
  const [selectedId, setSelectedId] = (0, import_react8.useState)(null);
7691
8357
  const [reviewId, setReviewId] = (0, import_react8.useState)(null);
8358
+ const [reviewButtonsHidden, setReviewButtonsHidden] = (0, import_react8.useState)(false);
7692
8359
  const reviewIdRef = (0, import_react8.useRef)(null);
7693
8360
  reviewIdRef.current = reviewId;
7694
8361
  const selectedIdRef = (0, import_react8.useRef)(null);
@@ -7750,6 +8417,7 @@ function AiSectionOverlay({
7750
8417
  }
7751
8418
  const found = readRect(sectionId) != null;
7752
8419
  setReviewId(found ? sectionId : null);
8420
+ setReviewButtonsHidden(e.data.hideButtons === true);
7753
8421
  postToParent2({ type: "ow:ai-review-started", sectionId, found });
7754
8422
  if (found) {
7755
8423
  document.querySelector(`[data-ohw-instance="${CSS.escape(sectionId)}"]`)?.scrollIntoView({ behavior: "smooth", block: "nearest" });
@@ -7877,13 +8545,16 @@ function AiSectionOverlay({
7877
8545
  border: `2px solid ${PRIMARY2}`,
7878
8546
  borderRadius: edgeAwareRadius(reviewRect),
7879
8547
  zIndex: 2147483200,
7880
- // The veil itself: swallows clicks so the section stays locked until decided.
8548
+ // The veil itself: swallows clicks so the section stays locked until decided. This
8549
+ // stopPropagation only guards the bubble phase; the bridge's capture-phase click
8550
+ // handlers additionally bail on `[data-ohw-ai-review]`, without which a click on
8551
+ // Accept/Discard resolves to the media beneath and opens the file picker.
7881
8552
  background: "rgba(8, 133, 254, 0.04)",
7882
8553
  pointerEvents: "auto",
7883
8554
  cursor: "default"
7884
8555
  },
7885
8556
  onClick: (e) => e.stopPropagation(),
7886
- children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
8557
+ children: !reviewButtonsHidden && /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
7887
8558
  "div",
7888
8559
  {
7889
8560
  style: {
@@ -10450,8 +11121,13 @@ function referenceBox(slot) {
10450
11121
  const neighbour = row ? Array.from(row.querySelectorAll('[data-ohw-editable="icon"]')).find(
10451
11122
  (el) => el !== slot && !el.hasAttribute("data-ohw-social-icon-placeholder")
10452
11123
  ) : null;
10453
- const source = (neighbour ?? slot).querySelector(GLYPH_SELECTOR);
10454
- const box = source?.getBoundingClientRect() ?? null;
11124
+ if (neighbour) {
11125
+ const box2 = neighbour.querySelector(GLYPH_SELECTOR)?.getBoundingClientRect();
11126
+ if (box2?.width && box2.height) return box2;
11127
+ }
11128
+ const own = slot.getBoundingClientRect();
11129
+ if (own.width && own.height) return own;
11130
+ const box = slot.querySelector(GLYPH_SELECTOR)?.getBoundingClientRect() ?? null;
10455
11131
  return box?.width && box.height ? box : null;
10456
11132
  }
10457
11133
  function iconMarkupSizedFor(slot, markup) {
@@ -13320,6 +13996,7 @@ function useSectionDrag({
13320
13996
  }
13321
13997
  const orderJson = JSON.stringify(entries);
13322
13998
  editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: orderJson };
13999
+ setAiSectionOrder(orderJson, window.location.pathname);
13323
14000
  postToParentRef.current({ type: "ow:change", nodes: [{ key: SECTION_ORDER_KEY, text: orderJson }] });
13324
14001
  applyPersistedOrder(entries);
13325
14002
  clearSectionDragVisuals();
@@ -15439,6 +16116,70 @@ function OhhwellsBridge() {
15439
16116
  const hoveredImageHasTextOverlapRef = (0, import_react17.useRef)(false);
15440
16117
  const dragOverElRef = (0, import_react17.useRef)(null);
15441
16118
  const [mediaHover, setMediaHover] = (0, import_react17.useState)(null);
16119
+ const [selectedMedia, setSelectedMedia] = (0, import_react17.useState)(null);
16120
+ const selectedMediaElRef = (0, import_react17.useRef)(null);
16121
+ const clearMediaSelection = (0, import_react17.useCallback)(() => {
16122
+ const prev = selectedMediaElRef.current;
16123
+ selectedMediaElRef.current = null;
16124
+ setSelectedMedia(null);
16125
+ const sectionEl = prev?.closest("[data-ohw-section]") ?? null;
16126
+ if (sectionEl) {
16127
+ postToParentRef.current({
16128
+ type: "ow:section-selected",
16129
+ sectionId: sectionEl.dataset.ohwSection ?? null,
16130
+ sectionLabel: sectionEl.dataset.ohwSectionLabel ?? titleCaseSectionId(sectionEl.dataset.ohwSection ?? ""),
16131
+ key: null
16132
+ });
16133
+ }
16134
+ }, []);
16135
+ const clearMediaSelectionRef = (0, import_react17.useRef)(clearMediaSelection);
16136
+ clearMediaSelectionRef.current = clearMediaSelection;
16137
+ const selectMediaElement = (0, import_react17.useCallback)((el) => {
16138
+ const r2 = el.getBoundingClientRect();
16139
+ const video = el.dataset.ohwEditable === "video" ? el.querySelector("video") : null;
16140
+ selectedMediaElRef.current = el;
16141
+ setSelectedMedia({
16142
+ key: el.dataset.ohwKey ?? "",
16143
+ rect: { top: r2.top, left: r2.left, width: r2.width, height: r2.height },
16144
+ elementType: el.dataset.ohwEditable ?? "image",
16145
+ hasTextOverlap: false,
16146
+ isDragOver: false,
16147
+ ...video ? { videoAutoplay: video.autoplay, videoMuted: video.muted } : {}
16148
+ });
16149
+ const sectionEl = el.closest("[data-ohw-section]");
16150
+ aiSectionApiRef.current?.selectFromElement(el, { report: false });
16151
+ postToParentRef.current({
16152
+ type: "ow:section-selected",
16153
+ sectionId: sectionEl?.dataset.ohwSection ?? null,
16154
+ sectionLabel: sectionEl ? sectionEl.dataset.ohwSectionLabel ?? titleCaseSectionId(sectionEl.dataset.ohwSection ?? "") : null,
16155
+ key: el.dataset.ohwKey ?? null,
16156
+ // Display name for the pill — the raw key prettifies into fragments ("Img"); the
16157
+ // bridge knows what the node IS, so it names it.
16158
+ keyLabel: el.dataset.ohwEditable === "video" ? "Video" : el.dataset.ohwEditable === "bg-image" ? "Background" : "Image"
16159
+ });
16160
+ }, []);
16161
+ const selectMediaElementRef = (0, import_react17.useRef)(selectMediaElement);
16162
+ selectMediaElementRef.current = selectMediaElement;
16163
+ (0, import_react17.useEffect)(() => {
16164
+ if (!selectedMedia) return;
16165
+ const update = () => {
16166
+ const el = selectedMediaElRef.current;
16167
+ if (!el || !el.isConnected) {
16168
+ clearMediaSelection();
16169
+ return;
16170
+ }
16171
+ const r2 = el.getBoundingClientRect();
16172
+ setSelectedMedia(
16173
+ (prev) => prev ? { ...prev, rect: { top: r2.top, left: r2.left, width: r2.width, height: r2.height } } : prev
16174
+ );
16175
+ };
16176
+ window.addEventListener("scroll", update, true);
16177
+ window.addEventListener("resize", update);
16178
+ return () => {
16179
+ window.removeEventListener("scroll", update, true);
16180
+ window.removeEventListener("resize", update);
16181
+ };
16182
+ }, [selectedMedia !== null]);
15442
16183
  const [carouselHover, setCarouselHover] = (0, import_react17.useState)(null);
15443
16184
  const [uploadingRects, setUploadingRects] = (0, import_react17.useState)({});
15444
16185
  const hoveredGapRef = (0, import_react17.useRef)(null);
@@ -15722,6 +16463,8 @@ function OhhwellsBridge() {
15722
16463
  const addNavAfterAnchorRef = (0, import_react17.useRef)(null);
15723
16464
  const editContentRef = (0, import_react17.useRef)({});
15724
16465
  const aiSectionsRef = (0, import_react17.useRef)("");
16466
+ const brandKitRef = (0, import_react17.useRef)("");
16467
+ const stylesRef = (0, import_react17.useRef)("");
15725
16468
  const pendingDeleteUndoRef = (0, import_react17.useRef)(null);
15726
16469
  const [sitePages, setSitePages] = (0, import_react17.useState)([]);
15727
16470
  const [sectionsByPath, setSectionsByPath] = (0, import_react17.useState)({});
@@ -17042,13 +17785,29 @@ function OhhwellsBridge() {
17042
17785
  }
17043
17786
  const applyContent = (content) => {
17044
17787
  const imageLoads = [];
17788
+ if (typeof content[BRAND_KIT_KEY] === "string") {
17789
+ brandKitRef.current = content[BRAND_KIT_KEY];
17790
+ applyBrandToDom(parseBrandKit(content[BRAND_KIT_KEY]));
17791
+ } else {
17792
+ brandKitRef.current = "";
17793
+ applyBrandToDom(null);
17794
+ }
17045
17795
  if (typeof content[AI_SECTIONS_KEY] === "string") {
17046
17796
  aiSectionsRef.current = content[AI_SECTIONS_KEY];
17797
+ setAiSectionOrder(content[SECTION_ORDER_KEY], window.location.pathname);
17047
17798
  applyAiSectionsToDom(parseAiSectionsState(content[AI_SECTIONS_KEY]));
17048
17799
  }
17800
+ if (typeof content[STYLE_STORE_KEY] === "string") {
17801
+ stylesRef.current = content[STYLE_STORE_KEY];
17802
+ applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
17803
+ }
17804
+ applyBrandChrome(content);
17049
17805
  for (const [key, val] of Object.entries(content)) {
17050
17806
  if (key === "__ohw_sections") continue;
17051
17807
  if (key === AI_SECTIONS_KEY) continue;
17808
+ if (key === BRAND_KIT_KEY) continue;
17809
+ if (key === STYLE_STORE_KEY) continue;
17810
+ if (BRAND_CHROME_KEYS.has(key)) continue;
17052
17811
  if (key === LOGO_PLACEHOLDER_KEY) continue;
17053
17812
  if (LOGO_IMAGE_KEYS.includes(key)) continue;
17054
17813
  if (applyVideoSettingNode(key, val)) continue;
@@ -17236,8 +17995,25 @@ function OhhwellsBridge() {
17236
17995
  initSectionInstancesFromContent(content, window.location.pathname);
17237
17996
  observer?.disconnect();
17238
17997
  try {
17998
+ applyBrandChrome(content);
17999
+ if (typeof content[BRAND_KIT_KEY] === "string") {
18000
+ applyBrandToDom(parseBrandKit(content[BRAND_KIT_KEY]));
18001
+ } else {
18002
+ applyBrandToDom(null);
18003
+ }
18004
+ if (typeof content[AI_SECTIONS_KEY] === "string") {
18005
+ setAiSectionOrder(content[SECTION_ORDER_KEY], window.location.pathname);
18006
+ applyAiSectionsToDom(parseAiSectionsState(content[AI_SECTIONS_KEY]));
18007
+ }
18008
+ if (typeof content[STYLE_STORE_KEY] === "string") {
18009
+ applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
18010
+ }
17239
18011
  for (const [key, val] of Object.entries(content)) {
17240
18012
  if (key === "__ohw_sections") continue;
18013
+ if (key === AI_SECTIONS_KEY) continue;
18014
+ if (key === BRAND_KIT_KEY) continue;
18015
+ if (key === STYLE_STORE_KEY) continue;
18016
+ if (BRAND_CHROME_KEYS.has(key)) continue;
17241
18017
  if (key === LOGO_PLACEHOLDER_KEY) continue;
17242
18018
  if (LOGO_IMAGE_KEYS.includes(key)) continue;
17243
18019
  if (applyVideoSettingNode(key, val)) continue;
@@ -17380,9 +18156,21 @@ function OhhwellsBridge() {
17380
18156
  }, [isEditMode, pathname, subdomain, fetchState, resyncSelectedNavigationItem]);
17381
18157
  (0, import_react17.useEffect)(() => {
17382
18158
  if (!isEditMode) return;
18159
+ let lastPosted = 0;
17383
18160
  const measure = () => {
17384
18161
  const h = document.body.scrollHeight;
17385
- if (h > 50) postToParent2({ type: "ow:height", height: h });
18162
+ if (h > 50 && Math.abs(h - lastPosted) > 1) {
18163
+ lastPosted = h;
18164
+ postToParent2({ type: "ow:height", height: h });
18165
+ }
18166
+ };
18167
+ let raf = null;
18168
+ const schedule = () => {
18169
+ if (raf != null) return;
18170
+ raf = requestAnimationFrame(() => {
18171
+ raf = null;
18172
+ measure();
18173
+ });
17386
18174
  };
17387
18175
  const t1 = setTimeout(measure, 50);
17388
18176
  const t2 = setTimeout(measure, 500);
@@ -17402,6 +18190,7 @@ function OhhwellsBridge() {
17402
18190
  return () => {
17403
18191
  clearTimeout(t1);
17404
18192
  clearTimeout(t2);
18193
+ if (raf != null) cancelAnimationFrame(raf);
17405
18194
  clearResizeTimers();
17406
18195
  window.removeEventListener("resize", handleResize);
17407
18196
  };
@@ -17644,10 +18433,14 @@ function OhhwellsBridge() {
17644
18433
  return;
17645
18434
  }
17646
18435
  const target = e.target;
18436
+ if (target.closest("[data-ohw-ai-review]")) return;
17647
18437
  if (target.closest("[data-ohw-toolbar]")) return;
17648
18438
  if (target.closest("[data-ohw-state-toggle]")) return;
17649
18439
  if (target.closest("[data-ohw-max-badge]")) return;
17650
18440
  if (isInsideLinkEditor(target)) return;
18441
+ if (selectedMediaElRef.current && !selectedMediaElRef.current.contains(target) && !target.closest("[data-ohw-media-overlay], [data-ohw-edit-chrome], [data-ohw-item-interaction]")) {
18442
+ clearMediaSelectionRef.current();
18443
+ }
17651
18444
  if (isInsideFloatingPanel(target)) return;
17652
18445
  if (target.closest("[data-ohw-form-toolbar]")) return;
17653
18446
  if (target.closest(
@@ -17817,8 +18610,11 @@ function OhhwellsBridge() {
17817
18610
  if (isMediaEditable(editable) && !buttonOnMedia) {
17818
18611
  e.preventDefault();
17819
18612
  e.stopPropagation();
17820
- aiSectionApiRef.current?.selectFromElement(editable);
17821
- postToParentRef.current({ type: "ow:image-pick", key: editable.dataset.ohwKey ?? "", elementType: editable.dataset.ohwEditable ?? "image" });
18613
+ if (selectedMediaElRef.current === editable) {
18614
+ postToParentRef.current({ type: "ow:image-pick", key: editable.dataset.ohwKey ?? "", elementType: editable.dataset.ohwEditable ?? "image" });
18615
+ } else {
18616
+ selectMediaElementRef.current(editable);
18617
+ }
17822
18618
  return;
17823
18619
  }
17824
18620
  const socialItem = getSocialItem(editable);
@@ -17959,6 +18755,7 @@ function OhhwellsBridge() {
17959
18755
  };
17960
18756
  const handleDblClick = (e) => {
17961
18757
  const target = e.target;
18758
+ if (target.closest("[data-ohw-ai-review]")) return;
17962
18759
  if (target.closest("[data-ohw-toolbar]")) return;
17963
18760
  if (target.closest("[data-ohw-state-toggle]")) return;
17964
18761
  if (target.closest("[data-ohw-max-badge]")) return;
@@ -18759,7 +19556,9 @@ function OhhwellsBridge() {
18759
19556
  return;
18760
19557
  }
18761
19558
  const { y } = toProbeCoords(clientX, clientY, fromParentViewport);
18762
- const sections = Array.from(document.querySelectorAll("[data-ohw-section]")).sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top);
19559
+ const sections = Array.from(document.querySelectorAll("[data-ohw-section]")).filter(
19560
+ (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
19561
+ ).sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top);
18763
19562
  const ZONE = 20;
18764
19563
  for (let i = 0; i < sections.length; i++) {
18765
19564
  const a = sections[i];
@@ -19091,10 +19890,23 @@ function OhhwellsBridge() {
19091
19890
  if (e.data?.type !== "ow:hydrate") return;
19092
19891
  const content = e.data.content;
19093
19892
  if (!content) return;
19893
+ if (typeof content[BRAND_KIT_KEY] === "string") {
19894
+ brandKitRef.current = content[BRAND_KIT_KEY];
19895
+ applyBrandToDom(parseBrandKit(content[BRAND_KIT_KEY]));
19896
+ } else {
19897
+ brandKitRef.current = "";
19898
+ applyBrandToDom(null);
19899
+ }
19094
19900
  if (typeof content[AI_SECTIONS_KEY] === "string") {
19095
19901
  aiSectionsRef.current = content[AI_SECTIONS_KEY];
19902
+ setAiSectionOrder(content[SECTION_ORDER_KEY], window.location.pathname);
19096
19903
  applyAiSectionsToDom(parseAiSectionsState(content[AI_SECTIONS_KEY]));
19097
19904
  }
19905
+ if (typeof content[STYLE_STORE_KEY] === "string") {
19906
+ stylesRef.current = content[STYLE_STORE_KEY];
19907
+ applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
19908
+ }
19909
+ applyBrandChrome(content);
19098
19910
  let sectionsJson = null;
19099
19911
  for (const [key, val] of Object.entries(content)) {
19100
19912
  if (key === "__ohw_sections") {
@@ -19102,6 +19914,9 @@ function OhhwellsBridge() {
19102
19914
  continue;
19103
19915
  }
19104
19916
  if (key === AI_SECTIONS_KEY) continue;
19917
+ if (key === BRAND_KIT_KEY) continue;
19918
+ if (key === STYLE_STORE_KEY) continue;
19919
+ if (BRAND_CHROME_KEYS.has(key)) continue;
19105
19920
  if (key === LOGO_PLACEHOLDER_KEY) continue;
19106
19921
  if (LOGO_IMAGE_KEYS.includes(key) && !String(val ?? "").trim()) continue;
19107
19922
  if (applyVideoSettingNode(key, val)) continue;
@@ -19117,6 +19932,8 @@ function OhhwellsBridge() {
19117
19932
  if (video && video.src !== val) applyVideoSrc(video, val);
19118
19933
  } else if (el.dataset.ohwEditable === "link") {
19119
19934
  applyLinkHref(el, val);
19935
+ } else if (el.dataset.ohwEditable === "icon") {
19936
+ applyIconMarkup(el, val);
19120
19937
  } else if (isIconMarkupValue(val)) {
19121
19938
  } else {
19122
19939
  el.innerHTML = val;
@@ -19201,12 +20018,21 @@ function OhhwellsBridge() {
19201
20018
  nodes: collectEditableNodes(editContentRef.current)
19202
20019
  });
19203
20020
  };
20021
+ const clearInteractionChrome = () => {
20022
+ deactivateRef.current();
20023
+ deselectRef.current();
20024
+ clearMediaSelectionRef.current();
20025
+ };
19204
20026
  const handleAiApplyTree = (e) => {
19205
20027
  if (e.data?.type !== "ow:ai-apply-tree") return;
19206
20028
  const payload = e.data.payload;
19207
20029
  if (!payload || typeof payload.id !== "string" || !isRenderableTree(payload.tree)) return;
20030
+ clearInteractionChrome();
19208
20031
  const previous = aiSectionsRef.current;
19209
- const nextState = applyTreeToState(parseAiSectionsState(previous), payload);
20032
+ const nextState = applyTreeToState(parseAiSectionsState(previous), {
20033
+ ...payload,
20034
+ path: payload.path ?? window.location.pathname
20035
+ });
19210
20036
  const nextValue = serializeAiSectionsState(nextState);
19211
20037
  aiSectionsRef.current = nextValue;
19212
20038
  applyAiSectionsToDom(nextState);
@@ -19227,6 +20053,7 @@ function OhhwellsBridge() {
19227
20053
  const exists = document.querySelector(`[data-ohw-section="${CSS.escape(sectionId)}"]`);
19228
20054
  if (!exists) return;
19229
20055
  if (isPageFrameSection(exists)) return;
20056
+ clearInteractionChrome();
19230
20057
  const previous = aiSectionsRef.current;
19231
20058
  const nextState = deleteSectionFromState(parseAiSectionsState(previous), sectionId);
19232
20059
  const nextValue = serializeAiSectionsState(nextState);
@@ -19242,14 +20069,45 @@ function OhhwellsBridge() {
19242
20069
  const handleAiSetSections = (e) => {
19243
20070
  if (e.data?.type !== "ow:ai-set-sections") return;
19244
20071
  const value = typeof e.data.value === "string" ? e.data.value : "";
20072
+ clearInteractionChrome();
19245
20073
  aiSectionsRef.current = value;
19246
20074
  applyAiSectionsToDom(parseAiSectionsState(value));
20075
+ applyStylesToDom(parseStyleStore(stylesRef.current));
19247
20076
  const restoredHeight = document.body.scrollHeight;
19248
20077
  if (restoredHeight > 50) postToParentRef.current({ type: "ow:height", height: restoredHeight });
19249
20078
  postToParentRef.current({ type: "ow:change", nodes: [{ key: AI_SECTIONS_KEY, text: value }] });
19250
20079
  postAiSectionsChanged();
19251
20080
  };
19252
20081
  window.addEventListener("message", handleAiSetSections);
20082
+ const handleAiSetBrand = (e) => {
20083
+ if (e.data?.type !== "ow:ai-set-brand") return;
20084
+ const value = typeof e.data.value === "string" ? e.data.value : "";
20085
+ const previous = brandKitRef.current;
20086
+ brandKitRef.current = value;
20087
+ applyBrandToDom(parseBrandKit(value));
20088
+ if (aiSectionsRef.current) applyAiSectionsToDom(parseAiSectionsState(aiSectionsRef.current));
20089
+ applyStylesToDom(parseStyleStore(stylesRef.current));
20090
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: BRAND_KIT_KEY, text: value }] });
20091
+ postToParentRef.current({ type: "ow:ai-brand-applied", previous, value });
20092
+ };
20093
+ window.addEventListener("message", handleAiSetBrand);
20094
+ const handleAiSetStyles = (e) => {
20095
+ if (e.data?.type !== "ow:ai-set-styles") return;
20096
+ const value = typeof e.data.value === "string" ? e.data.value : "";
20097
+ const previous = stylesRef.current;
20098
+ stylesRef.current = value;
20099
+ applyStylesToDom(parseStyleStore(value));
20100
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: STYLE_STORE_KEY, text: value }] });
20101
+ postToParentRef.current({ type: "ow:ai-styles-applied", previous, value });
20102
+ };
20103
+ window.addEventListener("message", handleAiSetStyles);
20104
+ const handleGetBrand = (e) => {
20105
+ if (e.data?.type !== "ow:get-brand") return;
20106
+ const template = deriveTemplateBrand();
20107
+ const value = brandKitRef.current || (template ? JSON.stringify(template) : "");
20108
+ postToParentRef.current({ type: "ow:brand-value", value });
20109
+ };
20110
+ window.addEventListener("message", handleGetBrand);
19253
20111
  const handleMoveSection = (e) => {
19254
20112
  if (e.data?.type !== "ow:move-section") return;
19255
20113
  const instanceId = typeof e.data.instanceId === "string" ? e.data.instanceId : "";
@@ -19259,6 +20117,7 @@ function OhhwellsBridge() {
19259
20117
  if (!entries) return;
19260
20118
  const orderJson = JSON.stringify(entries);
19261
20119
  editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: orderJson };
20120
+ setAiSectionOrder(orderJson, window.location.pathname);
19262
20121
  postToParentRef.current({ type: "ow:change", nodes: [{ key: SECTION_ORDER_KEY, text: orderJson }] });
19263
20122
  window.dispatchEvent(new Event("resize"));
19264
20123
  };
@@ -19322,6 +20181,7 @@ function OhhwellsBridge() {
19322
20181
  }
19323
20182
  deselectRef.current();
19324
20183
  deactivateRef.current();
20184
+ clearMediaSelectionRef.current();
19325
20185
  };
19326
20186
  window.addEventListener("message", handleDeactivate);
19327
20187
  const handleToastAction = (e) => {
@@ -19407,6 +20267,10 @@ function OhhwellsBridge() {
19407
20267
  const handleKeyDown = (e) => {
19408
20268
  if (e.key === "Escape" && document.querySelector("[data-ohw-section-picker]")) return;
19409
20269
  if (e.key === "Escape" && document.querySelector("[data-ohw-more-menu]")) return;
20270
+ if (e.key === "Escape" && selectedMediaElRef.current) {
20271
+ clearMediaSelectionRef.current();
20272
+ return;
20273
+ }
19410
20274
  if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "a" && activeElRef.current) {
19411
20275
  e.preventDefault();
19412
20276
  selectAllTextInEditable(activeElRef.current);
@@ -19566,6 +20430,12 @@ function OhhwellsBridge() {
19566
20430
  if (aiSectionsRef.current) {
19567
20431
  nodes.push({ key: AI_SECTIONS_KEY, type: "sections", text: aiSectionsRef.current });
19568
20432
  }
20433
+ if (stylesRef.current) {
20434
+ nodes.push({ key: STYLE_STORE_KEY, type: "styles", text: stylesRef.current });
20435
+ }
20436
+ if (brandKitRef.current) {
20437
+ nodes.push({ key: BRAND_KIT_KEY, type: "brand", text: brandKitRef.current });
20438
+ }
19569
20439
  document.querySelectorAll('[data-ohw-editable="form"]').forEach((form) => {
19570
20440
  const formKey = formKeyOf(form);
19571
20441
  if (!formKey) return;
@@ -19980,6 +20850,9 @@ function OhhwellsBridge() {
19980
20850
  window.removeEventListener("message", handleAiApplyTree);
19981
20851
  window.removeEventListener("message", handleAiDeleteSection);
19982
20852
  window.removeEventListener("message", handleAiSetSections);
20853
+ window.removeEventListener("message", handleAiSetBrand);
20854
+ window.removeEventListener("message", handleAiSetStyles);
20855
+ window.removeEventListener("message", handleGetBrand);
19983
20856
  window.removeEventListener("message", handleMoveSection);
19984
20857
  window.removeEventListener("message", handlePanelDragging);
19985
20858
  window.removeEventListener("message", handleDeleteSection);
@@ -20190,7 +21063,7 @@ function OhhwellsBridge() {
20190
21063
  postToParent2({
20191
21064
  type: "ow:ready",
20192
21065
  version: "1",
20193
- bridgeVersion: "0.1.77",
21066
+ bridgeVersion: "0.1.78",
20194
21067
  path: pathname,
20195
21068
  nodes: collectEditableNodes(editContentRef.current),
20196
21069
  sections
@@ -20597,11 +21470,22 @@ function OhhwellsBridge() {
20597
21470
  const showEditLink = toolbarShowEditLink;
20598
21471
  const currentSections = sectionsByPath[pathname] ?? [];
20599
21472
  linkPopoverOpenRef.current = linkPopover !== null;
21473
+ const handleMediaSelect = (0, import_react17.useCallback)((key) => {
21474
+ const el = hoveredImageRef.current?.dataset.ohwKey === key ? hoveredImageRef.current : Array.from(document.querySelectorAll(MEDIA_SELECTOR)).find(
21475
+ (m) => (m.dataset.ohwKey ?? "") === key
21476
+ ) ?? null;
21477
+ if (!el) return;
21478
+ selectMediaElementRef.current(el);
21479
+ }, []);
20600
21480
  const handleMediaReplace = (0, import_react17.useCallback)(
20601
21481
  (key) => {
20602
- postToParent2({ type: "ow:image-pick", key, elementType: mediaHover?.elementType ?? "image" });
21482
+ postToParent2({
21483
+ type: "ow:image-pick",
21484
+ key,
21485
+ elementType: mediaHover?.elementType ?? selectedMedia?.elementType ?? "image"
21486
+ });
20603
21487
  },
20604
- [postToParent2, mediaHover?.elementType]
21488
+ [postToParent2, mediaHover?.elementType, selectedMedia?.elementType]
20605
21489
  );
20606
21490
  const handleEditCarousel = (0, import_react17.useCallback)(
20607
21491
  (key) => {
@@ -20673,12 +21557,25 @@ function OhhwellsBridge() {
20673
21557
  },
20674
21558
  `uploading-${key}`
20675
21559
  )),
20676
- mediaHover && !(mediaHover.key in uploadingRects) && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
21560
+ mediaHover && !(mediaHover.key in uploadingRects) && mediaHover.key !== selectedMedia?.key && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
20677
21561
  MediaOverlay,
20678
21562
  {
20679
21563
  hover: mediaHover,
20680
21564
  isUploading: false,
20681
21565
  onReplace: handleMediaReplace,
21566
+ onSelect: handleMediaSelect,
21567
+ onVideoSettingsChange: handleVideoSettingsChange
21568
+ }
21569
+ ),
21570
+ selectedMedia && !(selectedMedia.key in uploadingRects) && !linkPopover && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
21571
+ MediaOverlay,
21572
+ {
21573
+ hover: selectedMedia,
21574
+ selected: true,
21575
+ hovered: mediaHover?.key === selectedMedia.key,
21576
+ isUploading: false,
21577
+ onReplace: handleMediaReplace,
21578
+ onSelect: handleMediaSelect,
20682
21579
  onVideoSettingsChange: handleVideoSettingsChange
20683
21580
  }
20684
21581
  ),