@ohhwells/bridge 0.1.76 → 0.1.77-next.232

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
@@ -46,6 +46,7 @@ __export(index_exports, {
46
46
  DropdownMenuItem: () => DropdownMenuItem,
47
47
  DropdownMenuSeparator: () => DropdownMenuSeparator,
48
48
  DropdownMenuTrigger: () => DropdownMenuTrigger,
49
+ EmptySection: () => EmptySection,
49
50
  ItemActionToolbar: () => ItemActionToolbar,
50
51
  ItemInteractionLayer: () => ItemInteractionLayer,
51
52
  LinkEditorPanel: () => LinkEditorPanel,
@@ -156,7 +157,12 @@ function parseAiSectionsState(raw) {
156
157
  media: entry.media && typeof entry.media === "object" ? entry.media : {}
157
158
  }));
158
159
  const removed = Array.isArray(parsed.removed) ? parsed.removed.filter((id) => typeof id === "string" && id.length > 0) : [];
159
- return { v: 1, sections, ...removed.length ? { removed } : {} };
160
+ return {
161
+ v: 1,
162
+ sections,
163
+ ...removed.length ? { removed } : {},
164
+ ...parsed.hideTemplate === true ? { hideTemplate: true } : {}
165
+ };
160
166
  } catch {
161
167
  return EMPTY_AI_SECTIONS;
162
168
  }
@@ -169,6 +175,7 @@ function applyTreeToState(state, payload) {
169
175
  const entry = {
170
176
  id: payload.id,
171
177
  label: payload.label ?? "Generated section",
178
+ ...typeof payload.path === "string" && payload.path ? { path: payload.path } : {},
172
179
  afterSection: payload.mode === "insert" && !insertBefore ? payload.targetSectionId ?? null : null,
173
180
  ...insertBefore && payload.targetSectionId ? { beforeSection: payload.targetSectionId } : {},
174
181
  ...payload.mode === "replace" && payload.targetSectionId ? { replaces: payload.targetSectionId } : {},
@@ -191,6 +198,317 @@ function deleteSectionFromState(state, sectionId) {
191
198
  return { ...state, v: 1, sections: state.sections, removed: [...removed, sectionId] };
192
199
  }
193
200
 
201
+ // src/lib/brand-chrome.ts
202
+ var BRAND_NAME_KEY = "__ohw_brand_name";
203
+ var BRAND_TITLE_KEY = "__ohw_site_title";
204
+ var BRAND_FAVICON_LETTER_KEY = "__ohw_favicon_letter";
205
+ var BRAND_CHROME_KEYS = /* @__PURE__ */ new Set([
206
+ BRAND_NAME_KEY,
207
+ BRAND_TITLE_KEY,
208
+ BRAND_FAVICON_LETTER_KEY
209
+ ]);
210
+ function upsertMeta(selector, attr, token, value) {
211
+ let el = document.head.querySelector(selector);
212
+ if (!el) {
213
+ el = document.createElement("meta");
214
+ el.setAttribute(attr, token);
215
+ document.head.appendChild(el);
216
+ }
217
+ if (el.getAttribute("content") !== value) el.setAttribute("content", value);
218
+ }
219
+ function escapeXml(value) {
220
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
221
+ }
222
+ function applyLetterFavicon(letter) {
223
+ 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>`;
224
+ const href = `data:image/svg+xml,${encodeURIComponent(svg)}`;
225
+ let link = document.head.querySelector('link[rel="icon"]');
226
+ if (!link) {
227
+ link = document.createElement("link");
228
+ link.rel = "icon";
229
+ document.head.appendChild(link);
230
+ }
231
+ link.type = "image/svg+xml";
232
+ if (link.href !== href) link.href = href;
233
+ }
234
+ function applyBrandChrome(content) {
235
+ const name = content[BRAND_NAME_KEY];
236
+ if (typeof name === "string" && name.length > 0) {
237
+ document.querySelectorAll("[data-ohw-wordmark]").forEach((el) => {
238
+ if (el.textContent !== name) el.textContent = name;
239
+ if (el.getAttribute("title") !== name) el.setAttribute("title", name);
240
+ });
241
+ }
242
+ const title = content[BRAND_TITLE_KEY];
243
+ if (typeof title === "string" && title.length > 0) {
244
+ if (document.title !== title) document.title = title;
245
+ upsertMeta('meta[property="og:title"]', "property", "og:title", title);
246
+ upsertMeta('meta[name="twitter:title"]', "name", "twitter:title", title);
247
+ }
248
+ const letter = content[BRAND_FAVICON_LETTER_KEY];
249
+ if (typeof letter === "string" && letter.length > 0) {
250
+ applyLetterFavicon(letter);
251
+ }
252
+ }
253
+
254
+ // src/lib/brand-kit.ts
255
+ var BRAND_KIT_KEY = "__ohw_brand";
256
+ var BRAND_VAR_PREFIX = "--ohw-brand-";
257
+ var BRAND_VAR_NAMES = ["primary", "accent", "light", "dark", "surface", "border", "muted"].map(
258
+ (role) => `${BRAND_VAR_PREFIX}${role}`
259
+ );
260
+ var FONT_VARS = {
261
+ heading: ["--font-heading", "--font-display", "--brand-font-heading"],
262
+ body: ["--font-body", "--brand-font-body"]
263
+ };
264
+ var BRAND_FONT_LINK_ID = "ohw-brand-fonts";
265
+ function brandColorVars(kit) {
266
+ const { dark, primary, accent, light } = kit.palette;
267
+ const mix = (a, aPct, b) => `color-mix(in srgb, ${a} ${aPct}%, ${b})`;
268
+ return {
269
+ [`${BRAND_VAR_PREFIX}primary`]: primary,
270
+ [`${BRAND_VAR_PREFIX}accent`]: accent,
271
+ [`${BRAND_VAR_PREFIX}light`]: light,
272
+ [`${BRAND_VAR_PREFIX}dark`]: dark,
273
+ [`${BRAND_VAR_PREFIX}surface`]: mix(light, 95, dark),
274
+ [`${BRAND_VAR_PREFIX}border`]: mix(light, 85, dark),
275
+ [`${BRAND_VAR_PREFIX}muted`]: mix(dark, 62, light)
276
+ };
277
+ }
278
+ function parseBrandKit(raw) {
279
+ if (!raw) return null;
280
+ try {
281
+ const parsed = JSON.parse(raw);
282
+ const p = parsed?.palette;
283
+ const f = parsed?.fonts;
284
+ 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") {
285
+ return null;
286
+ }
287
+ return {
288
+ palette: { dark: p.dark, primary: p.primary, accent: p.accent, light: p.light },
289
+ fonts: { heading: f.heading, body: f.body }
290
+ };
291
+ } catch {
292
+ return null;
293
+ }
294
+ }
295
+ function familyOf(stack) {
296
+ const first = stack.split(",")[0]?.trim() ?? "";
297
+ return first.replace(/^['"]|['"]$/g, "");
298
+ }
299
+ function loadBrandFonts(families) {
300
+ const unique = [...new Set(families.map((f) => f.trim()).filter(Boolean))];
301
+ if (unique.length === 0) return;
302
+ const spec = unique.map((f) => `${f.replace(/ /g, "+")}:400,500,600,700`).join("|");
303
+ const href = `https://fonts.googleapis.com/css?family=${spec}&display=swap`;
304
+ let link = document.getElementById(BRAND_FONT_LINK_ID);
305
+ if (!link) {
306
+ link = document.createElement("link");
307
+ link.id = BRAND_FONT_LINK_ID;
308
+ link.rel = "stylesheet";
309
+ document.head.appendChild(link);
310
+ }
311
+ if (link.href !== href) link.href = href;
312
+ }
313
+ function applyBrandToDom(kit) {
314
+ const root = document.documentElement;
315
+ if (!kit) {
316
+ for (const name of BRAND_VAR_NAMES) root.style.removeProperty(name);
317
+ for (const name of [...FONT_VARS.heading, ...FONT_VARS.body]) root.style.removeProperty(name);
318
+ document.getElementById(BRAND_FONT_LINK_ID)?.remove();
319
+ return;
320
+ }
321
+ for (const [name, value] of Object.entries(brandColorVars(kit))) root.style.setProperty(name, value);
322
+ for (const name of FONT_VARS.heading) root.style.setProperty(name, kit.fonts.heading);
323
+ for (const name of FONT_VARS.body) root.style.setProperty(name, kit.fonts.body);
324
+ loadBrandFonts([familyOf(kit.fonts.heading), familyOf(kit.fonts.body)]);
325
+ }
326
+
327
+ // src/lib/section-styles.ts
328
+ var STYLE_STORE_KEY = "__ohw_styles";
329
+ var STYLE_SHEET_ID = "ohw-section-styles";
330
+ function parseStyleStore(raw) {
331
+ if (!raw) return null;
332
+ try {
333
+ const parsed = JSON.parse(raw);
334
+ if (parsed?.v !== 1) return null;
335
+ return {
336
+ v: 1,
337
+ sections: typeof parsed.sections === "object" && parsed.sections ? parsed.sections : {},
338
+ nodes: typeof parsed.nodes === "object" && parsed.nodes ? parsed.nodes : {}
339
+ };
340
+ } catch {
341
+ return null;
342
+ }
343
+ }
344
+ var BG_VALUES = {
345
+ surface: "color-mix(in srgb, var(--ohw-brand-light, var(--color-light, #ECECEB)) 94%, var(--ohw-brand-dark, var(--color-dark, #0C0A09)))",
346
+ accent: "var(--ohw-brand-primary, var(--color-primary, #0078E5))",
347
+ "accent-soft": "color-mix(in srgb, var(--ohw-brand-primary, var(--color-primary, #0078E5)) 12%, var(--ohw-brand-light, var(--color-light, #FFFFFF)))"
348
+ };
349
+ var HEADLINE_SIZES = { md: 24, lg: 32, xl: 40, display: 56 };
350
+ function styleSheetCss() {
351
+ const rules = [];
352
+ for (const [tone, value] of Object.entries(BG_VALUES)) {
353
+ rules.push(`[data-ohw-style-bg="${tone}"] { background: ${value} !important; }`);
354
+ }
355
+ rules.push(
356
+ `[data-ohw-style-bg="accent"] { color: var(--ohw-brand-light, var(--color-light, #FFFFFF)) !important; }`
357
+ );
358
+ rules.push(
359
+ `[data-ohw-style-distribution="space-between"] { display: flex !important; flex-direction: column; justify-content: space-between; }`,
360
+ `[data-ohw-style-distribution="center"] { display: flex !important; flex-direction: column; justify-content: center; }`
361
+ );
362
+ for (const [scale, size] of Object.entries(HEADLINE_SIZES)) {
363
+ rules.push(
364
+ `[data-ohw-style-headline="${scale}"] :is(h1, h2, h3) { font-size: ${size}px !important; line-height: 1.15 !important; }`
365
+ );
366
+ }
367
+ rules.push(
368
+ `[data-ohw-style-aspect="fill-height"] img { height: 100% !important; aspect-ratio: auto !important; object-fit: cover; }`
369
+ );
370
+ for (const aspect of ["1:1", "4:5", "3:4", "3:2", "16:9", "2:1", "3:1"]) {
371
+ rules.push(
372
+ `[data-ohw-style-aspect="${aspect.replace(":", "-")}"] img { aspect-ratio: ${aspect.replace(":", " / ")} !important; height: auto !important; object-fit: cover; }`
373
+ );
374
+ }
375
+ const pad = { tight: 40, balanced: 64, airy: 96 };
376
+ for (const [spacing, px] of Object.entries(pad)) {
377
+ rules.push(
378
+ `[data-ohw-style-spacing="${spacing}"] { padding-top: ${px}px !important; padding-bottom: ${px}px !important; }`
379
+ );
380
+ }
381
+ return rules.join("\n");
382
+ }
383
+ var STYLE_FONT_LINK_ID = "ohw-style-fonts";
384
+ function loadStyleFonts(families) {
385
+ const unique = [...new Set(families.map((f) => f.trim()).filter(Boolean))];
386
+ const existing = document.getElementById(STYLE_FONT_LINK_ID);
387
+ if (unique.length === 0) {
388
+ existing?.remove();
389
+ return;
390
+ }
391
+ const spec = unique.map((f) => `${f.replace(/ /g, "+")}:400,500,600,700`).join("|");
392
+ const href = `https://fonts.googleapis.com/css?family=${spec}&display=swap`;
393
+ let link = existing;
394
+ if (!link) {
395
+ link = document.createElement("link");
396
+ link.id = STYLE_FONT_LINK_ID;
397
+ link.rel = "stylesheet";
398
+ document.head.appendChild(link);
399
+ }
400
+ if (link.href !== href) link.href = href;
401
+ }
402
+ var SECTION_ATTRS = {
403
+ sectionBackground: "data-ohw-style-bg",
404
+ textDistribution: "data-ohw-style-distribution",
405
+ headlineScale: "data-ohw-style-headline",
406
+ imageAspect: "data-ohw-style-aspect",
407
+ spacing: "data-ohw-style-spacing"
408
+ };
409
+ var NODE_WROTE_ATTR = "data-ohw-style-node";
410
+ var NODE_PROPS = ["color", "font-family", "font-size", "background"];
411
+ function saveInline(el, prop) {
412
+ const attr = `data-ohw-style-prev-${prop}`;
413
+ if (!el.hasAttribute(attr)) el.setAttribute(attr, el.style.getPropertyValue(prop));
414
+ }
415
+ function restoreInline(el, prop) {
416
+ const attr = `data-ohw-style-prev-${prop}`;
417
+ if (!el.hasAttribute(attr)) return;
418
+ const prev = el.getAttribute(attr) ?? "";
419
+ if (prev) el.style.setProperty(prop, prev);
420
+ else el.style.removeProperty(prop);
421
+ el.removeAttribute(attr);
422
+ }
423
+ function ensureStyleSheet() {
424
+ let el = document.getElementById(STYLE_SHEET_ID);
425
+ if (!el) {
426
+ el = document.createElement("style");
427
+ el.id = STYLE_SHEET_ID;
428
+ document.head.appendChild(el);
429
+ }
430
+ const css = styleSheetCss();
431
+ if (el.textContent !== css) el.textContent = css;
432
+ }
433
+ function clearSectionAttrs(root) {
434
+ for (const attr of Object.values(SECTION_ATTRS)) {
435
+ for (const el of Array.from(root.querySelectorAll(`[${attr}]`))) el.removeAttribute(attr);
436
+ }
437
+ for (const el of Array.from(root.querySelectorAll("[data-ohw-style-bgcolor]"))) {
438
+ restoreInline(el, "background");
439
+ el.removeAttribute("data-ohw-style-bgcolor");
440
+ }
441
+ }
442
+ function clearNodeProps(root) {
443
+ for (const el of Array.from(root.querySelectorAll(`[${NODE_WROTE_ATTR}]`))) {
444
+ const h = el;
445
+ for (const prop of NODE_PROPS) restoreInline(h, prop);
446
+ h.removeAttribute(NODE_WROTE_ATTR);
447
+ }
448
+ }
449
+ function buttonSurfaceOf(el) {
450
+ return el.closest("a, button") ?? el;
451
+ }
452
+ function applyStylesToDom(store) {
453
+ ensureStyleSheet();
454
+ clearSectionAttrs(document);
455
+ clearNodeProps(document);
456
+ loadStyleFonts(
457
+ store ? Object.values(store.nodes).flatMap((n) => n.fontFamily ? [n.fontFamily] : []) : []
458
+ );
459
+ if (!store) return;
460
+ for (const [sectionId, override] of Object.entries(store.sections)) {
461
+ const sections = document.querySelectorAll(
462
+ `[data-ohw-section="${CSS.escape(sectionId)}"]`
463
+ );
464
+ for (const section of Array.from(sections)) {
465
+ for (const [prop, attr] of Object.entries(SECTION_ATTRS)) {
466
+ const value = override[prop];
467
+ if (value === void 0) continue;
468
+ if (prop === "sectionBackground" && override.sectionBackgroundColor !== void 0) continue;
469
+ section.setAttribute(attr, String(value).replace(":", "-"));
470
+ }
471
+ if (override.sectionBackgroundColor !== void 0) {
472
+ saveInline(section, "background");
473
+ section.style.setProperty("background", override.sectionBackgroundColor, "important");
474
+ section.setAttribute("data-ohw-style-bgcolor", "");
475
+ }
476
+ }
477
+ }
478
+ for (const [key, override] of Object.entries(store.nodes)) {
479
+ const nodes = document.querySelectorAll(`[data-ohw-key="${CSS.escape(key)}"]`);
480
+ for (const el of Array.from(nodes)) {
481
+ if (override.color !== void 0) {
482
+ saveInline(el, "color");
483
+ el.style.setProperty("color", override.color, "important");
484
+ el.setAttribute(NODE_WROTE_ATTR, "");
485
+ }
486
+ if (override.fontFamily !== void 0) {
487
+ saveInline(el, "font-family");
488
+ el.style.setProperty("font-family", `'${override.fontFamily}'`, "important");
489
+ el.setAttribute(NODE_WROTE_ATTR, "");
490
+ }
491
+ if (override.fontSize !== void 0) {
492
+ saveInline(el, "font-size");
493
+ el.style.setProperty("font-size", `${override.fontSize}px`, "important");
494
+ el.setAttribute(NODE_WROTE_ATTR, "");
495
+ }
496
+ if (override.buttonBackground !== void 0 || override.buttonText !== void 0) {
497
+ const surface = buttonSurfaceOf(el);
498
+ if (override.buttonBackground !== void 0) {
499
+ saveInline(surface, "background");
500
+ surface.style.setProperty("background", override.buttonBackground, "important");
501
+ }
502
+ if (override.buttonText !== void 0) {
503
+ saveInline(surface, "color");
504
+ surface.style.setProperty("color", override.buttonText, "important");
505
+ }
506
+ surface.setAttribute(NODE_WROTE_ATTR, "");
507
+ }
508
+ }
509
+ }
510
+ }
511
+
194
512
  // src/ui/ai-tree/aiSectionsManager.tsx
195
513
  var import_react_dom = require("react-dom");
196
514
  var import_client = require("react-dom/client");
@@ -205,7 +523,8 @@ function lucideByName(name) {
205
523
  }
206
524
  var typeStyle = (spec, font) => ({
207
525
  fontFamily: font,
208
- fontSize: spec.size,
526
+ // Headings shrink with the viewport (reaching full size around ~900px wide); body copy stays put.
527
+ 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
528
  lineHeight: spec.line,
210
529
  fontWeight: spec.weight
211
530
  });
@@ -214,12 +533,58 @@ var featureLines = (value) => value.split(/\n|<br\s*\/?>/i).map((line) => line.t
214
533
  var CHECK_MASK = `data:image/svg+xml,${encodeURIComponent(
215
534
  '<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
535
  )}`;
536
+ var AI_MOBILE_CSS = [
537
+ "@media (max-width: 768px){",
538
+ "[data-ai-section]{overflow-x:hidden}",
539
+ "[data-ai-container]{padding:0 20px !important}",
540
+ "[data-ai-row]{display:flex !important;flex-direction:column !important;align-items:stretch !important}",
541
+ "[data-ai-cell]{width:100%;min-width:0}",
542
+ "[data-ai-grid]{grid-template-columns:1fr !important}",
543
+ // Group containers flatten to a column on phones; span placements come along for free.
544
+ "[data-ai-group]{display:flex !important;flex-direction:column !important}",
545
+ "[data-ai-group] > *{grid-column:auto !important}",
546
+ "[data-ai-section] img{max-width:100%}",
547
+ "}",
548
+ "@media (min-width: 769px) and (max-width: 1024px){",
549
+ "[data-ai-grid]{grid-template-columns:repeat(2, 1fr) !important}",
550
+ "}"
551
+ ].join("");
217
552
  var FEATURE_LINE_CSS = [
218
553
  "[data-ai-features]>div{position:relative;padding-left:40px;min-height:24px}",
219
554
  '[data-ai-features]>div::before{content:"";position:absolute;left:0;top:1px;width:24px;height:24px;',
220
555
  `background-color:currentColor;-webkit-mask:url("${CHECK_MASK}") no-repeat 0 0/24px 24px;`,
221
556
  `mask:url("${CHECK_MASK}") no-repeat 0 0/24px 24px}`
222
557
  ].join("");
558
+ function hexLuminance(color) {
559
+ const m = /^#([0-9a-f]{6})$/i.exec(color.trim());
560
+ if (!m) return null;
561
+ const [r2, g, b] = [0, 2, 4].map((i) => {
562
+ const c = parseInt(m[1].slice(i, i + 2), 16) / 255;
563
+ return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
564
+ });
565
+ return 0.2126 * r2 + 0.7152 * g + 0.0722 * b;
566
+ }
567
+ function hexContrast(a, b) {
568
+ const la = hexLuminance(a);
569
+ const lb = hexLuminance(b);
570
+ if (la === null || lb === null) return null;
571
+ const [hi, lo] = la > lb ? [la, lb] : [lb, la];
572
+ return (hi + 0.05) / (lo + 0.05);
573
+ }
574
+ function accentBandContext(brand) {
575
+ const p = brand.palette;
576
+ const lightWins = (hexContrast(p.light, p.primary) ?? 99) >= (hexContrast(p.dark, p.primary) ?? 0);
577
+ if (lightWins) {
578
+ return {
579
+ brand: { ...brand, palette: { dark: p.light, primary: p.light, accent: p.light, light: p.primary } },
580
+ buttonLabel: p.primary
581
+ };
582
+ }
583
+ return {
584
+ brand: { ...brand, palette: { dark: p.dark, primary: p.dark, accent: p.dark, light: p.light } },
585
+ buttonLabel: p.light
586
+ };
587
+ }
223
588
  function textAttrs(ctx, path) {
224
589
  return ctx.keyFor ? { "data-ohw-key": ctx.keyFor(path), "data-ohw-editable": "text" } : {};
225
590
  }
@@ -231,7 +596,12 @@ var AI_RESPONSIVE_CSS = [
231
596
  "@media (max-width: 640px) {",
232
597
  " [data-ai-responsive] [data-ai-grid] { grid-template-columns: 1fr !important; }",
233
598
  " [data-ai-responsive] [data-ai-columns] > * { grid-column: 1 / -1 !important; }",
599
+ // Group containers flatten to a column on phones; span placements come along for free.
600
+ " [data-ai-responsive] [data-ai-group] { display: flex !important; flex-direction: column !important; }",
601
+ " [data-ai-responsive] [data-ai-group] > * { grid-column: auto !important; }",
234
602
  " [data-ai-responsive] [data-ai-section-inner] { padding-left: 20px !important; padding-right: 20px !important; }",
603
+ " [data-ai-responsive] { overflow-x: hidden; }",
604
+ " [data-ai-responsive] img { max-width: 100%; }",
235
605
  "}"
236
606
  ].join("\n");
237
607
  var TRANSPARENT_PX = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
@@ -309,7 +679,7 @@ function ButtonEl({
309
679
  }) {
310
680
  const secondary = slots.variant === "secondary";
311
681
  const href = str(slots.href);
312
- const linkAttrs = ctx.keyFor && path ? { "data-ohw-href-key": ctx.keyFor(`${path}.href`) } : {};
682
+ const linkAttrs = ctx.keyFor && path ? { "data-ohw-href-key": ctx.keyFor(`${path}.href`), "data-ohw-role": "button" } : {};
313
683
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
314
684
  "a",
315
685
  {
@@ -325,7 +695,7 @@ function ButtonEl({
325
695
  textDecoration: "none",
326
696
  cursor: "pointer",
327
697
  ...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 }
698
+ ...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
699
  },
330
700
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { ...path ? textAttrs(ctx, `${path}.label`) : {}, children: str(slots.label) })
331
701
  }
@@ -831,7 +1201,24 @@ function CardBlock({ node, ctx, path }) {
831
1201
  minWidth: 0
832
1202
  },
833
1203
  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)(
1204
+ media && (horizontal ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1205
+ "div",
1206
+ {
1207
+ style: (
1208
+ // An icon hugs its glyph — flex:1 gave a 48px icon half the card and pushed the
1209
+ // text to the far side. Photos keep the half-and-half split. The inset has no
1210
+ // inner padding (the photo split absorbed that), so the icon carries its own gap.
1211
+ /^(lucide|simple):/.test(mediaRef) ? {
1212
+ flexShrink: 0,
1213
+ display: "flex",
1214
+ alignItems: "center",
1215
+ padding: mediaInset,
1216
+ [mediaPosition === "right" ? "marginLeft" : "marginRight"]: 20
1217
+ } : { flex: 1, minWidth: 0, display: "flex", alignItems: "center", padding: mediaInset }
1218
+ ),
1219
+ children: media
1220
+ }
1221
+ ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
835
1222
  "div",
836
1223
  {
837
1224
  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 +1309,44 @@ function AccordionBlock({ node, ctx, path }) {
922
1309
  ) })
923
1310
  ] }, i)) });
