@ohhwells/bridge 0.1.78 → 0.1.80

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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,61 @@ 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
+ "[data-ai-section] img{max-width:100%}",
546
+ "}",
547
+ "@media (min-width: 769px) and (max-width: 1024px){",
548
+ "[data-ai-grid]{grid-template-columns:repeat(2, 1fr) !important}",
549
+ "}"
550
+ ].join("");
217
551
  var FEATURE_LINE_CSS = [
218
552
  "[data-ai-features]>div{position:relative;padding-left:40px;min-height:24px}",
219
553
  '[data-ai-features]>div::before{content:"";position:absolute;left:0;top:1px;width:24px;height:24px;',
220
554
  `background-color:currentColor;-webkit-mask:url("${CHECK_MASK}") no-repeat 0 0/24px 24px;`,
221
555
  `mask:url("${CHECK_MASK}") no-repeat 0 0/24px 24px}`
222
556
  ].join("");
557
+ function hexLuminance(color) {
558
+ const m = /^#([0-9a-f]{6})$/i.exec(color.trim());
559
+ if (!m) return null;
560
+ const [r2, g, b] = [0, 2, 4].map((i) => {
561
+ const c = parseInt(m[1].slice(i, i + 2), 16) / 255;
562
+ return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
563
+ });
564
+ return 0.2126 * r2 + 0.7152 * g + 0.0722 * b;
565
+ }
566
+ function hexContrast(a, b) {
567
+ const la = hexLuminance(a);
568
+ const lb = hexLuminance(b);
569
+ if (la === null || lb === null) return null;
570
+ const [hi, lo] = la > lb ? [la, lb] : [lb, la];
571
+ return (hi + 0.05) / (lo + 0.05);
572
+ }
573
+ function accentBandContext(brand) {
574
+ const p = brand.palette;
575
+ const lightWins = (hexContrast(p.light, p.primary) ?? 99) >= (hexContrast(p.dark, p.primary) ?? 0);
576
+ if (lightWins) {
577
+ return {
578
+ brand: { ...brand, palette: { dark: p.light, primary: p.light, accent: p.light, light: p.primary } },
579
+ buttonLabel: p.primary
580
+ };
581
+ }
582
+ return {
583
+ brand: { ...brand, palette: { dark: p.dark, primary: p.dark, accent: p.dark, light: p.light } },
584
+ buttonLabel: p.light
585
+ };
586
+ }
223
587
  function textAttrs(ctx, path) {
224
588
  return ctx.keyFor ? { "data-ohw-key": ctx.keyFor(path), "data-ohw-editable": "text" } : {};
225
589
  }
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
590
  var TRANSPARENT_PX = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
238
591
  function MediaBox({
239
592
  refValue,
@@ -246,17 +599,13 @@ function MediaBox({
246
599
  const url = refValue ? ctx.resolveMedia(refValue) : null;
247
600
  const isIcon = /^(lucide|simple):/.test(refValue);
248
601
  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
- } : {};
602
+ const editAttrs = ctx.keyFor && editPath && !isIcon ? { "data-ohw-key": ctx.keyFor(editPath), "data-ohw-editable": "image" } : {};
253
603
  if (isIcon) {
254
604
  const Icon = refValue.startsWith("lucide:") ? lucideByName(refValue.slice(7)) : null;
255
605
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
256
606
  "span",
257
607
  {
258
608
  "data-ai-icon": refValue,
259
- ...editAttrs,
260
609
  style: {
261
610
  display: "inline-flex",
262
611
  width: 48,
@@ -309,7 +658,7 @@ function ButtonEl({
309
658
  }) {
310
659
  const secondary = slots.variant === "secondary";
311
660
  const href = str(slots.href);
312
- const linkAttrs = ctx.keyFor && path ? { "data-ohw-href-key": ctx.keyFor(`${path}.href`) } : {};
661
+ const linkAttrs = ctx.keyFor && path ? { "data-ohw-href-key": ctx.keyFor(`${path}.href`), "data-ohw-role": "button" } : {};
313
662
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
314
663
  "a",
315
664
  {
@@ -325,7 +674,7 @@ function ButtonEl({
325
674
  textDecoration: "none",
326
675
  cursor: "pointer",
327
676
  ...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 }
677
+ ...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
678
  },
330
679
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { ...path ? textAttrs(ctx, `${path}.label`) : {}, children: str(slots.label) })
331
680
  }
@@ -831,7 +1180,24 @@ function CardBlock({ node, ctx, path }) {
831
1180
  minWidth: 0
832
1181
  },
833
1182
  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)(
1183
+ media && (horizontal ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1184
+ "div",
1185
+ {
1186
+ style: (
1187
+ // An icon hugs its glyph — flex:1 gave a 48px icon half the card and pushed the
1188
+ // text to the far side. Photos keep the half-and-half split. The inset has no
1189
+ // inner padding (the photo split absorbed that), so the icon carries its own gap.
1190
+ /^(lucide|simple):/.test(mediaRef) ? {
1191
+ flexShrink: 0,
1192
+ display: "flex",
1193
+ alignItems: "center",
1194
+ padding: mediaInset,
1195
+ [mediaPosition === "right" ? "marginLeft" : "marginRight"]: 20
1196
+ } : { flex: 1, minWidth: 0, display: "flex", alignItems: "center", padding: mediaInset }
1197
+ ),
1198
+ children: media
1199
+ }
1200
+ ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
835
1201
  "div",
836
1202
  {
837
1203
  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 +1288,44 @@ function AccordionBlock({ node, ctx, path }) {
922
1288
  ) })
923
1289
  ] }, i)) });
924
1290
  }
1291
+ function useIsMobile() {
1292
+ const [mobile, setMobile] = import_react.default.useState(
1293
+ () => typeof window !== "undefined" && window.matchMedia("(max-width: 768px)").matches
1294
+ );
1295
+ import_react.default.useEffect(() => {
1296
+ const mq = window.matchMedia("(max-width: 768px)");
1297
+ const update = () => setMobile(mq.matches);
1298
+ update();
1299
+ mq.addEventListener("change", update);
1300
+ return () => mq.removeEventListener("change", update);
1301
+ }, []);
1302
+ return mobile;
1303
+ }
925
1304
  function Carousel({ items, itemsPerRow, ctx }) {
1305
+ const isMobile = useIsMobile();
1306
+ const perPage = isMobile ? 1 : itemsPerRow;
1307
+ const pages = Math.max(1, Math.ceil(items.length / perPage));
926
1308
  const [page, setPage] = import_react.default.useState(0);
927
- const pages = Math.max(1, Math.ceil(items.length / itemsPerRow));
928
1309
  const current = Math.min(page, pages - 1);
1310
+ if (pages <= 1) {
1311
+ const cols = Math.max(1, Math.min(items.length, itemsPerRow));
1312
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1313
+ "div",
1314
+ {
1315
+ "data-ai-grid": String(cols),
1316
+ style: {
1317
+ display: "grid",
1318
+ gridTemplateColumns: `repeat(${cols}, 1fr)`,
1319
+ gap: AI_TREE_TOKENS.spacing8,
1320
+ alignItems: "start"
1321
+ },
1322
+ children: items
1323
+ }
1324
+ );
1325
+ }
929
1326
  const pageGroups = Array.from(
930
1327
  { length: pages },
931
- (_, p) => items.slice(p * itemsPerRow, (p + 1) * itemsPerRow)
1328
+ (_, p) => items.slice(p * perPage, (p + 1) * perPage)
932
1329
  );
