@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.js CHANGED
@@ -83,7 +83,12 @@ function parseAiSectionsState(raw) {
83
83
  media: entry.media && typeof entry.media === "object" ? entry.media : {}
84
84
  }));
85
85
  const removed = Array.isArray(parsed.removed) ? parsed.removed.filter((id) => typeof id === "string" && id.length > 0) : [];
86
- return { v: 1, sections, ...removed.length ? { removed } : {} };
86
+ return {
87
+ v: 1,
88
+ sections,
89
+ ...removed.length ? { removed } : {},
90
+ ...parsed.hideTemplate === true ? { hideTemplate: true } : {}
91
+ };
87
92
  } catch {
88
93
  return EMPTY_AI_SECTIONS;
89
94
  }
@@ -96,6 +101,7 @@ function applyTreeToState(state, payload) {
96
101
  const entry = {
97
102
  id: payload.id,
98
103
  label: payload.label ?? "Generated section",
104
+ ...typeof payload.path === "string" && payload.path ? { path: payload.path } : {},
99
105
  afterSection: payload.mode === "insert" && !insertBefore ? payload.targetSectionId ?? null : null,
100
106
  ...insertBefore && payload.targetSectionId ? { beforeSection: payload.targetSectionId } : {},
101
107
  ...payload.mode === "replace" && payload.targetSectionId ? { replaces: payload.targetSectionId } : {},
@@ -118,6 +124,317 @@ function deleteSectionFromState(state, sectionId) {
118
124
  return { ...state, v: 1, sections: state.sections, removed: [...removed, sectionId] };
119
125
  }
120
126
 
127
+ // src/lib/brand-chrome.ts
128
+ var BRAND_NAME_KEY = "__ohw_brand_name";
129
+ var BRAND_TITLE_KEY = "__ohw_site_title";
130
+ var BRAND_FAVICON_LETTER_KEY = "__ohw_favicon_letter";
131
+ var BRAND_CHROME_KEYS = /* @__PURE__ */ new Set([
132
+ BRAND_NAME_KEY,
133
+ BRAND_TITLE_KEY,
134
+ BRAND_FAVICON_LETTER_KEY
135
+ ]);
136
+ function upsertMeta(selector, attr, token, value) {
137
+ let el = document.head.querySelector(selector);
138
+ if (!el) {
139
+ el = document.createElement("meta");
140
+ el.setAttribute(attr, token);
141
+ document.head.appendChild(el);
142
+ }
143
+ if (el.getAttribute("content") !== value) el.setAttribute("content", value);
144
+ }
145
+ function escapeXml(value) {
146
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
147
+ }
148
+ function applyLetterFavicon(letter) {
149
+ 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>`;
150
+ const href = `data:image/svg+xml,${encodeURIComponent(svg)}`;
151
+ let link = document.head.querySelector('link[rel="icon"]');
152
+ if (!link) {
153
+ link = document.createElement("link");
154
+ link.rel = "icon";
155
+ document.head.appendChild(link);
156
+ }
157
+ link.type = "image/svg+xml";
158
+ if (link.href !== href) link.href = href;
159
+ }
160
+ function applyBrandChrome(content) {
161
+ const name = content[BRAND_NAME_KEY];
162
+ if (typeof name === "string" && name.length > 0) {
163
+ document.querySelectorAll("[data-ohw-wordmark]").forEach((el) => {
164
+ if (el.textContent !== name) el.textContent = name;
165
+ if (el.getAttribute("title") !== name) el.setAttribute("title", name);
166
+ });
167
+ }
168
+ const title = content[BRAND_TITLE_KEY];
169
+ if (typeof title === "string" && title.length > 0) {
170
+ if (document.title !== title) document.title = title;
171
+ upsertMeta('meta[property="og:title"]', "property", "og:title", title);
172
+ upsertMeta('meta[name="twitter:title"]', "name", "twitter:title", title);
173
+ }
174
+ const letter = content[BRAND_FAVICON_LETTER_KEY];
175
+ if (typeof letter === "string" && letter.length > 0) {
176
+ applyLetterFavicon(letter);
177
+ }
178
+ }
179
+
180
+ // src/lib/brand-kit.ts
181
+ var BRAND_KIT_KEY = "__ohw_brand";
182
+ var BRAND_VAR_PREFIX = "--ohw-brand-";
183
+ var BRAND_VAR_NAMES = ["primary", "accent", "light", "dark", "surface", "border", "muted"].map(
184
+ (role) => `${BRAND_VAR_PREFIX}${role}`
185
+ );
186
+ var FONT_VARS = {
187
+ heading: ["--font-heading", "--font-display", "--brand-font-heading"],
188
+ body: ["--font-body", "--brand-font-body"]
189
+ };
190
+ var BRAND_FONT_LINK_ID = "ohw-brand-fonts";
191
+ function brandColorVars(kit) {
192
+ const { dark, primary, accent, light } = kit.palette;
193
+ const mix = (a, aPct, b) => `color-mix(in srgb, ${a} ${aPct}%, ${b})`;
194
+ return {
195
+ [`${BRAND_VAR_PREFIX}primary`]: primary,
196
+ [`${BRAND_VAR_PREFIX}accent`]: accent,
197
+ [`${BRAND_VAR_PREFIX}light`]: light,
198
+ [`${BRAND_VAR_PREFIX}dark`]: dark,
199
+ [`${BRAND_VAR_PREFIX}surface`]: mix(light, 95, dark),
200
+ [`${BRAND_VAR_PREFIX}border`]: mix(light, 85, dark),
201
+ [`${BRAND_VAR_PREFIX}muted`]: mix(dark, 62, light)
202
+ };
203
+ }
204
+ function parseBrandKit(raw) {
205
+ if (!raw) return null;
206
+ try {
207
+ const parsed = JSON.parse(raw);
208
+ const p = parsed?.palette;
209
+ const f = parsed?.fonts;
210
+ 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") {
211
+ return null;
212
+ }
213
+ return {
214
+ palette: { dark: p.dark, primary: p.primary, accent: p.accent, light: p.light },
215
+ fonts: { heading: f.heading, body: f.body }
216
+ };
217
+ } catch {
218
+ return null;
219
+ }
220
+ }
221
+ function familyOf(stack) {
222
+ const first = stack.split(",")[0]?.trim() ?? "";
223
+ return first.replace(/^['"]|['"]$/g, "");
224
+ }
225
+ function loadBrandFonts(families) {
226
+ const unique = [...new Set(families.map((f) => f.trim()).filter(Boolean))];
227
+ if (unique.length === 0) return;
228
+ const spec = unique.map((f) => `${f.replace(/ /g, "+")}:400,500,600,700`).join("|");
229
+ const href = `https://fonts.googleapis.com/css?family=${spec}&display=swap`;
230
+ let link = document.getElementById(BRAND_FONT_LINK_ID);
231
+ if (!link) {
232
+ link = document.createElement("link");
233
+ link.id = BRAND_FONT_LINK_ID;
234
+ link.rel = "stylesheet";
235
+ document.head.appendChild(link);
236
+ }
237
+ if (link.href !== href) link.href = href;
238
+ }
239
+ function applyBrandToDom(kit) {
240
+ const root = document.documentElement;
241
+ if (!kit) {
242
+ for (const name of BRAND_VAR_NAMES) root.style.removeProperty(name);
243
+ for (const name of [...FONT_VARS.heading, ...FONT_VARS.body]) root.style.removeProperty(name);
244
+ document.getElementById(BRAND_FONT_LINK_ID)?.remove();
245
+ return;
246
+ }
247
+ for (const [name, value] of Object.entries(brandColorVars(kit))) root.style.setProperty(name, value);
248
+ for (const name of FONT_VARS.heading) root.style.setProperty(name, kit.fonts.heading);
249
+ for (const name of FONT_VARS.body) root.style.setProperty(name, kit.fonts.body);
250
+ loadBrandFonts([familyOf(kit.fonts.heading), familyOf(kit.fonts.body)]);
251
+ }
252
+
253
+ // src/lib/section-styles.ts
254
+ var STYLE_STORE_KEY = "__ohw_styles";
255
+ var STYLE_SHEET_ID = "ohw-section-styles";
256
+ function parseStyleStore(raw) {
257
+ if (!raw) return null;
258
+ try {
259
+ const parsed = JSON.parse(raw);
260
+ if (parsed?.v !== 1) return null;
261
+ return {
262
+ v: 1,
263
+ sections: typeof parsed.sections === "object" && parsed.sections ? parsed.sections : {},
264
+ nodes: typeof parsed.nodes === "object" && parsed.nodes ? parsed.nodes : {}
265
+ };
266
+ } catch {
267
+ return null;
268
+ }
269
+ }
270
+ var BG_VALUES = {
271
+ surface: "color-mix(in srgb, var(--ohw-brand-light, var(--color-light, #ECECEB)) 94%, var(--ohw-brand-dark, var(--color-dark, #0C0A09)))",
272
+ accent: "var(--ohw-brand-primary, var(--color-primary, #0078E5))",
273
+ "accent-soft": "color-mix(in srgb, var(--ohw-brand-primary, var(--color-primary, #0078E5)) 12%, var(--ohw-brand-light, var(--color-light, #FFFFFF)))"
274
+ };
275
+ var HEADLINE_SIZES = { md: 24, lg: 32, xl: 40, display: 56 };
276
+ function styleSheetCss() {
277
+ const rules = [];
278
+ for (const [tone, value] of Object.entries(BG_VALUES)) {
279
+ rules.push(`[data-ohw-style-bg="${tone}"] { background: ${value} !important; }`);
280
+ }
281
+ rules.push(
282
+ `[data-ohw-style-bg="accent"] { color: var(--ohw-brand-light, var(--color-light, #FFFFFF)) !important; }`
283
+ );
284
+ rules.push(
285
+ `[data-ohw-style-distribution="space-between"] { display: flex !important; flex-direction: column; justify-content: space-between; }`,
286
+ `[data-ohw-style-distribution="center"] { display: flex !important; flex-direction: column; justify-content: center; }`
287
+ );
288
+ for (const [scale, size] of Object.entries(HEADLINE_SIZES)) {
289
+ rules.push(
290
+ `[data-ohw-style-headline="${scale}"] :is(h1, h2, h3) { font-size: ${size}px !important; line-height: 1.15 !important; }`
291
+ );
292
+ }
293
+ rules.push(
294
+ `[data-ohw-style-aspect="fill-height"] img { height: 100% !important; aspect-ratio: auto !important; object-fit: cover; }`
295
+ );
296
+ for (const aspect of ["1:1", "4:5", "3:4", "3:2", "16:9", "2:1", "3:1"]) {
297
+ rules.push(
298
+ `[data-ohw-style-aspect="${aspect.replace(":", "-")}"] img { aspect-ratio: ${aspect.replace(":", " / ")} !important; height: auto !important; object-fit: cover; }`
299
+ );
300
+ }
301
+ const pad = { tight: 40, balanced: 64, airy: 96 };
302
+ for (const [spacing, px] of Object.entries(pad)) {
303
+ rules.push(
304
+ `[data-ohw-style-spacing="${spacing}"] { padding-top: ${px}px !important; padding-bottom: ${px}px !important; }`
305
+ );
306
+ }
307
+ return rules.join("\n");
308
+ }
309
+ var STYLE_FONT_LINK_ID = "ohw-style-fonts";
310
+ function loadStyleFonts(families) {
311
+ const unique = [...new Set(families.map((f) => f.trim()).filter(Boolean))];
312
+ const existing = document.getElementById(STYLE_FONT_LINK_ID);
313
+ if (unique.length === 0) {
314
+ existing?.remove();
315
+ return;
316
+ }
317
+ const spec = unique.map((f) => `${f.replace(/ /g, "+")}:400,500,600,700`).join("|");
318
+ const href = `https://fonts.googleapis.com/css?family=${spec}&display=swap`;
319
+ let link = existing;
320
+ if (!link) {
321
+ link = document.createElement("link");
322
+ link.id = STYLE_FONT_LINK_ID;
323
+ link.rel = "stylesheet";
324
+ document.head.appendChild(link);
325
+ }
326
+ if (link.href !== href) link.href = href;
327
+ }
328
+ var SECTION_ATTRS = {
329
+ sectionBackground: "data-ohw-style-bg",
330
+ textDistribution: "data-ohw-style-distribution",
331
+ headlineScale: "data-ohw-style-headline",
332
+ imageAspect: "data-ohw-style-aspect",
333
+ spacing: "data-ohw-style-spacing"
334
+ };
335
+ var NODE_WROTE_ATTR = "data-ohw-style-node";
336
+ var NODE_PROPS = ["color", "font-family", "font-size", "background"];
337
+ function saveInline(el, prop) {
338
+ const attr = `data-ohw-style-prev-${prop}`;
339
+ if (!el.hasAttribute(attr)) el.setAttribute(attr, el.style.getPropertyValue(prop));
340
+ }
341
+ function restoreInline(el, prop) {
342
+ const attr = `data-ohw-style-prev-${prop}`;
343
+ if (!el.hasAttribute(attr)) return;
344
+ const prev = el.getAttribute(attr) ?? "";
345
+ if (prev) el.style.setProperty(prop, prev);
346
+ else el.style.removeProperty(prop);
347
+ el.removeAttribute(attr);
348
+ }
349
+ function ensureStyleSheet() {
350
+ let el = document.getElementById(STYLE_SHEET_ID);
351
+ if (!el) {
352
+ el = document.createElement("style");
353
+ el.id = STYLE_SHEET_ID;
354
+ document.head.appendChild(el);
355
+ }
356
+ const css = styleSheetCss();
357
+ if (el.textContent !== css) el.textContent = css;
358
+ }
359
+ function clearSectionAttrs(root) {
360
+ for (const attr of Object.values(SECTION_ATTRS)) {
361
+ for (const el of Array.from(root.querySelectorAll(`[${attr}]`))) el.removeAttribute(attr);
362
+ }
363
+ for (const el of Array.from(root.querySelectorAll("[data-ohw-style-bgcolor]"))) {
364
+ restoreInline(el, "background");
365
+ el.removeAttribute("data-ohw-style-bgcolor");
366
+ }
367
+ }
368
+ function clearNodeProps(root) {
369
+ for (const el of Array.from(root.querySelectorAll(`[${NODE_WROTE_ATTR}]`))) {
370
+ const h = el;
371
+ for (const prop of NODE_PROPS) restoreInline(h, prop);
372
+ h.removeAttribute(NODE_WROTE_ATTR);
373
+ }
374
+ }
375
+ function buttonSurfaceOf(el) {
376
+ return el.closest("a, button") ?? el;
377
+ }
378
+ function applyStylesToDom(store) {
379
+ ensureStyleSheet();
380
+ clearSectionAttrs(document);
381
+ clearNodeProps(document);
382
+ loadStyleFonts(
383
+ store ? Object.values(store.nodes).flatMap((n) => n.fontFamily ? [n.fontFamily] : []) : []
384
+ );
385
+ if (!store) return;
386
+ for (const [sectionId, override] of Object.entries(store.sections)) {
387
+ const sections = document.querySelectorAll(
388
+ `[data-ohw-section="${CSS.escape(sectionId)}"]`
389
+ );
390
+ for (const section of Array.from(sections)) {
391
+ for (const [prop, attr] of Object.entries(SECTION_ATTRS)) {
392
+ const value = override[prop];
393
+ if (value === void 0) continue;
394
+ if (prop === "sectionBackground" && override.sectionBackgroundColor !== void 0) continue;
395
+ section.setAttribute(attr, String(value).replace(":", "-"));
396
+ }
397
+ if (override.sectionBackgroundColor !== void 0) {
398
+ saveInline(section, "background");
399
+ section.style.setProperty("background", override.sectionBackgroundColor, "important");
400
+ section.setAttribute("data-ohw-style-bgcolor", "");
401
+ }
402
+ }
403
+ }
404
+ for (const [key, override] of Object.entries(store.nodes)) {
405
+ const nodes = document.querySelectorAll(`[data-ohw-key="${CSS.escape(key)}"]`);
406
+ for (const el of Array.from(nodes)) {
407
+ if (override.color !== void 0) {
408
+ saveInline(el, "color");
409
+ el.style.setProperty("color", override.color, "important");
410
+ el.setAttribute(NODE_WROTE_ATTR, "");
411
+ }
412
+ if (override.fontFamily !== void 0) {
413
+ saveInline(el, "font-family");
414
+ el.style.setProperty("font-family", `'${override.fontFamily}'`, "important");
415
+ el.setAttribute(NODE_WROTE_ATTR, "");
416
+ }
417
+ if (override.fontSize !== void 0) {
418
+ saveInline(el, "font-size");
419
+ el.style.setProperty("font-size", `${override.fontSize}px`, "important");
420
+ el.setAttribute(NODE_WROTE_ATTR, "");
421
+ }
422
+ if (override.buttonBackground !== void 0 || override.buttonText !== void 0) {
423
+ const surface = buttonSurfaceOf(el);
424
+ if (override.buttonBackground !== void 0) {
425
+ saveInline(surface, "background");
426
+ surface.style.setProperty("background", override.buttonBackground, "important");
427
+ }
428
+ if (override.buttonText !== void 0) {
429
+ saveInline(surface, "color");
430
+ surface.style.setProperty("color", override.buttonText, "important");
431
+ }
432
+ surface.setAttribute(NODE_WROTE_ATTR, "");
433
+ }
434
+ }
435
+ }
436
+ }
437
+
121
438
  // src/ui/ai-tree/aiSectionsManager.tsx
122
439
  import { flushSync } from "react-dom";
123
440
  import { createRoot } from "react-dom/client";
@@ -132,7 +449,8 @@ function lucideByName(name) {
132
449
  }
133
450
  var typeStyle = (spec, font) => ({
134
451
  fontFamily: font,
135
- fontSize: spec.size,
452
+ // Headings shrink with the viewport (reaching full size around ~900px wide); body copy stays put.
453
+ 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,
136
454
  lineHeight: spec.line,
137
455
  fontWeight: spec.weight
138
456
  });
@@ -141,12 +459,58 @@ var featureLines = (value) => value.split(/\n|<br\s*\/?>/i).map((line) => line.t
141
459
  var CHECK_MASK = `data:image/svg+xml,${encodeURIComponent(
142
460
  '<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>'
143
461
  )}`;
462
+ var AI_MOBILE_CSS = [
463
+ "@media (max-width: 768px){",
464
+ "[data-ai-section]{overflow-x:hidden}",
465
+ "[data-ai-container]{padding:0 20px !important}",
466
+ "[data-ai-row]{display:flex !important;flex-direction:column !important;align-items:stretch !important}",
467
+ "[data-ai-cell]{width:100%;min-width:0}",
468
+ "[data-ai-grid]{grid-template-columns:1fr !important}",
469
+ // Group containers flatten to a column on phones; span placements come along for free.
470
+ "[data-ai-group]{display:flex !important;flex-direction:column !important}",
471
+ "[data-ai-group] > *{grid-column:auto !important}",
472
+ "[data-ai-section] img{max-width:100%}",
473
+ "}",
474
+ "@media (min-width: 769px) and (max-width: 1024px){",
475
+ "[data-ai-grid]{grid-template-columns:repeat(2, 1fr) !important}",
476
+ "}"
477
+ ].join("");
144
478
  var FEATURE_LINE_CSS = [
145
479
  "[data-ai-features]>div{position:relative;padding-left:40px;min-height:24px}",
146
480
  '[data-ai-features]>div::before{content:"";position:absolute;left:0;top:1px;width:24px;height:24px;',
147
481
  `background-color:currentColor;-webkit-mask:url("${CHECK_MASK}") no-repeat 0 0/24px 24px;`,
148
482
  `mask:url("${CHECK_MASK}") no-repeat 0 0/24px 24px}`
149
483
  ].join("");
484
+ function hexLuminance(color) {
485
+ const m = /^#([0-9a-f]{6})$/i.exec(color.trim());
486
+ if (!m) return null;
487
+ const [r2, g, b] = [0, 2, 4].map((i) => {
488
+ const c = parseInt(m[1].slice(i, i + 2), 16) / 255;
489
+ return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
490
+ });
491
+ return 0.2126 * r2 + 0.7152 * g + 0.0722 * b;
492
+ }
493
+ function hexContrast(a, b) {
494
+ const la = hexLuminance(a);
495
+ const lb = hexLuminance(b);
496
+ if (la === null || lb === null) return null;
497
+ const [hi, lo] = la > lb ? [la, lb] : [lb, la];
498
+ return (hi + 0.05) / (lo + 0.05);
499
+ }
500
+ function accentBandContext(brand) {
501
+ const p = brand.palette;
502
+ const lightWins = (hexContrast(p.light, p.primary) ?? 99) >= (hexContrast(p.dark, p.primary) ?? 0);
503
+ if (lightWins) {
504
+ return {
505
+ brand: { ...brand, palette: { dark: p.light, primary: p.light, accent: p.light, light: p.primary } },
506
+ buttonLabel: p.primary
507
+ };
508
+ }
509
+ return {
510
+ brand: { ...brand, palette: { dark: p.dark, primary: p.dark, accent: p.dark, light: p.light } },
511
+ buttonLabel: p.light
512
+ };
513
+ }
150
514
  function textAttrs(ctx, path) {
151
515
  return ctx.keyFor ? { "data-ohw-key": ctx.keyFor(path), "data-ohw-editable": "text" } : {};
152
516
  }
@@ -158,7 +522,12 @@ var AI_RESPONSIVE_CSS = [
158
522
  "@media (max-width: 640px) {",
159
523
  " [data-ai-responsive] [data-ai-grid] { grid-template-columns: 1fr !important; }",
160
524
  " [data-ai-responsive] [data-ai-columns] > * { grid-column: 1 / -1 !important; }",
525
+ // Group containers flatten to a column on phones; span placements come along for free.
526
+ " [data-ai-responsive] [data-ai-group] { display: flex !important; flex-direction: column !important; }",
527
+ " [data-ai-responsive] [data-ai-group] > * { grid-column: auto !important; }",
161
528
  " [data-ai-responsive] [data-ai-section-inner] { padding-left: 20px !important; padding-right: 20px !important; }",
529
+ " [data-ai-responsive] { overflow-x: hidden; }",
530
+ " [data-ai-responsive] img { max-width: 100%; }",
162
531
  "}"
163
532
  ].join("\n");
164
533
  var TRANSPARENT_PX = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
@@ -236,7 +605,7 @@ function ButtonEl({
236
605
  }) {
237
606
  const secondary = slots.variant === "secondary";
238
607
  const href = str(slots.href);
239
- const linkAttrs = ctx.keyFor && path ? { "data-ohw-href-key": ctx.keyFor(`${path}.href`) } : {};
608
+ const linkAttrs = ctx.keyFor && path ? { "data-ohw-href-key": ctx.keyFor(`${path}.href`), "data-ohw-role": "button" } : {};
240
609
  return /* @__PURE__ */ jsx(
241
610
  "a",
242
611
  {
@@ -252,7 +621,7 @@ function ButtonEl({
252
621
  textDecoration: "none",
253
622
  cursor: "pointer",
254
623
  ...typeStyle(AI_TREE_TOKENS.type.button, ctx.brand.fonts.body),
255
- ...secondary ? { border: `1px solid ${ctx.brand.palette.dark}`, color: ctx.brand.palette.dark } : { background: ctx.brand.palette.primary, color: AI_TREE_TOKENS.textPrimaryForeground }
624
+ ...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 }
256
625
  },
257
626
  children: /* @__PURE__ */ jsx("span", { ...path ? textAttrs(ctx, `${path}.label`) : {}, children: str(slots.label) })
258
627
  }
@@ -758,7 +1127,24 @@ function CardBlock({ node, ctx, path }) {
758
1127
  minWidth: 0
759
1128
  },
760
1129
  children: [
761
- media && (horizontal ? /* @__PURE__ */ jsx("div", { style: { flex: 1, minWidth: 0, display: "flex", alignItems: "center", padding: mediaInset }, children: media }) : /* @__PURE__ */ jsx(
1130
+ media && (horizontal ? /* @__PURE__ */ jsx(
1131
+ "div",
1132
+ {
1133
+ style: (
1134
+ // An icon hugs its glyph — flex:1 gave a 48px icon half the card and pushed the
1135
+ // text to the far side. Photos keep the half-and-half split. The inset has no
1136
+ // inner padding (the photo split absorbed that), so the icon carries its own gap.
1137
+ /^(lucide|simple):/.test(mediaRef) ? {
1138
+ flexShrink: 0,
1139
+ display: "flex",
1140
+ alignItems: "center",
1141
+ padding: mediaInset,
1142
+ [mediaPosition === "right" ? "marginLeft" : "marginRight"]: 20
1143
+ } : { flex: 1, minWidth: 0, display: "flex", alignItems: "center", padding: mediaInset }
1144
+ ),
1145
+ children: media
1146
+ }
1147
+ ) : /* @__PURE__ */ jsx(
762
1148
  "div",
763
1149
  {
764
1150
  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" },
@@ -849,13 +1235,44 @@ function AccordionBlock({ node, ctx, path }) {
849
1235
  ) })
850
1236
  ] }, i)) });
851
1237
  }
1238
+ function useIsMobile() {
1239
+ const [mobile, setMobile] = React.useState(
1240
+ () => typeof window !== "undefined" && window.matchMedia("(max-width: 768px)").matches
1241
+ );
1242
+ React.useEffect(() => {
1243
+ const mq = window.matchMedia("(max-width: 768px)");
1244
+ const update = () => setMobile(mq.matches);
1245
+ update();
1246
+ mq.addEventListener("change", update);
1247
+ return () => mq.removeEventListener("change", update);
1248
+ }, []);
1249
+ return mobile;
1250
+ }
852
1251
  function Carousel({ items, itemsPerRow, ctx }) {
1252
+ const isMobile = useIsMobile();
1253
+ const perPage = isMobile ? 1 : itemsPerRow;
1254
+ const pages = Math.max(1, Math.ceil(items.length / perPage));
853
1255
  const [page, setPage] = React.useState(0);
854
- const pages = Math.max(1, Math.ceil(items.length / itemsPerRow));
855
1256
  const current = Math.min(page, pages - 1);
1257
+ if (pages <= 1) {
1258
+ const cols = Math.max(1, Math.min(items.length, itemsPerRow));
1259
+ return /* @__PURE__ */ jsx(
1260
+ "div",
1261
+ {
1262
+ "data-ai-grid": String(cols),
1263
+ style: {
1264
+ display: "grid",
1265
+ gridTemplateColumns: `repeat(${cols}, 1fr)`,
1266
+ gap: AI_TREE_TOKENS.spacing8,
1267
+ alignItems: "start"
1268
+ },
1269
+ children: items
1270
+ }
1271
+ );
1272
+ }
856
1273
  const pageGroups = Array.from(
857
1274
  { length: pages },
858
- (_, p) => items.slice(p * itemsPerRow, (p + 1) * itemsPerRow)
1275
+ (_, p) => items.slice(p * perPage, (p + 1) * perPage)
859
1276
  );
860
1277
  const chrome = (enabled) => ({
861
1278
  border: `1px solid ${ctx.brand.palette.dark}`,
@@ -880,55 +1297,69 @@ function Carousel({ items, itemsPerRow, ctx }) {
880
1297
  cursor: "pointer",
881
1298
  padding: 0
882
1299
  });
883
- return /* @__PURE__ */ jsxs("div", { "data-ai-carousel": "", style: { display: "flex", flexDirection: "column", gap: AI_TREE_TOKENS.spacing8, alignItems: "center" }, children: [
884
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: AI_TREE_TOKENS.spacing6, width: "100%" }, children: [
885
- /* @__PURE__ */ jsx(
886
- "button",
887
- {
888
- type: "button",
889
- "aria-label": "Previous",
890
- onClick: () => setPage((p) => Math.max(0, p - 1)),
891
- style: chrome(current > 0),
892
- children: /* @__PURE__ */ jsx(ArrowLeft, { size: 24, strokeWidth: 1.5, "aria-hidden": true })
893
- }
894
- ),
895
- /* @__PURE__ */ jsx("div", { style: { flex: 1, minWidth: 0, overflow: "hidden" }, children: /* @__PURE__ */ jsx(
1300
+ const viewport = /* @__PURE__ */ jsx("div", { style: { flex: isMobile ? "0 0 auto" : 1, minWidth: 0, width: "100%", overflow: "hidden" }, children: /* @__PURE__ */ jsx(
1301
+ "div",
1302
+ {
1303
+ style: {
1304
+ display: "flex",
1305
+ transform: `translateX(-${current * 100}%)`,
1306
+ transition: "transform 0.4s ease"
1307
+ },
1308
+ children: pageGroups.map((group, p) => /* @__PURE__ */ jsx(
896
1309
  "div",
897
1310
  {
1311
+ "data-ai-grid": String(perPage),
898
1312
  style: {
899
- display: "flex",
900
- transform: `translateX(-${current * 100}%)`,
901
- transition: "transform 0.4s ease"
1313
+ flex: "0 0 100%",
1314
+ display: "grid",
1315
+ gridTemplateColumns: `repeat(${perPage}, 1fr)`,
1316
+ gap: AI_TREE_TOKENS.spacing8,
1317
+ alignItems: "start"
902
1318
  },
903
- children: pageGroups.map((group, p) => /* @__PURE__ */ jsx(
904
- "div",
905
- {
906
- "data-ai-grid": String(itemsPerRow),
907
- style: {
908
- flex: "0 0 100%",
909
- display: "grid",
910
- gridTemplateColumns: `repeat(${itemsPerRow}, 1fr)`,
911
- gap: AI_TREE_TOKENS.spacing8,
912
- alignItems: "start"
913
- },
914
- children: group
915
- },
916
- p
917
- ))
918
- }
919
- ) }),
920
- /* @__PURE__ */ jsx(
921
- "button",
922
- {
923
- type: "button",
924
- "aria-label": "Next",
925
- onClick: () => setPage((p) => Math.min(pages - 1, p + 1)),
926
- style: chrome(current < pages - 1),
927
- children: /* @__PURE__ */ jsx(ArrowRight, { size: 24, strokeWidth: 1.5, "aria-hidden": true })
928
- }
929
- )
1319
+ children: group
1320
+ },
1321
+ p
1322
+ ))
1323
+ }
1324
+ ) });
1325
+ const prevBtn = /* @__PURE__ */ jsx(
1326
+ "button",
1327
+ {
1328
+ type: "button",
1329
+ "aria-label": "Previous",
1330
+ onClick: () => setPage((p) => Math.max(0, p - 1)),
1331
+ style: chrome(current > 0),
1332
+ children: /* @__PURE__ */ jsx(ArrowLeft, { size: 24, strokeWidth: 1.5, "aria-hidden": true })
1333
+ }
1334
+ );
1335
+ const nextBtn = /* @__PURE__ */ jsx(
1336
+ "button",
1337
+ {
1338
+ type: "button",
1339
+ "aria-label": "Next",
1340
+ onClick: () => setPage((p) => Math.min(pages - 1, p + 1)),
1341
+ style: chrome(current < pages - 1),
1342
+ children: /* @__PURE__ */ jsx(ArrowRight, { size: 24, strokeWidth: 1.5, "aria-hidden": true })
1343
+ }
1344
+ );
1345
+ const dots = /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: 9, justifyContent: "center" }, children: pageGroups.map((_, p) => /* @__PURE__ */ jsx("button", { type: "button", "aria-label": `Page ${p + 1}`, onClick: () => setPage(p), style: dot(p === current) }, p)) });
1346
+ if (isMobile) {
1347
+ return /* @__PURE__ */ jsxs("div", { "data-ai-carousel": "", style: { display: "flex", flexDirection: "column", gap: AI_TREE_TOKENS.spacing6, alignItems: "center" }, children: [
1348
+ viewport,
1349
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: AI_TREE_TOKENS.spacing6, justifyContent: "center" }, children: [
1350
+ prevBtn,
1351
+ nextBtn
1352
+ ] }),
1353
+ dots
1354
+ ] });
1355
+ }
1356
+ return /* @__PURE__ */ jsxs("div", { "data-ai-carousel": "", style: { display: "flex", flexDirection: "column", gap: AI_TREE_TOKENS.spacing8, alignItems: "center" }, children: [
1357
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: AI_TREE_TOKENS.spacing6, width: "100%" }, children: [
1358
+ prevBtn,
1359
+ viewport,
1360
+ nextBtn
930
1361
  ] }),
