@ohhwells/bridge 0.1.77 → 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 });
@@ -7433,6 +8099,10 @@ function topLevelSections() {
7433
8099
  function instanceIdOf(el) {
7434
8100
  return el.dataset.ohwInstance ?? el.dataset.ohwSection ?? "";
7435
8101
  }
8102
+ function findByInstanceId(instanceId) {
8103
+ const escapedId = CSS.escape(instanceId);
8104
+ return document.querySelector(`[data-ohw-instance="${escapedId}"]`) ?? document.querySelector(`[data-ohw-section="${escapedId}"]:not([data-ohw-instance])`);
8105
+ }
7436
8106
  function planSectionMove(instanceId, targetIndex, currentPath) {
7437
8107
  const sections = topLevelSections();
7438
8108
  const index = sections.findIndex((el) => instanceIdOf(el) === instanceId);
@@ -7468,7 +8138,7 @@ function syncRemovedFlags(entries) {
7468
8138
  }
7469
8139
  });
7470
8140
  for (const id of removedIds) {
7471
- const el = document.querySelector(`[data-ohw-instance="${CSS.escape(id)}"]`);
8141
+ const el = findByInstanceId(id);
7472
8142
  if (el) {
7473
8143
  el.style.display = "none";
7474
8144
  el.setAttribute(REMOVED_ATTR2, "");
@@ -7496,9 +8166,7 @@ function applyPersistedOrder(entries) {
7496
8166
  }
7497
8167
  }
7498
8168
  function setSectionRemoved(instanceId, currentPath, existingEntries, removed) {
7499
- const escapedId = CSS.escape(instanceId);
7500
- if (!document.querySelector(`[data-ohw-instance="${escapedId}"]`) && !document.querySelector(`[data-ohw-section="${escapedId}"]:not([data-ohw-instance])`))
7501
- return null;
8169
+ if (!findByInstanceId(instanceId)) return null;
7502
8170
  const byId = new Map(existingEntries.map((e) => [e.instanceId, e]));
7503
8171
  const allSections = Array.from(document.querySelectorAll("[data-ohw-section]")).filter(
7504
8172
  (el) => !el.parentElement?.closest("[data-ohw-section]") && !isChromeSection(el)
@@ -7687,6 +8355,7 @@ function AiSectionOverlay({
7687
8355
  }) {
7688
8356
  const [selectedId, setSelectedId] = (0, import_react8.useState)(null);
7689
8357
  const [reviewId, setReviewId] = (0, import_react8.useState)(null);
8358
+ const [reviewButtonsHidden, setReviewButtonsHidden] = (0, import_react8.useState)(false);
7690
8359
  const reviewIdRef = (0, import_react8.useRef)(null);
7691
8360
  reviewIdRef.current = reviewId;
7692
8361
  const selectedIdRef = (0, import_react8.useRef)(null);
@@ -7748,6 +8417,7 @@ function AiSectionOverlay({
7748
8417
  }
7749
8418
  const found = readRect(sectionId) != null;
7750
8419
  setReviewId(found ? sectionId : null);
8420
+ setReviewButtonsHidden(e.data.hideButtons === true);
7751
8421
  postToParent2({ type: "ow:ai-review-started", sectionId, found });
7752
8422
  if (found) {
7753
8423
  document.querySelector(`[data-ohw-instance="${CSS.escape(sectionId)}"]`)?.scrollIntoView({ behavior: "smooth", block: "nearest" });
@@ -7875,13 +8545,16 @@ function AiSectionOverlay({
7875
8545
  border: `2px solid ${PRIMARY2}`,
7876
8546
  borderRadius: edgeAwareRadius(reviewRect),
7877
8547
  zIndex: 2147483200,
7878
- // 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.
7879
8552
  background: "rgba(8, 133, 254, 0.04)",
7880
8553
  pointerEvents: "auto",
7881
8554
  cursor: "default"
7882
8555
  },
7883
8556
  onClick: (e) => e.stopPropagation(),
7884
- children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
8557
+ children: !reviewButtonsHidden && /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
7885
8558
  "div",
7886
8559
  {
7887
8560
  style: {
@@ -10448,8 +11121,13 @@ function referenceBox(slot) {
10448
11121
  const neighbour = row ? Array.from(row.querySelectorAll('[data-ohw-editable="icon"]')).find(
10449
11122
  (el) => el !== slot && !el.hasAttribute("data-ohw-social-icon-placeholder")
10450
11123
  ) : null;
10451
- const source = (neighbour ?? slot).querySelector(GLYPH_SELECTOR);
10452
- 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;
10453
11131
  return box?.width && box.height ? box : null;
10454
11132
  }
10455
11133
  function iconMarkupSizedFor(slot, markup) {
@@ -13318,6 +13996,7 @@ function useSectionDrag({
13318
13996
  }
13319
13997
  const orderJson = JSON.stringify(entries);
13320
13998
  editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: orderJson };
13999
+ setAiSectionOrder(orderJson, window.location.pathname);
13321
14000
  postToParentRef.current({ type: "ow:change", nodes: [{ key: SECTION_ORDER_KEY, text: orderJson }] });
13322
14001
  applyPersistedOrder(entries);
13323
14002
  clearSectionDragVisuals();
@@ -15437,6 +16116,70 @@ function OhhwellsBridge() {
15437
16116
  const hoveredImageHasTextOverlapRef = (0, import_react17.useRef)(false);
15438
16117
  const dragOverElRef = (0, import_react17.useRef)(null);
15439
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]);
15440
16183
  const [carouselHover, setCarouselHover] = (0, import_react17.useState)(null);
15441
16184
  const [uploadingRects, setUploadingRects] = (0, import_react17.useState)({});
15442
16185
  const hoveredGapRef = (0, import_react17.useRef)(null);
@@ -15720,6 +16463,8 @@ function OhhwellsBridge() {
15720
16463
  const addNavAfterAnchorRef = (0, import_react17.useRef)(null);
15721
16464
  const editContentRef = (0, import_react17.useRef)({});
15722
16465
  const aiSectionsRef = (0, import_react17.useRef)("");
16466
+ const brandKitRef = (0, import_react17.useRef)("");
16467
+ const stylesRef = (0, import_react17.useRef)("");
15723
16468
  const pendingDeleteUndoRef = (0, import_react17.useRef)(null);
15724
16469
  const [sitePages, setSitePages] = (0, import_react17.useState)([]);
15725
16470
  const [sectionsByPath, setSectionsByPath] = (0, import_react17.useState)({});
@@ -17040,13 +17785,29 @@ function OhhwellsBridge() {
17040
17785
  }
17041
17786
  const applyContent = (content) => {
17042
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
+ }
17043
17795
  if (typeof content[AI_SECTIONS_KEY] === "string") {
17044
17796
  aiSectionsRef.current = content[AI_SECTIONS_KEY];
17797
+ setAiSectionOrder(content[SECTION_ORDER_KEY], window.location.pathname);
17045
17798
  applyAiSectionsToDom(parseAiSectionsState(content[AI_SECTIONS_KEY]));
17046
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);
17047
17805
  for (const [key, val] of Object.entries(content)) {
17048
17806
  if (key === "__ohw_sections") continue;
17049
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;
17050
17811
  if (key === LOGO_PLACEHOLDER_KEY) continue;
17051
17812
  if (LOGO_IMAGE_KEYS.includes(key)) continue;
17052
17813
  if (applyVideoSettingNode(key, val)) continue;
@@ -17234,8 +17995,25 @@ function OhhwellsBridge() {
17234
17995
  initSectionInstancesFromContent(content, window.location.pathname);
17235
17996
  observer?.disconnect();
17236
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
+ }
17237
18011
  for (const [key, val] of Object.entries(content)) {
17238
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;
17239
18017
  if (key === LOGO_PLACEHOLDER_KEY) continue;
17240
18018
  if (LOGO_IMAGE_KEYS.includes(key)) continue;
17241
18019
  if (applyVideoSettingNode(key, val)) continue;
@@ -17378,9 +18156,21 @@ function OhhwellsBridge() {
17378
18156
  }, [isEditMode, pathname, subdomain, fetchState, resyncSelectedNavigationItem]);
17379
18157
  (0, import_react17.useEffect)(() => {
17380
18158
  if (!isEditMode) return;
18159
+ let lastPosted = 0;
17381
18160
  const measure = () => {
17382
18161
  const h = document.body.scrollHeight;
17383
- 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
+ });
17384
18174
  };
17385
18175
  const t1 = setTimeout(measure, 50);
17386
18176
  const t2 = setTimeout(measure, 500);
@@ -17400,6 +18190,7 @@ function OhhwellsBridge() {
17400
18190
  return () => {
17401
18191
  clearTimeout(t1);
17402
18192
  clearTimeout(t2);
18193
+ if (raf != null) cancelAnimationFrame(raf);
17403
18194
  clearResizeTimers();
17404
18195
  window.removeEventListener("resize", handleResize);
17405
18196
  };
@@ -17642,10 +18433,14 @@ function OhhwellsBridge() {
17642
18433
  return;
17643
18434
  }
17644
18435
  const target = e.target;
18436
+ if (target.closest("[data-ohw-ai-review]")) return;
17645
18437
  if (target.closest("[data-ohw-toolbar]")) return;
17646
18438
  if (target.closest("[data-ohw-state-toggle]")) return;
17647
18439
  if (target.closest("[data-ohw-max-badge]")) return;
17648
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
+ }
17649
18444
  if (isInsideFloatingPanel(target)) return;
17650
18445
  if (target.closest("[data-ohw-form-toolbar]")) return;
17651
18446
  if (target.closest(
@@ -17815,8 +18610,11 @@ function OhhwellsBridge() {
17815
18610
  if (isMediaEditable(editable) && !buttonOnMedia) {
17816
18611
  e.preventDefault();
17817
18612
  e.stopPropagation();
17818
- aiSectionApiRef.current?.selectFromElement(editable);
17819
- 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
+ }
17820
18618
  return;
17821
18619
  }
17822
18620
  const socialItem = getSocialItem(editable);
@@ -17957,6 +18755,7 @@ function OhhwellsBridge() {
17957
18755
  };
17958
18756
  const handleDblClick = (e) => {
17959
18757
  const target = e.target;
18758
+ if (target.closest("[data-ohw-ai-review]")) return;
17960
18759
  if (target.closest("[data-ohw-toolbar]")) return;
17961
18760
  if (target.closest("[data-ohw-state-toggle]")) return;
17962
18761
  if (target.closest("[data-ohw-max-badge]")) return;
@@ -18757,7 +19556,9 @@ function OhhwellsBridge() {
18757
19556
  return;
18758
19557
  }
18759
19558
  const { y } = toProbeCoords(clientX, clientY, fromParentViewport);
18760
- 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);
18761
19562
  const ZONE = 20;
18762
19563
  for (let i = 0; i < sections.length; i++) {
18763
19564
  const a = sections[i];
@@ -19089,10 +19890,23 @@ function OhhwellsBridge() {
19089
19890
  if (e.data?.type !== "ow:hydrate") return;
19090
19891
  const content = e.data.content;
19091
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
+ }
19092
19900
  if (typeof content[AI_SECTIONS_KEY] === "string") {
19093
19901
  aiSectionsRef.current = content[AI_SECTIONS_KEY];
19902
+ setAiSectionOrder(content[SECTION_ORDER_KEY], window.location.pathname);
19094
19903
  applyAiSectionsToDom(parseAiSectionsState(content[AI_SECTIONS_KEY]));
19095
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);
19096
19910
  let sectionsJson = null;
19097
19911
  for (const [key, val] of Object.entries(content)) {
19098
19912
  if (key === "__ohw_sections") {
@@ -19100,6 +19914,9 @@ function OhhwellsBridge() {
19100
19914
  continue;
19101
19915
  }
19102
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;
19103
19920
  if (key === LOGO_PLACEHOLDER_KEY) continue;
19104
19921
  if (LOGO_IMAGE_KEYS.includes(key) && !String(val ?? "").trim()) continue;
19105
19922
  if (applyVideoSettingNode(key, val)) continue;
@@ -19115,6 +19932,8 @@ function OhhwellsBridge() {
19115
19932
  if (video && video.src !== val) applyVideoSrc(video, val);
19116
19933
  } else if (el.dataset.ohwEditable === "link") {
19117
19934
  applyLinkHref(el, val);
19935
+ } else if (el.dataset.ohwEditable === "icon") {
19936
+ applyIconMarkup(el, val);
19118
19937
  } else if (isIconMarkupValue(val)) {
19119
19938
  } else {
19120
19939
  el.innerHTML = val;
@@ -19199,12 +20018,21 @@ function OhhwellsBridge() {
19199
20018
  nodes: collectEditableNodes(editContentRef.current)
19200
20019
  });
19201
20020
  };
20021
+ const clearInteractionChrome = () => {
20022
+ deactivateRef.current();
20023
+ deselectRef.current();
20024
+ clearMediaSelectionRef.current();
20025
+ };
19202
20026
  const handleAiApplyTree = (e) => {
19203
20027
  if (e.data?.type !== "ow:ai-apply-tree") return;
19204
20028
  const payload = e.data.payload;
19205
20029
  if (!payload || typeof payload.id !== "string" || !isRenderableTree(payload.tree)) return;
20030
+ clearInteractionChrome();
19206
20031
  const previous = aiSectionsRef.current;
19207
- const nextState = applyTreeToState(parseAiSectionsState(previous), payload);
20032
+ const nextState = applyTreeToState(parseAiSectionsState(previous), {
20033
+ ...payload,
20034
+ path: payload.path ?? window.location.pathname
20035
+ });
19208
20036
  const nextValue = serializeAiSectionsState(nextState);
19209
20037
  aiSectionsRef.current = nextValue;
19210
20038
  applyAiSectionsToDom(nextState);
@@ -19225,6 +20053,7 @@ function OhhwellsBridge() {
19225
20053
  const exists = document.querySelector(`[data-ohw-section="${CSS.escape(sectionId)}"]`);
19226
20054
  if (!exists) return;
19227
20055
  if (isPageFrameSection(exists)) return;
20056
+ clearInteractionChrome();
19228
20057
  const previous = aiSectionsRef.current;
19229
20058
  const nextState = deleteSectionFromState(parseAiSectionsState(previous), sectionId);
19230
20059
  const nextValue = serializeAiSectionsState(nextState);
@@ -19240,14 +20069,45 @@ function OhhwellsBridge() {
19240
20069
  const handleAiSetSections = (e) => {
19241
20070
  if (e.data?.type !== "ow:ai-set-sections") return;
19242
20071
  const value = typeof e.data.value === "string" ? e.data.value : "";
20072
+ clearInteractionChrome();
19243
20073
  aiSectionsRef.current = value;
19244
20074
  applyAiSectionsToDom(parseAiSectionsState(value));
20075
+ applyStylesToDom(parseStyleStore(stylesRef.current));
19245
20076
  const restoredHeight = document.body.scrollHeight;
19246
20077
  if (restoredHeight > 50) postToParentRef.current({ type: "ow:height", height: restoredHeight });
19247
20078
  postToParentRef.current({ type: "ow:change", nodes: [{ key: AI_SECTIONS_KEY, text: value }] });
19248
20079
  postAiSectionsChanged();
19249
20080
  };
19250
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);
19251
20111
  const handleMoveSection = (e) => {
19252
20112
  if (e.data?.type !== "ow:move-section") return;
19253
20113
  const instanceId = typeof e.data.instanceId === "string" ? e.data.instanceId : "";
@@ -19257,6 +20117,7 @@ function OhhwellsBridge() {
19257
20117
  if (!entries) return;
19258
20118
  const orderJson = JSON.stringify(entries);
19259
20119
  editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: orderJson };
20120
+ setAiSectionOrder(orderJson, window.location.pathname);
19260
20121
  postToParentRef.current({ type: "ow:change", nodes: [{ key: SECTION_ORDER_KEY, text: orderJson }] });
19261
20122
  window.dispatchEvent(new Event("resize"));
19262
20123
  };
@@ -19320,6 +20181,7 @@ function OhhwellsBridge() {
19320
20181
  }
19321
20182
  deselectRef.current();
19322
20183
  deactivateRef.current();
20184
+ clearMediaSelectionRef.current();
19323
20185
  };
19324
20186
  window.addEventListener("message", handleDeactivate);
19325
20187
  const handleToastAction = (e) => {
@@ -19405,6 +20267,10 @@ function OhhwellsBridge() {
19405
20267
  const handleKeyDown = (e) => {
19406
20268
  if (e.key === "Escape" && document.querySelector("[data-ohw-section-picker]")) return;
19407
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
+ }
19408
20274
  if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "a" && activeElRef.current) {
19409
20275
  e.preventDefault();
19410
20276
  selectAllTextInEditable(activeElRef.current);
@@ -19564,6 +20430,12 @@ function OhhwellsBridge() {
19564
20430
  if (aiSectionsRef.current) {
19565
20431
  nodes.push({ key: AI_SECTIONS_KEY, type: "sections", text: aiSectionsRef.current });
19566
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
+ }
19567
20439
  document.querySelectorAll('[data-ohw-editable="form"]').forEach((form) => {
19568
20440
  const formKey = formKeyOf(form);
19569
20441
  if (!formKey) return;
@@ -19978,6 +20850,9 @@ function OhhwellsBridge() {
19978
20850
  window.removeEventListener("message", handleAiApplyTree);
19979
20851
  window.removeEventListener("message", handleAiDeleteSection);
19980
20852
  window.removeEventListener("message", handleAiSetSections);
20853
+ window.removeEventListener("message", handleAiSetBrand);
20854
+ window.removeEventListener("message", handleAiSetStyles);
20855
+ window.removeEventListener("message", handleGetBrand);
19981
20856
  window.removeEventListener("message", handleMoveSection);
19982
20857
  window.removeEventListener("message", handlePanelDragging);
19983
20858
  window.removeEventListener("message", handleDeleteSection);
@@ -20188,7 +21063,7 @@ function OhhwellsBridge() {
20188
21063
  postToParent2({
20189
21064
  type: "ow:ready",
20190
21065
  version: "1",
20191
- bridgeVersion: "0.1.76",
21066
+ bridgeVersion: "0.1.78",
20192
21067
  path: pathname,
20193
21068
  nodes: collectEditableNodes(editContentRef.current),
20194
21069
  sections
@@ -20595,11 +21470,22 @@ function OhhwellsBridge() {
20595
21470
  const showEditLink = toolbarShowEditLink;
20596
21471
  const currentSections = sectionsByPath[pathname] ?? [];
20597
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
+ }, []);
20598
21480
  const handleMediaReplace = (0, import_react17.useCallback)(
20599
21481
  (key) => {
20600
- 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
+ });
20601
21487
  },
20602
- [postToParent2, mediaHover?.elementType]
21488
+ [postToParent2, mediaHover?.elementType, selectedMedia?.elementType]
20603
21489
  );
20604
21490
  const handleEditCarousel = (0, import_react17.useCallback)(
20605
21491
  (key) => {
@@ -20671,12 +21557,25 @@ function OhhwellsBridge() {
20671
21557
  },
20672
21558
  `uploading-${key}`
20673
21559
  )),
20674
- 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)(
20675
21561
  MediaOverlay,
20676
21562
  {
20677
21563
  hover: mediaHover,
20678
21564
  isUploading: false,
20679
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,
20680
21579
  onVideoSettingsChange: handleVideoSettingsChange
20681
21580
  }
20682
21581
  ),