933
1330
  const chrome = (enabled) => ({
934
1331
  border: `1px solid ${ctx.brand.palette.dark}`,
@@ -953,55 +1350,69 @@ function Carousel({ items, itemsPerRow, ctx }) {
953
1350
  cursor: "pointer",
954
1351
  padding: 0
955
1352
  });
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)(
1353
+ 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)(
1354
+ "div",
1355
+ {
1356
+ style: {
1357
+ display: "flex",
1358
+ transform: `translateX(-${current * 100}%)`,
1359
+ transition: "transform 0.4s ease"
1360
+ },
1361
+ children: pageGroups.map((group, p) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
969
1362
  "div",
970
1363
  {
1364
+ "data-ai-grid": String(perPage),
971
1365
  style: {
972
- display: "flex",
973
- transform: `translateX(-${current * 100}%)`,
974
- transition: "transform 0.4s ease"
1366
+ flex: "0 0 100%",
1367
+ display: "grid",
1368
+ gridTemplateColumns: `repeat(${perPage}, 1fr)`,
1369
+ gap: AI_TREE_TOKENS.spacing8,
1370
+ alignItems: "start"
975
1371
  },
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
- )
1372
+ children: group
1373
+ },
1374
+ p
1375
+ ))
1376
+ }
1377
+ ) });
1378
+ const prevBtn = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1379
+ "button",
1380
+ {
1381
+ type: "button",
1382
+ "aria-label": "Previous",
1383
+ onClick: () => setPage((p) => Math.max(0, p - 1)),
1384
+ style: chrome(current > 0),
1385
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.ArrowLeft, { size: 24, strokeWidth: 1.5, "aria-hidden": true })
1386
+ }
1387
+ );
1388
+ const nextBtn = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1389
+ "button",
1390
+ {
1391
+ type: "button",
1392
+ "aria-label": "Next",
1393
+ onClick: () => setPage((p) => Math.min(pages - 1, p + 1)),
1394
+ style: chrome(current < pages - 1),
1395
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.ArrowRight, { size: 24, strokeWidth: 1.5, "aria-hidden": true })
1396
+ }
1397
+ );
1398
+ 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)) });
1399
+ if (isMobile) {
1400
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { "data-ai-carousel": "", style: { display: "flex", flexDirection: "column", gap: AI_TREE_TOKENS.spacing6, alignItems: "center" }, children: [
1401
+ viewport,
1402
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: AI_TREE_TOKENS.spacing6, justifyContent: "center" }, children: [
1403
+ prevBtn,
1404
+ nextBtn
1405
+ ] }),
1406
+ dots
1407
+ ] });
1408
+ }
1409
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { "data-ai-carousel": "", style: { display: "flex", flexDirection: "column", gap: AI_TREE_TOKENS.spacing8, alignItems: "center" }, children: [
1410
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: AI_TREE_TOKENS.spacing6, width: "100%" }, children: [
1411
+ prevBtn,
1412
+ viewport,
1413
+ nextBtn
1003
1414
  ] }),
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)) })
1415
+ dots
1005
1416
  ] });
1006
1417
  }
1007
1418
  function CollectionBlock({ node, ctx, path }) {
@@ -1080,7 +1491,7 @@ function CollectionBlock({ node, ctx, path }) {
1080
1491
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1081
1492
  "div",
1082
1493
  {
1083
- "data-ai-grid": String(itemsPerRow),
1494
+ "data-ai-grid": "",
1084
1495
  style: {
1085
1496
  display: "grid",
1086
1497
  gridTemplateColumns: `repeat(${itemsPerRow}, 1fr)`,
@@ -1095,6 +1506,49 @@ function renderNode(node, ctx, path) {
1095
1506
  switch (node.type) {
1096
1507
  case "text":
1097
1508
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TextBlock, { slots, ctx, path });
1509
+ // Layout container: arranges child blocks, contributes no content of its own. `grid` is a
1510
+ // nested 12-column grid the children span (a collage is 3–5 media on it, bottom-aligned so
1511
+ // mixed aspects read as a composition); `split` is exactly two children at a ratio; `stack`
1512
+ // is a column. Children render through this same dispatcher, so edit markers, media
1513
+ // resolution, and copy paths all work unchanged inside a group.
1514
+ case "group": {
1515
+ const layout = str(slots.layout);
1516
+ const gap = slots.spacing === "tight" ? AI_TREE_TOKENS.spacing3 : slots.spacing === "airy" ? AI_TREE_TOKENS.spacing8 : AI_TREE_TOKENS.spacing6;
1517
+ const kids = (node.children ?? []).map((child, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1518
+ "div",
1519
+ {
1520
+ style: layout === "grid" ? {
1521
+ gridColumn: `span ${typeof child.span === "number" ? Math.min(12, Math.max(1, child.span)) : 12}`,
1522
+ minWidth: 0
1523
+ } : { minWidth: 0 },
1524
+ children: renderNode(child, ctx, `${path}.c${i}`)
1525
+ },
1526
+ i
1527
+ ));
1528
+ if (layout === "grid") {
1529
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1530
+ "div",
1531
+ {
1532
+ "data-ai-group": "grid",
1533
+ style: { display: "grid", gridTemplateColumns: "repeat(12, 1fr)", gap, alignItems: "end" },
1534
+ children: kids
1535
+ }
1536
+ );
1537
+ }
1538
+ if (layout === "split") {
1539
+ const ratio = str(slots.ratio);
1540
+ const cols = ratio === "3:5" ? "3fr 5fr" : ratio === "5:3" ? "5fr 3fr" : "1fr 1fr";
1541
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1542
+ "div",
1543
+ {
1544
+ "data-ai-group": "split",
1545
+ style: { display: "grid", gridTemplateColumns: cols, gap, alignItems: "center" },
1546
+ children: kids
1547
+ }
1548
+ );
1549
+ }
1550
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-ai-group": "stack", style: { display: "flex", flexDirection: "column", gap }, children: kids });
1551
+ }
1098
1552
  case "button":
1099
1553
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ButtonEl, { slots, ctx, path });
1100
1554
  case "button-row":
@@ -1175,33 +1629,111 @@ function renderNode(node, ctx, path) {
1175
1629
  }
1176
1630
  );
1177
1631
  }
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",
1193
- {
1194
- style: {
1195
- border: `1px solid ${ctx.brand.palette.accent}`,
1196
- borderRadius: AI_TREE_TOKENS.radiusButton,
1197
- height: cs.kind === "textarea" ? 96 : 42
1198
- }
1199
- }
1200
- )
1201
- ] }, i);
1202
- }
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
- }) });
1632
+ case "form": {
1633
+ const formAttrs = ctx.keyFor ? {
1634
+ "data-ohw-editable": "form",
1635
+ "data-ohw-key": ctx.keyFor(`${path}.form`),
1636
+ "data-ohw-success-text": "Thanks \u2014 we'll be in touch shortly."
1637
+ } : {};
1638
+ const fieldStyle = {
1639
+ width: "100%",
1640
+ boxSizing: "border-box",
1641
+ border: `1px solid color-mix(in srgb, ${ctx.brand.palette.dark} 45%, #ffffff)`,
1642
+ borderRadius: 0,
1643
+ padding: 12,
1644
+ background: "#fff",
1645
+ color: ctx.brand.palette.dark,
1646
+ outline: "none",
1647
+ ...typeStyle(AI_TREE_TOKENS.type.bodyM, ctx.brand.fonts.body)
1648
+ };
1649
+ const labelStyle = {
1650
+ ...typeStyle(AI_TREE_TOKENS.type.bodyMBold, ctx.brand.fonts.body),
1651
+ color: ctx.brand.palette.dark,
1652
+ textAlign: "left",
1653
+ width: "100%"
1654
+ };
1655
+ const centered = ctx.sectionAlignment === "center";
1656
+ const submitAlign = centered ? "center" : "flex-start";
1657
+ const children = node.children ?? [];
1658
+ return (
1659
+ // 32px between the field group and the submit. In a stacked (centered) section the form is
1660
+ // capped at 780px and centered — the section's 12-col grid would otherwise leave it hugging
1661
+ // the left edge; a split section lets it fill its own column.
1662
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1663
+ "form",
1664
+ {
1665
+ ...formAttrs,
1666
+ "data-ai-form": "",
1667
+ style: {
1668
+ display: "flex",
1669
+ flexDirection: "column",
1670
+ gap: 32,
1671
+ width: "100%",
1672
+ ...centered ? { maxWidth: 780, marginLeft: "auto", marginRight: "auto" } : {}
1673
+ },
1674
+ children: [
1675
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { display: "flex", flexDirection: "column", gap: 24, width: "100%", alignItems: "flex-start" }, children: children.map((child, i) => {
1676
+ if (child.type !== "input") return null;
1677
+ const cs = child.slots ?? {};
1678
+ const kind = str(cs.kind);
1679
+ const label = str(cs.label);
1680
+ const placeholder = str(cs.placeholder);
1681
+ const required = cs.required === true;
1682
+ const name = label.toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-+|-+$/gu, "") || `field-${i}`;
1683
+ const isTextarea = kind === "textarea";
1684
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: 8, width: "100%" }, children: [
1685
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { ...textAttrs(ctx, `${path}.c${i}.label`), style: labelStyle, children: label }),
1686
+ isTextarea ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1687
+ "textarea",
1688
+ {
1689
+ name,
1690
+ placeholder,
1691
+ required,
1692
+ style: { ...fieldStyle, height: 180, resize: "vertical" }
1693
+ }
1694
+ ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1695
+ "input",
1696
+ {
1697
+ name,
1698
+ type: kind === "email" ? "email" : "text",
1699
+ placeholder,
1700
+ required,
1701
+ style: { ...fieldStyle, height: 48 }
1702
+ }
1703
+ )
1704
+ ] }, i);
1705
+ }) }),
1706
+ children.map((child, i) => {
1707
+ if (child.type === "input") return null;
1708
+ const cs = child.slots ?? {};
1709
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1710
+ "button",
1711
+ {
1712
+ type: "submit",
1713
+ style: {
1714
+ alignSelf: submitAlign,
1715
+ border: "none",
1716
+ cursor: "pointer",
1717
+ padding: "12px 24px",
1718
+ // Corner radius follows the host template's own buttons (measured from a template
1719
+ // CTA); 8px only when the page has no template button to match.
1720
+ borderRadius: ctx.buttonRadius ?? 8,
1721
+ // Brand-styled: primary fill, brand-derived label colour (not a fixed token) so it
1722
+ // reads correctly on custom palettes.
1723
+ background: ctx.brand.palette.primary,
1724
+ color: ctx.buttonLabel ?? ctx.brand.palette.light,
1725
+ ...typeStyle(AI_TREE_TOKENS.type.button, ctx.brand.fonts.body)
1726
+ },
1727
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { ...textAttrs(ctx, `${path}.c${i}.label`), children: str(cs.label) })
1728
+ },
1729
+ i
1730
+ );
1731
+ })
1732
+ ]
1733
+ }
1734
+ )
1735
+ );
1736
+ }
1205
1737
  case "schedule-widget":