931
- pages > 1 && /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: 9, justifyContent: "center" }, children: pageGroups.map((_, p) => /* @__PURE__ */ jsx("button", { type: "button", "aria-label": `Page ${p + 1}`, onClick: () => setPage(p), style: dot(p === current) }, p)) })
1362
+ dots
932
1363
  ] });
933
1364
  }
934
1365
  function CollectionBlock({ node, ctx, path }) {
@@ -1022,6 +1453,49 @@ function renderNode(node, ctx, path) {
1022
1453
  switch (node.type) {
1023
1454
  case "text":
1024
1455
  return /* @__PURE__ */ jsx(TextBlock, { slots, ctx, path });
1456
+ // Layout container: arranges child blocks, contributes no content of its own. `grid` is a
1457
+ // nested 12-column grid the children span (a collage is 3–5 media on it, bottom-aligned so
1458
+ // mixed aspects read as a composition); `split` is exactly two children at a ratio; `stack`
1459
+ // is a column. Children render through this same dispatcher, so edit markers, media
1460
+ // resolution, and copy paths all work unchanged inside a group.
1461
+ case "group": {
1462
+ const layout = str(slots.layout);
1463
+ const gap = slots.spacing === "tight" ? AI_TREE_TOKENS.spacing3 : slots.spacing === "airy" ? AI_TREE_TOKENS.spacing8 : AI_TREE_TOKENS.spacing6;
1464
+ const kids = (node.children ?? []).map((child, i) => /* @__PURE__ */ jsx(
1465
+ "div",
1466
+ {
1467
+ style: layout === "grid" ? {
1468
+ gridColumn: `span ${typeof child.span === "number" ? Math.min(12, Math.max(1, child.span)) : 12}`,
1469
+ minWidth: 0
1470
+ } : { minWidth: 0 },
1471
+ children: renderNode(child, ctx, `${path}.c${i}`)
1472
+ },
1473
+ i
1474
+ ));
1475
+ if (layout === "grid") {
1476
+ return /* @__PURE__ */ jsx(
1477
+ "div",
1478
+ {
1479
+ "data-ai-group": "grid",
1480
+ style: { display: "grid", gridTemplateColumns: "repeat(12, 1fr)", gap, alignItems: "end" },
1481
+ children: kids
1482
+ }
1483
+ );
1484
+ }
1485
+ if (layout === "split") {
1486
+ const ratio = str(slots.ratio);
1487
+ const cols = ratio === "3:5" ? "3fr 5fr" : ratio === "5:3" ? "5fr 3fr" : "1fr 1fr";
1488
+ return /* @__PURE__ */ jsx(
1489
+ "div",
1490
+ {
1491
+ "data-ai-group": "split",
1492
+ style: { display: "grid", gridTemplateColumns: cols, gap, alignItems: "center" },
1493
+ children: kids
1494
+ }
1495
+ );
1496
+ }
1497
+ return /* @__PURE__ */ jsx("div", { "data-ai-group": "stack", style: { display: "flex", flexDirection: "column", gap }, children: kids });
1498
+ }
1025
1499
  case "button":
1026
1500
  return /* @__PURE__ */ jsx(ButtonEl, { slots, ctx, path });
1027
1501
  case "button-row":
@@ -1102,33 +1576,111 @@ function renderNode(node, ctx, path) {
1102
1576
  }
1103
1577
  );
1104
1578
  }
1105
- case "form":
1106
- return /* @__PURE__ */ jsx("div", { style: { display: "flex", flexDirection: "column", gap: AI_TREE_TOKENS.spacing4 }, children: (node.children ?? []).map((child, i) => {
1107
- if (child.type === "input") {
1108
- const cs = child.slots ?? {};
1109
- return /* @__PURE__ */ jsxs("div", { children: [
1110
- /* @__PURE__ */ jsx(
1111
- "div",
1112
- {
1113
- ...textAttrs(ctx, `${path}.c${i}.label`),
1114
- style: { ...typeStyle(AI_TREE_TOKENS.type.bodyMBold, ctx.brand.fonts.body), color: ctx.brand.palette.dark, marginBottom: 6 },
1115
- children: str(cs.label)
1116
- }
1117
- ),
1118
- /* @__PURE__ */ jsx(
1119
- "div",
1120
- {
1121
- style: {
1122
- border: `1px solid ${ctx.brand.palette.accent}`,
1123
- borderRadius: AI_TREE_TOKENS.radiusButton,
1124
- height: cs.kind === "textarea" ? 96 : 42
1125
- }
1126
- }
1127
- )
1128
- ] }, i);
1129
- }
1130
- return /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(ButtonEl, { slots: child.slots ?? {}, ctx, path: `${path}.c${i}` }) }, i);
1131
- }) });
1579
+ case "form": {
1580
+ const formAttrs = ctx.keyFor ? {
1581
+ "data-ohw-editable": "form",
1582
+ "data-ohw-key": ctx.keyFor(`${path}.form`),
1583
+ "data-ohw-success-text": "Thanks \u2014 we'll be in touch shortly."
1584
+ } : {};
1585
+ const fieldStyle = {
1586
+ width: "100%",
1587
+ boxSizing: "border-box",
1588
+ border: `1px solid color-mix(in srgb, ${ctx.brand.palette.dark} 45%, #ffffff)`,
1589
+ borderRadius: 0,
1590
+ padding: 12,
1591
+ background: "#fff",
1592
+ color: ctx.brand.palette.dark,
1593
+ outline: "none",
1594
+ ...typeStyle(AI_TREE_TOKENS.type.bodyM, ctx.brand.fonts.body)
1595
+ };
1596
+ const labelStyle = {
1597
+ ...typeStyle(AI_TREE_TOKENS.type.bodyMBold, ctx.brand.fonts.body),
1598
+ color: ctx.brand.palette.dark,
1599
+ textAlign: "left",
1600
+ width: "100%"
1601
+ };
1602
+ const centered = ctx.sectionAlignment === "center";
1603
+ const submitAlign = centered ? "center" : "flex-start";
1604
+ const children = node.children ?? [];
1605
+ return (
1606
+ // 32px between the field group and the submit. In a stacked (centered) section the form is
1607
+ // capped at 780px and centered — the section's 12-col grid would otherwise leave it hugging
1608
+ // the left edge; a split section lets it fill its own column.
1609
+ /* @__PURE__ */ jsxs(
1610
+ "form",
1611
+ {
1612
+ ...formAttrs,
1613
+ "data-ai-form": "",
1614
+ style: {
1615
+ display: "flex",
1616
+ flexDirection: "column",
1617
+ gap: 32,
1618
+ width: "100%",
1619
+ ...centered ? { maxWidth: 780, marginLeft: "auto", marginRight: "auto" } : {}
1620
+ },
1621
+ children: [
1622
+ /* @__PURE__ */ jsx("div", { style: { display: "flex", flexDirection: "column", gap: 24, width: "100%", alignItems: "flex-start" }, children: children.map((child, i) => {
1623
+ if (child.type !== "input") return null;
1624
+ const cs = child.slots ?? {};
1625
+ const kind = str(cs.kind);
1626
+ const label = str(cs.label);
1627
+ const placeholder = str(cs.placeholder);
1628
+ const required = cs.required === true;
1629
+ const name = label.toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-+|-+$/gu, "") || `field-${i}`;
1630
+ const isTextarea = kind === "textarea";
1631
+ return /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 8, width: "100%" }, children: [
1632
+ /* @__PURE__ */ jsx("label", { ...textAttrs(ctx, `${path}.c${i}.label`), style: labelStyle, children: label }),
1633
+ isTextarea ? /* @__PURE__ */ jsx(
1634
+ "textarea",
1635
+ {
1636
+ name,
1637
+ placeholder,
1638
+ required,
1639
+ style: { ...fieldStyle, height: 180, resize: "vertical" }
1640
+ }
1641
+ ) : /* @__PURE__ */ jsx(
1642
+ "input",
1643
+ {
1644
+ name,
1645
+ type: kind === "email" ? "email" : "text",
1646
+ placeholder,
1647
+ required,
1648
+ style: { ...fieldStyle, height: 48 }
1649
+ }
1650
+ )
1651
+ ] }, i);
1652
+ }) }),
1653
+ children.map((child, i) => {
1654
+ if (child.type === "input") return null;
1655
+ const cs = child.slots ?? {};
1656
+ return /* @__PURE__ */ jsx(
1657
+ "button",
1658
+ {
1659
+ type: "submit",
1660
+ style: {
1661
+ alignSelf: submitAlign,
1662
+ border: "none",
1663
+ cursor: "pointer",
1664
+ padding: "12px 24px",
1665
+ // Corner radius follows the host template's own buttons (measured from a template
1666
+ // CTA); 8px only when the page has no template button to match.
1667
+ borderRadius: ctx.buttonRadius ?? 8,
1668
+ // Brand-styled: primary fill, brand-derived label colour (not a fixed token) so it
1669
+ // reads correctly on custom palettes.
1670
+ background: ctx.brand.palette.primary,
1671
+ color: ctx.buttonLabel ?? ctx.brand.palette.light,
1672
+ ...typeStyle(AI_TREE_TOKENS.type.button, ctx.brand.fonts.body)
1673
+ },
1674
+ children: /* @__PURE__ */ jsx("span", { ...textAttrs(ctx, `${path}.c${i}.label`), children: str(cs.label) })
1675
+ },
1676
+ i
1677
+ );
1678
+ })
1679
+ ]
1680
+ }
1681
+ )
1682
+ );
1683
+ }
1132
1684
  case "schedule-widget":