924
1311
  }
1312
+ function useIsMobile() {
1313
+ const [mobile, setMobile] = import_react.default.useState(
1314
+ () => typeof window !== "undefined" && window.matchMedia("(max-width: 768px)").matches
1315
+ );
1316
+ import_react.default.useEffect(() => {
1317
+ const mq = window.matchMedia("(max-width: 768px)");
1318
+ const update = () => setMobile(mq.matches);
1319
+ update();
1320
+ mq.addEventListener("change", update);
1321
+ return () => mq.removeEventListener("change", update);
1322
+ }, []);
1323
+ return mobile;
1324
+ }
925
1325
  function Carousel({ items, itemsPerRow, ctx }) {
1326
+ const isMobile = useIsMobile();
1327
+ const perPage = isMobile ? 1 : itemsPerRow;
1328
+ const pages = Math.max(1, Math.ceil(items.length / perPage));
926
1329
  const [page, setPage] = import_react.default.useState(0);
927
- const pages = Math.max(1, Math.ceil(items.length / itemsPerRow));
928
1330
  const current = Math.min(page, pages - 1);
1331
+ if (pages <= 1) {
1332
+ const cols = Math.max(1, Math.min(items.length, itemsPerRow));
1333
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1334
+ "div",
1335
+ {
1336
+ "data-ai-grid": String(cols),
1337
+ style: {
1338
+ display: "grid",
1339
+ gridTemplateColumns: `repeat(${cols}, 1fr)`,
1340
+ gap: AI_TREE_TOKENS.spacing8,
1341
+ alignItems: "start"
1342
+ },
1343
+ children: items
1344
+ }
1345
+ );
1346
+ }
929
1347
  const pageGroups = Array.from(
930
1348
  { length: pages },
931
- (_, p) => items.slice(p * itemsPerRow, (p + 1) * itemsPerRow)
1349
+ (_, p) => items.slice(p * perPage, (p + 1) * perPage)
932
1350
  );
933
1351
  const chrome = (enabled) => ({
934
1352
  border: `1px solid ${ctx.brand.palette.dark}`,
@@ -953,55 +1371,69 @@ function Carousel({ items, itemsPerRow, ctx }) {
953
1371
  cursor: "pointer",
954
1372
  padding: 0
955
1373
  });
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)(
1374
+ 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)(
1375
+ "div",
1376
+ {
1377
+ style: {
1378
+ display: "flex",
1379
+ transform: `translateX(-${current * 100}%)`,
1380
+ transition: "transform 0.4s ease"
1381
+ },
1382
+ children: pageGroups.map((group, p) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
969
1383
  "div",
970
1384
  {
1385
+ "data-ai-grid": String(perPage),
971
1386
  style: {
972
- display: "flex",
973
- transform: `translateX(-${current * 100}%)`,
974
- transition: "transform 0.4s ease"
1387
+ flex: "0 0 100%",
1388
+ display: "grid",
1389
+ gridTemplateColumns: `repeat(${perPage}, 1fr)`,
1390
+ gap: AI_TREE_TOKENS.spacing8,
1391
+ alignItems: "start"
975
1392
  },
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
- )
1393
+ children: group
1394
+ },
1395
+ p
1396
+ ))
1397
+ }
1398
+ ) });
1399
+ const prevBtn = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1400
+ "button",
1401
+ {
1402
+ type: "button",
1403
+ "aria-label": "Previous",
1404
+ onClick: () => setPage((p) => Math.max(0, p - 1)),
1405
+ style: chrome(current > 0),
1406
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.ArrowLeft, { size: 24, strokeWidth: 1.5, "aria-hidden": true })
1407
+ }
1408
+ );
1409
+ const nextBtn = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1410
+ "button",
1411
+ {
1412
+ type: "button",
1413
+ "aria-label": "Next",
1414
+ onClick: () => setPage((p) => Math.min(pages - 1, p + 1)),
1415
+ style: chrome(current < pages - 1),
1416
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.ArrowRight, { size: 24, strokeWidth: 1.5, "aria-hidden": true })
1417
+ }
1418
+ );
1419
+ 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)) });
1420
+ if (isMobile) {
1421
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { "data-ai-carousel": "", style: { display: "flex", flexDirection: "column", gap: AI_TREE_TOKENS.spacing6, alignItems: "center" }, children: [
1422
+ viewport,
1423
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: AI_TREE_TOKENS.spacing6, justifyContent: "center" }, children: [
1424
+ prevBtn,
1425
+ nextBtn
1426
+ ] }),
1427
+ dots
1428
+ ] });
1429
+ }
1430
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { "data-ai-carousel": "", style: { display: "flex", flexDirection: "column", gap: AI_TREE_TOKENS.spacing8, alignItems: "center" }, children: [
1431
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: AI_TREE_TOKENS.spacing6, width: "100%" }, children: [
1432
+ prevBtn,
1433
+ viewport,
1434
+ nextBtn
1003
1435
  ] }),
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)) })
1436
+ dots
1005
1437
  ] });