1206
1738
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1207
1739
  "div",
@@ -1222,16 +1754,27 @@ function renderNode(node, ctx, path) {
1222
1754
  return null;
1223
1755
  }
1224
1756
  }
1225
- function AiTreeRenderer({ tree, brand, resolveMedia, editKeyPrefix }) {
1757
+ function AiTreeRenderer({
1758
+ tree,
1759
+ brand,
1760
+ buttonRadius,
1761
+ resolveMedia,
1762
+ editKeyPrefix
1763
+ }) {
1226
1764
  if (!isRenderableTree(tree)) {
1227
1765
  return null;
1228
1766
  }
1229
1767
  const resolvedBrand = brand ?? AI_DEFAULT_BRAND;
1768
+ const band = (tree.settings ?? {}).sectionBackground === "accent" ? accentBandContext(resolvedBrand) : null;
1769
+ const blockBrand = band?.brand ?? resolvedBrand;
1230
1770
  const ctx = {
1231
- brand: resolvedBrand,
1771
+ brand: blockBrand,
1232
1772
  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
1773
+ cardSurface: blockBrand.palette.light.toUpperCase() === AI_DEFAULT_BRAND.palette.light.toUpperCase() ? "#ECECEB" : `color-mix(in srgb, ${blockBrand.palette.light} 90%, ${blockBrand.palette.dark})`,
1774
+ keyFor: editKeyPrefix ? (path) => `${editKeyPrefix}.${path}` : null,
1775
+ sectionAlignment: (tree.settings ?? {}).alignment === "center" ? "center" : "left",
1776
+ buttonRadius,
1777
+ ...band ? { buttonLabel: band.buttonLabel } : {}
1235
1778
  };
1236
1779
  const settings = tree.settings ?? {};
1237
1780
  const pad = AI_TREE_TOKENS.sectionPadding[settings.spacing ?? "balanced"];
@@ -1239,27 +1782,41 @@ function AiTreeRenderer({ tree, brand, resolveMedia, editKeyPrefix }) {
1239
1782
  const overlayAlpha = { low: 0.25, medium: 0.45, high: 0.65 }[settings.overlayIntensity ?? "medium"];
1240
1783
  const backgroundUrl = isOverlay && settings.backgroundMedia ? ctx.resolveMedia(settings.backgroundMedia) : null;
1241
1784
  const bgAttrs = ctx.keyFor && isOverlay && settings.backgroundMedia ? { "data-ohw-key": ctx.keyFor("settings.backgroundMedia"), "data-ohw-editable": "bg-image" } : {};
1785
+ const toneBackground = (() => {
1786
+ const { dark, primary, light } = resolvedBrand.palette;
1787
+ switch (settings.sectionBackground) {
1788
+ case "surface":
1789
+ return `color-mix(in srgb, ${light} 94%, ${dark})`;
1790
+ case "accent":
1791
+ return primary;
1792
+ case "accent-soft":
1793
+ return `color-mix(in srgb, ${primary} 12%, ${light})`;
1794
+ default:
1795
+ return void 0;
1796
+ }
1797
+ })();
1798
+ const distributed = !isOverlay && settings.textDistribution;
1242
1799
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1243
1800
  "section",
1244
1801
  {
1245
1802
  "data-ai-section": tree.tag ?? "",
1246
1803
  ...bgAttrs,
1247
- "data-ai-responsive": "",
1248
1804
  style: {
1249
1805
  position: "relative",
1250
1806
  padding: `${pad}px 0`,
1251
- background: isOverlay && !backgroundUrl ? "#DBEAFE" : void 0,
1807
+ background: toneBackground ?? (isOverlay && !backgroundUrl ? "#DBEAFE" : void 0),
1252
1808
  backgroundImage: backgroundUrl ? `url(${backgroundUrl})` : void 0,
1253
1809
  backgroundSize: "cover",
1254
- backgroundPosition: "center"
1810
+ backgroundPosition: "center",
1811
+ color: settings.sectionBackground === "accent" ? blockBrand.palette.dark : void 0
1255
1812
  },
1256
1813
  children: [
1257
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: AI_RESPONSIVE_CSS }),
1258
1814
  isOverlay && backgroundUrl && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { position: "absolute", inset: 0, background: `rgba(255,255,255,${overlayAlpha})` } }),
1815
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: AI_MOBILE_CSS }),
1259
1816
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1260
1817
  "div",
1261
1818
  {
1262
- "data-ai-section-inner": "",
1819
+ "data-ai-container": "",
1263
1820
  style: {
1264
1821
  position: "relative",
1265
1822
  maxWidth: AI_TREE_TOKENS.sectionMaxWidth,
@@ -1270,15 +1827,29 @@ function AiTreeRenderer({ tree, brand, resolveMedia, editKeyPrefix }) {
1270
1827
  children: tree.rows.map((row, r2) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1271
1828
  "div",
1272
1829
  {
1273
- "data-ai-columns": "",
1830
+ "data-ai-row": "",
1274
1831
  style: {
1275
1832
  display: "grid",
1276
1833
  gridTemplateColumns: "repeat(12, 1fr)",
1277
1834
  gap: AI_TREE_TOKENS.spacing6,
1278
- alignItems: settings.verticalPosition === "top" ? "start" : "center",
1835
+ alignItems: distributed === "space-between" ? "stretch" : (distributed ?? settings.verticalPosition) === "top" ? "start" : "center",
1279
1836
  marginTop: r2 > 0 ? AI_TREE_TOKENS.spacing8 : 0
1280
1837
  },
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))
1838
+ children: (row.blocks ?? []).map((block, b) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1839
+ "div",
1840
+ {
1841
+ "data-ai-cell": "",
1842
+ style: {
1843
+ gridColumn: `span ${Math.min(12, Math.max(1, block.span))}`,
1844
+ minWidth: 0,
1845
+ // space-between: each column becomes a flex column whose content spreads over
1846
+ // the full row height instead of clumping at the top.
1847
+ ...distributed === "space-between" ? { display: "flex", flexDirection: "column", justifyContent: "space-between" } : {}
1848
+ },
1849
+ children: renderNode(block, ctx, `r${r2}.b${b}`)
1850
+ },
1851
+ b
1852
+ ))
1282
1853
  },
1283
1854
  r2
1284
1855
  ))
@@ -1294,17 +1865,36 @@ var import_jsx_runtime2 = require("react/jsx-runtime");
1294
1865
  var CONTAINER_ATTR = "data-ohw-ai-generated";
1295
1866
  var REPLACED_ATTR = "data-ohw-ai-replaced-by";
1296
1867
  var REMOVED_ATTR = "data-ohw-ai-removed";