1133
1685
  return /* @__PURE__ */ jsx(
1134
1686
  "div",
@@ -1149,16 +1701,27 @@ function renderNode(node, ctx, path) {
1149
1701
  return null;
1150
1702
  }
1151
1703
  }
1152
- function AiTreeRenderer({ tree, brand, resolveMedia, editKeyPrefix }) {
1704
+ function AiTreeRenderer({
1705
+ tree,
1706
+ brand,
1707
+ buttonRadius,
1708
+ resolveMedia,
1709
+ editKeyPrefix
1710
+ }) {
1153
1711
  if (!isRenderableTree(tree)) {
1154
1712
  return null;
1155
1713
  }
1156
1714
  const resolvedBrand = brand ?? AI_DEFAULT_BRAND;
1715
+ const band = (tree.settings ?? {}).sectionBackground === "accent" ? accentBandContext(resolvedBrand) : null;
1716
+ const blockBrand = band?.brand ?? resolvedBrand;
1157
1717
  const ctx = {
1158
- brand: resolvedBrand,
1718
+ brand: blockBrand,
1159
1719
  resolveMedia: resolveMedia ?? (() => null),
1160
- cardSurface: resolvedBrand.palette.light.toUpperCase() === AI_DEFAULT_BRAND.palette.light.toUpperCase() ? "#ECECEB" : `color-mix(in srgb, ${resolvedBrand.palette.light} 90%, ${resolvedBrand.palette.dark})`,
1161
- keyFor: editKeyPrefix ? (path) => `${editKeyPrefix}.${path}` : null
1720
+ cardSurface: blockBrand.palette.light.toUpperCase() === AI_DEFAULT_BRAND.palette.light.toUpperCase() ? "#ECECEB" : `color-mix(in srgb, ${blockBrand.palette.light} 90%, ${blockBrand.palette.dark})`,
1721
+ keyFor: editKeyPrefix ? (path) => `${editKeyPrefix}.${path}` : null,
1722
+ sectionAlignment: (tree.settings ?? {}).alignment === "center" ? "center" : "left",
1723
+ buttonRadius,
1724
+ ...band ? { buttonLabel: band.buttonLabel } : {}
1162
1725
  };
1163
1726
  const settings = tree.settings ?? {};
1164
1727
  const pad = AI_TREE_TOKENS.sectionPadding[settings.spacing ?? "balanced"];
@@ -1166,6 +1729,20 @@ function AiTreeRenderer({ tree, brand, resolveMedia, editKeyPrefix }) {
1166
1729
  const overlayAlpha = { low: 0.25, medium: 0.45, high: 0.65 }[settings.overlayIntensity ?? "medium"];
1167
1730
  const backgroundUrl = isOverlay && settings.backgroundMedia ? ctx.resolveMedia(settings.backgroundMedia) : null;
1168
1731
  const bgAttrs = ctx.keyFor && isOverlay && settings.backgroundMedia ? { "data-ohw-key": ctx.keyFor("settings.backgroundMedia"), "data-ohw-editable": "bg-image" } : {};
1732
+ const toneBackground = (() => {
1733
+ const { dark, primary, light } = resolvedBrand.palette;
1734
+ switch (settings.sectionBackground) {
1735
+ case "surface":
1736
+ return `color-mix(in srgb, ${light} 94%, ${dark})`;
1737
+ case "accent":
1738
+ return primary;
1739
+ case "accent-soft":
1740
+ return `color-mix(in srgb, ${primary} 12%, ${light})`;
1741
+ default:
1742
+ return void 0;
1743
+ }
1744
+ })();
1745
+ const distributed = !isOverlay && settings.textDistribution;
1169
1746
  return /* @__PURE__ */ jsxs(
1170
1747
  "section",
1171
1748
  {
@@ -1175,13 +1752,15 @@ function AiTreeRenderer({ tree, brand, resolveMedia, editKeyPrefix }) {
1175
1752
  style: {
1176
1753
  position: "relative",
1177
1754
  padding: `${pad}px 0`,
1178
- background: isOverlay && !backgroundUrl ? "#DBEAFE" : void 0,
1755
+ background: toneBackground ?? (isOverlay && !backgroundUrl ? "#DBEAFE" : void 0),
1179
1756
  backgroundImage: backgroundUrl ? `url(${backgroundUrl})` : void 0,
1180
1757
  backgroundSize: "cover",
1181
- backgroundPosition: "center"
1758
+ backgroundPosition: "center",
1759
+ color: settings.sectionBackground === "accent" ? blockBrand.palette.dark : void 0
1182
1760
  },
1183
1761
  children: [
1184
1762
  /* @__PURE__ */ jsx("style", { children: AI_RESPONSIVE_CSS }),
1763
+ /* @__PURE__ */ jsx("style", { children: AI_MOBILE_CSS }),
1185
1764
  isOverlay && backgroundUrl && /* @__PURE__ */ jsx("div", { style: { position: "absolute", inset: 0, background: `rgba(255,255,255,${overlayAlpha})` } }),
1186
1765
  /* @__PURE__ */ jsx(
1187
1766
  "div",
@@ -1202,10 +1781,24 @@ function AiTreeRenderer({ tree, brand, resolveMedia, editKeyPrefix }) {
1202
1781
  display: "grid",
1203
1782
  gridTemplateColumns: "repeat(12, 1fr)",
1204
1783
  gap: AI_TREE_TOKENS.spacing6,
1205
- alignItems: settings.verticalPosition === "top" ? "start" : "center",
1784
+ alignItems: distributed === "space-between" ? "stretch" : (distributed ?? settings.verticalPosition) === "top" ? "start" : "center",
1206
1785
  marginTop: r2 > 0 ? AI_TREE_TOKENS.spacing8 : 0
1207
1786
  },
1208
- children: (row.blocks ?? []).map((block, b) => /* @__PURE__ */ jsx("div", { style: { gridColumn: `span ${Math.min(12, Math.max(1, block.span))}`, minWidth: 0 }, children: renderNode(block, ctx, `r${r2}.b${b}`) }, b))
1787
+ children: (row.blocks ?? []).map((block, b) => /* @__PURE__ */ jsx(
1788
+ "div",
1789
+ {
1790
+ "data-ai-cell": "",
1791
+ style: {
1792
+ gridColumn: `span ${Math.min(12, Math.max(1, block.span))}`,
1793
+ minWidth: 0,
1794
+ // space-between: each column becomes a flex column whose content spreads over
1795
+ // the full row height instead of clumping at the top.
1796
+ ...distributed === "space-between" ? { display: "flex", flexDirection: "column", justifyContent: "space-between" } : {}
1797
+ },
1798
+ children: renderNode(block, ctx, `r${r2}.b${b}`)
1799
+ },
1800
+ b
1801
+ ))
1209
1802
  },
1210
1803
  r2
1211
1804
  ))
@@ -1221,17 +1814,36 @@ import { jsx as jsx2 } from "react/jsx-runtime";
1221
1814
  var CONTAINER_ATTR = "data-ohw-ai-generated";
1222
1815
  var REPLACED_ATTR = "data-ohw-ai-replaced-by";
1223
1816
  var REMOVED_ATTR = "data-ohw-ai-removed";
1817
+ var TEMPLATE_HIDDEN_ATTR = "data-ohw-ai-template-hidden";
1818
+ var CHROME_SECTION_IDS = /* @__PURE__ */ new Set(["navbar", "footer"]);
1819
+ function readRootVar(name) {
1820
+ if (typeof document === "undefined") return "";
1821
+ return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
1822
+ }
1823
+ function deriveBrandOverride() {
1824
+ const dark = readRootVar("--ohw-brand-dark");
1825
+ const primary = readRootVar("--ohw-brand-primary");
1826
+ const light = readRootVar("--ohw-brand-light");
1827
+ if (!dark || !primary || !light) return null;
1828
+ const accent = readRootVar("--ohw-brand-accent");
1829
+ const heading = readRootVar("--font-heading") || readRootVar("--font-display");
1830
+ const body = readRootVar("--font-body");
1831
+ return {
1832
+ palette: { dark, primary, accent: accent || dark, light },
1833
+ fonts: {
1834
+ heading: heading || AI_DEFAULT_BRAND.fonts.heading,
1835
+ body: body || AI_DEFAULT_BRAND.fonts.body
1836
+ }
1837
+ };
1838
+ }
1224
1839
  function deriveTemplateBrand() {
1225
- if (typeof document === "undefined") return null;
1226
- const cs = getComputedStyle(document.documentElement);
1227
- const read = (name) => cs.getPropertyValue(name).trim();
1228
- const dark = read("--color-dark");
1229
- const primary = read("--color-primary");
1230
- const light = read("--color-light");
1840
+ const dark = readRootVar("--color-dark");
1841
+ const primary = readRootVar("--color-primary");
1842
+ const light = readRootVar("--color-light");
1231
1843
  if (!dark || !primary || !light) return null;
1232
- const accent = read("--color-accent");
1233
- const heading = read("--font-heading") || read("--font-display");
1234
- const body = read("--font-body");
1844
+ const accent = readRootVar("--color-accent");
1845
+ const heading = readRootVar("--font-heading") || readRootVar("--font-display");
1846
+ const body = readRootVar("--font-body");
1235
1847
  return {
1236
1848
  palette: { dark, primary, accent: accent || dark, light },
1237
1849
  fonts: {
@@ -1240,6 +1852,13 @@ function deriveTemplateBrand() {
1240
1852
  }
1241
1853
  };
1242
1854
  }
1855
+ function deriveTemplateButtonRadius() {
1856
+ if (typeof document === "undefined") return null;
1857
+ const btn = document.querySelector('[data-ohw-role="button"]');
1858
+ if (!btn) return null;
1859
+ const radius = getComputedStyle(btn).borderTopLeftRadius;
1860
+ return radius || null;
1861
+ }
1243
1862
  var mounted = /* @__PURE__ */ new Map();
1244
1863
  function findTemplateSection(id) {
1245
1864
  for (const el of document.querySelectorAll(`[data-ohw-section="${CSS.escape(id)}"]`)) {
@@ -1303,6 +1922,24 @@ function syncRemovedSections(state) {
1303
1922
  }
1304
1923
  }
1305
1924
  }
1925
+ function syncTemplateHidden(state, pageHasSections) {
1926
+ const hide = state.hideTemplate === true && pageHasSections;
1927
+ for (const el of Array.from(document.querySelectorAll(`[${TEMPLATE_HIDDEN_ATTR}]`))) {
1928
+ if (!hide) {
1929
+ el.style.removeProperty("display");
1930
+ el.removeAttribute(TEMPLATE_HIDDEN_ATTR);
1931
+ }
1932
+ }
1933
+ if (!hide) return;
1934
+ for (const el of Array.from(document.querySelectorAll("[data-ohw-section]"))) {
1935
+ if (el.hasAttribute(CONTAINER_ATTR)) continue;
1936
+ if (CHROME_SECTION_IDS.has(el.getAttribute("data-ohw-section") ?? "")) continue;
1937
+ if (el.parentElement?.closest("[data-ohw-section]")) continue;
1938
+ if (el.hasAttribute(REPLACED_ATTR) || el.hasAttribute(REMOVED_ATTR)) continue;
1939
+ el.style.display = "none";
1940
+ el.setAttribute(TEMPLATE_HIDDEN_ATTR, "");
1941
+ }
1942
+ }
1306
1943
  function syncReplacedOriginals(state) {
1307
1944
  for (const el of document.querySelectorAll(`[${REPLACED_ATTR}]`)) {
1308
1945
  const byId = el.getAttribute(REPLACED_ATTR) ?? "";
@@ -1321,10 +1958,64 @@ function syncReplacedOriginals(state) {
1321
1958
  }
1322
1959
  }
1323
1960
  }
1961
+ var sectionOrderIndex = /* @__PURE__ */ new Map();
1962
+ function setAiSectionOrder(raw, currentPath) {
1963
+ const next = /* @__PURE__ */ new Map();
1964
+ if (raw) {
1965
+ try {
1966
+ const entries = JSON.parse(raw);
1967
+ for (const entry of entries) {
1968
+ if (!entry.pagePath || entry.pagePath === currentPath) next.set(entry.instanceId, entry.order);
1969
+ }
1970
+ } catch {
1971
+ }
1972
+ }
1973
+ sectionOrderIndex = next;
1974
+ }
1975
+ function applyExplicitOrder(entries) {
1976
+ if (sectionOrderIndex.size === 0) return entries;
1977
+ return entries.map((entry, index) => ({ entry, index, order: sectionOrderIndex.get(entry.id) })).sort((a, b) => {
1978
+ if (a.order === void 0 && b.order === void 0) return a.index - b.index;
1979
+ if (a.order === void 0) return 1;
1980
+ if (b.order === void 0) return -1;
1981
+ return a.order - b.order;
1982
+ }).map((item) => item.entry);
1983
+ }
1984
+ function orderByChain(sections) {
1985
+ const ids = new Set(sections.map((entry) => entry.id));
1986
+ const after = /* @__PURE__ */ new Map();
1987
+ const roots = [];
1988
+ for (const entry of sections) {
1989
+ const anchor = entry.replaces ?? entry.beforeSection ?? entry.afterSection ?? null;
1990
+ if (anchor && ids.has(anchor)) {
1991
+ const bucket = after.get(anchor);
1992
+ if (bucket) bucket.push(entry);
1993
+ else after.set(anchor, [entry]);
1994
+ } else {
1995
+ roots.push(entry);
1996
+ }
1997
+ }
1998
+ const out = [];
1999
+ const seen = /* @__PURE__ */ new Set();
2000
+ const visit = (entry) => {
2001
+ if (seen.has(entry.id)) return;
2002
+ seen.add(entry.id);
2003
+ out.push(entry);
2004
+ for (const child of after.get(entry.id) ?? []) visit(child);
2005
+ };
2006
+ for (const root of roots) visit(root);
2007
+ return out.length === sections.length ? out : sections;
2008
+ }
1324
2009
  function applyAiSectionsToDom(state, options) {
1325
2010
  if (typeof document === "undefined") return;
2011
+ const brandOverride = deriveBrandOverride();
1326
2012
  const templateBrand = deriveTemplateBrand();
1327
- const activeIds = new Set(state.sections.map((entry) => entry.id));
2013
+ const templateButtonRadius = deriveTemplateButtonRadius();
2014
+ const brandKey = brandOverride ? JSON.stringify(brandOverride) : "";
2015
+ const pagePath = window.location.pathname;
2016
+ const pageSections = state.sections.filter((entry) => (entry.path ?? "/") === pagePath);
2017
+ const activeIds = new Set(pageSections.map((entry) => entry.id));
2018
+ const ordered = state.hideTemplate === true ? applyExplicitOrder(orderByChain(pageSections)) : pageSections;
1328
2019
  for (const [id, section] of mounted) {
1329
2020
  if (!activeIds.has(id)) {
1330
2021
  section.root.unmount();
@@ -1332,8 +2023,8 @@ function applyAiSectionsToDom(state, options) {
1332
2023
  mounted.delete(id);
1333
2024
  }
1334
2025
  }
1335
- for (const entry of state.sections) {
1336
- const serialized = JSON.stringify(entry);
2026
+ for (const entry of ordered) {
2027
+ const serialized = JSON.stringify(entry) + brandKey;
1337
2028
  const existing = mounted.get(entry.id);
1338
2029
  if (existing && existing.serialized === serialized && existing.container.isConnected) {
1339
2030
  continue;
@@ -1358,7 +2049,8 @@ function applyAiSectionsToDom(state, options) {
1358
2049
  AiTreeRenderer,
1359
2050
  {
1360
2051
  tree: entry.tree,
1361
- brand: entry.brand ?? options?.brand ?? templateBrand ?? void 0,
2052
+ brand: brandOverride ?? entry.brand ?? options?.brand ?? templateBrand ?? void 0,
2053
+ buttonRadius: templateButtonRadius,
1362
2054
  resolveMedia,
1363
2055
  editKeyPrefix: `ai.${entry.id}`
1364
2056
  }
@@ -1367,8 +2059,20 @@ function applyAiSectionsToDom(state, options) {
1367
2059
  });
1368
2060
  mounted.set(entry.id, { root, container, serialized });
1369
2061
  }
2062
+ if (state.hideTemplate === true) {
2063
+ let prev = null;
2064
+ for (const entry of ordered) {
2065
+ const el = mounted.get(entry.id)?.container;
2066
+ if (!el) continue;
2067
+ if (prev && !(prev.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_FOLLOWING)) {
2068
+ prev.insertAdjacentElement("afterend", el);
2069
+ }
2070
+ prev = el;
2071
+ }
2072
+ }
1370
2073
  syncReplacedOriginals(state);
1371
2074
  syncRemovedSections(state);
2075
+ syncTemplateHidden(state, pageSections.length > 0);
1372
2076
  }
1373
2077
 
1374
2078
  // src/useLinkHrefGuardian.ts
@@ -1495,6 +2199,7 @@ function EmailCaptureModal({ title, subtitle, onSubmit, onClose }) {
1495
2199
  /* @__PURE__ */ jsx3(
1496
2200
  Dialog.Overlay,
1497
2201
  {
2202
+ "data-ohw-scheduling-modal": "",
1498
2203
  className: "fixed inset-0 z-50",
1499
2204
  style: { background: "rgba(0,0,0,0.45)" }
1500
2205
  }
@@ -1502,6 +2207,7 @@ function EmailCaptureModal({ title, subtitle, onSubmit, onClose }) {
1502
2207
  /* @__PURE__ */ jsxs2(
1503
2208
  Dialog.Content,
1504
2209
  {
2210
+ "data-ohw-scheduling-modal": "",
1505
2211
  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",
1506
2212
  style: { maxWidth: 400 },
1507
2213
  children: [
@@ -1975,7 +2681,7 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAf
1975
2681
  const autoId = useId();
1976
2682
  const insertAfter = insertAfterProp ?? autoId;
1977
2683
  const [schedule, setSchedule] = useState2(null);
1978
- const [loading, setLoading] = useState2(true);
2684
+ const [loading, setLoading] = useState2(initialScheduleId !== null);
1979
2685
  const [inEditor, setInEditor] = useState2(false);
1980
2686
  const [isHovered, setIsHovered] = useState2(false);
1981
2687
  const [modalState, setModalState] = useState2(null);
@@ -2149,8 +2855,10 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAf
2149
2855
  "*"
2150
2856
  );
2151
2857
  };
2152
- if (!inEditor && !loading && !schedule) return null;
2153
2858
  const sectionId = `scheduling-${insertAfter}`;
2859
+ if (!inEditor && !loading && !schedule) {
2860
+ return /* @__PURE__ */ jsx4("section", { "data-ohw-section": sectionId, "data-ohw-scheduling-anchor": insertAfter, style: { display: "none" } });
2861
+ }
2154
2862
  return /* @__PURE__ */ jsxs3(
2155
2863
  "section",
2156
2864
  {
@@ -7067,13 +7775,17 @@ function MediaOverlay({
7067
7775
  hover,
7068
7776
  isUploading,
7069
7777
  fadingOut = false,
7778
+ selected = false,
7779
+ hovered = false,
7070
7780
  onFadeOutComplete,
7071
7781
  onReplace,
7782
+ onSelect,
7072
7783
  onVideoSettingsChange
7073
7784
  }) {
7074
7785
  const { rect } = hover;
7075
7786
  const skeletonRef = React8.useRef(null);
7076
7787
  const isVideo = hover.elementType === "video";
7788
+ const showChrome = !selected || hovered;
7077
7789
  const autoplay = hover.videoAutoplay ?? true;
7078
7790
  const muted = hover.videoMuted ?? true;
7079
7791
  const probeRef = React8.useRef(null);
@@ -7087,6 +7799,7 @@ function MediaOverlay({
7087
7799
  (prev) => prev && prev.fullWidth === width && prev.height === height ? prev : { fullWidth: width, height }
7088
7800
  );
7089
7801
  }, [isVideo]);
7802
+ const replaceLabel = isVideo ? "Replace video" : hover.elementType === "bg-image" ? "Replace background" : "Replace image";
7090
7803
  const replaceMode = buttonSize ? replaceButtonMode({ width: rect.width, height: rect.height }, buttonSize) : "none";
7091
7804
  const box = {
7092
7805
  position: "fixed",
@@ -7120,7 +7833,7 @@ function MediaOverlay({
7120
7833
  }
7121
7834
  );
7122
7835
  }
7123
- const settingsBar = isVideo && !hover.isDragOver ? /* @__PURE__ */ jsxs7(
7836
+ const settingsBar = isVideo && !hover.isDragOver && showChrome ? /* @__PURE__ */ jsxs7(
7124
7837
  "div",
7125
7838
  {
7126
7839
  "data-ohw-bridge": "",
@@ -7190,10 +7903,12 @@ function MediaOverlay({
7190
7903
  // in-document, pointer-events does it natively. The button below opts back in, so
7191
7904
  // Replace still works.
7192
7905
  pointerEvents: hover.hasTextOverlap ? "none" : "auto",
7193
- boxShadow: "inset 0 0 0 1.5px var(--color-primary)",
7194
- background: "color-mix(in srgb, var(--color-primary) 20%, transparent)"
7906
+ // Selected: a firm component ring with no wash, so the image reads as chosen rather
7907
+ // than hovered. Hover keeps the existing tinted preview.
7908
+ boxShadow: selected ? "inset 0 0 0 2px var(--color-primary)" : "inset 0 0 0 1.5px var(--color-primary)",
7909
+ background: selected ? "transparent" : "color-mix(in srgb, var(--color-primary) 20%, transparent)"
7195
7910
  },
7196
- onClick: () => onReplace(hover.key),
7911
+ onClick: () => onSelect && !selected ? onSelect(hover.key) : onReplace(hover.key),
7197
7912
  children: [
7198
7913
  /* @__PURE__ */ jsxs7(
7199
7914
  Button,
@@ -7214,17 +7929,17 @@ function MediaOverlay({
7214
7929
  },
7215
7930
  children: [
7216
7931
  isVideo ? /* @__PURE__ */ jsx15(Film, { size: 14 }) : /* @__PURE__ */ jsx15(ImageIcon, { size: 14 }),
7217
- isVideo ? "Replace video" : "Replace image"
7932
+ replaceLabel
7218
7933
  ]
7219
7934
  }
7220
7935
  ),
7221
- replaceMode === "none" ? null : /* @__PURE__ */ jsxs7(
7936
+ showChrome && replaceMode !== "none" && /* @__PURE__ */ jsxs7(
7222
7937
  Button,
7223
7938
  {
7224
7939
  "data-ohw-media-overlay": "",
7225
7940
  variant: "outline",
7226
7941
  size: "sm",
7227
- "aria-label": isVideo ? "Replace video" : "Replace image",
7942
+ "aria-label": replaceLabel,
7228
7943
  className: "gap-1.5 cursor-pointer hover:bg-background",
7229
7944
  style: {
7230
7945
  ...OVERLAY_BUTTON_STYLE,
@@ -7247,7 +7962,7 @@ function MediaOverlay({
7247
7962
  },
7248
7963
  children: [
7249
7964
  isVideo ? /* @__PURE__ */ jsx15(Film, { size: 14 }) : /* @__PURE__ */ jsx15(ImageIcon, { size: 14 }),
7250
- replaceMode === "full" ? isVideo ? "Replace video" : "Replace image" : null
7965
+ replaceMode === "full" ? replaceLabel : null
7251
7966
  ]
7252
7967
  }
7253
7968
  )
@@ -7331,6 +8046,8 @@ function parseSectionsFromRoot(root) {
7331
8046
  const id = el.getAttribute("data-ohw-section") ?? "";
7332
8047
  if (!id || seen.has(id) || LINK_PICKER_EXCLUDED_IDS.has(id)) continue;
7333
8048
  if (el.parentElement?.closest("[data-ohw-section]")) continue;
8049
+ if (el.hasAttribute("data-ohw-ai-template-hidden") || el.hasAttribute("data-ohw-ai-removed") || el.hasAttribute("data-ohw-ai-replaced-by"))
8050
+ continue;
7334
8051
  seen.add(id);
7335
8052
  const label = el.getAttribute("data-ohw-section-label") ?? titleCaseSectionId(id);
7336
8053
  sections.push({ id, label });
@@ -7360,6 +8077,10 @@ function topLevelSections() {
7360
8077
  function instanceIdOf(el) {
7361
8078
  return el.dataset.ohwInstance ?? el.dataset.ohwSection ?? "";
7362
8079
  }
8080
+ function findByInstanceId(instanceId) {
8081
+ const escapedId = CSS.escape(instanceId);
8082
+ return document.querySelector(`[data-ohw-instance="${escapedId}"]`) ?? document.querySelector(`[data-ohw-section="${escapedId}"]:not([data-ohw-instance])`);
8083
+ }
7363
8084
  function planSectionMove(instanceId, targetIndex, currentPath) {
7364
8085
  const sections = topLevelSections();
7365
8086
  const index = sections.findIndex((el) => instanceIdOf(el) === instanceId);
@@ -7395,7 +8116,7 @@ function syncRemovedFlags(entries) {
7395
8116
  }
7396
8117
  });
7397
8118
  for (const id of removedIds) {
7398
- const el = document.querySelector(`[data-ohw-instance="${CSS.escape(id)}"]`);
8119
+ const el = findByInstanceId(id);
7399
8120
  if (el) {
7400
8121
  el.style.display = "none";
7401
8122
  el.setAttribute(REMOVED_ATTR2, "");
@@ -7423,9 +8144,7 @@ function applyPersistedOrder(entries) {
7423
8144
  }
7424
8145
  }
7425
8146
  function setSectionRemoved(instanceId, currentPath, existingEntries, removed) {
7426
- const escapedId = CSS.escape(instanceId);
7427
- if (!document.querySelector(`[data-ohw-instance="${escapedId}"]`) && !document.querySelector(`[data-ohw-section="${escapedId}"]:not([data-ohw-instance])`))
7428
- return null;
8147
+ if (!findByInstanceId(instanceId)) return null;
7429
8148
  const byId = new Map(existingEntries.map((e) => [e.instanceId, e]));
7430
8149
  const allSections = Array.from(document.querySelectorAll("[data-ohw-section]")).filter(
7431
8150
  (el) => !el.parentElement?.closest("[data-ohw-section]") && !isChromeSection(el)
@@ -7614,6 +8333,7 @@ function AiSectionOverlay({
7614
8333
  }) {
7615
8334
  const [selectedId, setSelectedId] = useState5(null);
7616
8335
  const [reviewId, setReviewId] = useState5(null);
8336
+ const [reviewButtonsHidden, setReviewButtonsHidden] = useState5(false);
7617
8337
  const reviewIdRef = useRef4(null);
7618
8338
  reviewIdRef.current = reviewId;
7619
8339
  const selectedIdRef = useRef4(null);
@@ -7675,6 +8395,7 @@ function AiSectionOverlay({
7675
8395
  }
7676
8396
  const found = readRect(sectionId) != null;
7677
8397
  setReviewId(found ? sectionId : null);
8398
+ setReviewButtonsHidden(e.data.hideButtons === true);
7678
8399
  postToParent2({ type: "ow:ai-review-started", sectionId, found });
7679
8400
  if (found) {
7680
8401
  document.querySelector(`[data-ohw-instance="${CSS.escape(sectionId)}"]`)?.scrollIntoView({ behavior: "smooth", block: "nearest" });
@@ -7802,13 +8523,16 @@ function AiSectionOverlay({
7802
8523
  border: `2px solid ${PRIMARY2}`,
7803
8524
  borderRadius: edgeAwareRadius(reviewRect),
7804
8525
  zIndex: 2147483200,
7805
- // The veil itself: swallows clicks so the section stays locked until decided.
8526
+ // The veil itself: swallows clicks so the section stays locked until decided. This
8527
+ // stopPropagation only guards the bubble phase; the bridge's capture-phase click
8528
+ // handlers additionally bail on `[data-ohw-ai-review]`, without which a click on
8529
+ // Accept/Discard resolves to the media beneath and opens the file picker.
7806
8530
  background: "rgba(8, 133, 254, 0.04)",
7807
8531
  pointerEvents: "auto",
7808
8532
  cursor: "default"
7809
8533
  },
7810
8534
  onClick: (e) => e.stopPropagation(),
7811
- children: /* @__PURE__ */ jsxs9(
8535
+ children: !reviewButtonsHidden && /* @__PURE__ */ jsxs9(
7812
8536
  "div",
7813
8537
  {
7814
8538
  style: {
@@ -10375,8 +11099,13 @@ function referenceBox(slot) {
10375
11099
  const neighbour = row ? Array.from(row.querySelectorAll('[data-ohw-editable="icon"]')).find(
10376
11100
  (el) => el !== slot && !el.hasAttribute("data-ohw-social-icon-placeholder")
10377
11101
  ) : null;
10378
- const source = (neighbour ?? slot).querySelector(GLYPH_SELECTOR);
10379
- const box = source?.getBoundingClientRect() ?? null;
11102
+ if (neighbour) {
11103
+ const box2 = neighbour.querySelector(GLYPH_SELECTOR)?.getBoundingClientRect();
11104
+ if (box2?.width && box2.height) return box2;
11105
+ }
11106
+ const own = slot.getBoundingClientRect();
11107
+ if (own.width && own.height) return own;
11108
+ const box = slot.querySelector(GLYPH_SELECTOR)?.getBoundingClientRect() ?? null;
10380
11109
  return box?.width && box.height ? box : null;
10381
11110
  }
10382
11111
  function iconMarkupSizedFor(slot, markup) {
@@ -12177,6 +12906,7 @@ function readLogoSizeState(content, placement) {
12177
12906
  function getLogoElement(el) {
12178
12907
  const marked = el.closest('[data-ohw-role="logo"], [data-ohw-logo]');
12179
12908
  if (marked) return marked;
12909
+ if (el.closest('[data-ohw-editable="icon"]')) return null;
12180
12910
  const root = el.closest("nav, [data-ohw-nav-root], footer");
12181
12911
  if (!root) return null;
12182
12912
  const anchor = el.closest("a");
@@ -13251,6 +13981,7 @@ function useSectionDrag({
13251
13981
  }
13252
13982
  const orderJson = JSON.stringify(entries);
13253
13983
  editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: orderJson };
13984
+ setAiSectionOrder(orderJson, window.location.pathname);
13254
13985
  postToParentRef.current({ type: "ow:change", nodes: [{ key: SECTION_ORDER_KEY, text: orderJson }] });
13255
13986
  applyPersistedOrder(entries);
13256
13987
  clearSectionDragVisuals();
@@ -14110,21 +14841,10 @@ function parseSchedulingInsertAfter(insertAfter) {
14110
14841
  insertBefore: insertAfter.slice(idx + INSERT_BEFORE_MARKER.length)
14111
14842
  };
14112
14843
  }
14113
- function resolveSchedulingInsert(insertAfter, explicitInsertBefore) {
14114
- const parsed = parseSchedulingInsertAfter(insertAfter);
14115
- const insertBefore = explicitInsertBefore ?? parsed.insertBefore;
14116
- const effectiveInsertAfter = insertBefore && parsed.insertBefore === null ? `${insertAfter}${INSERT_BEFORE_MARKER}${insertBefore}` : insertAfter;
14117
- return { effectiveInsertAfter, insertBefore };
14118
- }
14119
- function getSchedulingMountPoint(insertAfter) {
14120
- const { anchor } = parseSchedulingInsertAfter(insertAfter);
14121
- let anchorEl = document.querySelector(`[data-ohw-section="${anchor}"]`);
14122
- if (!anchorEl && anchor === "scheduling") {
14123
- const widgets = Array.from(document.querySelectorAll('[data-ohw-section^="scheduling-"]'));
14124
- anchorEl = widgets.at(-1) ?? null;
14125
- }
14126
- if (!anchorEl) return null;
14127
- return anchorEl.closest('[data-ohw-section-container="scheduling"]') ?? anchorEl;
14844
+ function resolveEntryAnchor(entry) {
14845
+ if (entry.anchorId !== void 0) return { anchorId: entry.anchorId, beforeId: entry.beforeId ?? null };
14846
+ const parsed = parseSchedulingInsertAfter(entry.insertAfter);
14847
+ return { anchorId: parsed.anchor, beforeId: parsed.insertBefore };
14128
14848
  }
14129
14849
  function schedulingMountDepth(insertAfter) {
14130
14850
  if (!insertAfter.includes(INSERT_BEFORE_MARKER)) return 0;
@@ -14141,8 +14861,7 @@ function getPageSchedulingEntries(raw) {
14141
14861
  }
14142
14862
  }
14143
14863
  function isSchedulingWidgetMissing(entry) {
14144
- const { effectiveInsertAfter } = resolveSchedulingInsert(entry.insertAfter);
14145
- return !document.querySelector(`[data-ohw-section="${schedulingSectionId(effectiveInsertAfter)}"]`);
14864
+ return !document.querySelector(`[data-ohw-section="${schedulingSectionId(entry.insertAfter)}"]`);
14146
14865
  }
14147
14866
  function hasMissingSchedulingWidgets(entries) {
14148
14867
  return entries.some(isSchedulingWidgetMissing);
@@ -14172,16 +14891,17 @@ function initSectionsFromContent(content, removeExisting = false) {
14172
14891
  } catch {
14173
14892
  }
14174
14893
  }
14175
- function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId, explicitInsertBefore) {
14176
- const { effectiveInsertAfter, insertBefore } = resolveSchedulingInsert(insertAfter, explicitInsertBefore);
14177
- const sectionId = schedulingSectionId(effectiveInsertAfter);
14894
+ function mountSchedulingWidget(anchorId, notifyOnConnect = false, scheduleId, beforeId, existingWidgetId) {
14895
+ const widgetId = existingWidgetId ?? (beforeId ? `${anchorId}${INSERT_BEFORE_MARKER}${beforeId}` : anchorId);
14896
+ const sectionId = schedulingSectionId(widgetId);
14178
14897
  if (document.querySelector(`[data-ohw-section="${sectionId}"]`)) return false;
14179
- const mountPoint = getSchedulingMountPoint(effectiveInsertAfter);
14180
- if (!mountPoint) return false;
14898
+ const anchorEl = document.querySelector(`[data-ohw-section="${anchorId}"]`);
14899
+ if (!anchorEl) return false;
14900
+ const mountPoint = anchorEl.closest('[data-ohw-section-container="scheduling"]') ?? anchorEl;
14181
14901
  const container = document.createElement("div");
14182
14902
  container.dataset.ohwSectionContainer = "scheduling";
14183
- if (insertBefore) {
14184
- const beforeAnchor = document.querySelector(`[data-ohw-section="${insertBefore}"]`);
14903
+ if (beforeId) {
14904
+ const beforeAnchor = document.querySelector(`[data-ohw-section="${beforeId}"]`);
14185
14905
  const beforePoint = beforeAnchor?.closest('[data-ohw-section-container="scheduling"]') ?? beforeAnchor;
14186
14906
  if (!beforePoint) return false;
14187
14907
  beforePoint.insertAdjacentElement("beforebegin", container);
@@ -14192,19 +14912,25 @@ function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId,
14192
14912
  }
14193
14913
  tail.insertAdjacentElement("afterend", container);
14194
14914
  }
14195
- const root = createRoot2(container);
14196
- flushSync2(() => {
14197
- root.render(
14198
- /* @__PURE__ */ jsx33(
14199
- SchedulingWidget,
14200
- {
14201
- notifyOnConnect,
14202
- initialScheduleId: scheduleId,
14203
- insertAfter: effectiveInsertAfter
14204
- }
14205
- )
14206
- );
14207
- });
14915
+ try {
14916
+ const root = createRoot2(container);
14917
+ flushSync2(() => {
14918
+ root.render(
14919
+ /* @__PURE__ */ jsx33(
14920
+ SchedulingWidget,
14921
+ {
14922
+ notifyOnConnect,
14923
+ initialScheduleId: scheduleId,
14924
+ insertAfter: widgetId
14925
+ }
14926
+ )
14927
+ );
14928
+ });
14929
+ } catch (err) {
14930
+ console.error("[ow:scheduling] render threw", err);
14931
+ container.remove();
14932
+ return false;
14933
+ }
14208
14934
  const tracker = getSectionsTracker();
14209
14935
  let sections = [];
14210
14936
  try {
@@ -14212,10 +14938,12 @@ function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId,
14212
14938
  } catch {
14213
14939
  }
14214
14940
  const inEditor = typeof window !== "undefined" && window.self !== window.top;
14215
- if (inEditor && !sections.find((s) => s.type === "scheduling" && s.insertAfter === effectiveInsertAfter)) {
14941
+ if (inEditor && !sections.find((s) => s.type === "scheduling" && s.insertAfter === widgetId)) {
14216
14942
  sections.push({
14217
14943
  type: "scheduling",
14218
- insertAfter: effectiveInsertAfter,
14944
+ insertAfter: widgetId,
14945
+ anchorId,
14946
+ beforeId: beforeId ?? null,
14219
14947
  pagePath: window.location.pathname,
14220
14948
  ...scheduleId ? { scheduleId } : {}
14221
14949
  });
@@ -14229,7 +14957,8 @@ function mountSchedulingEntries(entries, notifyOnConnect = false) {
14229
14957
  for (let i = pending.length - 1; i >= 0; i--) {
14230
14958
  const entry = pending[i];
14231
14959
  const shouldNotify = typeof notifyOnConnect === "function" ? notifyOnConnect(entry) : notifyOnConnect;
14232
- if (mountSchedulingWidget(entry.insertAfter, shouldNotify, entry.scheduleId)) {
14960
+ const { anchorId, beforeId } = resolveEntryAnchor(entry);
14961
+ if (mountSchedulingWidget(anchorId, shouldNotify, entry.scheduleId ?? null, beforeId, entry.insertAfter)) {
14233
14962
  pending.splice(i, 1);
14234
14963
  }
14235
14964
  }
@@ -14387,6 +15116,11 @@ function applyLinkByKey(key, val) {
14387
15116
  hrefAnchors.forEach((el) => applyLinkHref(el, val));
14388
15117
  }
14389
15118
  }
15119
+ function isInsideLinkEditor(target) {
15120
+ return Boolean(
15121
+ 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"]')
15122
+ );
15123
+ }
14390
15124
  function isInsideFloatingPanel(target) {
14391
15125
  return Boolean(target.closest("[data-ohw-floating-panel]"));
14392
15126
  }
@@ -14394,11 +15128,6 @@ function isPointOverFloatingPanel(clientX, clientY) {
14394
15128
  const el = document.elementFromPoint(clientX, clientY);
14395
15129
  return Boolean(el instanceof Element && el.closest("[data-ohw-floating-panel]"));
14396
15130
  }
14397
- function isInsideLinkEditor(target) {
14398
- return Boolean(
14399
- 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"]')
14400
- );
14401
- }
14402
15131
  function getHrefKeyFromElement(el) {
14403
15132
  if (!el) return null;
14404
15133
  const anchor = el.closest("[data-ohw-href-key]");
@@ -14657,7 +15386,7 @@ function getNavigationSelectionParent(el) {
14657
15386
  if (!isFooterLinksContainer(el) && (el.hasAttribute("data-ohw-footer-col") || el.hasAttribute("data-ohw-footer-column") || isInferredFooterGroup2(el))) {
14658
15387
  return getFooterLinksContainer();
14659
15388
  }
14660
- if (el.hasAttribute("data-ohw-nav-container") || el.hasAttribute("data-ohw-nav-drawer") || isFooterLinksContainer(el)) {
15389
+ 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)) {
14661
15390
  return getNavigationRoot(el);
14662
15391
  }
14663
15392
  return null;
@@ -14872,7 +15601,6 @@ var ICONS = {
14872
15601
  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"/>',
14873
15602
  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"/>'
14874
15603
  };
14875
- var HEIGHT_SETTLE_DELAYS = [150, 400, 800];
14876
15604
  var SELECTION_CHROME_GAP2 = 4;
14877
15605
  var TOOLBAR_STROKE_GAP2 = 4;
14878
15606
  var TOOLBAR_OFFSET_FROM_ELEMENT = SELECTION_CHROME_GAP2 + TOOLBAR_STROKE_GAP2;
@@ -15252,6 +15980,7 @@ function StateToggle({
15252
15980
  );
15253
15981
  }
15254
15982
  var contentCache = /* @__PURE__ */ new Map();
15983
+ var fetchedContentPaths = /* @__PURE__ */ new Set();
15255
15984
  var OHW_LOADER_STYLE = {
15256
15985
  position: "fixed",
15257
15986
  inset: 0,
@@ -15370,6 +16099,70 @@ function OhhwellsBridge() {
15370
16099
  const hoveredImageHasTextOverlapRef = useRef10(false);
15371
16100
  const dragOverElRef = useRef10(null);
15372
16101
  const [mediaHover, setMediaHover] = useState13(null);
16102
+ const [selectedMedia, setSelectedMedia] = useState13(null);
16103
+ const selectedMediaElRef = useRef10(null);
16104
+ const clearMediaSelection = useCallback8(() => {
16105
+ const prev = selectedMediaElRef.current;
16106
+ selectedMediaElRef.current = null;
16107
+ setSelectedMedia(null);
16108
+ const sectionEl = prev?.closest("[data-ohw-section]") ?? null;
16109
+ if (sectionEl) {
16110
+ postToParentRef.current({
16111
+ type: "ow:section-selected",
16112
+ sectionId: sectionEl.dataset.ohwSection ?? null,
16113
+ sectionLabel: sectionEl.dataset.ohwSectionLabel ?? titleCaseSectionId(sectionEl.dataset.ohwSection ?? ""),
16114
+ key: null
16115
+ });
16116
+ }
16117
+ }, []);
16118
+ const clearMediaSelectionRef = useRef10(clearMediaSelection);
16119
+ clearMediaSelectionRef.current = clearMediaSelection;
16120
+ const selectMediaElement = useCallback8((el) => {
16121
+ const r2 = el.getBoundingClientRect();
16122
+ const video = el.dataset.ohwEditable === "video" ? el.querySelector("video") : null;
16123
+ selectedMediaElRef.current = el;
16124
+ setSelectedMedia({
16125
+ key: el.dataset.ohwKey ?? "",
16126
+ rect: { top: r2.top, left: r2.left, width: r2.width, height: r2.height },
16127
+ elementType: el.dataset.ohwEditable ?? "image",
16128
+ hasTextOverlap: false,
16129
+ isDragOver: false,
16130
+ ...video ? { videoAutoplay: video.autoplay, videoMuted: video.muted } : {}
16131
+ });
16132
+ const sectionEl = el.closest("[data-ohw-section]");
16133
+ aiSectionApiRef.current?.selectFromElement(el, { report: false });
16134
+ postToParentRef.current({
16135
+ type: "ow:section-selected",
16136
+ sectionId: sectionEl?.dataset.ohwSection ?? null,
16137
+ sectionLabel: sectionEl ? sectionEl.dataset.ohwSectionLabel ?? titleCaseSectionId(sectionEl.dataset.ohwSection ?? "") : null,
16138
+ key: el.dataset.ohwKey ?? null,
16139
+ // Display name for the pill — the raw key prettifies into fragments ("Img"); the
16140
+ // bridge knows what the node IS, so it names it.
16141
+ keyLabel: el.dataset.ohwEditable === "video" ? "Video" : el.dataset.ohwEditable === "bg-image" ? "Background" : "Image"
16142
+ });
16143
+ }, []);
16144
+ const selectMediaElementRef = useRef10(selectMediaElement);
16145
+ selectMediaElementRef.current = selectMediaElement;
16146
+ useEffect13(() => {
16147
+ if (!selectedMedia) return;
16148
+ const update = () => {
16149
+ const el = selectedMediaElRef.current;
16150
+ if (!el || !el.isConnected) {
16151
+ clearMediaSelection();
16152
+ return;
16153
+ }
16154
+ const r2 = el.getBoundingClientRect();
16155
+ setSelectedMedia(
16156
+ (prev) => prev ? { ...prev, rect: { top: r2.top, left: r2.left, width: r2.width, height: r2.height } } : prev
16157
+ );
16158
+ };
16159
+ window.addEventListener("scroll", update, true);
16160
+ window.addEventListener("resize", update);
16161
+ return () => {
16162
+ window.removeEventListener("scroll", update, true);
16163
+ window.removeEventListener("resize", update);
16164
+ };
16165
+ }, [selectedMedia !== null]);
15373
16166
  const [carouselHover, setCarouselHover] = useState13(null);
15374
16167
  const [uploadingRects, setUploadingRects] = useState13({});
15375
16168
  const hoveredGapRef = useRef10(null);
@@ -15632,13 +16425,6 @@ function OhhwellsBridge() {
15632
16425
  const [isItemDragging, setIsItemDragging] = useState13(false);
15633
16426
  const [isFooterFrameSelection, setIsFooterFrameSelection] = useState13(false);
15634
16427
  isFooterFrameSelectionRef.current = isFooterFrameSelection;
15635
- const [floatingPanel, setFloatingPanel] = useState13(null);
15636
- const floatingPanelOpenRef = useRef10(false);
15637
- floatingPanelOpenRef.current = floatingPanel !== null;
15638
- const [floatingPanelPos, setFloatingPanelPos] = useState13(null);
15639
- const [logoSizeDraft, setLogoSizeDraft] = useState13(null);
15640
- const [editorViewport, setEditorViewport] = useState13("desktop");
15641
- const [parentScrollSnap, setParentScrollSnap] = useState13(null);
15642
16428
  const [navDropdownPreviewOpen, setNavDropdownPreviewOpen] = useState13(null);
15643
16429
  const [footerHeadingVisible, setFooterHeadingVisible] = useState13(null);
15644
16430
  const footerDragRef = useRef10(null);
@@ -15653,7 +16439,16 @@ function OhhwellsBridge() {
15653
16439
  const addNavAfterAnchorRef = useRef10(null);
15654
16440
  const editContentRef = useRef10({});
15655
16441
  const aiSectionsRef = useRef10("");
16442
+ const brandKitRef = useRef10("");
16443
+ const stylesRef = useRef10("");
15656
16444
  const pendingDeleteUndoRef = useRef10(null);
16445
+ const [floatingPanel, setFloatingPanel] = useState13(null);
16446
+ const floatingPanelOpenRef = useRef10(false);
16447
+ const setFloatingPanelRef = useRef10(setFloatingPanel);
16448
+ const [floatingPanelPos, setFloatingPanelPos] = useState13(null);
16449
+ const [logoSizeDraft, setLogoSizeDraft] = useState13(null);
16450
+ const [editorViewport, setEditorViewport] = useState13("desktop");
16451
+ const [parentScrollSnap, setParentScrollSnap] = useState13(null);
15657
16452
  const [sitePages, setSitePages] = useState13([]);
15658
16453
  const [sectionsByPath, setSectionsByPath] = useState13({});
15659
16454
  const sectionsPrefetchGenRef = useRef10(0);
@@ -15662,7 +16457,18 @@ function OhhwellsBridge() {
15662
16457
  const linkPopoverOpenRef = useRef10(false);
15663
16458
  const linkPopoverGraceUntilRef = useRef10(0);
15664
16459
  setLinkPopoverRef.current = setLinkPopover;
16460
+ setFloatingPanelRef.current = setFloatingPanel;
15665
16461
  linkPopoverSessionRef.current = linkPopover;
16462
+ floatingPanelOpenRef.current = Boolean(floatingPanel);
16463
+ useEffect13(() => {
16464
+ const syncViewport = () => {
16465
+ const next = window.innerWidth <= 480 ? "mobile" : "desktop";
16466
+ setEditorViewport((prev) => prev === next ? prev : next);
16467
+ };
16468
+ syncViewport();
16469
+ window.addEventListener("resize", syncViewport);
16470
+ return () => window.removeEventListener("resize", syncViewport);
16471
+ }, []);
15666
16472
  const {
15667
16473
  navDragRef,
15668
16474
  navDropSlots,
@@ -16973,15 +17779,31 @@ function OhhwellsBridge() {
16973
17779
  }
16974
17780
  const applyContent = (content) => {
16975
17781
  const imageLoads = [];
17782
+ if (typeof content[BRAND_KIT_KEY] === "string") {
17783
+ brandKitRef.current = content[BRAND_KIT_KEY];
17784
+ applyBrandToDom(parseBrandKit(content[BRAND_KIT_KEY]));
17785
+ } else {
17786
+ brandKitRef.current = "";
17787
+ applyBrandToDom(null);
17788
+ }
16976
17789
  if (typeof content[AI_SECTIONS_KEY] === "string") {
16977
17790
  aiSectionsRef.current = content[AI_SECTIONS_KEY];
17791
+ setAiSectionOrder(content[SECTION_ORDER_KEY], window.location.pathname);
16978
17792
  applyAiSectionsToDom(parseAiSectionsState(content[AI_SECTIONS_KEY]));
16979
17793
  }
17794
+ if (typeof content[STYLE_STORE_KEY] === "string") {
17795
+ stylesRef.current = content[STYLE_STORE_KEY];
17796
+ applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
17797
+ }
17798
+ applyBrandChrome(content);
16980
17799
  for (const [key, val] of Object.entries(content)) {
16981
17800
  if (key === "__ohw_sections") continue;
16982
17801
  if (key === AI_SECTIONS_KEY) continue;
16983
17802
  if (key === LOGO_PLACEHOLDER_KEY) continue;
16984
17803
  if (LOGO_IMAGE_KEYS.includes(key)) continue;
17804
+ if (key === BRAND_KIT_KEY) continue;
17805
+ if (key === STYLE_STORE_KEY) continue;
17806
+ if (BRAND_CHROME_KEYS.has(key)) continue;
16985
17807
  if (applyVideoSettingNode(key, val)) continue;
16986
17808
  if (applyCarouselNode(key, val)) continue;
16987
17809
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
@@ -17047,7 +17869,9 @@ function OhhwellsBridge() {
17047
17869
  let cancelled = false;
17048
17870
  setFetchState("loading");
17049
17871
  const apiUrl = process.env.NEXT_PUBLIC_FLOWOPS_API_URL ?? "https://flowops-backend-staging-2yfdh7pwpq-as.a.run.app";
17050
- fetch(`${apiUrl}/api/public/sites/${subdomain}/content`, { cache: "no-store" }).then((r2) => r2.ok ? r2.json() : null).then((data) => {
17872
+ const initialPath = pathname;
17873
+ fetchedContentPaths.add(`${subdomain}::${initialPath}`);
17874
+ fetch(`${apiUrl}/api/public/sites/${subdomain}/content?path=${encodeURIComponent(initialPath)}`, { cache: "no-store" }).then((r2) => r2.ok ? r2.json() : null).then((data) => {
17051
17875
  if (cancelled) return;
17052
17876
  const content = data?.content ?? {};
17053
17877
  contentCache.set(subdomain, content);
@@ -17167,10 +17991,28 @@ function OhhwellsBridge() {
17167
17991
  initSectionInstancesFromContent(content, window.location.pathname);
17168
17992
  observer?.disconnect();
17169
17993
  try {
17994
+ applyBrandChrome(content);
17995
+ if (typeof content[BRAND_KIT_KEY] === "string") {
17996
+ applyBrandToDom(parseBrandKit(content[BRAND_KIT_KEY]));
17997
+ } else {
17998
+ applyBrandToDom(null);
17999
+ }
18000
+ if (typeof content[AI_SECTIONS_KEY] === "string") {
18001
+ setAiSectionOrder(content[SECTION_ORDER_KEY], window.location.pathname);
18002
+ applyAiSectionsToDom(parseAiSectionsState(content[AI_SECTIONS_KEY]));
18003
+ }
18004
+ if (typeof content[STYLE_STORE_KEY] === "string") {
18005
+ applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
18006
+ }
17170
18007
  for (const [key, val] of Object.entries(content)) {
17171
18008
  if (key === "__ohw_sections") continue;
18009
+ if (key === AI_SECTIONS_KEY) continue;
17172
18010
  if (key === LOGO_PLACEHOLDER_KEY) continue;
17173
18011
  if (LOGO_IMAGE_KEYS.includes(key)) continue;
18012
+ if (key === BRAND_KIT_KEY) continue;
18013
+ if (key === STYLE_STORE_KEY) continue;
18014
+ if (key === STYLE_STORE_KEY) continue;
18015
+ if (BRAND_CHROME_KEYS.has(key)) continue;
17174
18016
  if (applyVideoSettingNode(key, val)) continue;
17175
18017
  if (applyCarouselNode(key, val)) continue;
17176
18018
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
@@ -17216,6 +18058,17 @@ function OhhwellsBridge() {
17216
18058
  debounceTimer = setTimeout(applyFromCache, 150);
17217
18059
  };
17218
18060
  applyFromCache();
18061
+ const pathCacheKey = `${subdomain}::${pathname}`;
18062
+ if (!fetchedContentPaths.has(pathCacheKey)) {
18063
+ fetchedContentPaths.add(pathCacheKey);
18064
+ const apiUrl = process.env.NEXT_PUBLIC_FLOWOPS_API_URL ?? "https://flowops-backend-staging-2yfdh7pwpq-as.a.run.app";
18065
+ fetch(`${apiUrl}/api/public/sites/${subdomain}/content?path=${encodeURIComponent(pathname)}`, { cache: "no-store" }).then((r2) => r2.ok ? r2.json() : null).then((data) => {
18066
+ if (!data?.content) return;
18067
+ contentCache.set(subdomain, data.content);
18068
+ applyFromCache();
18069
+ }).catch(() => {
18070
+ });
18071
+ }
17219
18072
  observer = new MutationObserver(scheduleApply);
17220
18073
  observer.observe(document.body, { childList: true, subtree: true });
17221
18074
  return () => {
@@ -17311,30 +18164,31 @@ function OhhwellsBridge() {
17311
18164
  }, [isEditMode, pathname, subdomain, fetchState, resyncSelectedNavigationItem]);
17312
18165
  useEffect13(() => {
17313
18166
  if (!isEditMode) return;
18167
+ let lastPosted = 0;
17314
18168
  const measure = () => {
17315
18169
  const h = document.body.scrollHeight;
17316
- if (h > 50) postToParent2({ type: "ow:height", height: h });
18170
+ if (h > 50 && Math.abs(h - lastPosted) > 1) {
18171
+ lastPosted = h;
18172
+ postToParent2({ type: "ow:height", height: h });
18173
+ }
18174
+ };
18175
+ let raf = null;
18176
+ const schedule = () => {
18177
+ if (raf != null) return;
18178
+ raf = requestAnimationFrame(() => {
18179
+ raf = null;
18180
+ measure();
18181
+ });
17317
18182
  };
17318
18183
  const t1 = setTimeout(measure, 50);
17319
18184
  const t2 = setTimeout(measure, 500);
17320
- let lastWidth = window.innerWidth;
17321
- let resizeTimers = [];
17322
- const clearResizeTimers = () => {
17323
- resizeTimers.forEach(clearTimeout);
17324
- resizeTimers = [];
17325
- };
17326
- const handleResize = () => {
17327
- if (window.innerWidth === lastWidth) return;
17328
- lastWidth = window.innerWidth;
17329
- clearResizeTimers();
17330
- resizeTimers = HEIGHT_SETTLE_DELAYS.map((delay) => setTimeout(measure, delay));
17331
- };
17332
- window.addEventListener("resize", handleResize);
18185
+ const ro = new ResizeObserver(schedule);
18186
+ ro.observe(document.body);
17333
18187
  return () => {
17334
18188
  clearTimeout(t1);
17335
18189
  clearTimeout(t2);
17336
- clearResizeTimers();
17337
- window.removeEventListener("resize", handleResize);
18190
+ if (raf != null) cancelAnimationFrame(raf);
18191
+ ro.disconnect();
17338
18192
  };
17339
18193
  }, [pathname, isEditMode, postToParent2]);
17340
18194
  useEffect13(() => {
@@ -17575,6 +18429,7 @@ function OhhwellsBridge() {
17575
18429
  return;
17576
18430
  }
17577
18431
  const target = e.target;
18432
+ if (target.closest("[data-ohw-ai-review]")) return;
17578
18433
  if (target.closest("[data-ohw-toolbar]")) return;
17579
18434
  if (target.closest("[data-ohw-state-toggle]")) return;
17580
18435
  if (target.closest("[data-ohw-max-badge]")) return;
@@ -17586,6 +18441,9 @@ function OhhwellsBridge() {
17586
18441
  )) {
17587
18442
  return;
17588
18443
  }
18444
+ if (selectedMediaElRef.current && !selectedMediaElRef.current.contains(target) && !target.closest("[data-ohw-media-overlay], [data-ohw-edit-chrome], [data-ohw-item-interaction]")) {
18445
+ clearMediaSelectionRef.current();
18446
+ }
17589
18447
  {
17590
18448
  const formEl = getFormElement(target);
17591
18449
  const onSuccessText = target.closest(`[${SUCCESS_TEXT_ATTR}]`);
@@ -17737,19 +18595,14 @@ function OhhwellsBridge() {
17737
18595
  }
17738
18596
  const clickedButton = findClosestButtonLike(target);
17739
18597
  const buttonOnMedia = Boolean(clickedButton && isMediaEditable(editable) && editable.contains(clickedButton));
17740
- console.log("[click-debug]", {
17741
- editableType: editable.dataset.ohwEditable,
17742
- editableTag: editable.tagName,
17743
- targetTag: target.tagName,
17744
- clickedButtonTag: clickedButton?.tagName ?? null,
17745
- buttonOnMedia,
17746
- isMediaEditableEditable: isMediaEditable(editable)
17747
- });
17748
18598
  if (isMediaEditable(editable) && !buttonOnMedia) {
17749
18599
  e.preventDefault();
17750
18600
  e.stopPropagation();
17751
- aiSectionApiRef.current?.selectFromElement(editable);
17752
- postToParentRef.current({ type: "ow:image-pick", key: editable.dataset.ohwKey ?? "", elementType: editable.dataset.ohwEditable ?? "image" });
18601
+ if (selectedMediaElRef.current === editable) {
18602
+ postToParentRef.current({ type: "ow:image-pick", key: editable.dataset.ohwKey ?? "", elementType: editable.dataset.ohwEditable ?? "image" });
18603
+ } else {
18604
+ selectMediaElementRef.current(editable);
18605
+ }
17753
18606
  return;
17754
18607
  }
17755
18608
  const socialItem = getSocialItem(editable);
@@ -17768,11 +18621,6 @@ function OhhwellsBridge() {
17768
18621
  const hrefLookupTarget = buttonOnMedia ? clickedButton : editable;
17769
18622
  const hrefCtx = getHrefKeyFromElement(hrefLookupTarget);
17770
18623
  const navAnchor = hrefCtx ? getNavigationItemAnchor(hrefCtx.anchor) : null;
17771
- console.log("[click-debug 2]", {
17772
- hrefLookupTargetTag: hrefLookupTarget.tagName,
17773
- hrefCtx: hrefCtx ? { key: hrefCtx.key } : null,
17774
- navAnchorTag: navAnchor?.tagName ?? null
17775
- });
17776
18624
  if (navAnchor) {
17777
18625
  e.preventDefault();
17778
18626
  e.stopPropagation();
@@ -17890,6 +18738,7 @@ function OhhwellsBridge() {
17890
18738
  };
17891
18739
  const handleDblClick = (e) => {
17892
18740
  const target = e.target;
18741
+ if (target.closest("[data-ohw-ai-review]")) return;
17893
18742
  if (target.closest("[data-ohw-toolbar]")) return;
17894
18743
  if (target.closest("[data-ohw-state-toggle]")) return;
17895
18744
  if (target.closest("[data-ohw-max-badge]")) return;
@@ -17941,6 +18790,9 @@ function OhhwellsBridge() {
17941
18790
  setHoveredItemRect(null);
17942
18791
  hoveredNavContainerRef.current = null;
17943
18792
  setHoveredNavContainerRect(null);
18793
+ siblingHintElRef.current = null;
18794
+ setSiblingHintRect(null);
18795
+ setSiblingHintRects([]);
17944
18796
  return;
17945
18797
  }
17946
18798
  {
@@ -18059,7 +18911,6 @@ function OhhwellsBridge() {
18059
18911
  hoveredNavContainerRef.current = null;
18060
18912
  setHoveredNavContainerRect(null);
18061
18913
  hoveredItemElRef.current = editable;
18062
- setHoveredItemRect(isSelectedForHoverRef.current(editable) ? null : editable.getBoundingClientRect());
18063
18914
  }
18064
18915
  }
18065
18916
  }
@@ -18356,7 +19207,7 @@ function OhhwellsBridge() {
18356
19207
  }
18357
19208
  };
18358
19209
  const probeImageAt = (clientX, clientY, isDragOver = false, fromParentViewport = false) => {
18359
- if (linkPopoverOpenRef.current) {
19210
+ if (linkPopoverOpenRef.current || floatingPanelOpenRef.current && isPointOverFloatingPanel(clientX, clientY)) {
18360
19211
  if (hoveredImageRef.current) {
18361
19212
  hoveredImageRef.current = null;
18362
19213
  hoveredImageHasTextOverlapRef.current = false;
@@ -18690,7 +19541,9 @@ function OhhwellsBridge() {
18690
19541
  return;
18691
19542
  }
18692
19543
  const { y } = toProbeCoords(clientX, clientY, fromParentViewport);
18693
- const sections = Array.from(document.querySelectorAll("[data-ohw-section]")).sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top);
19544
+ const sections = Array.from(document.querySelectorAll("[data-ohw-section]")).filter(
19545
+ (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
19546
+ ).sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top);
18694
19547
  const ZONE = 20;
18695
19548
  for (let i = 0; i < sections.length; i++) {
18696
19549
  const a = sections[i];
@@ -18719,8 +19572,7 @@ function OhhwellsBridge() {
18719
19572
  };
18720
19573
  const handleMouseMove = (e) => {
18721
19574
  const { clientX, clientY } = e;
18722
- if (pointOwnedByFloatingPanel(clientX, clientY)) return;
18723
- if (isOverEditorChrome(clientX, clientY)) {
19575
+ if (document.documentElement.hasAttribute("data-ohw-panel-dragging") || floatingPanelOpenRef.current && isPointOverFloatingPanel(clientX, clientY) || isOverEditorChrome(clientX, clientY)) {
18724
19576
  document.querySelectorAll("[data-ohw-hovered]").forEach((el) => el.removeAttribute("data-ohw-hovered"));
18725
19577
  formHoverElRef.current = null;
18726
19578
  setFormHoverRect(null);
@@ -18728,6 +19580,12 @@ function OhhwellsBridge() {
18728
19580
  setHoveredItemRect(null);
18729
19581
  hoveredNavContainerRef.current = null;
18730
19582
  setHoveredNavContainerRect(null);
19583
+ siblingHintElRef.current = null;
19584
+ setSiblingHintRect(null);
19585
+ setSiblingHintRects([]);
19586
+ dismissImageHover();
19587
+ clearImageHover();
19588
+ setSectionGap(null);
18731
19589
  return;
18732
19590
  }
18733
19591
  if (probeSocialsRowAt(clientX, clientY)) return;
@@ -18739,7 +19597,11 @@ function OhhwellsBridge() {
18739
19597
  if (e.data?.type !== "ow:pointer-sync") return;
18740
19598
  const { clientX, clientY } = e.data;
18741
19599
  if (typeof clientX !== "number" || typeof clientY !== "number") return;
18742
- if (pointOwnedByFloatingPanel(clientX, clientY)) return;
19600
+ if (document.documentElement.hasAttribute("data-ohw-panel-dragging") || floatingPanelOpenRef.current && isPointOverFloatingPanel(clientX, clientY)) {
19601
+ dismissImageHover();
19602
+ clearImageHover();
19603
+ return;
19604
+ }
18743
19605
  if (probeSocialsRowAt(clientX, clientY)) return;
18744
19606
  probeSectionGapAt(clientX, clientY);
18745
19607
  probeImageAt(clientX, clientY);
@@ -19022,10 +19884,23 @@ function OhhwellsBridge() {
19022
19884
  if (e.data?.type !== "ow:hydrate") return;
19023
19885
  const content = e.data.content;
19024
19886
  if (!content) return;
19887
+ if (typeof content[BRAND_KIT_KEY] === "string") {
19888
+ brandKitRef.current = content[BRAND_KIT_KEY];
19889
+ applyBrandToDom(parseBrandKit(content[BRAND_KIT_KEY]));
19890
+ } else {
19891
+ brandKitRef.current = "";
19892
+ applyBrandToDom(null);
19893
+ }
19025
19894
  if (typeof content[AI_SECTIONS_KEY] === "string") {
19026
19895
  aiSectionsRef.current = content[AI_SECTIONS_KEY];
19896
+ setAiSectionOrder(content[SECTION_ORDER_KEY], window.location.pathname);
19027
19897
  applyAiSectionsToDom(parseAiSectionsState(content[AI_SECTIONS_KEY]));
19028
19898
  }
19899
+ if (typeof content[STYLE_STORE_KEY] === "string") {
19900
+ stylesRef.current = content[STYLE_STORE_KEY];
19901
+ applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
19902
+ }
19903
+ applyBrandChrome(content);
19029
19904
  let sectionsJson = null;
19030
19905
  for (const [key, val] of Object.entries(content)) {
19031
19906
  if (key === "__ohw_sections") {
@@ -19035,6 +19910,9 @@ function OhhwellsBridge() {
19035
19910
  if (key === AI_SECTIONS_KEY) continue;
19036
19911
  if (key === LOGO_PLACEHOLDER_KEY) continue;
19037
19912
  if (LOGO_IMAGE_KEYS.includes(key) && !String(val ?? "").trim()) continue;
19913
+ if (key === BRAND_KIT_KEY) continue;
19914
+ if (key === STYLE_STORE_KEY) continue;
19915
+ if (BRAND_CHROME_KEYS.has(key)) continue;
19038
19916
  if (applyVideoSettingNode(key, val)) continue;
19039
19917
  if (applyCarouselNode(key, val)) continue;
19040
19918
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
@@ -19048,6 +19926,8 @@ function OhhwellsBridge() {
19048
19926
  if (video && video.src !== val) applyVideoSrc(video, val);
19049
19927
  } else if (el.dataset.ohwEditable === "link") {
19050
19928
  applyLinkHref(el, val);
19929
+ } else if (el.dataset.ohwEditable === "icon") {
19930
+ applyIconMarkup(el, val);
19051
19931
  } else if (isIconMarkupValue(val)) {
19052
19932
  } else {
19053
19933
  el.innerHTML = val;
@@ -19132,12 +20012,21 @@ function OhhwellsBridge() {
19132
20012
  nodes: collectEditableNodes(editContentRef.current)
19133
20013
  });
19134
20014
  };
20015
+ const clearInteractionChrome = () => {
20016
+ deactivateRef.current();
20017
+ deselectRef.current();
20018
+ clearMediaSelectionRef.current();
20019
+ };
19135
20020
  const handleAiApplyTree = (e) => {
19136
20021
  if (e.data?.type !== "ow:ai-apply-tree") return;
19137
20022
  const payload = e.data.payload;
19138
20023
  if (!payload || typeof payload.id !== "string" || !isRenderableTree(payload.tree)) return;
20024
+ clearInteractionChrome();
19139
20025
  const previous = aiSectionsRef.current;
19140
- const nextState = applyTreeToState(parseAiSectionsState(previous), payload);
20026
+ const nextState = applyTreeToState(parseAiSectionsState(previous), {
20027
+ ...payload,
20028
+ path: payload.path ?? window.location.pathname
20029
+ });
19141
20030
  const nextValue = serializeAiSectionsState(nextState);
19142
20031
  aiSectionsRef.current = nextValue;
19143
20032
  applyAiSectionsToDom(nextState);
@@ -19158,6 +20047,7 @@ function OhhwellsBridge() {
19158
20047
  const exists = document.querySelector(`[data-ohw-section="${CSS.escape(sectionId)}"]`);
19159
20048
  if (!exists) return;
19160
20049
  if (isPageFrameSection(exists)) return;
20050
+ clearInteractionChrome();
19161
20051
  const previous = aiSectionsRef.current;
19162
20052
  const nextState = deleteSectionFromState(parseAiSectionsState(previous), sectionId);
19163
20053
  const nextValue = serializeAiSectionsState(nextState);
@@ -19173,8 +20063,10 @@ function OhhwellsBridge() {
19173
20063
  const handleAiSetSections = (e) => {
19174
20064
  if (e.data?.type !== "ow:ai-set-sections") return;
19175
20065
  const value = typeof e.data.value === "string" ? e.data.value : "";
20066
+ clearInteractionChrome();
19176
20067
  aiSectionsRef.current = value;
19177
20068
  applyAiSectionsToDom(parseAiSectionsState(value));
20069
+ applyStylesToDom(parseStyleStore(stylesRef.current));
19178
20070
  const restoredHeight = document.body.scrollHeight;
19179
20071
  if (restoredHeight > 50) postToParentRef.current({ type: "ow:height", height: restoredHeight });
19180
20072
  postToParentRef.current({ type: "ow:change", nodes: [{ key: AI_SECTIONS_KEY, text: value }] });
@@ -19190,10 +20082,40 @@ function OhhwellsBridge() {
19190
20082
  if (!entries) return;
19191
20083
  const orderJson = JSON.stringify(entries);
19192
20084
  editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: orderJson };
20085
+ setAiSectionOrder(orderJson, window.location.pathname);
19193
20086
  postToParentRef.current({ type: "ow:change", nodes: [{ key: SECTION_ORDER_KEY, text: orderJson }] });
19194
20087
  window.dispatchEvent(new Event("resize"));
19195
20088
  };
19196
20089
  window.addEventListener("message", handleMoveSection);
20090
+ const handleAiSetBrand = (e) => {
20091
+ if (e.data?.type !== "ow:ai-set-brand") return;
20092
+ const value = typeof e.data.value === "string" ? e.data.value : "";
20093
+ const previous = brandKitRef.current;
20094
+ brandKitRef.current = value;
20095
+ applyBrandToDom(parseBrandKit(value));
20096
+ if (aiSectionsRef.current) applyAiSectionsToDom(parseAiSectionsState(aiSectionsRef.current));
20097
+ applyStylesToDom(parseStyleStore(stylesRef.current));
20098
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: BRAND_KIT_KEY, text: value }] });
20099
+ postToParentRef.current({ type: "ow:ai-brand-applied", previous, value });
20100
+ };
20101
+ window.addEventListener("message", handleAiSetBrand);
20102
+ const handleAiSetStyles = (e) => {
20103
+ if (e.data?.type !== "ow:ai-set-styles") return;
20104
+ const value = typeof e.data.value === "string" ? e.data.value : "";
20105
+ const previous = stylesRef.current;
20106
+ stylesRef.current = value;
20107
+ applyStylesToDom(parseStyleStore(value));
20108
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: STYLE_STORE_KEY, text: value }] });
20109
+ postToParentRef.current({ type: "ow:ai-styles-applied", previous, value });
20110
+ };
20111
+ window.addEventListener("message", handleAiSetStyles);
20112
+ const handleGetBrand = (e) => {
20113
+ if (e.data?.type !== "ow:get-brand") return;
20114
+ const template = deriveTemplateBrand();
20115
+ const value = brandKitRef.current || (template ? JSON.stringify(template) : "");
20116
+ postToParentRef.current({ type: "ow:brand-value", value });
20117
+ };
20118
+ window.addEventListener("message", handleGetBrand);
19197
20119
  const handlePanelDragging = (e) => {
19198
20120
  if (e.data?.type !== "ow:panel-dragging") return;
19199
20121
  if (e.data.dragging) document.documentElement.setAttribute("data-ohw-panel-dragging", "");
@@ -19251,8 +20173,15 @@ function OhhwellsBridge() {
19251
20173
  closeLinkPopoverRef.current();
19252
20174
  return;
19253
20175
  }
20176
+ if (floatingPanelOpenRef.current) {
20177
+ setFloatingPanelRef.current(null);
20178
+ deselectRef.current();
20179
+ deactivateRef.current();
20180
+ return;
20181
+ }
19254
20182
  deselectRef.current();
19255
20183
  deactivateRef.current();
20184
+ clearMediaSelectionRef.current();
19256
20185
  };
19257
20186
  window.addEventListener("message", handleDeactivate);
19258
20187
  const handleToastAction = (e) => {
@@ -19338,6 +20267,10 @@ function OhhwellsBridge() {
19338
20267
  const handleKeyDown = (e) => {
19339
20268
  if (e.key === "Escape" && document.querySelector("[data-ohw-section-picker]")) return;
19340
20269
  if (e.key === "Escape" && document.querySelector("[data-ohw-more-menu]")) return;
20270
+ if (e.key === "Escape" && selectedMediaElRef.current) {
20271
+ clearMediaSelectionRef.current();
20272
+ return;
20273
+ }
19341
20274
  if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "a" && activeElRef.current) {
19342
20275
  e.preventDefault();
19343
20276
  selectAllTextInEditable(activeElRef.current);
@@ -19497,6 +20430,12 @@ function OhhwellsBridge() {
19497
20430
  if (aiSectionsRef.current) {
19498
20431
  nodes.push({ key: AI_SECTIONS_KEY, type: "sections", text: aiSectionsRef.current });
19499
20432
  }
20433
+ if (stylesRef.current) {
20434
+ nodes.push({ key: STYLE_STORE_KEY, type: "styles", text: stylesRef.current });
20435
+ }
20436
+ if (brandKitRef.current) {
20437
+ nodes.push({ key: BRAND_KIT_KEY, type: "brand", text: brandKitRef.current });
20438
+ }
19500
20439
  document.querySelectorAll('[data-ohw-editable="form"]').forEach((form) => {
19501
20440
  const formKey = formKeyOf(form);
19502
20441
  if (!formKey) return;
@@ -19514,8 +20453,12 @@ function OhhwellsBridge() {
19514
20453
  if (inserted) {
19515
20454
  const tracker = getSectionsTracker();
19516
20455
  postToParentRef.current({ type: "ow:change", nodes: [{ key: "__ohw_sections", text: tracker.textContent ?? "[]" }] });
19517
- const h = document.body.scrollHeight;
19518
- if (h > 50) postToParentRef.current({ type: "ow:height", height: h });
20456
+ const reportHeight = () => {
20457
+ const h = document.body.scrollHeight;
20458
+ if (h > 50) postToParentRef.current({ type: "ow:height", height: h });
20459
+ };
20460
+ reportHeight();
20461
+ setTimeout(reportHeight, 500);
19519
20462
  }
19520
20463
  };
19521
20464
  const handleSwitchSchedule = (e) => {
@@ -19912,13 +20855,16 @@ function OhhwellsBridge() {
19912
20855
  window.removeEventListener("message", handleAiDeleteSection);
19913
20856
  window.removeEventListener("message", handleAiSetSections);
19914
20857
  window.removeEventListener("message", handleMoveSection);
20858
+ window.removeEventListener("message", handleAiSetBrand);
20859
+ window.removeEventListener("message", handleAiSetStyles);
20860
+ window.removeEventListener("message", handleGetBrand);
19915
20861
  window.removeEventListener("message", handlePanelDragging);
19916
20862
  window.removeEventListener("message", handleDeleteSection);
19917
20863
  window.removeEventListener("message", handleDeactivate);
19918
- document.documentElement.removeAttribute("data-ohw-panel-dragging");
19919
20864
  window.removeEventListener("message", handleToastAction);
19920
20865
  window.removeEventListener("message", handleFormCount);
19921
20866
  window.removeEventListener("message", handleUiEscape);
20867
+ document.documentElement.removeAttribute("data-ohw-panel-dragging");
19922
20868
  autoSaveTimers.current.forEach(clearTimeout);
19923
20869
  autoSaveTimers.current.clear();
19924
20870
  if (imageUnhoverTimerRef.current) clearTimeout(imageUnhoverTimerRef.current);
@@ -20121,7 +21067,7 @@ function OhhwellsBridge() {
20121
21067
  postToParent2({
20122
21068
  type: "ow:ready",
20123
21069
  version: "1",
20124
- bridgeVersion: "0.1.76",
21070
+ bridgeVersion: "0.1.78",
20125
21071
  path: pathname,
20126
21072
  nodes: collectEditableNodes(editContentRef.current),
20127
21073
  sections
@@ -20528,11 +21474,22 @@ function OhhwellsBridge() {
20528
21474
  const showEditLink = toolbarShowEditLink;
20529
21475
  const currentSections = sectionsByPath[pathname] ?? [];
20530
21476
  linkPopoverOpenRef.current = linkPopover !== null;
21477
+ const handleMediaSelect = useCallback8((key) => {
21478
+ const el = hoveredImageRef.current?.dataset.ohwKey === key ? hoveredImageRef.current : Array.from(document.querySelectorAll(MEDIA_SELECTOR)).find(
21479
+ (m) => (m.dataset.ohwKey ?? "") === key
21480
+ ) ?? null;
21481
+ if (!el) return;
21482
+ selectMediaElementRef.current(el);
21483
+ }, []);
20531
21484
  const handleMediaReplace = useCallback8(
20532
21485
  (key) => {
20533
- postToParent2({ type: "ow:image-pick", key, elementType: mediaHover?.elementType ?? "image" });
21486
+ postToParent2({
21487
+ type: "ow:image-pick",
21488
+ key,
21489
+ elementType: mediaHover?.elementType ?? selectedMedia?.elementType ?? "image"
21490
+ });
20534
21491
  },
20535
- [postToParent2, mediaHover?.elementType]
21492
+ [postToParent2, mediaHover?.elementType, selectedMedia?.elementType]
20536
21493
  );
20537
21494
  const handleEditCarousel = useCallback8(
20538
21495
  (key) => {
@@ -20604,12 +21561,25 @@ function OhhwellsBridge() {
20604
21561
  },
20605
21562
  `uploading-${key}`
20606
21563
  )),
20607
- mediaHover && !(mediaHover.key in uploadingRects) && /* @__PURE__ */ jsx33(
21564
+ mediaHover && !(mediaHover.key in uploadingRects) && mediaHover.key !== selectedMedia?.key && /* @__PURE__ */ jsx33(
20608
21565
  MediaOverlay,
20609
21566
  {
20610
21567
  hover: mediaHover,
20611
21568
  isUploading: false,
20612
21569
  onReplace: handleMediaReplace,
21570
+ onSelect: handleMediaSelect,
21571
+ onVideoSettingsChange: handleVideoSettingsChange
21572
+ }
21573
+ ),
21574
+ selectedMedia && !(selectedMedia.key in uploadingRects) && !linkPopover && /* @__PURE__ */ jsx33(
21575
+ MediaOverlay,
21576
+ {
21577
+ hover: selectedMedia,
21578
+ selected: true,
21579
+ hovered: mediaHover?.key === selectedMedia.key,
21580
+ isUploading: false,
21581
+ onReplace: handleMediaReplace,
21582
+ onSelect: handleMediaSelect,
20613
21583
  onVideoSettingsChange: handleVideoSettingsChange
20614
21584
  }
20615
21585
  ),
@@ -21015,6 +21985,59 @@ function OhhwellsBridge() {
21015
21985
  ) : null
21016
21986
  ] });
21017
21987
  }
21988
+
21989
+ // src/ui/EmptySection.tsx
21990
+ import Link3 from "next/link";
21991
+ import { Fragment as Fragment9, jsx as jsx34, jsxs as jsxs21 } from "react/jsx-runtime";
21992
+ function EmptySection({ title, homeHref = "/", eyebrowKey, titleKey, subtitleKey }) {
21993
+ return /* @__PURE__ */ jsxs21(Fragment9, { children: [
21994
+ /* @__PURE__ */ jsx34(
21995
+ "p",
21996
+ {
21997
+ style: {
21998
+ fontFamily: "var(--brand-font-body)",
21999
+ fontSize: "0.75rem",
22000
+ fontWeight: 500,
22001
+ letterSpacing: "0.15em",
22002
+ textTransform: "uppercase",
22003
+ color: "var(--brand-accent)",
22004
+ marginBottom: "1.5rem"
22005
+ },
22006
+ children: /* @__PURE__ */ jsx34(Link3, { href: homeHref, style: { color: "inherit" }, children: /* @__PURE__ */ jsx34("span", { ...eyebrowKey ? { "data-ohw-editable": "text", "data-ohw-key": eyebrowKey } : {}, children: "Home" }) })
22007
+ }
22008
+ ),
22009
+ /* @__PURE__ */ jsx34(
22010
+ "h1",
22011
+ {
22012
+ style: {
22013
+ fontFamily: "var(--brand-font-heading)",
22014
+ fontSize: "clamp(2rem, 3.5vw, 2.75rem)",
22015
+ lineHeight: 1.1,
22016
+ letterSpacing: "-0.025em",
22017
+ color: "var(--brand-text)",
22018
+ marginBottom: "1rem"
22019
+ },
22020
+ ...titleKey ? { "data-ohw-editable": "text", "data-ohw-key": titleKey, "data-ohw-max-length": 80 } : {},
22021
+ children: title
22022
+ }
22023
+ ),
22024
+ /* @__PURE__ */ jsx34(
22025
+ "p",
22026
+ {
22027
+ style: {
22028
+ fontFamily: "var(--brand-font-body)",
22029
+ fontSize: "1rem",
22030
+ lineHeight: 1.7,
22031
+ fontWeight: 300,
22032
+ color: "var(--brand-text-muted)",
22033
+ maxWidth: "340px"
22034
+ },
22035
+ ...subtitleKey ? { "data-ohw-editable": "text", "data-ohw-key": subtitleKey, "data-ohw-max-length": 160 } : {},
22036
+ children: "This page doesn't have any content yet."
22037
+ }
22038
+ )
22039
+ ] });
22040
+ }
21018
22041
  export {
21019
22042
  AI_DEFAULT_BRAND,
21020
22043
  AI_TREE_SCHEMA_VERSIONS,
@@ -21031,6 +22054,7 @@ export {
21031
22054
  DropdownMenuItem,
21032
22055
  DropdownMenuSeparator,
21033
22056
  DropdownMenuTrigger,
22057
+ EmptySection,
21034
22058
  ItemActionToolbar,
21035
22059
  ItemInteractionLayer,
21036
22060
  LinkEditorPanel,