1006
1438
  }
1007
1439
  function CollectionBlock({ node, ctx, path }) {
@@ -1095,6 +1527,49 @@ function renderNode(node, ctx, path) {
1095
1527
  switch (node.type) {
1096
1528
  case "text":
1097
1529
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TextBlock, { slots, ctx, path });
1530
+ // Layout container: arranges child blocks, contributes no content of its own. `grid` is a
1531
+ // nested 12-column grid the children span (a collage is 3–5 media on it, bottom-aligned so
1532
+ // mixed aspects read as a composition); `split` is exactly two children at a ratio; `stack`
1533
+ // is a column. Children render through this same dispatcher, so edit markers, media
1534
+ // resolution, and copy paths all work unchanged inside a group.
1535
+ case "group": {
1536
+ const layout = str(slots.layout);
1537
+ const gap = slots.spacing === "tight" ? AI_TREE_TOKENS.spacing3 : slots.spacing === "airy" ? AI_TREE_TOKENS.spacing8 : AI_TREE_TOKENS.spacing6;
1538
+ const kids = (node.children ?? []).map((child, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1539
+ "div",
1540
+ {
1541
+ style: layout === "grid" ? {
1542
+ gridColumn: `span ${typeof child.span === "number" ? Math.min(12, Math.max(1, child.span)) : 12}`,
1543
+ minWidth: 0
1544
+ } : { minWidth: 0 },
1545
+ children: renderNode(child, ctx, `${path}.c${i}`)
1546
+ },
1547
+ i
1548
+ ));
1549
+ if (layout === "grid") {
1550
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1551
+ "div",
1552
+ {
1553
+ "data-ai-group": "grid",
1554
+ style: { display: "grid", gridTemplateColumns: "repeat(12, 1fr)", gap, alignItems: "end" },
1555
+ children: kids
1556
+ }
1557
+ );
1558
+ }
1559
+ if (layout === "split") {
1560
+ const ratio = str(slots.ratio);
1561
+ const cols = ratio === "3:5" ? "3fr 5fr" : ratio === "5:3" ? "5fr 3fr" : "1fr 1fr";
1562
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1563
+ "div",
1564
+ {
1565
+ "data-ai-group": "split",
1566
+ style: { display: "grid", gridTemplateColumns: cols, gap, alignItems: "center" },
1567
+ children: kids
1568
+ }
1569
+ );
1570
+ }
1571
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-ai-group": "stack", style: { display: "flex", flexDirection: "column", gap }, children: kids });
1572
+ }
1098
1573
  case "button":
1099
1574
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ButtonEl, { slots, ctx, path });
1100
1575
  case "button-row":
@@ -1175,33 +1650,111 @@ function renderNode(node, ctx, path) {
1175
1650
  }
1176
1651
  );
1177
1652
  }
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
- }) });
1653
+ case "form": {
1654
+ const formAttrs = ctx.keyFor ? {
1655
+ "data-ohw-editable": "form",
1656
+ "data-ohw-key": ctx.keyFor(`${path}.form`),
1657
+ "data-ohw-success-text": "Thanks \u2014 we'll be in touch shortly."
1658
+ } : {};
1659
+ const fieldStyle = {
1660
+ width: "100%",
1661
+ boxSizing: "border-box",
1662
+ border: `1px solid color-mix(in srgb, ${ctx.brand.palette.dark} 45%, #ffffff)`,
1663
+ borderRadius: 0,
1664
+ padding: 12,
1665
+ background: "#fff",
1666
+ color: ctx.brand.palette.dark,
1667
+ outline: "none",
1668
+ ...typeStyle(AI_TREE_TOKENS.type.bodyM, ctx.brand.fonts.body)
1669
+ };
1670
+ const labelStyle = {
1671
+ ...typeStyle(AI_TREE_TOKENS.type.bodyMBold, ctx.brand.fonts.body),
1672
+ color: ctx.brand.palette.dark,
1673
+ textAlign: "left",
1674
+ width: "100%"
1675
+ };
1676
+ const centered = ctx.sectionAlignment === "center";
1677
+ const submitAlign = centered ? "center" : "flex-start";
1678
+ const children = node.children ?? [];
1679
+ return (
1680
+ // 32px between the field group and the submit. In a stacked (centered) section the form is
1681
+ // capped at 780px and centered — the section's 12-col grid would otherwise leave it hugging
1682
+ // the left edge; a split section lets it fill its own column.
1683
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1684
+ "form",
1685
+ {
1686
+ ...formAttrs,
1687
+ "data-ai-form": "",
1688
+ style: {
1689
+ display: "flex",
1690
+ flexDirection: "column",
1691
+ gap: 32,
1692
+ width: "100%",
1693
+ ...centered ? { maxWidth: 780, marginLeft: "auto", marginRight: "auto" } : {}
1694
+ },
1695
+ children: [
1696
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { display: "flex", flexDirection: "column", gap: 24, width: "100%", alignItems: "flex-start" }, children: children.map((child, i) => {
1697
+ if (child.type !== "input") return null;
1698
+ const cs = child.slots ?? {};
1699
+ const kind = str(cs.kind);
1700
+ const label = str(cs.label);
1701
+ const placeholder = str(cs.placeholder);
1702
+ const required = cs.required === true;
1703
+ const name = label.toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-+|-+$/gu, "") || `field-${i}`;
1704
+ const isTextarea = kind === "textarea";
1705
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: 8, width: "100%" }, children: [
1706
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { ...textAttrs(ctx, `${path}.c${i}.label`), style: labelStyle, children: label }),
1707
+ isTextarea ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1708
+ "textarea",
1709
+ {
1710
+ name,
1711
+ placeholder,
1712
+ required,
1713
+ style: { ...fieldStyle, height: 180, resize: "vertical" }
1714
+ }
1715
+ ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1716
+ "input",
1717
+ {
1718
+ name,
1719
+ type: kind === "email" ? "email" : "text",
1720
+ placeholder,
1721
+ required,
1722
+ style: { ...fieldStyle, height: 48 }
1723
+ }
1724
+ )
1725
+ ] }, i);
1726
+ }) }),
1727
+ children.map((child, i) => {
1728
+ if (child.type === "input") return null;
1729
+ const cs = child.slots ?? {};
1730
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1731
+ "button",
1732
+ {
1733
+ type: "submit",
1734
+ style: {
1735
+ alignSelf: submitAlign,
1736
+ border: "none",
1737
+ cursor: "pointer",
1738
+ padding: "12px 24px",
1739
+ // Corner radius follows the host template's own buttons (measured from a template
1740
+ // CTA); 8px only when the page has no template button to match.
1741
+ borderRadius: ctx.buttonRadius ?? 8,
1742
+ // Brand-styled: primary fill, brand-derived label colour (not a fixed token) so it
1743
+ // reads correctly on custom palettes.
1744
+ background: ctx.brand.palette.primary,
1745
+ color: ctx.buttonLabel ?? ctx.brand.palette.light,
1746
+ ...typeStyle(AI_TREE_TOKENS.type.button, ctx.brand.fonts.body)
1747
+ },
1748
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { ...textAttrs(ctx, `${path}.c${i}.label`), children: str(cs.label) })
1749
+ },
1750
+ i
1751
+ );
1752
+ })
1753
+ ]
1754
+ }
1755
+ )
1756
+ );
1757
+ }
1205
1758
  case "schedule-widget":
1206
1759
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1207
1760
  "div",
@@ -1222,16 +1775,27 @@ function renderNode(node, ctx, path) {
1222
1775
  return null;
1223
1776
  }
1224
1777
  }
1225
- function AiTreeRenderer({ tree, brand, resolveMedia, editKeyPrefix }) {
1778
+ function AiTreeRenderer({
1779
+ tree,
1780
+ brand,
1781
+ buttonRadius,
1782
+ resolveMedia,
1783
+ editKeyPrefix
1784
+ }) {
1226
1785
  if (!isRenderableTree(tree)) {
1227
1786
  return null;
1228
1787
  }
1229
1788
  const resolvedBrand = brand ?? AI_DEFAULT_BRAND;
1789
+ const band = (tree.settings ?? {}).sectionBackground === "accent" ? accentBandContext(resolvedBrand) : null;
1790
+ const blockBrand = band?.brand ?? resolvedBrand;
1230
1791
  const ctx = {
1231
- brand: resolvedBrand,
1792
+ brand: blockBrand,
1232
1793
  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
1794
+ cardSurface: blockBrand.palette.light.toUpperCase() === AI_DEFAULT_BRAND.palette.light.toUpperCase() ? "#ECECEB" : `color-mix(in srgb, ${blockBrand.palette.light} 90%, ${blockBrand.palette.dark})`,
1795
+ keyFor: editKeyPrefix ? (path) => `${editKeyPrefix}.${path}` : null,
1796
+ sectionAlignment: (tree.settings ?? {}).alignment === "center" ? "center" : "left",
1797
+ buttonRadius,
1798
+ ...band ? { buttonLabel: band.buttonLabel } : {}
1235
1799
  };
1236
1800
  const settings = tree.settings ?? {};
1237
1801
  const pad = AI_TREE_TOKENS.sectionPadding[settings.spacing ?? "balanced"];
@@ -1239,6 +1803,20 @@ function AiTreeRenderer({ tree, brand, resolveMedia, editKeyPrefix }) {
1239
1803
  const overlayAlpha = { low: 0.25, medium: 0.45, high: 0.65 }[settings.overlayIntensity ?? "medium"];
1240
1804
  const backgroundUrl = isOverlay && settings.backgroundMedia ? ctx.resolveMedia(settings.backgroundMedia) : null;
1241
1805
  const bgAttrs = ctx.keyFor && isOverlay && settings.backgroundMedia ? { "data-ohw-key": ctx.keyFor("settings.backgroundMedia"), "data-ohw-editable": "bg-image" } : {};
1806
+ const toneBackground = (() => {
1807
+ const { dark, primary, light } = resolvedBrand.palette;
1808
+ switch (settings.sectionBackground) {
1809
+ case "surface":
1810
+ return `color-mix(in srgb, ${light} 94%, ${dark})`;
1811
+ case "accent":
1812
+ return primary;
1813
+ case "accent-soft":
1814
+ return `color-mix(in srgb, ${primary} 12%, ${light})`;
1815
+ default:
1816
+ return void 0;
1817
+ }
1818
+ })();
1819
+ const distributed = !isOverlay && settings.textDistribution;
1242
1820
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1243
1821
  "section",
1244
1822
  {
@@ -1248,13 +1826,15 @@ function AiTreeRenderer({ tree, brand, resolveMedia, editKeyPrefix }) {
1248
1826
  style: {
1249
1827
  position: "relative",
1250
1828
  padding: `${pad}px 0`,
1251
- background: isOverlay && !backgroundUrl ? "#DBEAFE" : void 0,
1829
+ background: toneBackground ?? (isOverlay && !backgroundUrl ? "#DBEAFE" : void 0),
1252
1830
  backgroundImage: backgroundUrl ? `url(${backgroundUrl})` : void 0,
1253
1831
  backgroundSize: "cover",
1254
- backgroundPosition: "center"
1832
+ backgroundPosition: "center",
1833
+ color: settings.sectionBackground === "accent" ? blockBrand.palette.dark : void 0
1255
1834
  },
1256
1835
  children: [
1257
1836
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: AI_RESPONSIVE_CSS }),
1837
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: AI_MOBILE_CSS }),
1258
1838
  isOverlay && backgroundUrl && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { position: "absolute", inset: 0, background: `rgba(255,255,255,${overlayAlpha})` } }),
1259
1839
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1260
1840
  "div",
@@ -1275,10 +1855,24 @@ function AiTreeRenderer({ tree, brand, resolveMedia, editKeyPrefix }) {
1275
1855
  display: "grid",
1276
1856
  gridTemplateColumns: "repeat(12, 1fr)",
1277
1857
  gap: AI_TREE_TOKENS.spacing6,
1278
- alignItems: settings.verticalPosition === "top" ? "start" : "center",
1858
+ alignItems: distributed === "space-between" ? "stretch" : (distributed ?? settings.verticalPosition) === "top" ? "start" : "center",
1279
1859
  marginTop: r2 > 0 ? AI_TREE_TOKENS.spacing8 : 0
1280
1860
  },
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))
1861
+ children: (row.blocks ?? []).map((block, b) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1862
+ "div",
1863
+ {
1864
+ "data-ai-cell": "",
1865
+ style: {
1866
+ gridColumn: `span ${Math.min(12, Math.max(1, block.span))}`,
1867
+ minWidth: 0,
1868
+ // space-between: each column becomes a flex column whose content spreads over
1869
+ // the full row height instead of clumping at the top.
1870
+ ...distributed === "space-between" ? { display: "flex", flexDirection: "column", justifyContent: "space-between" } : {}
1871
+ },
1872
+ children: renderNode(block, ctx, `r${r2}.b${b}`)
1873
+ },
1874
+ b
1875
+ ))
1282
1876
  },
1283
1877
  r2
1284
1878
  ))
@@ -1294,17 +1888,36 @@ var import_jsx_runtime2 = require("react/jsx-runtime");
1294
1888
  var CONTAINER_ATTR = "data-ohw-ai-generated";
1295
1889
  var REPLACED_ATTR = "data-ohw-ai-replaced-by";
1296
1890
  var REMOVED_ATTR = "data-ohw-ai-removed";
1891
+ var TEMPLATE_HIDDEN_ATTR = "data-ohw-ai-template-hidden";
1892
+ var CHROME_SECTION_IDS = /* @__PURE__ */ new Set(["navbar", "footer"]);
1893
+ function readRootVar(name) {
1894
+ if (typeof document === "undefined") return "";
1895
+ return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
1896
+ }
1897
+ function deriveBrandOverride() {
1898
+ const dark = readRootVar("--ohw-brand-dark");
1899
+ const primary = readRootVar("--ohw-brand-primary");
1900
+ const light = readRootVar("--ohw-brand-light");
1901
+ if (!dark || !primary || !light) return null;
1902
+ const accent = readRootVar("--ohw-brand-accent");
1903
+ const heading = readRootVar("--font-heading") || readRootVar("--font-display");
1904
+ const body = readRootVar("--font-body");
1905
+ return {
1906
+ palette: { dark, primary, accent: accent || dark, light },
1907
+ fonts: {
1908
+ heading: heading || AI_DEFAULT_BRAND.fonts.heading,
1909
+ body: body || AI_DEFAULT_BRAND.fonts.body
1910
+ }
1911
+ };
1912
+ }
1297
1913
  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");
1914
+ const dark = readRootVar("--color-dark");
1915
+ const primary = readRootVar("--color-primary");
1916
+ const light = readRootVar("--color-light");
1304
1917
  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");