1868
+ var TEMPLATE_HIDDEN_ATTR = "data-ohw-ai-template-hidden";
1869
+ var CHROME_SECTION_IDS = /* @__PURE__ */ new Set(["navbar", "footer"]);
1870
+ function readRootVar(name) {
1871
+ if (typeof document === "undefined") return "";
1872
+ return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
1873
+ }
1874
+ function deriveBrandOverride() {
1875
+ const dark = readRootVar("--ohw-brand-dark");
1876
+ const primary = readRootVar("--ohw-brand-primary");
1877
+ const light = readRootVar("--ohw-brand-light");
1878
+ if (!dark || !primary || !light) return null;
1879
+ const accent = readRootVar("--ohw-brand-accent");
1880
+ const heading = readRootVar("--font-heading") || readRootVar("--font-display");
1881
+ const body = readRootVar("--font-body");
1882
+ return {
1883
+ palette: { dark, primary, accent: accent || dark, light },
1884
+ fonts: {
1885
+ heading: heading || AI_DEFAULT_BRAND.fonts.heading,
1886
+ body: body || AI_DEFAULT_BRAND.fonts.body
1887
+ }
1888
+ };
1889
+ }
1297
1890
  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");
1891
+ const dark = readRootVar("--color-dark");
1892
+ const primary = readRootVar("--color-primary");
1893
+ const light = readRootVar("--color-light");
1304
1894
  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");
1895
+ const accent = readRootVar("--color-accent");
1896
+ const heading = readRootVar("--font-heading") || readRootVar("--font-display");
1897
+ const body = readRootVar("--font-body");
1308
1898
  return {
1309
1899
  palette: { dark, primary, accent: accent || dark, light },
1310
1900
  fonts: {
@@ -1313,6 +1903,13 @@ function deriveTemplateBrand() {
1313
1903
  }
1314
1904
  };
1315
1905
  }
1906
+ function deriveTemplateButtonRadius() {
1907
+ if (typeof document === "undefined") return null;
1908
+ const btn = document.querySelector('[data-ohw-role="button"]');
1909
+ if (!btn) return null;
1910
+ const radius = getComputedStyle(btn).borderTopLeftRadius;
1911
+ return radius || null;
1912
+ }
1316
1913
  var mounted = /* @__PURE__ */ new Map();
1317
1914
  function findTemplateSection(id) {
1318
1915
  for (const el of document.querySelectorAll(`[data-ohw-section="${CSS.escape(id)}"]`)) {
@@ -1376,6 +1973,24 @@ function syncRemovedSections(state) {
1376
1973
  }
1377
1974
  }
1378
1975
  }
1976
+ function syncTemplateHidden(state, pageHasSections) {
1977
+ const hide = state.hideTemplate === true && pageHasSections;
1978
+ for (const el of Array.from(document.querySelectorAll(`[${TEMPLATE_HIDDEN_ATTR}]`))) {
1979
+ if (!hide) {
1980
+ el.style.removeProperty("display");
1981
+ el.removeAttribute(TEMPLATE_HIDDEN_ATTR);
1982
+ }
1983
+ }
1984
+ if (!hide) return;
1985
+ for (const el of Array.from(document.querySelectorAll("[data-ohw-section]"))) {
1986
+ if (el.hasAttribute(CONTAINER_ATTR)) continue;
1987
+ if (CHROME_SECTION_IDS.has(el.getAttribute("data-ohw-section") ?? "")) continue;
1988
+ if (el.parentElement?.closest("[data-ohw-section]")) continue;
1989
+ if (el.hasAttribute(REPLACED_ATTR) || el.hasAttribute(REMOVED_ATTR)) continue;
1990
+ el.style.display = "none";
1991
+ el.setAttribute(TEMPLATE_HIDDEN_ATTR, "");
1992
+ }
1993
+ }
1379
1994
  function syncReplacedOriginals(state) {
1380
1995
  for (const el of document.querySelectorAll(`[${REPLACED_ATTR}]`)) {
1381
1996
  const byId = el.getAttribute(REPLACED_ATTR) ?? "";
@@ -1394,10 +2009,64 @@ function syncReplacedOriginals(state) {
1394
2009
  }
1395
2010
  }
1396
2011
  }
