@ohhwells/bridge 0.1.77 → 0.1.78-next.234

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
@@ -1568,6 +2273,7 @@ function EmailCaptureModal({ title, subtitle, onSubmit, onClose }) {
1568
2273
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1569
2274
  import_radix_ui.Dialog.Overlay,
1570
2275
  {
2276
+ "data-ohw-scheduling-modal": "",
1571
2277
  className: "fixed inset-0 z-50",
1572
2278
  style: { background: "rgba(0,0,0,0.45)" }
1573
2279
  }
@@ -1575,6 +2281,7 @@ function EmailCaptureModal({ title, subtitle, onSubmit, onClose }) {
1575
2281
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1576
2282
  import_radix_ui.Dialog.Content,
1577
2283
  {
2284
+ "data-ohw-scheduling-modal": "",
1578
2285
  className: "fixed left-1/2 top-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2 bg-white rounded-xl shadow-xl outline-none font-body box-border overflow-hidden",
1579
2286
  style: { maxWidth: 400 },
1580
2287
  children: [
@@ -2048,7 +2755,7 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAf
2048
2755
  const autoId = (0, import_react5.useId)();
2049
2756
  const insertAfter = insertAfterProp ?? autoId;
2050
2757
  const [schedule, setSchedule] = (0, import_react5.useState)(null);
2051
- const [loading, setLoading] = (0, import_react5.useState)(true);
2758
+ const [loading, setLoading] = (0, import_react5.useState)(initialScheduleId !== null);
2052
2759
  const [inEditor, setInEditor] = (0, import_react5.useState)(false);
2053
2760
  const [isHovered, setIsHovered] = (0, import_react5.useState)(false);
2054
2761
  const [modalState, setModalState] = (0, import_react5.useState)(null);
@@ -2222,8 +2929,10 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAf
2222
2929
  "*"
2223
2930
  );
2224
2931
  };
2225
- if (!inEditor && !loading && !schedule) return null;
2226
2932
  const sectionId = `scheduling-${insertAfter}`;
2933
+ if (!inEditor && !loading && !schedule) {
2934
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("section", { "data-ohw-section": sectionId, "data-ohw-scheduling-anchor": insertAfter, style: { display: "none" } });
2935
+ }
2227
2936
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2228
2937
  "section",
2229
2938
  {
@@ -7140,13 +7849,17 @@ function MediaOverlay({
7140
7849
  hover,
7141
7850
  isUploading,
7142
7851
  fadingOut = false,
7852
+ selected = false,
7853
+ hovered = false,
7143
7854
  onFadeOutComplete,
7144
7855
  onReplace,
7856
+ onSelect,
7145
7857
  onVideoSettingsChange
7146
7858
  }) {
7147
7859
  const { rect } = hover;
7148
7860
  const skeletonRef = React8.useRef(null);
7149
7861
  const isVideo = hover.elementType === "video";
7862
+ const showChrome = !selected || hovered;
7150
7863
  const autoplay = hover.videoAutoplay ?? true;
7151
7864
  const muted = hover.videoMuted ?? true;
7152
7865
  const probeRef = React8.useRef(null);
@@ -7160,6 +7873,7 @@ function MediaOverlay({
7160
7873
  (prev) => prev && prev.fullWidth === width && prev.height === height ? prev : { fullWidth: width, height }
7161
7874
  );
7162
7875
  }, [isVideo]);
7876
+ const replaceLabel = isVideo ? "Replace video" : hover.elementType === "bg-image" ? "Replace background" : "Replace image";
7163
7877
  const replaceMode = buttonSize ? replaceButtonMode({ width: rect.width, height: rect.height }, buttonSize) : "none";
7164
7878
  const box = {
7165
7879
  position: "fixed",
@@ -7193,7 +7907,7 @@ function MediaOverlay({
7193
7907
  }
7194
7908
  );
7195
7909
  }
7196
- const settingsBar = isVideo && !hover.isDragOver ? /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
7910
+ const settingsBar = isVideo && !hover.isDragOver && showChrome ? /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
7197
7911
  "div",
7198
7912
  {
7199
7913
  "data-ohw-bridge": "",
@@ -7263,10 +7977,12 @@ function MediaOverlay({
7263
7977
  // in-document, pointer-events does it natively. The button below opts back in, so
7264
7978
  // Replace still works.
7265
7979
  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)"
7980
+ // Selected: a firm component ring with no wash, so the image reads as chosen rather
7981
+ // than hovered. Hover keeps the existing tinted preview.
7982
+ boxShadow: selected ? "inset 0 0 0 2px var(--color-primary)" : "inset 0 0 0 1.5px var(--color-primary)",
7983
+ background: selected ? "transparent" : "color-mix(in srgb, var(--color-primary) 20%, transparent)"
7268
7984
  },
7269
- onClick: () => onReplace(hover.key),
7985
+ onClick: () => onSelect && !selected ? onSelect(hover.key) : onReplace(hover.key),
7270
7986
  children: [
7271
7987
  /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
7272
7988
  Button,
@@ -7287,17 +8003,17 @@ function MediaOverlay({
7287
8003
  },
7288
8004
  children: [
7289
8005
  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"
8006
+ replaceLabel
7291
8007
  ]
7292
8008
  }
7293
8009
  ),
7294
- replaceMode === "none" ? null : /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
8010
+ showChrome && replaceMode !== "none" && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
7295
8011
  Button,
7296
8012
  {
7297
8013
  "data-ohw-media-overlay": "",
7298
8014
  variant: "outline",
7299
8015
  size: "sm",
7300
- "aria-label": isVideo ? "Replace video" : "Replace image",
8016
+ "aria-label": replaceLabel,
7301
8017
  className: "gap-1.5 cursor-pointer hover:bg-background",
7302
8018
  style: {
7303
8019
  ...OVERLAY_BUTTON_STYLE,
@@ -7320,7 +8036,7 @@ function MediaOverlay({
7320
8036
  },
7321
8037
  children: [
7322
8038
  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
8039
+ replaceMode === "full" ? replaceLabel : null
7324
8040
  ]
7325
8041
  }
7326
8042
  )
@@ -7404,6 +8120,8 @@ function parseSectionsFromRoot(root) {
7404
8120
  const id = el.getAttribute("data-ohw-section") ?? "";
7405
8121
  if (!id || seen.has(id) || LINK_PICKER_EXCLUDED_IDS.has(id)) continue;
7406
8122
  if (el.parentElement?.closest("[data-ohw-section]")) continue;
8123
+ if (el.hasAttribute("data-ohw-ai-template-hidden") || el.hasAttribute("data-ohw-ai-removed") || el.hasAttribute("data-ohw-ai-replaced-by"))
8124
+ continue;
7407
8125
  seen.add(id);
7408
8126
  const label = el.getAttribute("data-ohw-section-label") ?? titleCaseSectionId(id);
7409
8127
  sections.push({ id, label });
@@ -7433,6 +8151,10 @@ function topLevelSections() {
7433
8151
  function instanceIdOf(el) {
7434
8152
  return el.dataset.ohwInstance ?? el.dataset.ohwSection ?? "";
7435
8153
  }
8154
+ function findByInstanceId(instanceId) {
8155
+ const escapedId = CSS.escape(instanceId);
8156
+ return document.querySelector(`[data-ohw-instance="${escapedId}"]`) ?? document.querySelector(`[data-ohw-section="${escapedId}"]:not([data-ohw-instance])`);
8157
+ }
7436
8158
  function planSectionMove(instanceId, targetIndex, currentPath) {
7437
8159
  const sections = topLevelSections();
7438
8160
  const index = sections.findIndex((el) => instanceIdOf(el) === instanceId);
@@ -7468,7 +8190,7 @@ function syncRemovedFlags(entries) {
7468
8190
  }
7469
8191
  });
7470
8192
  for (const id of removedIds) {
7471
- const el = document.querySelector(`[data-ohw-instance="${CSS.escape(id)}"]`);
8193
+ const el = findByInstanceId(id);
7472
8194
  if (el) {
7473
8195
  el.style.display = "none";
7474
8196
  el.setAttribute(REMOVED_ATTR2, "");
@@ -7496,9 +8218,7 @@ function applyPersistedOrder(entries) {
7496
8218
  }
7497
8219
  }
7498
8220
  function setSectionRemoved(instanceId, currentPath, existingEntries, removed) {
7499
- const escapedId = CSS.escape(instanceId);
7500
- if (!document.querySelector(`[data-ohw-instance="${escapedId}"]`) && !document.querySelector(`[data-ohw-section="${escapedId}"]:not([data-ohw-instance])`))
7501
- return null;
8221
+ if (!findByInstanceId(instanceId)) return null;
7502
8222
  const byId = new Map(existingEntries.map((e) => [e.instanceId, e]));
7503
8223
  const allSections = Array.from(document.querySelectorAll("[data-ohw-section]")).filter(
7504
8224
  (el) => !el.parentElement?.closest("[data-ohw-section]") && !isChromeSection(el)
@@ -7687,6 +8407,7 @@ function AiSectionOverlay({
7687
8407
  }) {
7688
8408
  const [selectedId, setSelectedId] = (0, import_react8.useState)(null);
7689
8409
  const [reviewId, setReviewId] = (0, import_react8.useState)(null);
8410
+ const [reviewButtonsHidden, setReviewButtonsHidden] = (0, import_react8.useState)(false);
7690
8411
  const reviewIdRef = (0, import_react8.useRef)(null);
7691
8412
  reviewIdRef.current = reviewId;
7692
8413
  const selectedIdRef = (0, import_react8.useRef)(null);
@@ -7748,6 +8469,7 @@ function AiSectionOverlay({
7748
8469
  }
7749
8470
  const found = readRect(sectionId) != null;
7750
8471
  setReviewId(found ? sectionId : null);
8472
+ setReviewButtonsHidden(e.data.hideButtons === true);
7751
8473
  postToParent2({ type: "ow:ai-review-started", sectionId, found });
7752
8474
  if (found) {
7753
8475
  document.querySelector(`[data-ohw-instance="${CSS.escape(sectionId)}"]`)?.scrollIntoView({ behavior: "smooth", block: "nearest" });
@@ -7875,13 +8597,16 @@ function AiSectionOverlay({
7875
8597
  border: `2px solid ${PRIMARY2}`,
7876
8598
  borderRadius: edgeAwareRadius(reviewRect),
7877
8599
  zIndex: 2147483200,
7878
- // The veil itself: swallows clicks so the section stays locked until decided.
8600
+ // The veil itself: swallows clicks so the section stays locked until decided. This
8601
+ // stopPropagation only guards the bubble phase; the bridge's capture-phase click
8602
+ // handlers additionally bail on `[data-ohw-ai-review]`, without which a click on
8603
+ // Accept/Discard resolves to the media beneath and opens the file picker.
7879
8604
  background: "rgba(8, 133, 254, 0.04)",
7880
8605
  pointerEvents: "auto",
7881
8606
  cursor: "default"
7882
8607
  },
7883
8608
  onClick: (e) => e.stopPropagation(),
7884
- children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
8609
+ children: !reviewButtonsHidden && /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
7885
8610
  "div",
7886
8611
  {
7887
8612
  style: {
@@ -10448,8 +11173,13 @@ function referenceBox(slot) {
10448
11173
  const neighbour = row ? Array.from(row.querySelectorAll('[data-ohw-editable="icon"]')).find(
10449
11174
  (el) => el !== slot && !el.hasAttribute("data-ohw-social-icon-placeholder")
10450
11175
  ) : null;
10451
- const source = (neighbour ?? slot).querySelector(GLYPH_SELECTOR);
10452
- const box = source?.getBoundingClientRect() ?? null;
11176
+ if (neighbour) {
11177
+ const box2 = neighbour.querySelector(GLYPH_SELECTOR)?.getBoundingClientRect();
11178
+ if (box2?.width && box2.height) return box2;
11179
+ }
11180
+ const own = slot.getBoundingClientRect();
11181
+ if (own.width && own.height) return own;
11182
+ const box = slot.querySelector(GLYPH_SELECTOR)?.getBoundingClientRect() ?? null;
10453
11183
  return box?.width && box.height ? box : null;
10454
11184
  }
10455
11185
  function iconMarkupSizedFor(slot, markup) {
@@ -12250,6 +12980,7 @@ function readLogoSizeState(content, placement) {
12250
12980
  function getLogoElement(el) {
12251
12981
  const marked = el.closest('[data-ohw-role="logo"], [data-ohw-logo]');
12252
12982
  if (marked) return marked;
12983
+ if (el.closest('[data-ohw-editable="icon"]')) return null;
12253
12984
  const root = el.closest("nav, [data-ohw-nav-root], footer");
12254
12985
  if (!root) return null;
12255
12986
  const anchor = el.closest("a");
@@ -13318,6 +14049,7 @@ function useSectionDrag({
13318
14049
  }
13319
14050
  const orderJson = JSON.stringify(entries);
13320
14051
  editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: orderJson };
14052
+ setAiSectionOrder(orderJson, window.location.pathname);
13321
14053
  postToParentRef.current({ type: "ow:change", nodes: [{ key: SECTION_ORDER_KEY, text: orderJson }] });
13322
14054
  applyPersistedOrder(entries);
13323
14055
  clearSectionDragVisuals();
@@ -14177,21 +14909,10 @@ function parseSchedulingInsertAfter(insertAfter) {
14177
14909
  insertBefore: insertAfter.slice(idx + INSERT_BEFORE_MARKER.length)
14178
14910
  };
14179
14911
  }
14180
- function resolveSchedulingInsert(insertAfter, explicitInsertBefore) {
14181
- const parsed = parseSchedulingInsertAfter(insertAfter);
14182
- const insertBefore = explicitInsertBefore ?? parsed.insertBefore;
14183
- const effectiveInsertAfter = insertBefore && parsed.insertBefore === null ? `${insertAfter}${INSERT_BEFORE_MARKER}${insertBefore}` : insertAfter;
14184
- return { effectiveInsertAfter, insertBefore };
14185
- }
14186
- function getSchedulingMountPoint(insertAfter) {
14187
- const { anchor } = parseSchedulingInsertAfter(insertAfter);
14188
- let anchorEl = document.querySelector(`[data-ohw-section="${anchor}"]`);
14189
- if (!anchorEl && anchor === "scheduling") {
14190
- const widgets = Array.from(document.querySelectorAll('[data-ohw-section^="scheduling-"]'));
14191
- anchorEl = widgets.at(-1) ?? null;
14192
- }
14193
- if (!anchorEl) return null;
14194
- return anchorEl.closest('[data-ohw-section-container="scheduling"]') ?? anchorEl;
14912
+ function resolveEntryAnchor(entry) {
14913
+ if (entry.anchorId !== void 0) return { anchorId: entry.anchorId, beforeId: entry.beforeId ?? null };
14914
+ const parsed = parseSchedulingInsertAfter(entry.insertAfter);
14915
+ return { anchorId: parsed.anchor, beforeId: parsed.insertBefore };
14195
14916
  }
14196
14917
  function schedulingMountDepth(insertAfter) {
14197
14918
  if (!insertAfter.includes(INSERT_BEFORE_MARKER)) return 0;
@@ -14208,8 +14929,7 @@ function getPageSchedulingEntries(raw) {
14208
14929
  }
14209
14930
  }
14210
14931
  function isSchedulingWidgetMissing(entry) {
14211
- const { effectiveInsertAfter } = resolveSchedulingInsert(entry.insertAfter);
14212
- return !document.querySelector(`[data-ohw-section="${schedulingSectionId(effectiveInsertAfter)}"]`);
14932
+ return !document.querySelector(`[data-ohw-section="${schedulingSectionId(entry.insertAfter)}"]`);
14213
14933
  }
14214
14934
  function hasMissingSchedulingWidgets(entries) {
14215
14935
  return entries.some(isSchedulingWidgetMissing);
@@ -14239,16 +14959,17 @@ function initSectionsFromContent(content, removeExisting = false) {
14239
14959
  } catch {
14240
14960
  }
14241
14961
  }
14242
- function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId, explicitInsertBefore) {
14243
- const { effectiveInsertAfter, insertBefore } = resolveSchedulingInsert(insertAfter, explicitInsertBefore);
14244
- const sectionId = schedulingSectionId(effectiveInsertAfter);
14962
+ function mountSchedulingWidget(anchorId, notifyOnConnect = false, scheduleId, beforeId, existingWidgetId) {
14963
+ const widgetId = existingWidgetId ?? (beforeId ? `${anchorId}${INSERT_BEFORE_MARKER}${beforeId}` : anchorId);
14964
+ const sectionId = schedulingSectionId(widgetId);
14245
14965
  if (document.querySelector(`[data-ohw-section="${sectionId}"]`)) return false;
14246
- const mountPoint = getSchedulingMountPoint(effectiveInsertAfter);
14247
- if (!mountPoint) return false;
14966
+ const anchorEl = document.querySelector(`[data-ohw-section="${anchorId}"]`);
14967
+ if (!anchorEl) return false;
14968
+ const mountPoint = anchorEl.closest('[data-ohw-section-container="scheduling"]') ?? anchorEl;
14248
14969
  const container = document.createElement("div");
14249
14970
  container.dataset.ohwSectionContainer = "scheduling";
14250
- if (insertBefore) {
14251
- const beforeAnchor = document.querySelector(`[data-ohw-section="${insertBefore}"]`);
14971
+ if (beforeId) {
14972
+ const beforeAnchor = document.querySelector(`[data-ohw-section="${beforeId}"]`);
14252
14973
  const beforePoint = beforeAnchor?.closest('[data-ohw-section-container="scheduling"]') ?? beforeAnchor;
14253
14974
  if (!beforePoint) return false;
14254
14975
  beforePoint.insertAdjacentElement("beforebegin", container);
@@ -14259,19 +14980,25 @@ function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId,
14259
14980
  }
14260
14981
  tail.insertAdjacentElement("afterend", container);
14261
14982
  }
14262
- const root = (0, import_client2.createRoot)(container);
14263
- (0, import_react_dom3.flushSync)(() => {
14264
- root.render(
14265
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
14266
- SchedulingWidget,
14267
- {
14268
- notifyOnConnect,
14269
- initialScheduleId: scheduleId,
14270
- insertAfter: effectiveInsertAfter
14271
- }
14272
- )
14273
- );
14274
- });
14983
+ try {
14984
+ const root = (0, import_client2.createRoot)(container);
14985
+ (0, import_react_dom3.flushSync)(() => {
14986
+ root.render(
14987
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
14988
+ SchedulingWidget,
14989
+ {
14990
+ notifyOnConnect,
14991
+ initialScheduleId: scheduleId,
14992
+ insertAfter: widgetId
14993
+ }
14994
+ )
14995
+ );
14996
+ });
14997
+ } catch (err) {
14998
+ console.error("[ow:scheduling] render threw", err);
14999
+ container.remove();
15000
+ return false;
15001
+ }
14275
15002
  const tracker = getSectionsTracker();
14276
15003
  let sections = [];
14277
15004
  try {
@@ -14279,10 +15006,12 @@ function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId,
14279
15006
  } catch {
14280
15007
  }
14281
15008
  const inEditor = typeof window !== "undefined" && window.self !== window.top;
14282
- if (inEditor && !sections.find((s) => s.type === "scheduling" && s.insertAfter === effectiveInsertAfter)) {
15009
+ if (inEditor && !sections.find((s) => s.type === "scheduling" && s.insertAfter === widgetId)) {
14283
15010
  sections.push({
14284
15011
  type: "scheduling",
14285
- insertAfter: effectiveInsertAfter,
15012
+ insertAfter: widgetId,
15013
+ anchorId,
15014
+ beforeId: beforeId ?? null,
14286
15015
  pagePath: window.location.pathname,
14287
15016
  ...scheduleId ? { scheduleId } : {}
14288
15017
  });
@@ -14296,7 +15025,8 @@ function mountSchedulingEntries(entries, notifyOnConnect = false) {
14296
15025
  for (let i = pending.length - 1; i >= 0; i--) {
14297
15026
  const entry = pending[i];
14298
15027
  const shouldNotify = typeof notifyOnConnect === "function" ? notifyOnConnect(entry) : notifyOnConnect;
14299
- if (mountSchedulingWidget(entry.insertAfter, shouldNotify, entry.scheduleId)) {
15028
+ const { anchorId, beforeId } = resolveEntryAnchor(entry);
15029
+ if (mountSchedulingWidget(anchorId, shouldNotify, entry.scheduleId ?? null, beforeId, entry.insertAfter)) {
14300
15030
  pending.splice(i, 1);
14301
15031
  }
14302
15032
  }
@@ -14454,6 +15184,11 @@ function applyLinkByKey(key, val) {
14454
15184
  hrefAnchors.forEach((el) => applyLinkHref(el, val));
14455
15185
  }
14456
15186
  }
15187
+ function isInsideLinkEditor(target) {
15188
+ return Boolean(
15189
+ 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"]')
15190
+ );
15191
+ }
14457
15192
  function isInsideFloatingPanel(target) {
14458
15193
  return Boolean(target.closest("[data-ohw-floating-panel]"));
14459
15194
  }
@@ -14461,11 +15196,6 @@ function isPointOverFloatingPanel(clientX, clientY) {
14461
15196
  const el = document.elementFromPoint(clientX, clientY);
14462
15197
  return Boolean(el instanceof Element && el.closest("[data-ohw-floating-panel]"));
14463
15198
  }
14464
- function isInsideLinkEditor(target) {
14465
- return Boolean(
14466
- 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"]')
14467
- );
14468
- }
14469
15199
  function getHrefKeyFromElement(el) {
14470
15200
  if (!el) return null;
14471
15201
  const anchor = el.closest("[data-ohw-href-key]");
@@ -14724,7 +15454,7 @@ function getNavigationSelectionParent(el) {
14724
15454
  if (!isFooterLinksContainer(el) && (el.hasAttribute("data-ohw-footer-col") || el.hasAttribute("data-ohw-footer-column") || isInferredFooterGroup2(el))) {
14725
15455
  return getFooterLinksContainer();
14726
15456
  }
14727
- if (el.hasAttribute("data-ohw-nav-container") || el.hasAttribute("data-ohw-nav-drawer") || isFooterLinksContainer(el)) {
15457
+ 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)) {
14728
15458
  return getNavigationRoot(el);
14729
15459
  }
14730
15460
  return null;
@@ -14939,7 +15669,6 @@ var ICONS = {
14939
15669
  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"/>',
14940
15670
  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"/>'
14941
15671
  };
14942
- var HEIGHT_SETTLE_DELAYS = [150, 400, 800];
14943
15672
  var SELECTION_CHROME_GAP2 = 4;
14944
15673
  var TOOLBAR_STROKE_GAP2 = 4;
14945
15674
  var TOOLBAR_OFFSET_FROM_ELEMENT = SELECTION_CHROME_GAP2 + TOOLBAR_STROKE_GAP2;
@@ -15319,6 +16048,7 @@ function StateToggle({
15319
16048
  );
15320
16049
  }
15321
16050
  var contentCache = /* @__PURE__ */ new Map();
16051
+ var fetchedContentPaths = /* @__PURE__ */ new Set();
15322
16052
  var OHW_LOADER_STYLE = {
15323
16053
  position: "fixed",
15324
16054
  inset: 0,
@@ -15437,6 +16167,70 @@ function OhhwellsBridge() {
15437
16167
  const hoveredImageHasTextOverlapRef = (0, import_react17.useRef)(false);
15438
16168
  const dragOverElRef = (0, import_react17.useRef)(null);
15439
16169
  const [mediaHover, setMediaHover] = (0, import_react17.useState)(null);
16170
+ const [selectedMedia, setSelectedMedia] = (0, import_react17.useState)(null);
16171
+ const selectedMediaElRef = (0, import_react17.useRef)(null);
16172
+ const clearMediaSelection = (0, import_react17.useCallback)(() => {
16173
+ const prev = selectedMediaElRef.current;
16174
+ selectedMediaElRef.current = null;
16175
+ setSelectedMedia(null);
16176
+ const sectionEl = prev?.closest("[data-ohw-section]") ?? null;
16177
+ if (sectionEl) {
16178
+ postToParentRef.current({
16179
+ type: "ow:section-selected",
16180
+ sectionId: sectionEl.dataset.ohwSection ?? null,
16181
+ sectionLabel: sectionEl.dataset.ohwSectionLabel ?? titleCaseSectionId(sectionEl.dataset.ohwSection ?? ""),
16182
+ key: null
16183
+ });
16184
+ }
16185
+ }, []);
16186
+ const clearMediaSelectionRef = (0, import_react17.useRef)(clearMediaSelection);
16187
+ clearMediaSelectionRef.current = clearMediaSelection;
16188
+ const selectMediaElement = (0, import_react17.useCallback)((el) => {
16189
+ const r2 = el.getBoundingClientRect();
16190
+ const video = el.dataset.ohwEditable === "video" ? el.querySelector("video") : null;
16191
+ selectedMediaElRef.current = el;
16192
+ setSelectedMedia({
16193
+ key: el.dataset.ohwKey ?? "",
16194
+ rect: { top: r2.top, left: r2.left, width: r2.width, height: r2.height },
16195
+ elementType: el.dataset.ohwEditable ?? "image",
16196
+ hasTextOverlap: false,
16197
+ isDragOver: false,
16198
+ ...video ? { videoAutoplay: video.autoplay, videoMuted: video.muted } : {}
16199
+ });
16200
+ const sectionEl = el.closest("[data-ohw-section]");
16201
+ aiSectionApiRef.current?.selectFromElement(el, { report: false });
16202
+ postToParentRef.current({
16203
+ type: "ow:section-selected",
16204
+ sectionId: sectionEl?.dataset.ohwSection ?? null,
16205
+ sectionLabel: sectionEl ? sectionEl.dataset.ohwSectionLabel ?? titleCaseSectionId(sectionEl.dataset.ohwSection ?? "") : null,
16206
+ key: el.dataset.ohwKey ?? null,
16207
+ // Display name for the pill — the raw key prettifies into fragments ("Img"); the
16208
+ // bridge knows what the node IS, so it names it.
16209
+ keyLabel: el.dataset.ohwEditable === "video" ? "Video" : el.dataset.ohwEditable === "bg-image" ? "Background" : "Image"
16210
+ });
16211
+ }, []);
16212
+ const selectMediaElementRef = (0, import_react17.useRef)(selectMediaElement);
16213
+ selectMediaElementRef.current = selectMediaElement;
16214
+ (0, import_react17.useEffect)(() => {
16215
+ if (!selectedMedia) return;
16216
+ const update = () => {
16217
+ const el = selectedMediaElRef.current;
16218
+ if (!el || !el.isConnected) {
16219
+ clearMediaSelection();
16220
+ return;
16221
+ }
16222
+ const r2 = el.getBoundingClientRect();
16223
+ setSelectedMedia(
16224
+ (prev) => prev ? { ...prev, rect: { top: r2.top, left: r2.left, width: r2.width, height: r2.height } } : prev
16225
+ );
16226
+ };
16227
+ window.addEventListener("scroll", update, true);
16228
+ window.addEventListener("resize", update);
16229
+ return () => {
16230
+ window.removeEventListener("scroll", update, true);
16231
+ window.removeEventListener("resize", update);
16232
+ };
16233
+ }, [selectedMedia !== null]);
15440
16234
  const [carouselHover, setCarouselHover] = (0, import_react17.useState)(null);
15441
16235
  const [uploadingRects, setUploadingRects] = (0, import_react17.useState)({});
15442
16236
  const hoveredGapRef = (0, import_react17.useRef)(null);
@@ -15699,13 +16493,6 @@ function OhhwellsBridge() {
15699
16493
  const [isItemDragging, setIsItemDragging] = (0, import_react17.useState)(false);
15700
16494
  const [isFooterFrameSelection, setIsFooterFrameSelection] = (0, import_react17.useState)(false);
15701
16495
  isFooterFrameSelectionRef.current = isFooterFrameSelection;
15702
- const [floatingPanel, setFloatingPanel] = (0, import_react17.useState)(null);
15703
- const floatingPanelOpenRef = (0, import_react17.useRef)(false);
15704
- floatingPanelOpenRef.current = floatingPanel !== null;
15705
- const [floatingPanelPos, setFloatingPanelPos] = (0, import_react17.useState)(null);
15706
- const [logoSizeDraft, setLogoSizeDraft] = (0, import_react17.useState)(null);
15707
- const [editorViewport, setEditorViewport] = (0, import_react17.useState)("desktop");
15708
- const [parentScrollSnap, setParentScrollSnap] = (0, import_react17.useState)(null);
15709
16496
  const [navDropdownPreviewOpen, setNavDropdownPreviewOpen] = (0, import_react17.useState)(null);
15710
16497
  const [footerHeadingVisible, setFooterHeadingVisible] = (0, import_react17.useState)(null);
15711
16498
  const footerDragRef = (0, import_react17.useRef)(null);
@@ -15720,7 +16507,16 @@ function OhhwellsBridge() {
15720
16507
  const addNavAfterAnchorRef = (0, import_react17.useRef)(null);
15721
16508
  const editContentRef = (0, import_react17.useRef)({});
15722
16509
  const aiSectionsRef = (0, import_react17.useRef)("");
16510
+ const brandKitRef = (0, import_react17.useRef)("");
16511
+ const stylesRef = (0, import_react17.useRef)("");
15723
16512
  const pendingDeleteUndoRef = (0, import_react17.useRef)(null);
16513
+ const [floatingPanel, setFloatingPanel] = (0, import_react17.useState)(null);
16514
+ const floatingPanelOpenRef = (0, import_react17.useRef)(false);
16515
+ const setFloatingPanelRef = (0, import_react17.useRef)(setFloatingPanel);
16516
+ const [floatingPanelPos, setFloatingPanelPos] = (0, import_react17.useState)(null);
16517
+ const [logoSizeDraft, setLogoSizeDraft] = (0, import_react17.useState)(null);
16518
+ const [editorViewport, setEditorViewport] = (0, import_react17.useState)("desktop");
16519
+ const [parentScrollSnap, setParentScrollSnap] = (0, import_react17.useState)(null);
15724
16520
  const [sitePages, setSitePages] = (0, import_react17.useState)([]);
15725
16521
  const [sectionsByPath, setSectionsByPath] = (0, import_react17.useState)({});
15726
16522
  const sectionsPrefetchGenRef = (0, import_react17.useRef)(0);
@@ -15729,7 +16525,18 @@ function OhhwellsBridge() {
15729
16525
  const linkPopoverOpenRef = (0, import_react17.useRef)(false);
15730
16526
  const linkPopoverGraceUntilRef = (0, import_react17.useRef)(0);
15731
16527
  setLinkPopoverRef.current = setLinkPopover;
16528
+ setFloatingPanelRef.current = setFloatingPanel;
15732
16529
  linkPopoverSessionRef.current = linkPopover;
16530
+ floatingPanelOpenRef.current = Boolean(floatingPanel);
16531
+ (0, import_react17.useEffect)(() => {
16532
+ const syncViewport = () => {
16533
+ const next = window.innerWidth <= 480 ? "mobile" : "desktop";
16534
+ setEditorViewport((prev) => prev === next ? prev : next);
16535
+ };
16536
+ syncViewport();
16537
+ window.addEventListener("resize", syncViewport);
16538
+ return () => window.removeEventListener("resize", syncViewport);
16539
+ }, []);
15733
16540
  const {
15734
16541
  navDragRef,
15735
16542
  navDropSlots,
@@ -17040,15 +17847,31 @@ function OhhwellsBridge() {
17040
17847
  }
17041
17848
  const applyContent = (content) => {
17042
17849
  const imageLoads = [];
17850
+ if (typeof content[BRAND_KIT_KEY] === "string") {
17851
+ brandKitRef.current = content[BRAND_KIT_KEY];
17852
+ applyBrandToDom(parseBrandKit(content[BRAND_KIT_KEY]));
17853
+ } else {
17854
+ brandKitRef.current = "";
17855
+ applyBrandToDom(null);
17856
+ }
17043
17857
  if (typeof content[AI_SECTIONS_KEY] === "string") {
17044
17858
  aiSectionsRef.current = content[AI_SECTIONS_KEY];
17859
+ setAiSectionOrder(content[SECTION_ORDER_KEY], window.location.pathname);
17045
17860
  applyAiSectionsToDom(parseAiSectionsState(content[AI_SECTIONS_KEY]));
17046
17861
  }
17862
+ if (typeof content[STYLE_STORE_KEY] === "string") {
17863
+ stylesRef.current = content[STYLE_STORE_KEY];
17864
+ applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
17865
+ }
17866
+ applyBrandChrome(content);
17047
17867
  for (const [key, val] of Object.entries(content)) {
17048
17868
  if (key === "__ohw_sections") continue;
17049
17869
  if (key === AI_SECTIONS_KEY) continue;
17050
17870
  if (key === LOGO_PLACEHOLDER_KEY) continue;
17051
17871
  if (LOGO_IMAGE_KEYS.includes(key)) continue;
17872
+ if (key === BRAND_KIT_KEY) continue;
17873
+ if (key === STYLE_STORE_KEY) continue;
17874
+ if (BRAND_CHROME_KEYS.has(key)) continue;
17052
17875
  if (applyVideoSettingNode(key, val)) continue;
17053
17876
  if (applyCarouselNode(key, val)) continue;
17054
17877
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
@@ -17114,7 +17937,9 @@ function OhhwellsBridge() {
17114
17937
  let cancelled = false;
17115
17938
  setFetchState("loading");
17116
17939
  const apiUrl = process.env.NEXT_PUBLIC_FLOWOPS_API_URL ?? "https://flowops-backend-staging-2yfdh7pwpq-as.a.run.app";
17117
- fetch(`${apiUrl}/api/public/sites/${subdomain}/content`, { cache: "no-store" }).then((r2) => r2.ok ? r2.json() : null).then((data) => {
17940
+ const initialPath = pathname;
17941
+ fetchedContentPaths.add(`${subdomain}::${initialPath}`);
17942
+ fetch(`${apiUrl}/api/public/sites/${subdomain}/content?path=${encodeURIComponent(initialPath)}`, { cache: "no-store" }).then((r2) => r2.ok ? r2.json() : null).then((data) => {
17118
17943
  if (cancelled) return;
17119
17944
  const content = data?.content ?? {};
17120
17945
  contentCache.set(subdomain, content);
@@ -17234,10 +18059,28 @@ function OhhwellsBridge() {
17234
18059
  initSectionInstancesFromContent(content, window.location.pathname);
17235
18060
  observer?.disconnect();
17236
18061
  try {
18062
+ applyBrandChrome(content);
18063
+ if (typeof content[BRAND_KIT_KEY] === "string") {
18064
+ applyBrandToDom(parseBrandKit(content[BRAND_KIT_KEY]));
18065
+ } else {
18066
+ applyBrandToDom(null);
18067
+ }
18068
+ if (typeof content[AI_SECTIONS_KEY] === "string") {
18069
+ setAiSectionOrder(content[SECTION_ORDER_KEY], window.location.pathname);
18070
+ applyAiSectionsToDom(parseAiSectionsState(content[AI_SECTIONS_KEY]));
18071
+ }
18072
+ if (typeof content[STYLE_STORE_KEY] === "string") {
18073
+ applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
18074
+ }
17237
18075
  for (const [key, val] of Object.entries(content)) {
17238
18076
  if (key === "__ohw_sections") continue;
18077
+ if (key === AI_SECTIONS_KEY) continue;
17239
18078
  if (key === LOGO_PLACEHOLDER_KEY) continue;
17240
18079
  if (LOGO_IMAGE_KEYS.includes(key)) continue;
18080
+ if (key === BRAND_KIT_KEY) continue;
18081
+ if (key === STYLE_STORE_KEY) continue;
18082
+ if (key === STYLE_STORE_KEY) continue;
18083
+ if (BRAND_CHROME_KEYS.has(key)) continue;
17241
18084
  if (applyVideoSettingNode(key, val)) continue;
17242
18085
  if (applyCarouselNode(key, val)) continue;
17243
18086
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
@@ -17283,6 +18126,17 @@ function OhhwellsBridge() {
17283
18126
  debounceTimer = setTimeout(applyFromCache, 150);
17284
18127
  };
17285
18128
  applyFromCache();
18129
+ const pathCacheKey = `${subdomain}::${pathname}`;
18130
+ if (!fetchedContentPaths.has(pathCacheKey)) {
18131
+ fetchedContentPaths.add(pathCacheKey);
18132
+ const apiUrl = process.env.NEXT_PUBLIC_FLOWOPS_API_URL ?? "https://flowops-backend-staging-2yfdh7pwpq-as.a.run.app";
18133
+ fetch(`${apiUrl}/api/public/sites/${subdomain}/content?path=${encodeURIComponent(pathname)}`, { cache: "no-store" }).then((r2) => r2.ok ? r2.json() : null).then((data) => {
18134
+ if (!data?.content) return;
18135
+ contentCache.set(subdomain, data.content);
18136
+ applyFromCache();
18137
+ }).catch(() => {
18138
+ });
18139
+ }
17286
18140
  observer = new MutationObserver(scheduleApply);
17287
18141
  observer.observe(document.body, { childList: true, subtree: true });
17288
18142
  return () => {
@@ -17378,30 +18232,31 @@ function OhhwellsBridge() {
17378
18232
  }, [isEditMode, pathname, subdomain, fetchState, resyncSelectedNavigationItem]);
17379
18233
  (0, import_react17.useEffect)(() => {
17380
18234
  if (!isEditMode) return;
18235
+ let lastPosted = 0;
17381
18236
  const measure = () => {
17382
18237
  const h = document.body.scrollHeight;
17383
- if (h > 50) postToParent2({ type: "ow:height", height: h });
18238
+ if (h > 50 && Math.abs(h - lastPosted) > 1) {
18239
+ lastPosted = h;
18240
+ postToParent2({ type: "ow:height", height: h });
18241
+ }
18242
+ };
18243
+ let raf = null;
18244
+ const schedule = () => {
18245
+ if (raf != null) return;
18246
+ raf = requestAnimationFrame(() => {
18247
+ raf = null;
18248
+ measure();
18249
+ });
17384
18250
  };
17385
18251
  const t1 = setTimeout(measure, 50);
17386
18252
  const t2 = setTimeout(measure, 500);
17387
- let lastWidth = window.innerWidth;
17388
- let resizeTimers = [];
17389
- const clearResizeTimers = () => {
17390
- resizeTimers.forEach(clearTimeout);
17391
- resizeTimers = [];
17392
- };
17393
- const handleResize = () => {
17394
- if (window.innerWidth === lastWidth) return;
17395
- lastWidth = window.innerWidth;
17396
- clearResizeTimers();
17397
- resizeTimers = HEIGHT_SETTLE_DELAYS.map((delay) => setTimeout(measure, delay));
17398
- };
17399
- window.addEventListener("resize", handleResize);
18253
+ const ro = new ResizeObserver(schedule);
18254
+ ro.observe(document.body);
17400
18255
  return () => {
17401
18256
  clearTimeout(t1);
17402
18257
  clearTimeout(t2);
17403
- clearResizeTimers();
17404
- window.removeEventListener("resize", handleResize);
18258
+ if (raf != null) cancelAnimationFrame(raf);
18259
+ ro.disconnect();
17405
18260
  };
17406
18261
  }, [pathname, isEditMode, postToParent2]);
17407
18262
  (0, import_react17.useEffect)(() => {
@@ -17642,6 +18497,7 @@ function OhhwellsBridge() {
17642
18497
  return;
17643
18498
  }
17644
18499
  const target = e.target;
18500
+ if (target.closest("[data-ohw-ai-review]")) return;
17645
18501
  if (target.closest("[data-ohw-toolbar]")) return;
17646
18502
  if (target.closest("[data-ohw-state-toggle]")) return;
17647
18503
  if (target.closest("[data-ohw-max-badge]")) return;
@@ -17653,6 +18509,9 @@ function OhhwellsBridge() {
17653
18509
  )) {
17654
18510
  return;
17655
18511
  }
18512
+ if (selectedMediaElRef.current && !selectedMediaElRef.current.contains(target) && !target.closest("[data-ohw-media-overlay], [data-ohw-edit-chrome], [data-ohw-item-interaction]")) {
18513
+ clearMediaSelectionRef.current();
18514
+ }
17656
18515
  {
17657
18516
  const formEl = getFormElement(target);
17658
18517
  const onSuccessText = target.closest(`[${SUCCESS_TEXT_ATTR}]`);
@@ -17804,19 +18663,14 @@ function OhhwellsBridge() {
17804
18663
  }
17805
18664
  const clickedButton = findClosestButtonLike(target);
17806
18665
  const buttonOnMedia = Boolean(clickedButton && isMediaEditable(editable) && editable.contains(clickedButton));
17807
- console.log("[click-debug]", {
17808
- editableType: editable.dataset.ohwEditable,
17809
- editableTag: editable.tagName,
17810
- targetTag: target.tagName,
17811
- clickedButtonTag: clickedButton?.tagName ?? null,
17812
- buttonOnMedia,
17813
- isMediaEditableEditable: isMediaEditable(editable)
17814
- });
17815
18666
  if (isMediaEditable(editable) && !buttonOnMedia) {
17816
18667
  e.preventDefault();
17817
18668
  e.stopPropagation();
17818
- aiSectionApiRef.current?.selectFromElement(editable);
17819
- postToParentRef.current({ type: "ow:image-pick", key: editable.dataset.ohwKey ?? "", elementType: editable.dataset.ohwEditable ?? "image" });
18669
+ if (selectedMediaElRef.current === editable) {
18670
+ postToParentRef.current({ type: "ow:image-pick", key: editable.dataset.ohwKey ?? "", elementType: editable.dataset.ohwEditable ?? "image" });
18671
+ } else {
18672
+ selectMediaElementRef.current(editable);
18673
+ }
17820
18674
  return;
17821
18675
  }
17822
18676
  const socialItem = getSocialItem(editable);
@@ -17835,11 +18689,6 @@ function OhhwellsBridge() {
17835
18689
  const hrefLookupTarget = buttonOnMedia ? clickedButton : editable;
17836
18690
  const hrefCtx = getHrefKeyFromElement(hrefLookupTarget);
17837
18691
  const navAnchor = hrefCtx ? getNavigationItemAnchor(hrefCtx.anchor) : null;
17838
- console.log("[click-debug 2]", {
17839
- hrefLookupTargetTag: hrefLookupTarget.tagName,
17840
- hrefCtx: hrefCtx ? { key: hrefCtx.key } : null,
17841
- navAnchorTag: navAnchor?.tagName ?? null
17842
- });
17843
18692
  if (navAnchor) {
17844
18693
  e.preventDefault();
17845
18694
  e.stopPropagation();
@@ -17957,6 +18806,7 @@ function OhhwellsBridge() {
17957
18806
  };
17958
18807
  const handleDblClick = (e) => {
17959
18808
  const target = e.target;
18809
+ if (target.closest("[data-ohw-ai-review]")) return;
17960
18810
  if (target.closest("[data-ohw-toolbar]")) return;
17961
18811
  if (target.closest("[data-ohw-state-toggle]")) return;
17962
18812
  if (target.closest("[data-ohw-max-badge]")) return;
@@ -18008,6 +18858,9 @@ function OhhwellsBridge() {
18008
18858
  setHoveredItemRect(null);
18009
18859
  hoveredNavContainerRef.current = null;
18010
18860
  setHoveredNavContainerRect(null);
18861
+ siblingHintElRef.current = null;
18862
+ setSiblingHintRect(null);
18863
+ setSiblingHintRects([]);
18011
18864
  return;
18012
18865
  }
18013
18866
  {
@@ -18126,7 +18979,6 @@ function OhhwellsBridge() {
18126
18979
  hoveredNavContainerRef.current = null;
18127
18980
  setHoveredNavContainerRect(null);
18128
18981
  hoveredItemElRef.current = editable;
18129
- setHoveredItemRect(isSelectedForHoverRef.current(editable) ? null : editable.getBoundingClientRect());
18130
18982
  }
18131
18983
  }
18132
18984
  }
@@ -18423,7 +19275,7 @@ function OhhwellsBridge() {
18423
19275
  }
18424
19276
  };
18425
19277
  const probeImageAt = (clientX, clientY, isDragOver = false, fromParentViewport = false) => {
18426
- if (linkPopoverOpenRef.current) {
19278
+ if (linkPopoverOpenRef.current || floatingPanelOpenRef.current && isPointOverFloatingPanel(clientX, clientY)) {
18427
19279
  if (hoveredImageRef.current) {
18428
19280
  hoveredImageRef.current = null;
18429
19281
  hoveredImageHasTextOverlapRef.current = false;
@@ -18757,7 +19609,9 @@ function OhhwellsBridge() {
18757
19609
  return;
18758
19610
  }
18759
19611
  const { y } = toProbeCoords(clientX, clientY, fromParentViewport);
18760
- const sections = Array.from(document.querySelectorAll("[data-ohw-section]")).sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top);
19612
+ const sections = Array.from(document.querySelectorAll("[data-ohw-section]")).filter(
19613
+ (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
19614
+ ).sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top);
18761
19615
  const ZONE = 20;
18762
19616
  for (let i = 0; i < sections.length; i++) {
18763
19617
  const a = sections[i];
@@ -18786,8 +19640,7 @@ function OhhwellsBridge() {
18786
19640
  };
18787
19641
  const handleMouseMove = (e) => {
18788
19642
  const { clientX, clientY } = e;
18789
- if (pointOwnedByFloatingPanel(clientX, clientY)) return;
18790
- if (isOverEditorChrome(clientX, clientY)) {
19643
+ if (document.documentElement.hasAttribute("data-ohw-panel-dragging") || floatingPanelOpenRef.current && isPointOverFloatingPanel(clientX, clientY) || isOverEditorChrome(clientX, clientY)) {
18791
19644
  document.querySelectorAll("[data-ohw-hovered]").forEach((el) => el.removeAttribute("data-ohw-hovered"));
18792
19645
  formHoverElRef.current = null;
18793
19646
  setFormHoverRect(null);
@@ -18795,6 +19648,12 @@ function OhhwellsBridge() {
18795
19648
  setHoveredItemRect(null);
18796
19649
  hoveredNavContainerRef.current = null;
18797
19650
  setHoveredNavContainerRect(null);
19651
+ siblingHintElRef.current = null;
19652
+ setSiblingHintRect(null);
19653
+ setSiblingHintRects([]);
19654
+ dismissImageHover();
19655
+ clearImageHover();
19656
+ setSectionGap(null);
18798
19657
  return;
18799
19658
  }
18800
19659
  if (probeSocialsRowAt(clientX, clientY)) return;
@@ -18806,7 +19665,11 @@ function OhhwellsBridge() {
18806
19665
  if (e.data?.type !== "ow:pointer-sync") return;
18807
19666
  const { clientX, clientY } = e.data;
18808
19667
  if (typeof clientX !== "number" || typeof clientY !== "number") return;
18809
- if (pointOwnedByFloatingPanel(clientX, clientY)) return;
19668
+ if (document.documentElement.hasAttribute("data-ohw-panel-dragging") || floatingPanelOpenRef.current && isPointOverFloatingPanel(clientX, clientY)) {
19669
+ dismissImageHover();
19670
+ clearImageHover();
19671
+ return;
19672
+ }
18810
19673
  if (probeSocialsRowAt(clientX, clientY)) return;
18811
19674
  probeSectionGapAt(clientX, clientY);
18812
19675
  probeImageAt(clientX, clientY);
@@ -19089,10 +19952,23 @@ function OhhwellsBridge() {
19089
19952
  if (e.data?.type !== "ow:hydrate") return;
19090
19953
  const content = e.data.content;
19091
19954
  if (!content) return;
19955
+ if (typeof content[BRAND_KIT_KEY] === "string") {
19956
+ brandKitRef.current = content[BRAND_KIT_KEY];
19957
+ applyBrandToDom(parseBrandKit(content[BRAND_KIT_KEY]));
19958
+ } else {
19959
+ brandKitRef.current = "";
19960
+ applyBrandToDom(null);
19961
+ }
19092
19962
  if (typeof content[AI_SECTIONS_KEY] === "string") {
19093
19963
  aiSectionsRef.current = content[AI_SECTIONS_KEY];
19964
+ setAiSectionOrder(content[SECTION_ORDER_KEY], window.location.pathname);
19094
19965
  applyAiSectionsToDom(parseAiSectionsState(content[AI_SECTIONS_KEY]));
19095
19966
  }
19967
+ if (typeof content[STYLE_STORE_KEY] === "string") {
19968
+ stylesRef.current = content[STYLE_STORE_KEY];
19969
+ applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
19970
+ }
19971
+ applyBrandChrome(content);
19096
19972
  let sectionsJson = null;
19097
19973
  for (const [key, val] of Object.entries(content)) {
19098
19974
  if (key === "__ohw_sections") {
@@ -19102,6 +19978,9 @@ function OhhwellsBridge() {
19102
19978
  if (key === AI_SECTIONS_KEY) continue;
19103
19979
  if (key === LOGO_PLACEHOLDER_KEY) continue;
19104
19980
  if (LOGO_IMAGE_KEYS.includes(key) && !String(val ?? "").trim()) continue;
19981
+ if (key === BRAND_KIT_KEY) continue;
19982
+ if (key === STYLE_STORE_KEY) continue;
19983
+ if (BRAND_CHROME_KEYS.has(key)) continue;
19105
19984
  if (applyVideoSettingNode(key, val)) continue;
19106
19985
  if (applyCarouselNode(key, val)) continue;
19107
19986
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
@@ -19115,6 +19994,8 @@ function OhhwellsBridge() {
19115
19994
  if (video && video.src !== val) applyVideoSrc(video, val);
19116
19995
  } else if (el.dataset.ohwEditable === "link") {
19117
19996
  applyLinkHref(el, val);
19997
+ } else if (el.dataset.ohwEditable === "icon") {
19998
+ applyIconMarkup(el, val);
19118
19999
  } else if (isIconMarkupValue(val)) {
19119
20000
  } else {
19120
20001
  el.innerHTML = val;
@@ -19199,12 +20080,21 @@ function OhhwellsBridge() {
19199
20080
  nodes: collectEditableNodes(editContentRef.current)
19200
20081
  });
19201
20082
  };
20083
+ const clearInteractionChrome = () => {
20084
+ deactivateRef.current();
20085
+ deselectRef.current();
20086
+ clearMediaSelectionRef.current();
20087
+ };
19202
20088
  const handleAiApplyTree = (e) => {
19203
20089
  if (e.data?.type !== "ow:ai-apply-tree") return;
19204
20090
  const payload = e.data.payload;
19205
20091
  if (!payload || typeof payload.id !== "string" || !isRenderableTree(payload.tree)) return;
20092
+ clearInteractionChrome();
19206
20093
  const previous = aiSectionsRef.current;
19207
- const nextState = applyTreeToState(parseAiSectionsState(previous), payload);
20094
+ const nextState = applyTreeToState(parseAiSectionsState(previous), {
20095
+ ...payload,
20096
+ path: payload.path ?? window.location.pathname
20097
+ });
19208
20098
  const nextValue = serializeAiSectionsState(nextState);
19209
20099
  aiSectionsRef.current = nextValue;
19210
20100
  applyAiSectionsToDom(nextState);
@@ -19225,6 +20115,7 @@ function OhhwellsBridge() {
19225
20115
  const exists = document.querySelector(`[data-ohw-section="${CSS.escape(sectionId)}"]`);
19226
20116
  if (!exists) return;
19227
20117
  if (isPageFrameSection(exists)) return;
20118
+ clearInteractionChrome();
19228
20119
  const previous = aiSectionsRef.current;
19229
20120
  const nextState = deleteSectionFromState(parseAiSectionsState(previous), sectionId);
19230
20121
  const nextValue = serializeAiSectionsState(nextState);
@@ -19240,8 +20131,10 @@ function OhhwellsBridge() {
19240
20131
  const handleAiSetSections = (e) => {
19241
20132
  if (e.data?.type !== "ow:ai-set-sections") return;
19242
20133
  const value = typeof e.data.value === "string" ? e.data.value : "";
20134
+ clearInteractionChrome();
19243
20135
  aiSectionsRef.current = value;
19244
20136
  applyAiSectionsToDom(parseAiSectionsState(value));
20137
+ applyStylesToDom(parseStyleStore(stylesRef.current));
19245
20138
  const restoredHeight = document.body.scrollHeight;
19246
20139
  if (restoredHeight > 50) postToParentRef.current({ type: "ow:height", height: restoredHeight });
19247
20140
  postToParentRef.current({ type: "ow:change", nodes: [{ key: AI_SECTIONS_KEY, text: value }] });
@@ -19257,10 +20150,40 @@ function OhhwellsBridge() {
19257
20150
  if (!entries) return;
19258
20151
  const orderJson = JSON.stringify(entries);
19259
20152
  editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: orderJson };
20153
+ setAiSectionOrder(orderJson, window.location.pathname);
19260
20154
  postToParentRef.current({ type: "ow:change", nodes: [{ key: SECTION_ORDER_KEY, text: orderJson }] });
19261
20155
  window.dispatchEvent(new Event("resize"));
19262
20156
  };
19263
20157
  window.addEventListener("message", handleMoveSection);
20158
+ const handleAiSetBrand = (e) => {
20159
+ if (e.data?.type !== "ow:ai-set-brand") return;
20160
+ const value = typeof e.data.value === "string" ? e.data.value : "";
20161
+ const previous = brandKitRef.current;
20162
+ brandKitRef.current = value;
20163
+ applyBrandToDom(parseBrandKit(value));
20164
+ if (aiSectionsRef.current) applyAiSectionsToDom(parseAiSectionsState(aiSectionsRef.current));
20165
+ applyStylesToDom(parseStyleStore(stylesRef.current));
20166
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: BRAND_KIT_KEY, text: value }] });
20167
+ postToParentRef.current({ type: "ow:ai-brand-applied", previous, value });
20168
+ };
20169
+ window.addEventListener("message", handleAiSetBrand);
20170
+ const handleAiSetStyles = (e) => {
20171
+ if (e.data?.type !== "ow:ai-set-styles") return;
20172
+ const value = typeof e.data.value === "string" ? e.data.value : "";
20173
+ const previous = stylesRef.current;
20174
+ stylesRef.current = value;
20175
+ applyStylesToDom(parseStyleStore(value));
20176
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: STYLE_STORE_KEY, text: value }] });
20177
+ postToParentRef.current({ type: "ow:ai-styles-applied", previous, value });
20178
+ };
20179
+ window.addEventListener("message", handleAiSetStyles);
20180
+ const handleGetBrand = (e) => {
20181
+ if (e.data?.type !== "ow:get-brand") return;
20182
+ const template = deriveTemplateBrand();
20183
+ const value = brandKitRef.current || (template ? JSON.stringify(template) : "");
20184
+ postToParentRef.current({ type: "ow:brand-value", value });
20185
+ };
20186
+ window.addEventListener("message", handleGetBrand);
19264
20187
  const handlePanelDragging = (e) => {
19265
20188
  if (e.data?.type !== "ow:panel-dragging") return;
19266
20189
  if (e.data.dragging) document.documentElement.setAttribute("data-ohw-panel-dragging", "");
@@ -19318,8 +20241,15 @@ function OhhwellsBridge() {
19318
20241
  closeLinkPopoverRef.current();
19319
20242
  return;
19320
20243
  }
20244
+ if (floatingPanelOpenRef.current) {
20245
+ setFloatingPanelRef.current(null);
20246
+ deselectRef.current();
20247
+ deactivateRef.current();
20248
+ return;
20249
+ }
19321
20250
  deselectRef.current();
19322
20251
  deactivateRef.current();
20252
+ clearMediaSelectionRef.current();
19323
20253
  };
19324
20254
  window.addEventListener("message", handleDeactivate);
19325
20255
  const handleToastAction = (e) => {
@@ -19405,6 +20335,10 @@ function OhhwellsBridge() {
19405
20335
  const handleKeyDown = (e) => {
19406
20336
  if (e.key === "Escape" && document.querySelector("[data-ohw-section-picker]")) return;
19407
20337
  if (e.key === "Escape" && document.querySelector("[data-ohw-more-menu]")) return;
20338
+ if (e.key === "Escape" && selectedMediaElRef.current) {
20339
+ clearMediaSelectionRef.current();
20340
+ return;
20341
+ }
19408
20342
  if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "a" && activeElRef.current) {
19409
20343
  e.preventDefault();
19410
20344
  selectAllTextInEditable(activeElRef.current);
@@ -19564,6 +20498,12 @@ function OhhwellsBridge() {
19564
20498
  if (aiSectionsRef.current) {
19565
20499
  nodes.push({ key: AI_SECTIONS_KEY, type: "sections", text: aiSectionsRef.current });
19566
20500
  }
20501
+ if (stylesRef.current) {
20502
+ nodes.push({ key: STYLE_STORE_KEY, type: "styles", text: stylesRef.current });
20503
+ }
20504
+ if (brandKitRef.current) {
20505
+ nodes.push({ key: BRAND_KIT_KEY, type: "brand", text: brandKitRef.current });
20506
+ }
19567
20507
  document.querySelectorAll('[data-ohw-editable="form"]').forEach((form) => {
19568
20508
  const formKey = formKeyOf(form);
19569
20509
  if (!formKey) return;
@@ -19581,8 +20521,12 @@ function OhhwellsBridge() {
19581
20521
  if (inserted) {
19582
20522
  const tracker = getSectionsTracker();
19583
20523
  postToParentRef.current({ type: "ow:change", nodes: [{ key: "__ohw_sections", text: tracker.textContent ?? "[]" }] });
19584
- const h = document.body.scrollHeight;
19585
- if (h > 50) postToParentRef.current({ type: "ow:height", height: h });
20524
+ const reportHeight = () => {
20525
+ const h = document.body.scrollHeight;
20526
+ if (h > 50) postToParentRef.current({ type: "ow:height", height: h });
20527
+ };
20528
+ reportHeight();
20529
+ setTimeout(reportHeight, 500);
19586
20530
  }
19587
20531
  };
19588
20532
  const handleSwitchSchedule = (e) => {
@@ -19979,13 +20923,16 @@ function OhhwellsBridge() {
19979
20923
  window.removeEventListener("message", handleAiDeleteSection);
19980
20924
  window.removeEventListener("message", handleAiSetSections);
19981
20925
  window.removeEventListener("message", handleMoveSection);
20926
+ window.removeEventListener("message", handleAiSetBrand);
20927
+ window.removeEventListener("message", handleAiSetStyles);
20928
+ window.removeEventListener("message", handleGetBrand);
19982
20929
  window.removeEventListener("message", handlePanelDragging);
19983
20930
  window.removeEventListener("message", handleDeleteSection);
19984
20931
  window.removeEventListener("message", handleDeactivate);
19985
- document.documentElement.removeAttribute("data-ohw-panel-dragging");
19986
20932
  window.removeEventListener("message", handleToastAction);
19987
20933
  window.removeEventListener("message", handleFormCount);
19988
20934
  window.removeEventListener("message", handleUiEscape);
20935
+ document.documentElement.removeAttribute("data-ohw-panel-dragging");
19989
20936
  autoSaveTimers.current.forEach(clearTimeout);
19990
20937
  autoSaveTimers.current.clear();
19991
20938
  if (imageUnhoverTimerRef.current) clearTimeout(imageUnhoverTimerRef.current);
@@ -20188,7 +21135,7 @@ function OhhwellsBridge() {
20188
21135
  postToParent2({
20189
21136
  type: "ow:ready",
20190
21137
  version: "1",
20191
- bridgeVersion: "0.1.76",
21138
+ bridgeVersion: "0.1.78",
20192
21139
  path: pathname,
20193
21140
  nodes: collectEditableNodes(editContentRef.current),
20194
21141
  sections
@@ -20595,11 +21542,22 @@ function OhhwellsBridge() {
20595
21542
  const showEditLink = toolbarShowEditLink;
20596
21543
  const currentSections = sectionsByPath[pathname] ?? [];
20597
21544
  linkPopoverOpenRef.current = linkPopover !== null;
21545
+ const handleMediaSelect = (0, import_react17.useCallback)((key) => {
21546
+ const el = hoveredImageRef.current?.dataset.ohwKey === key ? hoveredImageRef.current : Array.from(document.querySelectorAll(MEDIA_SELECTOR)).find(
21547
+ (m) => (m.dataset.ohwKey ?? "") === key
21548
+ ) ?? null;
21549
+ if (!el) return;
21550
+ selectMediaElementRef.current(el);
21551
+ }, []);
20598
21552
  const handleMediaReplace = (0, import_react17.useCallback)(
20599
21553
  (key) => {
20600
- postToParent2({ type: "ow:image-pick", key, elementType: mediaHover?.elementType ?? "image" });
21554
+ postToParent2({
21555
+ type: "ow:image-pick",
21556
+ key,
21557
+ elementType: mediaHover?.elementType ?? selectedMedia?.elementType ?? "image"
21558
+ });
20601
21559
  },
20602
- [postToParent2, mediaHover?.elementType]
21560
+ [postToParent2, mediaHover?.elementType, selectedMedia?.elementType]
20603
21561
  );
20604
21562
  const handleEditCarousel = (0, import_react17.useCallback)(
20605
21563
  (key) => {
@@ -20671,12 +21629,25 @@ function OhhwellsBridge() {
20671
21629
  },
20672
21630
  `uploading-${key}`
20673
21631
  )),
20674
- mediaHover && !(mediaHover.key in uploadingRects) && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
21632
+ mediaHover && !(mediaHover.key in uploadingRects) && mediaHover.key !== selectedMedia?.key && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
20675
21633
  MediaOverlay,
20676
21634
  {
20677
21635
  hover: mediaHover,
20678
21636
  isUploading: false,
20679
21637
  onReplace: handleMediaReplace,
21638
+ onSelect: handleMediaSelect,
21639
+ onVideoSettingsChange: handleVideoSettingsChange
21640
+ }
21641
+ ),
21642
+ selectedMedia && !(selectedMedia.key in uploadingRects) && !linkPopover && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
21643
+ MediaOverlay,
21644
+ {
21645
+ hover: selectedMedia,
21646
+ selected: true,
21647
+ hovered: mediaHover?.key === selectedMedia.key,
21648
+ isUploading: false,
21649
+ onReplace: handleMediaReplace,
21650
+ onSelect: handleMediaSelect,
20680
21651
  onVideoSettingsChange: handleVideoSettingsChange
20681
21652
  }
20682
21653
  ),
@@ -21082,6 +22053,59 @@ function OhhwellsBridge() {
21082
22053
  ) : null
21083
22054
  ] });
21084
22055
  }
22056
+
22057
+ // src/ui/EmptySection.tsx
22058
+ var import_link = __toESM(require("next/link"), 1);
22059
+ var import_jsx_runtime34 = require("react/jsx-runtime");
22060
+ function EmptySection({ title, homeHref = "/", eyebrowKey, titleKey, subtitleKey }) {
22061
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(import_jsx_runtime34.Fragment, { children: [
22062
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
22063
+ "p",
22064
+ {
22065
+ style: {
22066
+ fontFamily: "var(--brand-font-body)",
22067
+ fontSize: "0.75rem",
22068
+ fontWeight: 500,
22069
+ letterSpacing: "0.15em",
22070
+ textTransform: "uppercase",
22071
+ color: "var(--brand-accent)",
22072
+ marginBottom: "1.5rem"
22073
+ },
22074
+ 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" }) })
22075
+ }
22076
+ ),
22077
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
22078
+ "h1",
22079
+ {
22080
+ style: {
22081
+ fontFamily: "var(--brand-font-heading)",
22082
+ fontSize: "clamp(2rem, 3.5vw, 2.75rem)",
22083
+ lineHeight: 1.1,
22084
+ letterSpacing: "-0.025em",
22085
+ color: "var(--brand-text)",
22086
+ marginBottom: "1rem"
22087
+ },
22088
+ ...titleKey ? { "data-ohw-editable": "text", "data-ohw-key": titleKey, "data-ohw-max-length": 80 } : {},
22089
+ children: title
22090
+ }
22091
+ ),
22092
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
22093
+ "p",
22094
+ {
22095
+ style: {
22096
+ fontFamily: "var(--brand-font-body)",
22097
+ fontSize: "1rem",
22098
+ lineHeight: 1.7,
22099
+ fontWeight: 300,
22100
+ color: "var(--brand-text-muted)",
22101
+ maxWidth: "340px"
22102
+ },
22103
+ ...subtitleKey ? { "data-ohw-editable": "text", "data-ohw-key": subtitleKey, "data-ohw-max-length": 160 } : {},
22104
+ children: "This page doesn't have any content yet."
22105
+ }
22106
+ )
22107
+ ] });
22108
+ }
21085
22109
  // Annotate the CommonJS export names for ESM import in node:
21086
22110
  0 && (module.exports = {
21087
22111
  AI_DEFAULT_BRAND,
@@ -21099,6 +22123,7 @@ function OhhwellsBridge() {
21099
22123
  DropdownMenuItem,
21100
22124
  DropdownMenuSeparator,
21101
22125
  DropdownMenuTrigger,
22126
+ EmptySection,
21102
22127
  ItemActionToolbar,
21103
22128
  ItemInteractionLayer,
21104
22129
  LinkEditorPanel,