1918
+ const accent = readRootVar("--color-accent");
1919
+ const heading = readRootVar("--font-heading") || readRootVar("--font-display");
1920
+ const body = readRootVar("--font-body");
1308
1921
  return {
1309
1922
  palette: { dark, primary, accent: accent || dark, light },
1310
1923
  fonts: {
@@ -1313,6 +1926,13 @@ function deriveTemplateBrand() {
1313
1926
  }
1314
1927
  };
1315
1928
  }
1929
+ function deriveTemplateButtonRadius() {
1930
+ if (typeof document === "undefined") return null;
1931
+ const btn = document.querySelector('[data-ohw-role="button"]');
1932
+ if (!btn) return null;
1933
+ const radius = getComputedStyle(btn).borderTopLeftRadius;
1934
+ return radius || null;
1935
+ }
1316
1936
  var mounted = /* @__PURE__ */ new Map();
1317
1937
  function findTemplateSection(id) {
1318
1938
  for (const el of document.querySelectorAll(`[data-ohw-section="${CSS.escape(id)}"]`)) {
@@ -1376,6 +1996,24 @@ function syncRemovedSections(state) {
1376
1996
  }
1377
1997
  }
1378
1998
  }
1999
+ function syncTemplateHidden(state, pageHasSections) {
2000
+ const hide = state.hideTemplate === true && pageHasSections;
2001
+ for (const el of Array.from(document.querySelectorAll(`[${TEMPLATE_HIDDEN_ATTR}]`))) {
2002
+ if (!hide) {
2003
+ el.style.removeProperty("display");
2004
+ el.removeAttribute(TEMPLATE_HIDDEN_ATTR);
2005
+ }
2006
+ }
2007
+ if (!hide) return;
2008
+ for (const el of Array.from(document.querySelectorAll("[data-ohw-section]"))) {
2009
+ if (el.hasAttribute(CONTAINER_ATTR)) continue;
2010
+ if (CHROME_SECTION_IDS.has(el.getAttribute("data-ohw-section") ?? "")) continue;
2011
+ if (el.parentElement?.closest("[data-ohw-section]")) continue;
2012
+ if (el.hasAttribute(REPLACED_ATTR) || el.hasAttribute(REMOVED_ATTR)) continue;
2013
+ el.style.display = "none";
2014
+ el.setAttribute(TEMPLATE_HIDDEN_ATTR, "");
2015
+ }
2016
+ }
1379
2017
  function syncReplacedOriginals(state) {
1380
2018
  for (const el of document.querySelectorAll(`[${REPLACED_ATTR}]`)) {
1381
2019
  const byId = el.getAttribute(REPLACED_ATTR) ?? "";
@@ -1394,10 +2032,64 @@ function syncReplacedOriginals(state) {
1394
2032
  }
1395
2033
  }
1396
2034
  }