2012
+ var sectionOrderIndex = /* @__PURE__ */ new Map();
2013
+ function setAiSectionOrder(raw, currentPath) {
2014
+ const next = /* @__PURE__ */ new Map();
2015
+ if (raw) {
2016
+ try {
2017
+ const entries = JSON.parse(raw);
2018
+ for (const entry of entries) {
2019
+ if (!entry.pagePath || entry.pagePath === currentPath) next.set(entry.instanceId, entry.order);
2020
+ }
2021
+ } catch {
2022
+ }
2023
+ }
2024
+ sectionOrderIndex = next;
2025
+ }
2026
+ function applyExplicitOrder(entries) {
2027
+ if (sectionOrderIndex.size === 0) return entries;
2028
+ return entries.map((entry, index) => ({ entry, index, order: sectionOrderIndex.get(entry.id) })).sort((a, b) => {
2029
+ if (a.order === void 0 && b.order === void 0) return a.index - b.index;
2030
+ if (a.order === void 0) return 1;
2031
+ if (b.order === void 0) return -1;
2032
+ return a.order - b.order;
2033
+ }).map((item) => item.entry);
2034
+ }
2035
+ function orderByChain(sections) {
2036
+ const ids = new Set(sections.map((entry) => entry.id));
2037
+ const after = /* @__PURE__ */ new Map();
2038
+ const roots = [];
2039
+ for (const entry of sections) {
2040
+ const anchor = entry.replaces ?? entry.beforeSection ?? entry.afterSection ?? null;
2041
+ if (anchor && ids.has(anchor)) {
2042
+ const bucket = after.get(anchor);
2043
+ if (bucket) bucket.push(entry);
2044
+ else after.set(anchor, [entry]);
2045
+ } else {
2046
+ roots.push(entry);
2047
+ }
2048
+ }
2049
+ const out = [];
2050
+ const seen = /* @__PURE__ */ new Set();
2051
+ const visit = (entry) => {
2052
+ if (seen.has(entry.id)) return;
2053
+ seen.add(entry.id);
2054
+ out.push(entry);
2055
+ for (const child of after.get(entry.id) ?? []) visit(child);
2056
+ };
2057
+ for (const root of roots) visit(root);
2058
+ return out.length === sections.length ? out : sections;
2059
+ }
1397
2060
  function applyAiSectionsToDom(state, options) {
1398
2061
  if (typeof document === "undefined") return;
2062
+ const brandOverride = deriveBrandOverride();
1399
2063
  const templateBrand = deriveTemplateBrand();
1400
- const activeIds = new Set(state.sections.map((entry) => entry.id));
2064
+ const templateButtonRadius = deriveTemplateButtonRadius();
2065
+ const brandKey = brandOverride ? JSON.stringify(brandOverride) : "";
2066
+ const pagePath = window.location.pathname;
2067
+ const pageSections = state.sections.filter((entry) => (entry.path ?? "/") === pagePath);
2068
+ const activeIds = new Set(pageSections.map((entry) => entry.id));
2069
+ const ordered = state.hideTemplate === true ? applyExplicitOrder(orderByChain(pageSections)) : pageSections;
1401
2070
  for (const [id, section] of mounted) {
1402
2071
  if (!activeIds.has(id)) {
1403
2072
  section.root.unmount();
@@ -1405,8 +2074,8 @@ function applyAiSectionsToDom(state, options) {
1405
2074
  mounted.delete(id);
1406
2075
  }
1407
2076
  }
1408
- for (const entry of state.sections) {
1409
- const serialized = JSON.stringify(entry);
2077
+ for (const entry of ordered) {
2078
+ const serialized = JSON.stringify(entry) + brandKey;
1410
2079
  const existing = mounted.get(entry.id);
1411
2080
  if (existing && existing.serialized === serialized && existing.container.isConnected) {
1412
2081
  continue;
@@ -1431,7 +2100,8 @@ function applyAiSectionsToDom(state, options) {
1431
2100
  AiTreeRenderer,
1432
2101
  {
1433
2102
  tree: entry.tree,
1434
- brand: entry.brand ?? options?.brand ?? templateBrand ?? void 0,
2103
+ brand: brandOverride ?? entry.brand ?? options?.brand ?? templateBrand ?? void 0,
2104
+ buttonRadius: templateButtonRadius,
1435
2105
  resolveMedia,
1436
2106
  editKeyPrefix: `ai.${entry.id}`
1437
2107
  }
@@ -1440,8 +2110,20 @@ function applyAiSectionsToDom(state, options) {
1440
2110
  });
1441
2111
  mounted.set(entry.id, { root, container, serialized });
1442
2112
  }
2113
+ if (state.hideTemplate === true) {
2114
+ let prev = null;
2115
+ for (const entry of ordered) {
2116
+ const el = mounted.get(entry.id)?.container;
2117
+ if (!el) continue;
2118
+ if (prev && !(prev.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_FOLLOWING)) {
2119
+ prev.insertAdjacentElement("afterend", el);
2120
+ }
2121
+ prev = el;
2122
+ }
2123
+ }
1443
2124
  syncReplacedOriginals(state);
1444
2125
  syncRemovedSections(state);
2126
+ syncTemplateHidden(state, pageSections.length > 0);
1445
2127
  }
1446
2128
 
1447
2129
  // src/useLinkHrefGuardian.ts
@@ -7140,13 +7822,17 @@ function MediaOverlay({
7140
7822
  hover,
7141
7823
  isUploading,
7142
7824
  fadingOut = false,
7825
+ selected = false,
7826
+ hovered = false,
7143
7827
  onFadeOutComplete,
7144
7828
  onReplace,
7829
+ onSelect,
7145
7830
  onVideoSettingsChange
7146
7831
  }) {
7147
7832
  const { rect } = hover;
7148
7833
  const skeletonRef = React8.useRef(null);
7149
7834
  const isVideo = hover.elementType === "video";
7835
+ const showChrome = !selected || hovered;
7150
7836
  const autoplay = hover.videoAutoplay ?? true;
7151
7837
  const muted = hover.videoMuted ?? true;
7152
7838
  const probeRef = React8.useRef(null);
@@ -7193,7 +7879,7 @@ function MediaOverlay({
7193
7879
  }
7194
7880
  );
7195
7881
  }
7196
- const settingsBar = isVideo && !hover.isDragOver ? /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
7882
+ const settingsBar = isVideo && !hover.isDragOver && showChrome ? /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
7197
7883
  "div",
7198
7884
  {
7199
7885
  "data-ohw-bridge": "",
@@ -7263,10 +7949,12 @@ function MediaOverlay({
7263
7949
  // in-document, pointer-events does it natively. The button below opts back in, so
7264
7950
  // Replace still works.
7265
7951
  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)"
7952
+ // Selected: a firm component ring with no wash, so the image reads as chosen rather
7953
+ // than hovered. Hover keeps the existing tinted preview.
7954
+ boxShadow: selected ? "inset 0 0 0 2px var(--color-primary)" : "inset 0 0 0 1.5px var(--color-primary)",
7955
+ background: selected ? "transparent" : "color-mix(in srgb, var(--color-primary) 20%, transparent)"
7268
7956
  },
7269
- onClick: () => onReplace(hover.key),
7957
+ onClick: () => onSelect && !selected ? onSelect(hover.key) : onReplace(hover.key),
7270
7958
  children: [
7271
7959
  /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
7272
7960
  Button,
@@ -7320,7 +8008,7 @@ function MediaOverlay({
7320
8008
  },
7321
8009
  children: [
7322
8010
  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
8011
+ replaceMode === "full" ? isVideo ? "Replace video" : hover.elementType === "bg-image" ? "Replace background" : "Replace image" : null
7324
8012
  ]
7325
8013
  }
7326
8014
  )
@@ -7404,6 +8092,8 @@ function parseSectionsFromRoot(root) {
7404
8092
  const id = el.getAttribute("data-ohw-section") ?? "";
7405
8093
  if (!id || seen.has(id) || LINK_PICKER_EXCLUDED_IDS.has(id)) continue;
7406
8094
  if (el.parentElement?.closest("[data-ohw-section]")) continue;
8095
+ if (el.hasAttribute("data-ohw-ai-template-hidden") || el.hasAttribute("data-ohw-ai-removed") || el.hasAttribute("data-ohw-ai-replaced-by"))
8096
+ continue;
7407
8097
  seen.add(id);
7408
8098
  const label = el.getAttribute("data-ohw-section-label") ?? titleCaseSectionId(id);
7409
8099
  sections.push({ id, label });
@@ -7689,6 +8379,7 @@ function AiSectionOverlay({
7689
8379
  }) {
7690
8380
  const [selectedId, setSelectedId] = (0, import_react8.useState)(null);
7691
8381
  const [reviewId, setReviewId] = (0, import_react8.useState)(null);
8382
+ const [reviewButtonsHidden, setReviewButtonsHidden] = (0, import_react8.useState)(false);
7692
8383
  const reviewIdRef = (0, import_react8.useRef)(null);
7693
8384
  reviewIdRef.current = reviewId;
7694
8385
  const selectedIdRef = (0, import_react8.useRef)(null);
@@ -7750,6 +8441,7 @@ function AiSectionOverlay({
7750
8441
  }
7751
8442
  const found = readRect(sectionId) != null;
7752
8443
  setReviewId(found ? sectionId : null);
8444
+ setReviewButtonsHidden(e.data.hideButtons === true);
7753
8445
  postToParent2({ type: "ow:ai-review-started", sectionId, found });
7754
8446
  if (found) {
7755
8447
  document.querySelector(`[data-ohw-instance="${CSS.escape(sectionId)}"]`)?.scrollIntoView({ behavior: "smooth", block: "nearest" });
@@ -7877,13 +8569,16 @@ function AiSectionOverlay({
7877
8569
  border: `2px solid ${PRIMARY2}`,
7878
8570
  borderRadius: edgeAwareRadius(reviewRect),
7879
8571
  zIndex: 2147483200,
7880
- // The veil itself: swallows clicks so the section stays locked until decided.
8572
+ // The veil itself: swallows clicks so the section stays locked until decided. This
8573
+ // stopPropagation only guards the bubble phase; the bridge's capture-phase click
8574
+ // handlers additionally bail on `[data-ohw-ai-review]`, without which a click on
8575
+ // Accept/Discard resolves to the media beneath and opens the file picker.
7881
8576
  background: "rgba(8, 133, 254, 0.04)",
7882
8577
  pointerEvents: "auto",
7883
8578
  cursor: "default"
7884
8579
  },
7885
8580
  onClick: (e) => e.stopPropagation(),
7886
- children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
8581
+ children: !reviewButtonsHidden && /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
7887
8582
  "div",
7888
8583
  {
7889
8584
  style: {
@@ -10450,8 +11145,13 @@ function referenceBox(slot) {
10450
11145
  const neighbour = row ? Array.from(row.querySelectorAll('[data-ohw-editable="icon"]')).find(
10451
11146
  (el) => el !== slot && !el.hasAttribute("data-ohw-social-icon-placeholder")
10452
11147
  ) : null;
10453
- const source = (neighbour ?? slot).querySelector(GLYPH_SELECTOR);
10454
- const box = source?.getBoundingClientRect() ?? null;
11148
+ if (neighbour) {
11149
+ const box2 = neighbour.querySelector(GLYPH_SELECTOR)?.getBoundingClientRect();
11150
+ if (box2?.width && box2.height) return box2;
11151
+ }
11152
+ const own = slot.getBoundingClientRect();
11153
+ if (own.width && own.height) return own;
11154
+ const box = slot.querySelector(GLYPH_SELECTOR)?.getBoundingClientRect() ?? null;
10455
11155
  return box?.width && box.height ? box : null;
10456
11156
  }
10457
11157
  function iconMarkupSizedFor(slot, markup) {
@@ -13320,6 +14020,7 @@ function useSectionDrag({
13320
14020
  }
13321
14021
  const orderJson = JSON.stringify(entries);
13322
14022
  editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: orderJson };
14023
+ setAiSectionOrder(orderJson, window.location.pathname);
13323
14024
  postToParentRef.current({ type: "ow:change", nodes: [{ key: SECTION_ORDER_KEY, text: orderJson }] });
13324
14025
  applyPersistedOrder(entries);
13325
14026
  clearSectionDragVisuals();
@@ -15439,6 +16140,70 @@ function OhhwellsBridge() {
15439
16140
  const hoveredImageHasTextOverlapRef = (0, import_react17.useRef)(false);
15440
16141
  const dragOverElRef = (0, import_react17.useRef)(null);
15441
16142
  const [mediaHover, setMediaHover] = (0, import_react17.useState)(null);
16143
+ const [selectedMedia, setSelectedMedia] = (0, import_react17.useState)(null);
16144
+ const selectedMediaElRef = (0, import_react17.useRef)(null);
16145
+ const clearMediaSelection = (0, import_react17.useCallback)(() => {
16146
+ const prev = selectedMediaElRef.current;
16147
+ selectedMediaElRef.current = null;
16148
+ setSelectedMedia(null);
16149
+ const sectionEl = prev?.closest("[data-ohw-section]") ?? null;
16150
+ if (sectionEl) {
16151
+ postToParentRef.current({
16152
+ type: "ow:section-selected",
16153
+ sectionId: sectionEl.dataset.ohwSection ?? null,
16154
+ sectionLabel: sectionEl.dataset.ohwSectionLabel ?? titleCaseSectionId(sectionEl.dataset.ohwSection ?? ""),
16155
+ key: null
16156
+ });
16157
+ }
16158
+ }, []);
16159
+ const clearMediaSelectionRef = (0, import_react17.useRef)(clearMediaSelection);
16160
+ clearMediaSelectionRef.current = clearMediaSelection;
16161
+ const selectMediaElement = (0, import_react17.useCallback)((el) => {
16162
+ const r2 = el.getBoundingClientRect();
16163
+ const video = el.dataset.ohwEditable === "video" ? el.querySelector("video") : null;
16164
+ selectedMediaElRef.current = el;
16165
+ setSelectedMedia({
16166
+ key: el.dataset.ohwKey ?? "",
16167
+ rect: { top: r2.top, left: r2.left, width: r2.width, height: r2.height },
16168
+ elementType: el.dataset.ohwEditable ?? "image",
16169
+ hasTextOverlap: false,
16170
+ isDragOver: false,
16171
+ ...video ? { videoAutoplay: video.autoplay, videoMuted: video.muted } : {}
16172
+ });
16173
+ const sectionEl = el.closest("[data-ohw-section]");
16174
+ aiSectionApiRef.current?.selectFromElement(el, { report: false });
16175
+ postToParentRef.current({
16176
+ type: "ow:section-selected",
16177
+ sectionId: sectionEl?.dataset.ohwSection ?? null,
16178
+ sectionLabel: sectionEl ? sectionEl.dataset.ohwSectionLabel ?? titleCaseSectionId(sectionEl.dataset.ohwSection ?? "") : null,
16179
+ key: el.dataset.ohwKey ?? null,
16180
+ // Display name for the pill — the raw key prettifies into fragments ("Img"); the
16181
+ // bridge knows what the node IS, so it names it.
16182
+ keyLabel: el.dataset.ohwEditable === "video" ? "Video" : el.dataset.ohwEditable === "bg-image" ? "Background" : "Image"
16183
+ });
16184
+ }, []);
16185
+ const selectMediaElementRef = (0, import_react17.useRef)(selectMediaElement);
16186
+ selectMediaElementRef.current = selectMediaElement;
16187
+ (0, import_react17.useEffect)(() => {
16188
+ if (!selectedMedia) return;
16189
+ const update = () => {
16190
+ const el = selectedMediaElRef.current;
16191
+ if (!el || !el.isConnected) {
16192
+ clearMediaSelection();
16193
+ return;
16194
+ }
16195
+ const r2 = el.getBoundingClientRect();
16196
+ setSelectedMedia(
16197
+ (prev) => prev ? { ...prev, rect: { top: r2.top, left: r2.left, width: r2.width, height: r2.height } } : prev
16198
+ );
16199
+ };
16200
+ window.addEventListener("scroll", update, true);
16201
+ window.addEventListener("resize", update);
16202
+ return () => {
16203
+ window.removeEventListener("scroll", update, true);
16204
+ window.removeEventListener("resize", update);
16205
+ };
16206
+ }, [selectedMedia !== null]);
15442
16207
  const [carouselHover, setCarouselHover] = (0, import_react17.useState)(null);
15443
16208
  const [uploadingRects, setUploadingRects] = (0, import_react17.useState)({});
15444
16209
  const hoveredGapRef = (0, import_react17.useRef)(null);
@@ -15722,6 +16487,8 @@ function OhhwellsBridge() {
15722
16487
  const addNavAfterAnchorRef = (0, import_react17.useRef)(null);
15723
16488
  const editContentRef = (0, import_react17.useRef)({});
15724
16489
  const aiSectionsRef = (0, import_react17.useRef)("");
16490
+ const brandKitRef = (0, import_react17.useRef)("");
16491
+ const stylesRef = (0, import_react17.useRef)("");
15725
16492
  const pendingDeleteUndoRef = (0, import_react17.useRef)(null);
15726
16493
  const [sitePages, setSitePages] = (0, import_react17.useState)([]);
15727
16494
  const [sectionsByPath, setSectionsByPath] = (0, import_react17.useState)({});
@@ -17042,13 +17809,29 @@ function OhhwellsBridge() {
17042
17809
  }
17043
17810
  const applyContent = (content) => {
17044
17811
  const imageLoads = [];
17812
+ if (typeof content[BRAND_KIT_KEY] === "string") {
17813
+ brandKitRef.current = content[BRAND_KIT_KEY];
17814
+ applyBrandToDom(parseBrandKit(content[BRAND_KIT_KEY]));
17815
+ } else {
17816
+ brandKitRef.current = "";
17817
+ applyBrandToDom(null);
17818
+ }
17045
17819
  if (typeof content[AI_SECTIONS_KEY] === "string") {
17046
17820
  aiSectionsRef.current = content[AI_SECTIONS_KEY];
17821
+ setAiSectionOrder(content[SECTION_ORDER_KEY], window.location.pathname);
17047
17822
  applyAiSectionsToDom(parseAiSectionsState(content[AI_SECTIONS_KEY]));
17048
17823
  }
17824
+ if (typeof content[STYLE_STORE_KEY] === "string") {
17825
+ stylesRef.current = content[STYLE_STORE_KEY];
17826
+ applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
17827
+ }
17828
+ applyBrandChrome(content);
17049
17829
  for (const [key, val] of Object.entries(content)) {
17050
17830
  if (key === "__ohw_sections") continue;
17051
17831
  if (key === AI_SECTIONS_KEY) continue;
17832
+ if (key === BRAND_KIT_KEY) continue;
17833
+ if (key === STYLE_STORE_KEY) continue;
17834
+ if (BRAND_CHROME_KEYS.has(key)) continue;
17052
17835
  if (key === LOGO_PLACEHOLDER_KEY) continue;
17053
17836
  if (LOGO_IMAGE_KEYS.includes(key)) continue;
17054
17837
  if (applyVideoSettingNode(key, val)) continue;
@@ -17236,8 +18019,25 @@ function OhhwellsBridge() {
17236
18019
  initSectionInstancesFromContent(content, window.location.pathname);
17237
18020
  observer?.disconnect();
17238
18021
  try {
18022
+ applyBrandChrome(content);
18023
+ if (typeof content[BRAND_KIT_KEY] === "string") {
18024
+ applyBrandToDom(parseBrandKit(content[BRAND_KIT_KEY]));
18025
+ } else {
18026
+ applyBrandToDom(null);
18027
+ }
18028
+ if (typeof content[AI_SECTIONS_KEY] === "string") {
18029
+ setAiSectionOrder(content[SECTION_ORDER_KEY], window.location.pathname);
18030
+ applyAiSectionsToDom(parseAiSectionsState(content[AI_SECTIONS_KEY]));
18031
+ }
18032
+ if (typeof content[STYLE_STORE_KEY] === "string") {
18033
+ applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
18034
+ }
17239
18035
  for (const [key, val] of Object.entries(content)) {
17240
18036
  if (key === "__ohw_sections") continue;
18037
+ if (key === AI_SECTIONS_KEY) continue;
18038
+ if (key === BRAND_KIT_KEY) continue;
18039
+ if (key === STYLE_STORE_KEY) continue;
18040
+ if (BRAND_CHROME_KEYS.has(key)) continue;
17241
18041
  if (key === LOGO_PLACEHOLDER_KEY) continue;
17242
18042
  if (LOGO_IMAGE_KEYS.includes(key)) continue;
17243
18043
  if (applyVideoSettingNode(key, val)) continue;
@@ -17380,9 +18180,21 @@ function OhhwellsBridge() {
17380
18180
  }, [isEditMode, pathname, subdomain, fetchState, resyncSelectedNavigationItem]);
17381
18181
  (0, import_react17.useEffect)(() => {
17382
18182
  if (!isEditMode) return;
18183
+ let lastPosted = 0;
17383
18184
  const measure = () => {
17384
18185
  const h = document.body.scrollHeight;
17385
- if (h > 50) postToParent2({ type: "ow:height", height: h });
18186
+ if (h > 50 && Math.abs(h - lastPosted) > 1) {
18187
+ lastPosted = h;
18188
+ postToParent2({ type: "ow:height", height: h });
18189
+ }
18190
+ };
18191
+ let raf = null;
18192
+ const schedule = () => {
18193
+ if (raf != null) return;
18194
+ raf = requestAnimationFrame(() => {
18195
+ raf = null;
18196
+ measure();
18197
+ });
17386
18198
  };
17387
18199
  const t1 = setTimeout(measure, 50);
17388
18200
  const t2 = setTimeout(measure, 500);
@@ -17402,6 +18214,7 @@ function OhhwellsBridge() {
17402
18214
  return () => {
17403
18215
  clearTimeout(t1);
17404
18216
  clearTimeout(t2);
18217
+ if (raf != null) cancelAnimationFrame(raf);
17405
18218
  clearResizeTimers();
17406
18219
  window.removeEventListener("resize", handleResize);
17407
18220
  };
@@ -17644,10 +18457,14 @@ function OhhwellsBridge() {
17644
18457
  return;
17645
18458
  }
17646
18459
  const target = e.target;
18460
+ if (target.closest("[data-ohw-ai-review]")) return;
17647
18461
  if (target.closest("[data-ohw-toolbar]")) return;
17648
18462
  if (target.closest("[data-ohw-state-toggle]")) return;
17649
18463
  if (target.closest("[data-ohw-max-badge]")) return;
17650
18464
  if (isInsideLinkEditor(target)) return;
18465
+ if (selectedMediaElRef.current && !selectedMediaElRef.current.contains(target) && !target.closest("[data-ohw-media-overlay], [data-ohw-edit-chrome], [data-ohw-item-interaction]")) {
18466
+ clearMediaSelectionRef.current();
18467
+ }
17651
18468
  if (isInsideFloatingPanel(target)) return;
17652
18469
  if (target.closest("[data-ohw-form-toolbar]")) return;
17653
18470
  if (target.closest(
@@ -17817,8 +18634,11 @@ function OhhwellsBridge() {
17817
18634
  if (isMediaEditable(editable) && !buttonOnMedia) {
17818
18635
  e.preventDefault();
17819
18636
  e.stopPropagation();
17820
- aiSectionApiRef.current?.selectFromElement(editable);
17821
- postToParentRef.current({ type: "ow:image-pick", key: editable.dataset.ohwKey ?? "", elementType: editable.dataset.ohwEditable ?? "image" });
18637
+ if (selectedMediaElRef.current === editable) {
18638
+ postToParentRef.current({ type: "ow:image-pick", key: editable.dataset.ohwKey ?? "", elementType: editable.dataset.ohwEditable ?? "image" });
18639
+ } else {
18640
+ selectMediaElementRef.current(editable);
18641
+ }
17822
18642
  return;
17823
18643
  }
17824
18644
  const socialItem = getSocialItem(editable);
@@ -17959,6 +18779,7 @@ function OhhwellsBridge() {
17959
18779
  };
17960
18780
  const handleDblClick = (e) => {
17961
18781
  const target = e.target;
18782
+ if (target.closest("[data-ohw-ai-review]")) return;
17962
18783
  if (target.closest("[data-ohw-toolbar]")) return;
17963
18784
  if (target.closest("[data-ohw-state-toggle]")) return;
17964
18785
  if (target.closest("[data-ohw-max-badge]")) return;
@@ -18759,7 +19580,9 @@ function OhhwellsBridge() {
18759
19580
  return;
18760
19581
  }
18761
19582
  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);
19583
+ const sections = Array.from(document.querySelectorAll("[data-ohw-section]")).filter(
19584
+ (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
19585
+ ).sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top);
18763
19586
  const ZONE = 20;
18764
19587
  for (let i = 0; i < sections.length; i++) {
18765
19588
  const a = sections[i];
@@ -19091,10 +19914,23 @@ function OhhwellsBridge() {
19091
19914
  if (e.data?.type !== "ow:hydrate") return;
19092
19915
  const content = e.data.content;
19093
19916
  if (!content) return;
19917
+ if (typeof content[BRAND_KIT_KEY] === "string") {
19918
+ brandKitRef.current = content[BRAND_KIT_KEY];
19919
+ applyBrandToDom(parseBrandKit(content[BRAND_KIT_KEY]));
19920
+ } else {
19921
+ brandKitRef.current = "";
19922
+ applyBrandToDom(null);
19923
+ }
19094
19924
  if (typeof content[AI_SECTIONS_KEY] === "string") {
19095
19925
  aiSectionsRef.current = content[AI_SECTIONS_KEY];
19926
+ setAiSectionOrder(content[SECTION_ORDER_KEY], window.location.pathname);
19096
19927
  applyAiSectionsToDom(parseAiSectionsState(content[AI_SECTIONS_KEY]));
19097
19928
  }
19929
+ if (typeof content[STYLE_STORE_KEY] === "string") {
19930
+ stylesRef.current = content[STYLE_STORE_KEY];
19931
+ applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
19932
+ }
19933
+ applyBrandChrome(content);
19098
19934
  let sectionsJson = null;
19099
19935
  for (const [key, val] of Object.entries(content)) {
19100
19936
  if (key === "__ohw_sections") {
@@ -19102,6 +19938,9 @@ function OhhwellsBridge() {
19102
19938
  continue;
19103
19939
  }
19104
19940
  if (key === AI_SECTIONS_KEY) continue;
19941
+ if (key === BRAND_KIT_KEY) continue;
19942
+ if (key === STYLE_STORE_KEY) continue;
19943
+ if (BRAND_CHROME_KEYS.has(key)) continue;
19105
19944
  if (key === LOGO_PLACEHOLDER_KEY) continue;
19106
19945
  if (LOGO_IMAGE_KEYS.includes(key) && !String(val ?? "").trim()) continue;
19107
19946
  if (applyVideoSettingNode(key, val)) continue;
@@ -19117,6 +19956,8 @@ function OhhwellsBridge() {
19117
19956
  if (video && video.src !== val) applyVideoSrc(video, val);
19118
19957
  } else if (el.dataset.ohwEditable === "link") {
19119
19958
  applyLinkHref(el, val);
19959
+ } else if (el.dataset.ohwEditable === "icon") {
19960
+ applyIconMarkup(el, val);
19120
19961
  } else if (isIconMarkupValue(val)) {
19121
19962
  } else {
19122
19963
  el.innerHTML = val;
@@ -19201,12 +20042,21 @@ function OhhwellsBridge() {
19201
20042
  nodes: collectEditableNodes(editContentRef.current)
19202
20043
  });
19203
20044
  };
20045
+ const clearInteractionChrome = () => {
20046
+ deactivateRef.current();
20047
+ deselectRef.current();
20048
+ clearMediaSelectionRef.current();
20049
+ };
19204
20050
  const handleAiApplyTree = (e) => {
19205
20051
  if (e.data?.type !== "ow:ai-apply-tree") return;
19206
20052
  const payload = e.data.payload;
19207
20053
  if (!payload || typeof payload.id !== "string" || !isRenderableTree(payload.tree)) return;
20054
+ clearInteractionChrome();
19208
20055
  const previous = aiSectionsRef.current;
19209
- const nextState = applyTreeToState(parseAiSectionsState(previous), payload);
20056
+ const nextState = applyTreeToState(parseAiSectionsState(previous), {
20057
+ ...payload,
20058
+ path: payload.path ?? window.location.pathname
20059
+ });
19210
20060
  const nextValue = serializeAiSectionsState(nextState);
19211
20061
  aiSectionsRef.current = nextValue;
19212
20062
  applyAiSectionsToDom(nextState);
@@ -19227,6 +20077,7 @@ function OhhwellsBridge() {
19227
20077
  const exists = document.querySelector(`[data-ohw-section="${CSS.escape(sectionId)}"]`);
19228
20078
  if (!exists) return;
19229
20079
  if (isPageFrameSection(exists)) return;
20080
+ clearInteractionChrome();
19230
20081
  const previous = aiSectionsRef.current;
19231
20082
  const nextState = deleteSectionFromState(parseAiSectionsState(previous), sectionId);
19232
20083
  const nextValue = serializeAiSectionsState(nextState);
@@ -19242,14 +20093,45 @@ function OhhwellsBridge() {
19242
20093
  const handleAiSetSections = (e) => {
19243
20094
  if (e.data?.type !== "ow:ai-set-sections") return;
19244
20095
  const value = typeof e.data.value === "string" ? e.data.value : "";
20096
+ clearInteractionChrome();
19245
20097
  aiSectionsRef.current = value;
19246
20098
  applyAiSectionsToDom(parseAiSectionsState(value));
20099
+ applyStylesToDom(parseStyleStore(stylesRef.current));
19247
20100
  const restoredHeight = document.body.scrollHeight;
19248
20101
  if (restoredHeight > 50) postToParentRef.current({ type: "ow:height", height: restoredHeight });
19249
20102
  postToParentRef.current({ type: "ow:change", nodes: [{ key: AI_SECTIONS_KEY, text: value }] });
19250
20103
  postAiSectionsChanged();
19251
20104
  };
19252
20105
  window.addEventListener("message", handleAiSetSections);
20106
+ const handleAiSetBrand = (e) => {
20107
+ if (e.data?.type !== "ow:ai-set-brand") return;
20108
+ const value = typeof e.data.value === "string" ? e.data.value : "";
20109
+ const previous = brandKitRef.current;
20110
+ brandKitRef.current = value;
20111
+ applyBrandToDom(parseBrandKit(value));
20112
+ if (aiSectionsRef.current) applyAiSectionsToDom(parseAiSectionsState(aiSectionsRef.current));
20113
+ applyStylesToDom(parseStyleStore(stylesRef.current));
20114
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: BRAND_KIT_KEY, text: value }] });
20115
+ postToParentRef.current({ type: "ow:ai-brand-applied", previous, value });
20116
+ };
20117
+ window.addEventListener("message", handleAiSetBrand);
20118
+ const handleAiSetStyles = (e) => {
20119
+ if (e.data?.type !== "ow:ai-set-styles") return;
20120
+ const value = typeof e.data.value === "string" ? e.data.value : "";
20121
+ const previous = stylesRef.current;
20122
+ stylesRef.current = value;
20123
+ applyStylesToDom(parseStyleStore(value));
20124
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: STYLE_STORE_KEY, text: value }] });
20125
+ postToParentRef.current({ type: "ow:ai-styles-applied", previous, value });
20126
+ };
20127
+ window.addEventListener("message", handleAiSetStyles);
20128
+ const handleGetBrand = (e) => {
20129
+ if (e.data?.type !== "ow:get-brand") return;
20130
+ const template = deriveTemplateBrand();
20131
+ const value = brandKitRef.current || (template ? JSON.stringify(template) : "");
20132
+ postToParentRef.current({ type: "ow:brand-value", value });
20133
+ };
20134
+ window.addEventListener("message", handleGetBrand);
19253
20135
  const handleMoveSection = (e) => {
19254
20136
  if (e.data?.type !== "ow:move-section") return;
19255
20137
  const instanceId = typeof e.data.instanceId === "string" ? e.data.instanceId : "";
@@ -19259,6 +20141,7 @@ function OhhwellsBridge() {
19259
20141
  if (!entries) return;
19260
20142
  const orderJson = JSON.stringify(entries);
19261
20143
  editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: orderJson };
20144
+ setAiSectionOrder(orderJson, window.location.pathname);
19262
20145
  postToParentRef.current({ type: "ow:change", nodes: [{ key: SECTION_ORDER_KEY, text: orderJson }] });
19263
20146
  window.dispatchEvent(new Event("resize"));
19264
20147
  };
@@ -19322,6 +20205,7 @@ function OhhwellsBridge() {
19322
20205
  }
19323
20206
  deselectRef.current();
19324
20207
  deactivateRef.current();
20208
+ clearMediaSelectionRef.current();
19325
20209
  };
19326
20210
  window.addEventListener("message", handleDeactivate);
19327
20211
  const handleToastAction = (e) => {
@@ -19407,6 +20291,10 @@ function OhhwellsBridge() {
19407
20291
  const handleKeyDown = (e) => {
19408
20292
  if (e.key === "Escape" && document.querySelector("[data-ohw-section-picker]")) return;
19409
20293
  if (e.key === "Escape" && document.querySelector("[data-ohw-more-menu]")) return;
20294
+ if (e.key === "Escape" && selectedMediaElRef.current) {
20295
+ clearMediaSelectionRef.current();
20296
+ return;
20297
+ }
19410
20298
  if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "a" && activeElRef.current) {
19411
20299
  e.preventDefault();
19412
20300
  selectAllTextInEditable(activeElRef.current);
@@ -19566,6 +20454,12 @@ function OhhwellsBridge() {
19566
20454
  if (aiSectionsRef.current) {
19567
20455
  nodes.push({ key: AI_SECTIONS_KEY, type: "sections", text: aiSectionsRef.current });
19568
20456
  }
20457
+ if (stylesRef.current) {
20458
+ nodes.push({ key: STYLE_STORE_KEY, type: "styles", text: stylesRef.current });
20459
+ }
20460
+ if (brandKitRef.current) {
20461
+ nodes.push({ key: BRAND_KIT_KEY, type: "brand", text: brandKitRef.current });
20462
+ }
19569
20463
  document.querySelectorAll('[data-ohw-editable="form"]').forEach((form) => {
19570
20464
  const formKey = formKeyOf(form);
19571
20465
  if (!formKey) return;
@@ -19980,6 +20874,9 @@ function OhhwellsBridge() {
19980
20874
  window.removeEventListener("message", handleAiApplyTree);
19981
20875
  window.removeEventListener("message", handleAiDeleteSection);
19982
20876
  window.removeEventListener("message", handleAiSetSections);
20877
+ window.removeEventListener("message", handleAiSetBrand);
20878
+ window.removeEventListener("message", handleAiSetStyles);
20879
+ window.removeEventListener("message", handleGetBrand);
19983
20880
  window.removeEventListener("message", handleMoveSection);
19984
20881
  window.removeEventListener("message", handlePanelDragging);
19985
20882
  window.removeEventListener("message", handleDeleteSection);
@@ -20190,7 +21087,7 @@ function OhhwellsBridge() {
20190
21087
  postToParent2({
20191
21088
  type: "ow:ready",
20192
21089
  version: "1",
20193
- bridgeVersion: "0.1.77",
21090
+ bridgeVersion: "0.1.79",
20194
21091
  path: pathname,
20195
21092
  nodes: collectEditableNodes(editContentRef.current),
20196
21093
  sections
@@ -20597,11 +21494,22 @@ function OhhwellsBridge() {
20597
21494
  const showEditLink = toolbarShowEditLink;
20598
21495
  const currentSections = sectionsByPath[pathname] ?? [];
20599
21496
  linkPopoverOpenRef.current = linkPopover !== null;
21497
+ const handleMediaSelect = (0, import_react17.useCallback)((key) => {
21498
+ const el = hoveredImageRef.current?.dataset.ohwKey === key ? hoveredImageRef.current : Array.from(document.querySelectorAll(MEDIA_SELECTOR)).find(
21499
+ (m) => (m.dataset.ohwKey ?? "") === key
21500
+ ) ?? null;
21501
+ if (!el) return;
21502
+ selectMediaElementRef.current(el);
21503
+ }, []);
20600
21504
  const handleMediaReplace = (0, import_react17.useCallback)(
20601
21505
  (key) => {
20602
- postToParent2({ type: "ow:image-pick", key, elementType: mediaHover?.elementType ?? "image" });
21506
+ postToParent2({
21507
+ type: "ow:image-pick",
21508
+ key,
21509
+ elementType: mediaHover?.elementType ?? selectedMedia?.elementType ?? "image"
21510
+ });
20603
21511
  },
20604
- [postToParent2, mediaHover?.elementType]
21512
+ [postToParent2, mediaHover?.elementType, selectedMedia?.elementType]
20605
21513
  );
20606
21514
  const handleEditCarousel = (0, import_react17.useCallback)(
20607
21515
  (key) => {
@@ -20673,12 +21581,25 @@ function OhhwellsBridge() {
20673
21581
  },
20674
21582
  `uploading-${key}`
20675
21583
  )),
20676
- mediaHover && !(mediaHover.key in uploadingRects) && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
21584
+ mediaHover && !(mediaHover.key in uploadingRects) && mediaHover.key !== selectedMedia?.key && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
20677
21585
  MediaOverlay,
20678
21586
  {
20679
21587
  hover: mediaHover,
20680
21588
  isUploading: false,
20681
21589
  onReplace: handleMediaReplace,
21590
+ onSelect: handleMediaSelect,
21591
+ onVideoSettingsChange: handleVideoSettingsChange
21592
+ }
21593
+ ),
21594
+ selectedMedia && !(selectedMedia.key in uploadingRects) && !linkPopover && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
21595
+ MediaOverlay,
21596
+ {
21597
+ hover: selectedMedia,
21598
+ selected: true,
21599
+ hovered: mediaHover?.key === selectedMedia.key,
21600
+ isUploading: false,
21601
+ onReplace: handleMediaReplace,
21602
+ onSelect: handleMediaSelect,
20682
21603
  onVideoSettingsChange: handleVideoSettingsChange
20683
21604
  }
20684
21605
  ),