2035
+ var sectionOrderIndex = /* @__PURE__ */ new Map();
2036
+ function setAiSectionOrder(raw, currentPath) {
2037
+ const next = /* @__PURE__ */ new Map();
2038
+ if (raw) {
2039
+ try {
2040
+ const entries = JSON.parse(raw);
2041
+ for (const entry of entries) {
2042
+ if (!entry.pagePath || entry.pagePath === currentPath) next.set(entry.instanceId, entry.order);
2043
+ }
2044
+ } catch {
2045
+ }
2046
+ }
2047
+ sectionOrderIndex = next;
2048
+ }
2049
+ function applyExplicitOrder(entries) {
2050
+ if (sectionOrderIndex.size === 0) return entries;
2051
+ return entries.map((entry, index) => ({ entry, index, order: sectionOrderIndex.get(entry.id) })).sort((a, b) => {
2052
+ if (a.order === void 0 && b.order === void 0) return a.index - b.index;
2053
+ if (a.order === void 0) return 1;
2054
+ if (b.order === void 0) return -1;
2055
+ return a.order - b.order;
2056
+ }).map((item) => item.entry);
2057
+ }
2058
+ function orderByChain(sections) {
2059
+ const ids = new Set(sections.map((entry) => entry.id));
2060
+ const after = /* @__PURE__ */ new Map();
2061
+ const roots = [];
2062
+ for (const entry of sections) {
2063
+ const anchor = entry.replaces ?? entry.beforeSection ?? entry.afterSection ?? null;
2064
+ if (anchor && ids.has(anchor)) {
2065
+ const bucket = after.get(anchor);
2066
+ if (bucket) bucket.push(entry);
2067
+ else after.set(anchor, [entry]);
2068
+ } else {
2069
+ roots.push(entry);
2070
+ }
2071
+ }
2072
+ const out = [];
2073
+ const seen = /* @__PURE__ */ new Set();
2074
+ const visit = (entry) => {
2075
+ if (seen.has(entry.id)) return;
2076
+ seen.add(entry.id);
2077
+ out.push(entry);
2078
+ for (const child of after.get(entry.id) ?? []) visit(child);
2079
+ };
2080
+ for (const root of roots) visit(root);
2081
+ return out.length === sections.length ? out : sections;
2082
+ }
1397
2083
  function applyAiSectionsToDom(state, options) {
1398
2084
  if (typeof document === "undefined") return;
2085
+ const brandOverride = deriveBrandOverride();
1399
2086
  const templateBrand = deriveTemplateBrand();
1400
- const activeIds = new Set(state.sections.map((entry) => entry.id));
2087
+ const templateButtonRadius = deriveTemplateButtonRadius();
2088
+ const brandKey = brandOverride ? JSON.stringify(brandOverride) : "";
2089
+ const pagePath = window.location.pathname;
2090
+ const pageSections = state.sections.filter((entry) => (entry.path ?? "/") === pagePath);
2091
+ const activeIds = new Set(pageSections.map((entry) => entry.id));
2092
+ const ordered = state.hideTemplate === true ? applyExplicitOrder(orderByChain(pageSections)) : pageSections;
1401
2093
  for (const [id, section] of mounted) {
1402
2094
  if (!activeIds.has(id)) {
1403
2095
  section.root.unmount();
@@ -1405,8 +2097,8 @@ function applyAiSectionsToDom(state, options) {
1405
2097
  mounted.delete(id);
1406
2098
  }
1407
2099
  }
1408
- for (const entry of state.sections) {
1409
- const serialized = JSON.stringify(entry);
2100
+ for (const entry of ordered) {
2101
+ const serialized = JSON.stringify(entry) + brandKey;
1410
2102
  const existing = mounted.get(entry.id);
1411
2103
  if (existing && existing.serialized === serialized && existing.container.isConnected) {
1412
2104
  continue;
@@ -1431,7 +2123,8 @@ function applyAiSectionsToDom(state, options) {
1431
2123
  AiTreeRenderer,
1432
2124
  {
1433
2125
  tree: entry.tree,
1434
- brand: entry.brand ?? options?.brand ?? templateBrand ?? void 0,
2126
+ brand: brandOverride ?? entry.brand ?? options?.brand ?? templateBrand ?? void 0,
2127
+ buttonRadius: templateButtonRadius,
1435
2128
  resolveMedia,
1436
2129
  editKeyPrefix: `ai.${entry.id}`
1437
2130
  }
@@ -1440,8 +2133,20 @@ function applyAiSectionsToDom(state, options) {
1440
2133
  });
1441
2134
  mounted.set(entry.id, { root, container, serialized });
1442
2135
  }
2136
+ if (state.hideTemplate === true) {
2137
+ let prev = null;
2138
+ for (const entry of ordered) {
2139
+ const el = mounted.get(entry.id)?.container;
2140
+ if (!el) continue;
2141
+ if (prev && !(prev.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_FOLLOWING)) {
2142
+ prev.insertAdjacentElement("afterend", el);
2143
+ }
2144
+ prev = el;
2145
+ }
2146
+ }
1443
2147
  syncReplacedOriginals(state);
1444
2148
  syncRemovedSections(state);
2149
+ syncTemplateHidden(state, pageSections.length > 0);
1445
2150
  }
1446
2151
 
1447
2152
  // src/useLinkHrefGuardian.ts
@@ -2048,7 +2753,7 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAf
2048
2753
  const autoId = (0, import_react5.useId)();
2049
2754
  const insertAfter = insertAfterProp ?? autoId;
2050
2755
  const [schedule, setSchedule] = (0, import_react5.useState)(null);
2051
- const [loading, setLoading] = (0, import_react5.useState)(true);
2756
+ const [loading, setLoading] = (0, import_react5.useState)(initialScheduleId !== null);
2052
2757
  const [inEditor, setInEditor] = (0, import_react5.useState)(false);
2053
2758
  const [isHovered, setIsHovered] = (0, import_react5.useState)(false);
2054
2759
  const [modalState, setModalState] = (0, import_react5.useState)(null);
@@ -2222,8 +2927,10 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAf
2222
2927
  "*"
2223
2928
  );
2224
2929
  };
2225
- if (!inEditor && !loading && !schedule) return null;
2226
2930
  const sectionId = `scheduling-${insertAfter}`;
2931
+ if (!inEditor && !loading && !schedule) {
2932
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("section", { "data-ohw-section": sectionId, "data-ohw-scheduling-anchor": insertAfter, style: { display: "none" } });
2933
+ }
2227
2934
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2228
2935
  "section",
2229
2936
  {
@@ -7140,13 +7847,17 @@ function MediaOverlay({
7140
7847
  hover,
7141
7848
  isUploading,
7142
7849
  fadingOut = false,
7850
+ selected = false,
7851
+ hovered = false,
7143
7852
  onFadeOutComplete,
7144
7853
  onReplace,
7854
+ onSelect,
7145
7855
  onVideoSettingsChange
7146
7856
  }) {
7147
7857
  const { rect } = hover;
7148
7858
  const skeletonRef = React8.useRef(null);
7149
7859
  const isVideo = hover.elementType === "video";
7860
+ const showChrome = !selected || hovered;
7150
7861
  const autoplay = hover.videoAutoplay ?? true;
7151
7862
  const muted = hover.videoMuted ?? true;
7152
7863
  const probeRef = React8.useRef(null);
@@ -7160,6 +7871,7 @@ function MediaOverlay({
7160
7871
  (prev) => prev && prev.fullWidth === width && prev.height === height ? prev : { fullWidth: width, height }
7161
7872
  );
7162
7873
  }, [isVideo]);
7874
+ const replaceLabel = isVideo ? "Replace video" : hover.elementType === "bg-image" ? "Replace background" : "Replace image";
7163
7875
  const replaceMode = buttonSize ? replaceButtonMode({ width: rect.width, height: rect.height }, buttonSize) : "none";
7164
7876
  const box = {
7165
7877
  position: "fixed",
@@ -7193,7 +7905,7 @@ function MediaOverlay({
7193
7905
  }
7194
7906
  );
7195
7907
  }
7196
- const settingsBar = isVideo && !hover.isDragOver ? /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
7908
+ const settingsBar = isVideo && !hover.isDragOver && showChrome ? /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
7197
7909
  "div",
7198
7910
  {
7199
7911
  "data-ohw-bridge": "",
@@ -7263,10 +7975,12 @@ function MediaOverlay({
7263
7975
  // in-document, pointer-events does it natively. The button below opts back in, so
7264
7976
  // Replace still works.
7265
7977
  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)"
7978
+ // Selected: a firm component ring with no wash, so the image reads as chosen rather
7979
+ // than hovered. Hover keeps the existing tinted preview.
7980
+ boxShadow: selected ? "inset 0 0 0 2px var(--color-primary)" : "inset 0 0 0 1.5px var(--color-primary)",
7981
+ background: selected ? "transparent" : "color-mix(in srgb, var(--color-primary) 20%, transparent)"
7268
7982
  },
7269
- onClick: () => onReplace(hover.key),
7983
+ onClick: () => onSelect && !selected ? onSelect(hover.key) : onReplace(hover.key),
7270
7984
  children: [
7271
7985
  /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
7272
7986
  Button,
@@ -7287,17 +8001,17 @@ function MediaOverlay({
7287
8001
  },
7288
8002
  children: [
7289
8003
  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 }),
7290
- isVideo ? "Replace video" : "Replace image"
8004
+ replaceLabel
7291
8005
  ]
7292
8006
  }
7293
8007
  ),
7294
- replaceMode === "none" ? null : /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
8008
+ showChrome && replaceMode !== "none" && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
7295
8009
  Button,
7296
8010
  {
7297
8011
  "data-ohw-media-overlay": "",
7298
8012
  variant: "outline",
7299
8013
  size: "sm",
7300
- "aria-label": isVideo ? "Replace video" : "Replace image",
8014
+ "aria-label": replaceLabel,
7301
8015
  className: "gap-1.5 cursor-pointer hover:bg-background",
7302
8016
  style: {
7303
8017
  ...OVERLAY_BUTTON_STYLE,
@@ -7320,7 +8034,7 @@ function MediaOverlay({
7320
8034
  },
7321
8035
  children: [
7322
8036
  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
8037
+ replaceMode === "full" ? replaceLabel : null
7324
8038
  ]
7325
8039
  }
7326
8040
  )
@@ -7404,6 +8118,8 @@ function parseSectionsFromRoot(root) {
7404
8118
  const id = el.getAttribute("data-ohw-section") ?? "";
7405
8119
  if (!id || seen.has(id) || LINK_PICKER_EXCLUDED_IDS.has(id)) continue;
7406
8120
  if (el.parentElement?.closest("[data-ohw-section]")) continue;
8121
+ if (el.hasAttribute("data-ohw-ai-template-hidden") || el.hasAttribute("data-ohw-ai-removed") || el.hasAttribute("data-ohw-ai-replaced-by"))
8122
+ continue;
7407
8123
  seen.add(id);
7408
8124
  const label = el.getAttribute("data-ohw-section-label") ?? titleCaseSectionId(id);
7409
8125
  sections.push({ id, label });
@@ -7433,6 +8149,10 @@ function topLevelSections() {
7433
8149
  function instanceIdOf(el) {
7434
8150
  return el.dataset.ohwInstance ?? el.dataset.ohwSection ?? "";
7435
8151
  }
8152
+ function findByInstanceId(instanceId) {
8153
+ const escapedId = CSS.escape(instanceId);
8154
+ return document.querySelector(`[data-ohw-instance="${escapedId}"]`) ?? document.querySelector(`[data-ohw-section="${escapedId}"]:not([data-ohw-instance])`);
8155
+ }
7436
8156
  function planSectionMove(instanceId, targetIndex, currentPath) {
7437
8157
  const sections = topLevelSections();
7438
8158
  const index = sections.findIndex((el) => instanceIdOf(el) === instanceId);
@@ -7468,7 +8188,7 @@ function syncRemovedFlags(entries) {
7468
8188
  }
7469
8189
  });
7470
8190
  for (const id of removedIds) {
7471
- const el = document.querySelector(`[data-ohw-instance="${CSS.escape(id)}"]`);
8191
+ const el = findByInstanceId(id);
7472
8192
  if (el) {
7473
8193
  el.style.display = "none";
7474
8194
  el.setAttribute(REMOVED_ATTR2, "");
@@ -7496,7 +8216,7 @@ function applyPersistedOrder(entries) {
7496
8216
  }
7497
8217
  }
7498
8218
  function setSectionRemoved(instanceId, currentPath, existingEntries, removed) {
7499
- if (!document.querySelector(`[data-ohw-instance="${CSS.escape(instanceId)}"]`)) return null;
8219
+ if (!findByInstanceId(instanceId)) return null;
7500
8220
  const byId = new Map(existingEntries.map((e) => [e.instanceId, e]));
7501
8221
  const allSections = Array.from(document.querySelectorAll("[data-ohw-section]")).filter(
7502
8222
  (el) => !el.parentElement?.closest("[data-ohw-section]") && !isChromeSection(el)
@@ -7685,6 +8405,7 @@ function AiSectionOverlay({
7685
8405
  }) {
7686
8406
  const [selectedId, setSelectedId] = (0, import_react8.useState)(null);
7687
8407
  const [reviewId, setReviewId] = (0, import_react8.useState)(null);
8408
+ const [reviewButtonsHidden, setReviewButtonsHidden] = (0, import_react8.useState)(false);
7688
8409
  const reviewIdRef = (0, import_react8.useRef)(null);
7689
8410
  reviewIdRef.current = reviewId;
7690
8411
  const selectedIdRef = (0, import_react8.useRef)(null);
@@ -7746,6 +8467,7 @@ function AiSectionOverlay({
7746
8467
  }
7747
8468
  const found = readRect(sectionId) != null;
7748
8469
  setReviewId(found ? sectionId : null);
8470
+ setReviewButtonsHidden(e.data.hideButtons === true);
7749
8471
  postToParent2({ type: "ow:ai-review-started", sectionId, found });
7750
8472
  if (found) {
7751
8473
  document.querySelector(`[data-ohw-instance="${CSS.escape(sectionId)}"]`)?.scrollIntoView({ behavior: "smooth", block: "nearest" });
@@ -7873,13 +8595,16 @@ function AiSectionOverlay({
7873
8595
  border: `2px solid ${PRIMARY2}`,
7874
8596
  borderRadius: edgeAwareRadius(reviewRect),
7875
8597
  zIndex: 2147483200,
7876
- // The veil itself: swallows clicks so the section stays locked until decided.
8598
+ // The veil itself: swallows clicks so the section stays locked until decided. This
8599
+ // stopPropagation only guards the bubble phase; the bridge's capture-phase click
8600
+ // handlers additionally bail on `[data-ohw-ai-review]`, without which a click on
8601
+ // Accept/Discard resolves to the media beneath and opens the file picker.
7877
8602
  background: "rgba(8, 133, 254, 0.04)",
7878
8603
  pointerEvents: "auto",
7879
8604
  cursor: "default"
7880
8605
  },
7881
8606
  onClick: (e) => e.stopPropagation(),
7882
- children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
8607
+ children: !reviewButtonsHidden && /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
7883
8608
  "div",
7884
8609
  {
7885
8610
  style: {
@@ -10446,8 +11171,13 @@ function referenceBox(slot) {
10446
11171
  const neighbour = row ? Array.from(row.querySelectorAll('[data-ohw-editable="icon"]')).find(
10447
11172
  (el) => el !== slot && !el.hasAttribute("data-ohw-social-icon-placeholder")
10448
11173
  ) : null;
10449
- const source = (neighbour ?? slot).querySelector(GLYPH_SELECTOR);
10450
- const box = source?.getBoundingClientRect() ?? null;
11174
+ if (neighbour) {
11175
+ const box2 = neighbour.querySelector(GLYPH_SELECTOR)?.getBoundingClientRect();
11176
+ if (box2?.width && box2.height) return box2;
11177
+ }
11178
+ const own = slot.getBoundingClientRect();
11179
+ if (own.width && own.height) return own;
11180
+ const box = slot.querySelector(GLYPH_SELECTOR)?.getBoundingClientRect() ?? null;
10451
11181
  return box?.width && box.height ? box : null;
10452
11182
  }
10453
11183
  function iconMarkupSizedFor(slot, markup) {
@@ -12248,6 +12978,7 @@ function readLogoSizeState(content, placement) {
12248
12978
  function getLogoElement(el) {
12249
12979
  const marked = el.closest('[data-ohw-role="logo"], [data-ohw-logo]');
12250
12980
  if (marked) return marked;
12981
+ if (el.closest('[data-ohw-editable="icon"]')) return null;
12251
12982
  const root = el.closest("nav, [data-ohw-nav-root], footer");
12252
12983
  if (!root) return null;
12253
12984
  const anchor = el.closest("a");
@@ -13316,6 +14047,7 @@ function useSectionDrag({
13316
14047
  }
13317
14048
  const orderJson = JSON.stringify(entries);
13318
14049
  editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: orderJson };
14050
+ setAiSectionOrder(orderJson, window.location.pathname);
13319
14051
  postToParentRef.current({ type: "ow:change", nodes: [{ key: SECTION_ORDER_KEY, text: orderJson }] });
13320
14052
  applyPersistedOrder(entries);
13321
14053
  clearSectionDragVisuals();
@@ -14175,21 +14907,10 @@ function parseSchedulingInsertAfter(insertAfter) {
14175
14907
  insertBefore: insertAfter.slice(idx + INSERT_BEFORE_MARKER.length)
14176
14908
  };
14177
14909
  }
14178
- function resolveSchedulingInsert(insertAfter, explicitInsertBefore) {
14179
- const parsed = parseSchedulingInsertAfter(insertAfter);
14180
- const insertBefore = explicitInsertBefore ?? parsed.insertBefore;
14181
- const effectiveInsertAfter = insertBefore && parsed.insertBefore === null ? `${insertAfter}${INSERT_BEFORE_MARKER}${insertBefore}` : insertAfter;
14182
- return { effectiveInsertAfter, insertBefore };
14183
- }
14184
- function getSchedulingMountPoint(insertAfter) {
14185
- const { anchor } = parseSchedulingInsertAfter(insertAfter);
14186
- let anchorEl = document.querySelector(`[data-ohw-section="${anchor}"]`);
14187
- if (!anchorEl && anchor === "scheduling") {
14188
- const widgets = Array.from(document.querySelectorAll('[data-ohw-section^="scheduling-"]'));
14189
- anchorEl = widgets.at(-1) ?? null;
14190
- }
14191
- if (!anchorEl) return null;
14192
- return anchorEl.closest('[data-ohw-section-container="scheduling"]') ?? anchorEl;
14910
+ function resolveEntryAnchor(entry) {
14911
+ if (entry.anchorId !== void 0) return { anchorId: entry.anchorId, beforeId: entry.beforeId ?? null };
14912
+ const parsed = parseSchedulingInsertAfter(entry.insertAfter);
14913
+ return { anchorId: parsed.anchor, beforeId: parsed.insertBefore };
14193
14914
  }
14194
14915
  function schedulingMountDepth(insertAfter) {
14195
14916
  if (!insertAfter.includes(INSERT_BEFORE_MARKER)) return 0;
@@ -14206,8 +14927,7 @@ function getPageSchedulingEntries(raw) {
14206
14927
  }
14207
14928
  }
14208
14929
  function isSchedulingWidgetMissing(entry) {
14209
- const { effectiveInsertAfter } = resolveSchedulingInsert(entry.insertAfter);
14210
- return !document.querySelector(`[data-ohw-section="${schedulingSectionId(effectiveInsertAfter)}"]`);
14930
+ return !document.querySelector(`[data-ohw-section="${schedulingSectionId(entry.insertAfter)}"]`);
14211
14931
  }
14212
14932
  function hasMissingSchedulingWidgets(entries) {
14213
14933
  return entries.some(isSchedulingWidgetMissing);
@@ -14237,16 +14957,17 @@ function initSectionsFromContent(content, removeExisting = false) {
14237
14957
  } catch {
14238
14958
  }
14239
14959
  }
14240
- function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId, explicitInsertBefore) {
14241
- const { effectiveInsertAfter, insertBefore } = resolveSchedulingInsert(insertAfter, explicitInsertBefore);
14242
- const sectionId = schedulingSectionId(effectiveInsertAfter);
14960
+ function mountSchedulingWidget(anchorId, notifyOnConnect = false, scheduleId, beforeId, existingWidgetId) {
14961
+ const widgetId = existingWidgetId ?? (beforeId ? `${anchorId}${INSERT_BEFORE_MARKER}${beforeId}` : anchorId);
14962
+ const sectionId = schedulingSectionId(widgetId);
14243
14963
  if (document.querySelector(`[data-ohw-section="${sectionId}"]`)) return false;
14244
- const mountPoint = getSchedulingMountPoint(effectiveInsertAfter);
14245
- if (!mountPoint) return false;
14964
+ const anchorEl = document.querySelector(`[data-ohw-section="${anchorId}"]`);
14965
+ if (!anchorEl) return false;
14966
+ const mountPoint = anchorEl.closest('[data-ohw-section-container="scheduling"]') ?? anchorEl;
14246
14967
  const container = document.createElement("div");
14247
14968
  container.dataset.ohwSectionContainer = "scheduling";
14248
- if (insertBefore) {
14249
- const beforeAnchor = document.querySelector(`[data-ohw-section="${insertBefore}"]`);
14969
+ if (beforeId) {
14970
+ const beforeAnchor = document.querySelector(`[data-ohw-section="${beforeId}"]`);
14250
14971
  const beforePoint = beforeAnchor?.closest('[data-ohw-section-container="scheduling"]') ?? beforeAnchor;
14251
14972
  if (!beforePoint) return false;
14252
14973
  beforePoint.insertAdjacentElement("beforebegin", container);
@@ -14257,19 +14978,25 @@ function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId,
14257
14978
  }
14258
14979
  tail.insertAdjacentElement("afterend", container);
14259
14980
  }
14260
- const root = (0, import_client2.createRoot)(container);
14261
- (0, import_react_dom3.flushSync)(() => {
14262
- root.render(
14263
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
14264
- SchedulingWidget,
14265
- {
14266
- notifyOnConnect,
14267
- initialScheduleId: scheduleId,
14268
- insertAfter: effectiveInsertAfter
14269
- }
14270
- )
14271
- );
14272
- });
14981
+ try {
14982
+ const root = (0, import_client2.createRoot)(container);
14983
+ (0, import_react_dom3.flushSync)(() => {
14984
+ root.render(
14985
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
14986
+ SchedulingWidget,
14987
+ {
14988
+ notifyOnConnect,
14989
+ initialScheduleId: scheduleId,
14990
+ insertAfter: widgetId
14991
+ }
14992
+ )
14993
+ );
14994
+ });
14995
+ } catch (err) {
14996
+ console.error("[ow:scheduling] render threw", err);
14997
+ container.remove();
14998
+ return false;
14999
+ }
14273
15000
  const tracker = getSectionsTracker();
14274
15001
  let sections = [];
14275
15002
  try {
@@ -14277,10 +15004,12 @@ function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId,
14277
15004
  } catch {
14278
15005
  }
14279
15006
  const inEditor = typeof window !== "undefined" && window.self !== window.top;
14280
- if (inEditor && !sections.find((s) => s.type === "scheduling" && s.insertAfter === effectiveInsertAfter)) {
15007
+ if (inEditor && !sections.find((s) => s.type === "scheduling" && s.insertAfter === widgetId)) {
14281
15008
  sections.push({
14282
15009
  type: "scheduling",
14283
- insertAfter: effectiveInsertAfter,
15010
+ insertAfter: widgetId,
15011
+ anchorId,
15012
+ beforeId: beforeId ?? null,
14284
15013
  pagePath: window.location.pathname,
14285
15014
  ...scheduleId ? { scheduleId } : {}
14286
15015
  });
@@ -14294,7 +15023,8 @@ function mountSchedulingEntries(entries, notifyOnConnect = false) {
14294
15023
  for (let i = pending.length - 1; i >= 0; i--) {
14295
15024
  const entry = pending[i];
14296
15025
  const shouldNotify = typeof notifyOnConnect === "function" ? notifyOnConnect(entry) : notifyOnConnect;
14297
- if (mountSchedulingWidget(entry.insertAfter, shouldNotify, entry.scheduleId)) {
15026
+ const { anchorId, beforeId } = resolveEntryAnchor(entry);
15027
+ if (mountSchedulingWidget(anchorId, shouldNotify, entry.scheduleId ?? null, beforeId, entry.insertAfter)) {
14298
15028
  pending.splice(i, 1);
14299
15029
  }
14300
15030
  }
@@ -14452,6 +15182,11 @@ function applyLinkByKey(key, val) {
14452
15182
  hrefAnchors.forEach((el) => applyLinkHref(el, val));
14453
15183
  }
14454
15184
  }
15185
+ function isInsideLinkEditor(target) {
15186
+ return Boolean(
15187
+ target.closest("[data-ohw-link-popover-root]") || target.closest("[data-ohw-link-modal-root]") || target.closest("[data-ohw-link-page-dropdown]") || target.closest("[data-ohw-section-picker]") || target.closest("[data-ohw-navbar-container-chrome]") || target.closest("[data-ohw-navbar-add-button]") || target.closest("[data-ohw-footer-container-chrome]") || target.closest("[data-ohw-footer-add-button]") || target.closest("[data-ohw-more-menu]") || target.closest('[data-slot="dropdown-menu-content"]') || target.closest('[data-slot="popover-content"]') || target.closest('[data-slot="dialog-content"]') || target.closest('[data-slot="dialog-overlay"]')
15188
+ );
15189
+ }
14455
15190
  function isInsideFloatingPanel(target) {
14456
15191
  return Boolean(target.closest("[data-ohw-floating-panel]"));
14457
15192
  }
@@ -14459,11 +15194,6 @@ function isPointOverFloatingPanel(clientX, clientY) {
14459
15194
  const el = document.elementFromPoint(clientX, clientY);
14460
15195
  return Boolean(el instanceof Element && el.closest("[data-ohw-floating-panel]"));
14461
15196
  }
14462
- function isInsideLinkEditor(target) {
14463
- return Boolean(
14464
- target.closest("[data-ohw-link-popover-root]") || target.closest("[data-ohw-link-modal-root]") || target.closest("[data-ohw-link-page-dropdown]") || target.closest("[data-ohw-section-picker]") || target.closest("[data-ohw-navbar-container-chrome]") || target.closest("[data-ohw-navbar-add-button]") || target.closest("[data-ohw-footer-container-chrome]") || target.closest("[data-ohw-footer-add-button]") || target.closest("[data-ohw-more-menu]") || target.closest('[data-slot="dropdown-menu-content"]') || target.closest('[data-slot="popover-content"]') || target.closest('[data-slot="dialog-content"]') || target.closest('[data-slot="dialog-overlay"]')
14465
- );
14466
- }
14467
15197
  function getHrefKeyFromElement(el) {
14468
15198
  if (!el) return null;
14469
15199
  const anchor = el.closest("[data-ohw-href-key]");
@@ -14722,7 +15452,7 @@ function getNavigationSelectionParent(el) {
14722
15452
  if (!isFooterLinksContainer(el) && (el.hasAttribute("data-ohw-footer-col") || el.hasAttribute("data-ohw-footer-column") || isInferredFooterGroup2(el))) {
14723
15453
  return getFooterLinksContainer();
14724
15454
  }
14725
- if (el.hasAttribute("data-ohw-nav-container") || el.hasAttribute("data-ohw-nav-drawer") || isFooterLinksContainer(el)) {
15455
+ if (el.hasAttribute("data-ohw-nav-container") || el.hasAttribute("data-ohw-nav-drawer") || el.hasAttribute("data-ohw-footer-col") || el.hasAttribute("data-ohw-footer-column") || isFooterLinksContainer(el) || isInferredFooterGroup2(el)) {
14726
15456
  return getNavigationRoot(el);
14727
15457
  }
14728
15458
  return null;
@@ -14937,7 +15667,6 @@ var ICONS = {
14937
15667
  insertUnorderedList: '<line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/>',
14938
15668
  insertOrderedList: '<line x1="10" y1="6" x2="21" y2="6"/><line x1="10" y1="12" x2="21" y2="12"/><line x1="10" y1="18" x2="21" y2="18"/><path d="M4 6h1v4"/><path d="M4 10h2"/><path d="M6 18H4c0-1 2-2 2-3s-1-1.5-2-1"/>'
14939
15669
  };
14940
- var HEIGHT_SETTLE_DELAYS = [150, 400, 800];
14941
15670
  var SELECTION_CHROME_GAP2 = 4;
14942
15671
  var TOOLBAR_STROKE_GAP2 = 4;
14943
15672
  var TOOLBAR_OFFSET_FROM_ELEMENT = SELECTION_CHROME_GAP2 + TOOLBAR_STROKE_GAP2;
@@ -15317,6 +16046,7 @@ function StateToggle({
15317
16046
  );
15318
16047
  }
15319
16048
  var contentCache = /* @__PURE__ */ new Map();
16049
+ var fetchedContentPaths = /* @__PURE__ */ new Set();
15320
16050
  var OHW_LOADER_STYLE = {
15321
16051
  position: "fixed",
15322
16052
  inset: 0,
@@ -15435,6 +16165,70 @@ function OhhwellsBridge() {
15435
16165
  const hoveredImageHasTextOverlapRef = (0, import_react17.useRef)(false);
15436
16166
  const dragOverElRef = (0, import_react17.useRef)(null);
15437
16167
  const [mediaHover, setMediaHover] = (0, import_react17.useState)(null);
16168
+ const [selectedMedia, setSelectedMedia] = (0, import_react17.useState)(null);
16169
+ const selectedMediaElRef = (0, import_react17.useRef)(null);
16170
+ const clearMediaSelection = (0, import_react17.useCallback)(() => {
16171
+ const prev = selectedMediaElRef.current;
16172
+ selectedMediaElRef.current = null;
16173
+ setSelectedMedia(null);
16174
+ const sectionEl = prev?.closest("[data-ohw-section]") ?? null;
16175
+ if (sectionEl) {
16176
+ postToParentRef.current({
16177
+ type: "ow:section-selected",
16178
+ sectionId: sectionEl.dataset.ohwSection ?? null,
16179
+ sectionLabel: sectionEl.dataset.ohwSectionLabel ?? titleCaseSectionId(sectionEl.dataset.ohwSection ?? ""),
16180
+ key: null
16181
+ });
16182
+ }
16183
+ }, []);
16184
+ const clearMediaSelectionRef = (0, import_react17.useRef)(clearMediaSelection);
16185
+ clearMediaSelectionRef.current = clearMediaSelection;
16186
+ const selectMediaElement = (0, import_react17.useCallback)((el) => {
16187
+ const r2 = el.getBoundingClientRect();
16188
+ const video = el.dataset.ohwEditable === "video" ? el.querySelector("video") : null;
16189
+ selectedMediaElRef.current = el;
16190
+ setSelectedMedia({
16191
+ key: el.dataset.ohwKey ?? "",
16192
+ rect: { top: r2.top, left: r2.left, width: r2.width, height: r2.height },
16193
+ elementType: el.dataset.ohwEditable ?? "image",
16194
+ hasTextOverlap: false,
16195
+ isDragOver: false,
16196
+ ...video ? { videoAutoplay: video.autoplay, videoMuted: video.muted } : {}
16197
+ });
16198
+ const sectionEl = el.closest("[data-ohw-section]");
16199
+ aiSectionApiRef.current?.selectFromElement(el, { report: false });
16200
+ postToParentRef.current({
16201
+ type: "ow:section-selected",
16202
+ sectionId: sectionEl?.dataset.ohwSection ?? null,
16203
+ sectionLabel: sectionEl ? sectionEl.dataset.ohwSectionLabel ?? titleCaseSectionId(sectionEl.dataset.ohwSection ?? "") : null,
16204
+ key: el.dataset.ohwKey ?? null,
16205
+ // Display name for the pill — the raw key prettifies into fragments ("Img"); the
16206
+ // bridge knows what the node IS, so it names it.
16207
+ keyLabel: el.dataset.ohwEditable === "video" ? "Video" : el.dataset.ohwEditable === "bg-image" ? "Background" : "Image"
16208
+ });
16209
+ }, []);
16210
+ const selectMediaElementRef = (0, import_react17.useRef)(selectMediaElement);
16211
+ selectMediaElementRef.current = selectMediaElement;
16212
+ (0, import_react17.useEffect)(() => {
16213
+ if (!selectedMedia) return;
16214
+ const update = () => {
16215
+ const el = selectedMediaElRef.current;
16216
+ if (!el || !el.isConnected) {
16217
+ clearMediaSelection();
16218
+ return;
16219
+ }
16220
+ const r2 = el.getBoundingClientRect();
16221
+ setSelectedMedia(
16222
+ (prev) => prev ? { ...prev, rect: { top: r2.top, left: r2.left, width: r2.width, height: r2.height } } : prev
16223
+ );
16224
+ };
16225
+ window.addEventListener("scroll", update, true);
16226
+ window.addEventListener("resize", update);
16227
+ return () => {
16228
+ window.removeEventListener("scroll", update, true);
16229
+ window.removeEventListener("resize", update);
16230
+ };
16231
+ }, [selectedMedia !== null]);
15438
16232
  const [carouselHover, setCarouselHover] = (0, import_react17.useState)(null);
15439
16233
  const [uploadingRects, setUploadingRects] = (0, import_react17.useState)({});
15440
16234
  const hoveredGapRef = (0, import_react17.useRef)(null);
@@ -15697,13 +16491,6 @@ function OhhwellsBridge() {
15697
16491
  const [isItemDragging, setIsItemDragging] = (0, import_react17.useState)(false);
15698
16492
  const [isFooterFrameSelection, setIsFooterFrameSelection] = (0, import_react17.useState)(false);
15699
16493
  isFooterFrameSelectionRef.current = isFooterFrameSelection;
15700
- const [floatingPanel, setFloatingPanel] = (0, import_react17.useState)(null);
15701
- const floatingPanelOpenRef = (0, import_react17.useRef)(false);
15702
- floatingPanelOpenRef.current = floatingPanel !== null;
15703
- const [floatingPanelPos, setFloatingPanelPos] = (0, import_react17.useState)(null);
15704
- const [logoSizeDraft, setLogoSizeDraft] = (0, import_react17.useState)(null);
15705
- const [editorViewport, setEditorViewport] = (0, import_react17.useState)("desktop");
15706
- const [parentScrollSnap, setParentScrollSnap] = (0, import_react17.useState)(null);
15707
16494
  const [navDropdownPreviewOpen, setNavDropdownPreviewOpen] = (0, import_react17.useState)(null);
15708
16495
  const [footerHeadingVisible, setFooterHeadingVisible] = (0, import_react17.useState)(null);
15709
16496
  const footerDragRef = (0, import_react17.useRef)(null);
@@ -15718,7 +16505,16 @@ function OhhwellsBridge() {
15718
16505
  const addNavAfterAnchorRef = (0, import_react17.useRef)(null);
15719
16506
  const editContentRef = (0, import_react17.useRef)({});
15720
16507
  const aiSectionsRef = (0, import_react17.useRef)("");
16508
+ const brandKitRef = (0, import_react17.useRef)("");
16509
+ const stylesRef = (0, import_react17.useRef)("");
15721
16510
  const pendingDeleteUndoRef = (0, import_react17.useRef)(null);
16511
+ const [floatingPanel, setFloatingPanel] = (0, import_react17.useState)(null);
16512
+ const floatingPanelOpenRef = (0, import_react17.useRef)(false);
16513
+ const setFloatingPanelRef = (0, import_react17.useRef)(setFloatingPanel);
16514
+ const [floatingPanelPos, setFloatingPanelPos] = (0, import_react17.useState)(null);
16515
+ const [logoSizeDraft, setLogoSizeDraft] = (0, import_react17.useState)(null);
16516
+ const [editorViewport, setEditorViewport] = (0, import_react17.useState)("desktop");
16517
+ const [parentScrollSnap, setParentScrollSnap] = (0, import_react17.useState)(null);
15722
16518
  const [sitePages, setSitePages] = (0, import_react17.useState)([]);
15723
16519
  const [sectionsByPath, setSectionsByPath] = (0, import_react17.useState)({});
15724
16520
  const sectionsPrefetchGenRef = (0, import_react17.useRef)(0);
@@ -15727,7 +16523,18 @@ function OhhwellsBridge() {
15727
16523
  const linkPopoverOpenRef = (0, import_react17.useRef)(false);
15728
16524
  const linkPopoverGraceUntilRef = (0, import_react17.useRef)(0);
15729
16525
  setLinkPopoverRef.current = setLinkPopover;
16526
+ setFloatingPanelRef.current = setFloatingPanel;
15730
16527
  linkPopoverSessionRef.current = linkPopover;
16528
+ floatingPanelOpenRef.current = Boolean(floatingPanel);
16529
+ (0, import_react17.useEffect)(() => {
16530
+ const syncViewport = () => {
16531
+ const next = window.innerWidth <= 480 ? "mobile" : "desktop";
16532
+ setEditorViewport((prev) => prev === next ? prev : next);
16533
+ };
16534
+ syncViewport();
16535
+ window.addEventListener("resize", syncViewport);
16536
+ return () => window.removeEventListener("resize", syncViewport);
16537
+ }, []);
15731
16538
  const {
15732
16539
  navDragRef,
15733
16540
  navDropSlots,
@@ -17038,15 +17845,31 @@ function OhhwellsBridge() {
17038
17845
  }
17039
17846
  const applyContent = (content) => {
17040
17847
  const imageLoads = [];
17848
+ if (typeof content[BRAND_KIT_KEY] === "string") {
17849
+ brandKitRef.current = content[BRAND_KIT_KEY];
17850
+ applyBrandToDom(parseBrandKit(content[BRAND_KIT_KEY]));
17851
+ } else {
17852
+ brandKitRef.current = "";
17853
+ applyBrandToDom(null);
17854
+ }
17041
17855
  if (typeof content[AI_SECTIONS_KEY] === "string") {
17042
17856
  aiSectionsRef.current = content[AI_SECTIONS_KEY];
17857
+ setAiSectionOrder(content[SECTION_ORDER_KEY], window.location.pathname);
17043
17858
  applyAiSectionsToDom(parseAiSectionsState(content[AI_SECTIONS_KEY]));
17044
17859
  }
17860
+ if (typeof content[STYLE_STORE_KEY] === "string") {
17861
+ stylesRef.current = content[STYLE_STORE_KEY];
17862
+ applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
17863
+ }
17864
+ applyBrandChrome(content);
17045
17865
  for (const [key, val] of Object.entries(content)) {
17046
17866
  if (key === "__ohw_sections") continue;
17047
17867
  if (key === AI_SECTIONS_KEY) continue;
17048
17868
  if (key === LOGO_PLACEHOLDER_KEY) continue;
17049
17869
  if (LOGO_IMAGE_KEYS.includes(key)) continue;
17870
+ if (key === BRAND_KIT_KEY) continue;
17871
+ if (key === STYLE_STORE_KEY) continue;
17872
+ if (BRAND_CHROME_KEYS.has(key)) continue;
17050
17873
  if (applyVideoSettingNode(key, val)) continue;
17051
17874
  if (applyCarouselNode(key, val)) continue;
17052
17875
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
@@ -17112,7 +17935,9 @@ function OhhwellsBridge() {
17112
17935
  let cancelled = false;
17113
17936
  setFetchState("loading");
17114
17937
  const apiUrl = process.env.NEXT_PUBLIC_FLOWOPS_API_URL ?? "https://flowops-backend-staging-2yfdh7pwpq-as.a.run.app";
17115
- fetch(`${apiUrl}/api/public/sites/${subdomain}/content`, { cache: "no-store" }).then((r2) => r2.ok ? r2.json() : null).then((data) => {
17938
+ const initialPath = pathname;
17939
+ fetchedContentPaths.add(`${subdomain}::${initialPath}`);
17940
+ fetch(`${apiUrl}/api/public/sites/${subdomain}/content?path=${encodeURIComponent(initialPath)}`, { cache: "no-store" }).then((r2) => r2.ok ? r2.json() : null).then((data) => {
17116
17941
  if (cancelled) return;
17117
17942
  const content = data?.content ?? {};
17118
17943
  contentCache.set(subdomain, content);
@@ -17232,10 +18057,28 @@ function OhhwellsBridge() {
17232
18057
  initSectionInstancesFromContent(content, window.location.pathname);
17233
18058
  observer?.disconnect();
17234
18059
  try {
18060
+ applyBrandChrome(content);
18061
+ if (typeof content[BRAND_KIT_KEY] === "string") {
18062
+ applyBrandToDom(parseBrandKit(content[BRAND_KIT_KEY]));
18063
+ } else {
18064
+ applyBrandToDom(null);
18065
+ }
18066
+ if (typeof content[AI_SECTIONS_KEY] === "string") {
18067
+ setAiSectionOrder(content[SECTION_ORDER_KEY], window.location.pathname);
18068
+ applyAiSectionsToDom(parseAiSectionsState(content[AI_SECTIONS_KEY]));
18069
+ }
18070
+ if (typeof content[STYLE_STORE_KEY] === "string") {
18071
+ applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
18072
+ }
17235
18073
  for (const [key, val] of Object.entries(content)) {
17236
18074
  if (key === "__ohw_sections") continue;
18075
+ if (key === AI_SECTIONS_KEY) continue;
17237
18076
  if (key === LOGO_PLACEHOLDER_KEY) continue;
17238
18077
  if (LOGO_IMAGE_KEYS.includes(key)) continue;
18078
+ if (key === BRAND_KIT_KEY) continue;
18079
+ if (key === STYLE_STORE_KEY) continue;
18080
+ if (key === STYLE_STORE_KEY) continue;
18081
+ if (BRAND_CHROME_KEYS.has(key)) continue;
17239
18082
  if (applyVideoSettingNode(key, val)) continue;
17240
18083
  if (applyCarouselNode(key, val)) continue;
17241
18084
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
@@ -17281,6 +18124,17 @@ function OhhwellsBridge() {
17281
18124
  debounceTimer = setTimeout(applyFromCache, 150);
17282
18125
  };
17283
18126
  applyFromCache();
18127
+ const pathCacheKey = `${subdomain}::${pathname}`;
18128
+ if (!fetchedContentPaths.has(pathCacheKey)) {
18129
+ fetchedContentPaths.add(pathCacheKey);
18130
+ const apiUrl = process.env.NEXT_PUBLIC_FLOWOPS_API_URL ?? "https://flowops-backend-staging-2yfdh7pwpq-as.a.run.app";
18131
+ fetch(`${apiUrl}/api/public/sites/${subdomain}/content?path=${encodeURIComponent(pathname)}`, { cache: "no-store" }).then((r2) => r2.ok ? r2.json() : null).then((data) => {
18132
+ if (!data?.content) return;
18133
+ contentCache.set(subdomain, data.content);
18134
+ applyFromCache();
18135
+ }).catch(() => {
18136
+ });
18137
+ }
17284
18138
  observer = new MutationObserver(scheduleApply);
17285
18139
  observer.observe(document.body, { childList: true, subtree: true });
17286
18140
  return () => {
@@ -17376,30 +18230,31 @@ function OhhwellsBridge() {
17376
18230
  }, [isEditMode, pathname, subdomain, fetchState, resyncSelectedNavigationItem]);
17377
18231
  (0, import_react17.useEffect)(() => {
17378
18232
  if (!isEditMode) return;
18233
+ let lastPosted = 0;
17379
18234
  const measure = () => {
17380
18235
  const h = document.body.scrollHeight;
17381
- if (h > 50) postToParent2({ type: "ow:height", height: h });
18236
+ if (h > 50 && Math.abs(h - lastPosted) > 1) {
18237
+ lastPosted = h;
18238
+ postToParent2({ type: "ow:height", height: h });
18239
+ }
18240
+ };
18241
+ let raf = null;
18242
+ const schedule = () => {
18243
+ if (raf != null) return;
18244
+ raf = requestAnimationFrame(() => {
18245
+ raf = null;
18246
+ measure();
18247
+ });
17382
18248
  };
17383
18249
  const t1 = setTimeout(measure, 50);
17384
18250
  const t2 = setTimeout(measure, 500);
17385
- let lastWidth = window.innerWidth;
17386
- let resizeTimers = [];
17387
- const clearResizeTimers = () => {
17388
- resizeTimers.forEach(clearTimeout);
17389
- resizeTimers = [];
17390
- };
17391
- const handleResize = () => {
17392
- if (window.innerWidth === lastWidth) return;
17393
- lastWidth = window.innerWidth;
17394
- clearResizeTimers();
17395
- resizeTimers = HEIGHT_SETTLE_DELAYS.map((delay) => setTimeout(measure, delay));
17396
- };
17397
- window.addEventListener("resize", handleResize);
18251
+ const ro = new ResizeObserver(schedule);
18252
+ ro.observe(document.body);
17398
18253
  return () => {
17399
18254
  clearTimeout(t1);
17400
18255
  clearTimeout(t2);
17401
- clearResizeTimers();
17402
- window.removeEventListener("resize", handleResize);
18256
+ if (raf != null) cancelAnimationFrame(raf);
18257
+ ro.disconnect();
17403
18258
  };
17404
18259
  }, [pathname, isEditMode, postToParent2]);
17405
18260
  (0, import_react17.useEffect)(() => {
@@ -17640,6 +18495,7 @@ function OhhwellsBridge() {
17640
18495
  return;
17641
18496
  }
17642
18497
  const target = e.target;
18498
+ if (target.closest("[data-ohw-ai-review]")) return;
17643
18499
  if (target.closest("[data-ohw-toolbar]")) return;
17644
18500
  if (target.closest("[data-ohw-state-toggle]")) return;
17645
18501
  if (target.closest("[data-ohw-max-badge]")) return;
@@ -17651,6 +18507,9 @@ function OhhwellsBridge() {
17651
18507
  )) {
17652
18508
  return;
17653
18509
  }
18510
+ if (selectedMediaElRef.current && !selectedMediaElRef.current.contains(target) && !target.closest("[data-ohw-media-overlay], [data-ohw-edit-chrome], [data-ohw-item-interaction]")) {
18511
+ clearMediaSelectionRef.current();
18512
+ }
17654
18513
  {
17655
18514
  const formEl = getFormElement(target);
17656
18515
  const onSuccessText = target.closest(`[${SUCCESS_TEXT_ATTR}]`);
@@ -17802,19 +18661,14 @@ function OhhwellsBridge() {
17802
18661
  }
17803
18662
  const clickedButton = findClosestButtonLike(target);
17804
18663
  const buttonOnMedia = Boolean(clickedButton && isMediaEditable(editable) && editable.contains(clickedButton));
17805
- console.log("[click-debug]", {
17806
- editableType: editable.dataset.ohwEditable,
17807
- editableTag: editable.tagName,
17808
- targetTag: target.tagName,
17809
- clickedButtonTag: clickedButton?.tagName ?? null,
17810
- buttonOnMedia,
17811
- isMediaEditableEditable: isMediaEditable(editable)
17812
- });
17813
18664
  if (isMediaEditable(editable) && !buttonOnMedia) {
17814
18665
  e.preventDefault();
17815
18666
  e.stopPropagation();
17816
- aiSectionApiRef.current?.selectFromElement(editable);
17817
- postToParentRef.current({ type: "ow:image-pick", key: editable.dataset.ohwKey ?? "", elementType: editable.dataset.ohwEditable ?? "image" });
18667
+ if (selectedMediaElRef.current === editable) {
18668
+ postToParentRef.current({ type: "ow:image-pick", key: editable.dataset.ohwKey ?? "", elementType: editable.dataset.ohwEditable ?? "image" });
18669
+ } else {
18670
+ selectMediaElementRef.current(editable);
18671
+ }
17818
18672
  return;
17819
18673
  }
17820
18674
  const socialItem = getSocialItem(editable);
@@ -17833,11 +18687,6 @@ function OhhwellsBridge() {
17833
18687
  const hrefLookupTarget = buttonOnMedia ? clickedButton : editable;
17834
18688
  const hrefCtx = getHrefKeyFromElement(hrefLookupTarget);
17835
18689
  const navAnchor = hrefCtx ? getNavigationItemAnchor(hrefCtx.anchor) : null;
17836
- console.log("[click-debug 2]", {
17837
- hrefLookupTargetTag: hrefLookupTarget.tagName,
17838
- hrefCtx: hrefCtx ? { key: hrefCtx.key } : null,
17839
- navAnchorTag: navAnchor?.tagName ?? null
17840
- });
17841
18690
  if (navAnchor) {
17842
18691
  e.preventDefault();
17843
18692
  e.stopPropagation();
@@ -17955,6 +18804,7 @@ function OhhwellsBridge() {
17955
18804
  };
17956
18805
  const handleDblClick = (e) => {
17957
18806
  const target = e.target;
18807
+ if (target.closest("[data-ohw-ai-review]")) return;
17958
18808
  if (target.closest("[data-ohw-toolbar]")) return;
17959
18809
  if (target.closest("[data-ohw-state-toggle]")) return;
17960
18810
  if (target.closest("[data-ohw-max-badge]")) return;
@@ -18006,6 +18856,9 @@ function OhhwellsBridge() {
18006
18856
  setHoveredItemRect(null);
18007
18857
  hoveredNavContainerRef.current = null;
18008
18858
  setHoveredNavContainerRect(null);
18859
+ siblingHintElRef.current = null;
18860
+ setSiblingHintRect(null);
18861
+ setSiblingHintRects([]);
18009
18862
  return;
18010
18863
  }
18011
18864
  {
@@ -18124,7 +18977,6 @@ function OhhwellsBridge() {
18124
18977
  hoveredNavContainerRef.current = null;
18125
18978
  setHoveredNavContainerRect(null);
18126
18979
  hoveredItemElRef.current = editable;
18127
- setHoveredItemRect(isSelectedForHoverRef.current(editable) ? null : editable.getBoundingClientRect());
18128
18980
  }
18129
18981
  }
18130
18982
  }
@@ -18421,7 +19273,7 @@ function OhhwellsBridge() {
18421
19273
  }
18422
19274
  };
18423
19275
  const probeImageAt = (clientX, clientY, isDragOver = false, fromParentViewport = false) => {
18424
- if (linkPopoverOpenRef.current) {
19276
+ if (linkPopoverOpenRef.current || floatingPanelOpenRef.current && isPointOverFloatingPanel(clientX, clientY)) {
18425
19277
  if (hoveredImageRef.current) {
18426
19278
  hoveredImageRef.current = null;
18427
19279
  hoveredImageHasTextOverlapRef.current = false;
@@ -18755,7 +19607,9 @@ function OhhwellsBridge() {
18755
19607
  return;
18756
19608
  }
18757
19609
  const { y } = toProbeCoords(clientX, clientY, fromParentViewport);
18758
- const sections = Array.from(document.querySelectorAll("[data-ohw-section]")).sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top);
19610
+ const sections = Array.from(document.querySelectorAll("[data-ohw-section]")).filter(
19611
+ (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
19612
+ ).sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top);
18759
19613
  const ZONE = 20;
18760
19614
  for (let i = 0; i < sections.length; i++) {
18761
19615
  const a = sections[i];
@@ -18784,8 +19638,7 @@ function OhhwellsBridge() {
18784
19638
  };
18785
19639
  const handleMouseMove = (e) => {
18786
19640
  const { clientX, clientY } = e;
18787
- if (pointOwnedByFloatingPanel(clientX, clientY)) return;
18788
- if (isOverEditorChrome(clientX, clientY)) {
19641
+ if (document.documentElement.hasAttribute("data-ohw-panel-dragging") || floatingPanelOpenRef.current && isPointOverFloatingPanel(clientX, clientY) || isOverEditorChrome(clientX, clientY)) {
18789
19642
  document.querySelectorAll("[data-ohw-hovered]").forEach((el) => el.removeAttribute("data-ohw-hovered"));
18790
19643
  formHoverElRef.current = null;
18791
19644
  setFormHoverRect(null);
@@ -18793,6 +19646,12 @@ function OhhwellsBridge() {
18793
19646
  setHoveredItemRect(null);
18794
19647
  hoveredNavContainerRef.current = null;
18795
19648
  setHoveredNavContainerRect(null);
19649
+ siblingHintElRef.current = null;
19650
+ setSiblingHintRect(null);
19651
+ setSiblingHintRects([]);
19652
+ dismissImageHover();
19653
+ clearImageHover();
19654
+ setSectionGap(null);
18796
19655
  return;
18797
19656
  }
18798
19657
  if (probeSocialsRowAt(clientX, clientY)) return;
@@ -18804,7 +19663,11 @@ function OhhwellsBridge() {
18804
19663
  if (e.data?.type !== "ow:pointer-sync") return;
18805
19664
  const { clientX, clientY } = e.data;
18806
19665
  if (typeof clientX !== "number" || typeof clientY !== "number") return;
18807
- if (pointOwnedByFloatingPanel(clientX, clientY)) return;
19666
+ if (document.documentElement.hasAttribute("data-ohw-panel-dragging") || floatingPanelOpenRef.current && isPointOverFloatingPanel(clientX, clientY)) {
19667
+ dismissImageHover();
19668
+ clearImageHover();
19669
+ return;
19670
+ }
18808
19671
  if (probeSocialsRowAt(clientX, clientY)) return;
18809
19672
  probeSectionGapAt(clientX, clientY);
18810
19673
  probeImageAt(clientX, clientY);
@@ -19087,10 +19950,23 @@ function OhhwellsBridge() {
19087
19950
  if (e.data?.type !== "ow:hydrate") return;
19088
19951
  const content = e.data.content;
19089
19952
  if (!content) return;
19953
+ if (typeof content[BRAND_KIT_KEY] === "string") {
19954
+ brandKitRef.current = content[BRAND_KIT_KEY];
19955
+ applyBrandToDom(parseBrandKit(content[BRAND_KIT_KEY]));
19956
+ } else {
19957
+ brandKitRef.current = "";
19958
+ applyBrandToDom(null);
19959
+ }
19090
19960
  if (typeof content[AI_SECTIONS_KEY] === "string") {
19091
19961
  aiSectionsRef.current = content[AI_SECTIONS_KEY];
19962
+ setAiSectionOrder(content[SECTION_ORDER_KEY], window.location.pathname);
19092
19963
  applyAiSectionsToDom(parseAiSectionsState(content[AI_SECTIONS_KEY]));
19093
19964
  }
19965
+ if (typeof content[STYLE_STORE_KEY] === "string") {
19966
+ stylesRef.current = content[STYLE_STORE_KEY];
19967
+ applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
19968
+ }
19969
+ applyBrandChrome(content);
19094
19970
  let sectionsJson = null;
19095
19971
  for (const [key, val] of Object.entries(content)) {
19096
19972
  if (key === "__ohw_sections") {
@@ -19100,6 +19976,9 @@ function OhhwellsBridge() {
19100
19976
  if (key === AI_SECTIONS_KEY) continue;
19101
19977
  if (key === LOGO_PLACEHOLDER_KEY) continue;
19102
19978
  if (LOGO_IMAGE_KEYS.includes(key) && !String(val ?? "").trim()) continue;
19979
+ if (key === BRAND_KIT_KEY) continue;
19980
+ if (key === STYLE_STORE_KEY) continue;
19981
+ if (BRAND_CHROME_KEYS.has(key)) continue;
19103
19982
  if (applyVideoSettingNode(key, val)) continue;
19104
19983
  if (applyCarouselNode(key, val)) continue;
19105
19984
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
@@ -19113,6 +19992,8 @@ function OhhwellsBridge() {
19113
19992
  if (video && video.src !== val) applyVideoSrc(video, val);
19114
19993
  } else if (el.dataset.ohwEditable === "link") {
19115
19994
  applyLinkHref(el, val);
19995
+ } else if (el.dataset.ohwEditable === "icon") {
19996
+ applyIconMarkup(el, val);
19116
19997
  } else if (isIconMarkupValue(val)) {
19117
19998
  } else {
19118
19999
  el.innerHTML = val;
@@ -19197,12 +20078,21 @@ function OhhwellsBridge() {
19197
20078
  nodes: collectEditableNodes(editContentRef.current)
19198
20079
  });
19199
20080
  };
20081
+ const clearInteractionChrome = () => {
20082
+ deactivateRef.current();
20083
+ deselectRef.current();
20084
+ clearMediaSelectionRef.current();
20085
+ };
19200
20086
  const handleAiApplyTree = (e) => {
19201
20087
  if (e.data?.type !== "ow:ai-apply-tree") return;
19202
20088
  const payload = e.data.payload;
19203
20089
  if (!payload || typeof payload.id !== "string" || !isRenderableTree(payload.tree)) return;
20090
+ clearInteractionChrome();
19204
20091
  const previous = aiSectionsRef.current;
19205
- const nextState = applyTreeToState(parseAiSectionsState(previous), payload);
20092
+ const nextState = applyTreeToState(parseAiSectionsState(previous), {
20093
+ ...payload,
20094
+ path: payload.path ?? window.location.pathname
20095
+ });
19206
20096
  const nextValue = serializeAiSectionsState(nextState);
19207
20097
  aiSectionsRef.current = nextValue;
19208
20098
  applyAiSectionsToDom(nextState);
@@ -19223,6 +20113,7 @@ function OhhwellsBridge() {
19223
20113
  const exists = document.querySelector(`[data-ohw-section="${CSS.escape(sectionId)}"]`);
19224
20114
  if (!exists) return;
19225
20115
  if (isPageFrameSection(exists)) return;
20116
+ clearInteractionChrome();
19226
20117
  const previous = aiSectionsRef.current;
19227
20118
  const nextState = deleteSectionFromState(parseAiSectionsState(previous), sectionId);
19228
20119
  const nextValue = serializeAiSectionsState(nextState);
@@ -19238,8 +20129,10 @@ function OhhwellsBridge() {
19238
20129
  const handleAiSetSections = (e) => {
19239
20130
  if (e.data?.type !== "ow:ai-set-sections") return;
19240
20131
  const value = typeof e.data.value === "string" ? e.data.value : "";
20132
+ clearInteractionChrome();
19241
20133
  aiSectionsRef.current = value;
19242
20134
  applyAiSectionsToDom(parseAiSectionsState(value));
20135
+ applyStylesToDom(parseStyleStore(stylesRef.current));
19243
20136
  const restoredHeight = document.body.scrollHeight;
19244
20137
  if (restoredHeight > 50) postToParentRef.current({ type: "ow:height", height: restoredHeight });
19245
20138
  postToParentRef.current({ type: "ow:change", nodes: [{ key: AI_SECTIONS_KEY, text: value }] });
@@ -19255,10 +20148,40 @@ function OhhwellsBridge() {
19255
20148
  if (!entries) return;
19256
20149
  const orderJson = JSON.stringify(entries);
19257
20150
  editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: orderJson };
20151
+ setAiSectionOrder(orderJson, window.location.pathname);
19258
20152
  postToParentRef.current({ type: "ow:change", nodes: [{ key: SECTION_ORDER_KEY, text: orderJson }] });
19259
20153
  window.dispatchEvent(new Event("resize"));
19260
20154
  };
19261
20155
  window.addEventListener("message", handleMoveSection);
20156
+ const handleAiSetBrand = (e) => {
20157
+ if (e.data?.type !== "ow:ai-set-brand") return;
20158
+ const value = typeof e.data.value === "string" ? e.data.value : "";
20159
+ const previous = brandKitRef.current;
20160
+ brandKitRef.current = value;
20161
+ applyBrandToDom(parseBrandKit(value));
20162
+ if (aiSectionsRef.current) applyAiSectionsToDom(parseAiSectionsState(aiSectionsRef.current));
20163
+ applyStylesToDom(parseStyleStore(stylesRef.current));
20164
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: BRAND_KIT_KEY, text: value }] });
20165
+ postToParentRef.current({ type: "ow:ai-brand-applied", previous, value });
20166
+ };
20167
+ window.addEventListener("message", handleAiSetBrand);
20168
+ const handleAiSetStyles = (e) => {
20169
+ if (e.data?.type !== "ow:ai-set-styles") return;
20170
+ const value = typeof e.data.value === "string" ? e.data.value : "";
20171
+ const previous = stylesRef.current;
20172
+ stylesRef.current = value;
20173
+ applyStylesToDom(parseStyleStore(value));
20174
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: STYLE_STORE_KEY, text: value }] });
20175
+ postToParentRef.current({ type: "ow:ai-styles-applied", previous, value });
20176
+ };
20177
+ window.addEventListener("message", handleAiSetStyles);
20178
+ const handleGetBrand = (e) => {
20179
+ if (e.data?.type !== "ow:get-brand") return;
20180
+ const template = deriveTemplateBrand();
20181
+ const value = brandKitRef.current || (template ? JSON.stringify(template) : "");
20182
+ postToParentRef.current({ type: "ow:brand-value", value });
20183
+ };
20184
+ window.addEventListener("message", handleGetBrand);
19262
20185
  const handlePanelDragging = (e) => {
19263
20186
  if (e.data?.type !== "ow:panel-dragging") return;
19264
20187
  if (e.data.dragging) document.documentElement.setAttribute("data-ohw-panel-dragging", "");
@@ -19316,8 +20239,15 @@ function OhhwellsBridge() {
19316
20239
  closeLinkPopoverRef.current();
19317
20240
  return;
19318
20241
  }
20242
+ if (floatingPanelOpenRef.current) {
20243
+ setFloatingPanelRef.current(null);
20244
+ deselectRef.current();
20245
+ deactivateRef.current();
20246
+ return;
20247
+ }
19319
20248
  deselectRef.current();
19320
20249
  deactivateRef.current();
20250
+ clearMediaSelectionRef.current();
19321
20251
  };
19322
20252
  window.addEventListener("message", handleDeactivate);
19323
20253
  const handleToastAction = (e) => {
@@ -19403,6 +20333,10 @@ function OhhwellsBridge() {
19403
20333
  const handleKeyDown = (e) => {
19404
20334
  if (e.key === "Escape" && document.querySelector("[data-ohw-section-picker]")) return;
19405
20335
  if (e.key === "Escape" && document.querySelector("[data-ohw-more-menu]")) return;
20336
+ if (e.key === "Escape" && selectedMediaElRef.current) {
20337
+ clearMediaSelectionRef.current();
20338
+ return;
20339
+ }
19406
20340
  if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "a" && activeElRef.current) {
19407
20341
  e.preventDefault();
19408
20342
  selectAllTextInEditable(activeElRef.current);
@@ -19562,6 +20496,12 @@ function OhhwellsBridge() {
19562
20496
  if (aiSectionsRef.current) {
19563
20497
  nodes.push({ key: AI_SECTIONS_KEY, type: "sections", text: aiSectionsRef.current });
19564
20498
  }
20499
+ if (stylesRef.current) {
20500
+ nodes.push({ key: STYLE_STORE_KEY, type: "styles", text: stylesRef.current });
20501
+ }
20502
+ if (brandKitRef.current) {
20503
+ nodes.push({ key: BRAND_KIT_KEY, type: "brand", text: brandKitRef.current });
20504
+ }
19565
20505
  document.querySelectorAll('[data-ohw-editable="form"]').forEach((form) => {
19566
20506
  const formKey = formKeyOf(form);
19567
20507
  if (!formKey) return;
@@ -19579,8 +20519,12 @@ function OhhwellsBridge() {
19579
20519
  if (inserted) {
19580
20520
  const tracker = getSectionsTracker();
19581
20521
  postToParentRef.current({ type: "ow:change", nodes: [{ key: "__ohw_sections", text: tracker.textContent ?? "[]" }] });
19582
- const h = document.body.scrollHeight;
19583
- if (h > 50) postToParentRef.current({ type: "ow:height", height: h });
20522
+ const reportHeight = () => {
20523
+ const h = document.body.scrollHeight;
20524
+ if (h > 50) postToParentRef.current({ type: "ow:height", height: h });
20525
+ };
20526
+ reportHeight();
20527
+ setTimeout(reportHeight, 500);
19584
20528
  }
19585
20529
  };
19586
20530
  const handleSwitchSchedule = (e) => {
@@ -19977,13 +20921,16 @@ function OhhwellsBridge() {
19977
20921
  window.removeEventListener("message", handleAiDeleteSection);
19978
20922
  window.removeEventListener("message", handleAiSetSections);
19979
20923
  window.removeEventListener("message", handleMoveSection);
20924
+ window.removeEventListener("message", handleAiSetBrand);
20925
+ window.removeEventListener("message", handleAiSetStyles);
20926
+ window.removeEventListener("message", handleGetBrand);
19980
20927
  window.removeEventListener("message", handlePanelDragging);
19981
20928
  window.removeEventListener("message", handleDeleteSection);
19982
20929
  window.removeEventListener("message", handleDeactivate);
19983
- document.documentElement.removeAttribute("data-ohw-panel-dragging");
19984
20930
  window.removeEventListener("message", handleToastAction);
19985
20931
  window.removeEventListener("message", handleFormCount);
19986
20932
  window.removeEventListener("message", handleUiEscape);
20933
+ document.documentElement.removeAttribute("data-ohw-panel-dragging");
19987
20934
  autoSaveTimers.current.forEach(clearTimeout);
19988
20935
  autoSaveTimers.current.clear();
19989
20936
  if (imageUnhoverTimerRef.current) clearTimeout(imageUnhoverTimerRef.current);
@@ -20186,7 +21133,7 @@ function OhhwellsBridge() {
20186
21133
  postToParent2({
20187
21134
  type: "ow:ready",
20188
21135
  version: "1",
20189
- bridgeVersion: "0.1.75",
21136
+ bridgeVersion: "0.1.77",
20190
21137
  path: pathname,
20191
21138
  nodes: collectEditableNodes(editContentRef.current),
20192
21139
  sections
@@ -20593,11 +21540,22 @@ function OhhwellsBridge() {
20593
21540
  const showEditLink = toolbarShowEditLink;
20594
21541
  const currentSections = sectionsByPath[pathname] ?? [];
20595
21542
  linkPopoverOpenRef.current = linkPopover !== null;
21543
+ const handleMediaSelect = (0, import_react17.useCallback)((key) => {
21544
+ const el = hoveredImageRef.current?.dataset.ohwKey === key ? hoveredImageRef.current : Array.from(document.querySelectorAll(MEDIA_SELECTOR)).find(
21545
+ (m) => (m.dataset.ohwKey ?? "") === key
21546
+ ) ?? null;
21547
+ if (!el) return;
21548
+ selectMediaElementRef.current(el);
21549
+ }, []);
20596
21550
  const handleMediaReplace = (0, import_react17.useCallback)(
20597
21551
  (key) => {
20598
- postToParent2({ type: "ow:image-pick", key, elementType: mediaHover?.elementType ?? "image" });
21552
+ postToParent2({
21553
+ type: "ow:image-pick",
21554
+ key,
21555
+ elementType: mediaHover?.elementType ?? selectedMedia?.elementType ?? "image"
21556
+ });
20599
21557
  },
20600
- [postToParent2, mediaHover?.elementType]
21558
+ [postToParent2, mediaHover?.elementType, selectedMedia?.elementType]
20601
21559
  );
20602
21560
  const handleEditCarousel = (0, import_react17.useCallback)(
20603
21561
  (key) => {
@@ -20669,12 +21627,25 @@ function OhhwellsBridge() {
20669
21627
  },
20670
21628
  `uploading-${key}`
20671
21629
  )),
20672
- mediaHover && !(mediaHover.key in uploadingRects) && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
21630
+ mediaHover && !(mediaHover.key in uploadingRects) && mediaHover.key !== selectedMedia?.key && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
20673
21631
  MediaOverlay,
20674
21632
  {
20675
21633
  hover: mediaHover,
20676
21634
  isUploading: false,
20677
21635
  onReplace: handleMediaReplace,
21636
+ onSelect: handleMediaSelect,
21637
+ onVideoSettingsChange: handleVideoSettingsChange
21638
+ }
21639
+ ),
21640
+ selectedMedia && !(selectedMedia.key in uploadingRects) && !linkPopover && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
21641
+ MediaOverlay,
21642
+ {
21643
+ hover: selectedMedia,
21644
+ selected: true,
21645
+ hovered: mediaHover?.key === selectedMedia.key,
21646
+ isUploading: false,
21647
+ onReplace: handleMediaReplace,
21648
+ onSelect: handleMediaSelect,
20678
21649
  onVideoSettingsChange: handleVideoSettingsChange
20679
21650
  }
20680
21651
  ),
@@ -21080,6 +22051,59 @@ function OhhwellsBridge() {
21080
22051
  ) : null
21081
22052
  ] });
21082
22053
  }
22054
+
22055
+ // src/ui/EmptySection.tsx
22056
+ var import_link = __toESM(require("next/link"), 1);
22057
+ var import_jsx_runtime34 = require("react/jsx-runtime");
22058
+ function EmptySection({ title, homeHref = "/", eyebrowKey, titleKey, subtitleKey }) {
22059
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(import_jsx_runtime34.Fragment, { children: [
22060
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
22061
+ "p",
22062
+ {
22063
+ style: {
22064
+ fontFamily: "var(--brand-font-body)",
22065
+ fontSize: "0.75rem",
22066
+ fontWeight: 500,
22067
+ letterSpacing: "0.15em",
22068
+ textTransform: "uppercase",
22069
+ color: "var(--brand-accent)",
22070
+ marginBottom: "1.5rem"
22071
+ },
22072
+ children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_link.default, { href: homeHref, style: { color: "inherit" }, children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { ...eyebrowKey ? { "data-ohw-editable": "text", "data-ohw-key": eyebrowKey } : {}, children: "Home" }) })
22073
+ }
22074
+ ),
22075
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
22076
+ "h1",
22077
+ {
22078
+ style: {
22079
+ fontFamily: "var(--brand-font-heading)",
22080
+ fontSize: "clamp(2rem, 3.5vw, 2.75rem)",
22081
+ lineHeight: 1.1,
22082
+ letterSpacing: "-0.025em",
22083
+ color: "var(--brand-text)",
22084
+ marginBottom: "1rem"
22085
+ },
22086
+ ...titleKey ? { "data-ohw-editable": "text", "data-ohw-key": titleKey, "data-ohw-max-length": 80 } : {},
22087
+ children: title
22088
+ }
22089
+ ),
22090
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
22091
+ "p",
22092
+ {
22093
+ style: {
22094
+ fontFamily: "var(--brand-font-body)",
22095
+ fontSize: "1rem",
22096
+ lineHeight: 1.7,
22097
+ fontWeight: 300,
22098
+ color: "var(--brand-text-muted)",
22099
+ maxWidth: "340px"
22100
+ },
22101
+ ...subtitleKey ? { "data-ohw-editable": "text", "data-ohw-key": subtitleKey, "data-ohw-max-length": 160 } : {},
22102
+ children: "This page doesn't have any content yet."
22103
+ }
22104
+ )
22105
+ ] });
22106
+ }
21083
22107
  // Annotate the CommonJS export names for ESM import in node:
21084
22108
  0 && (module.exports = {
21085
22109
  AI_DEFAULT_BRAND,
@@ -21097,6 +22121,7 @@ function OhhwellsBridge() {
21097
22121
  DropdownMenuItem,
21098
22122
  DropdownMenuSeparator,
21099
22123
  DropdownMenuTrigger,
22124
+ EmptySection,
21100
22125
  ItemActionToolbar,
21101
22126
  ItemInteractionLayer,
21102
22127
  LinkEditorPanel,