@ohhwells/bridge 0.1.78 → 0.1.79-next.238

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,320 @@ 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)) return;
340
+ const value = el.style.getPropertyValue(prop) || (prop === "background" ? el.style.getPropertyValue("background-color") : "");
341
+ el.setAttribute(attr, value);
342
+ }
343
+ function restoreInline(el, prop) {
344
+ const attr = `data-ohw-style-prev-${prop}`;
345
+ if (!el.hasAttribute(attr)) return;
346
+ const prev = el.getAttribute(attr) ?? "";
347
+ if (prev) el.style.setProperty(prop, prev);
348
+ else el.style.removeProperty(prop);
349
+ el.removeAttribute(attr);
350
+ }
351
+ function ensureStyleSheet() {
352
+ let el = document.getElementById(STYLE_SHEET_ID);
353
+ if (!el) {
354
+ el = document.createElement("style");
355
+ el.id = STYLE_SHEET_ID;
356
+ document.head.appendChild(el);
357
+ }
358
+ const css = styleSheetCss();
359
+ if (el.textContent !== css) el.textContent = css;
360
+ }
361
+ function clearSectionAttrs(root) {
362
+ for (const attr of Object.values(SECTION_ATTRS)) {
363
+ for (const el of Array.from(root.querySelectorAll(`[${attr}]`))) el.removeAttribute(attr);
364
+ }
365
+ for (const el of Array.from(root.querySelectorAll("[data-ohw-style-bgcolor]"))) {
366
+ restoreInline(el, "background");
367
+ el.removeAttribute("data-ohw-style-bgcolor");
368
+ }
369
+ }
370
+ function clearNodeProps(root) {
371
+ for (const el of Array.from(root.querySelectorAll(`[${NODE_WROTE_ATTR}]`))) {
372
+ const h = el;
373
+ for (const prop of NODE_PROPS) restoreInline(h, prop);
374
+ h.removeAttribute(NODE_WROTE_ATTR);
375
+ }
376
+ }
377
+ function buttonSurfaceOf(el) {
378
+ return el.closest("a, button") ?? el;
379
+ }
380
+ function applyStylesToDom(store) {
381
+ ensureStyleSheet();
382
+ clearSectionAttrs(document);
383
+ clearNodeProps(document);
384
+ loadStyleFonts(
385
+ store ? Object.values(store.nodes).flatMap((n) => n.fontFamily ? [n.fontFamily] : []) : []
386
+ );
387
+ if (!store) return;
388
+ for (const [sectionId, override] of Object.entries(store.sections)) {
389
+ const sections = document.querySelectorAll(
390
+ `[data-ohw-section="${CSS.escape(sectionId)}"]`
391
+ );
392
+ for (const marker of Array.from(sections)) {
393
+ const section = marker.querySelector(":scope > [data-ai-section]") ?? marker;
394
+ for (const [prop, attr] of Object.entries(SECTION_ATTRS)) {
395
+ const value = override[prop];
396
+ if (value === void 0) continue;
397
+ if (prop === "sectionBackground" && override.sectionBackgroundColor !== void 0) continue;
398
+ section.setAttribute(attr, String(value).replace(":", "-"));
399
+ }
400
+ if (override.sectionBackgroundColor !== void 0) {
401
+ saveInline(section, "background");
402
+ section.style.setProperty("background", override.sectionBackgroundColor, "important");
403
+ section.setAttribute("data-ohw-style-bgcolor", "");
404
+ }
405
+ }
406
+ }
407
+ for (const [key, override] of Object.entries(store.nodes)) {
408
+ const nodes = document.querySelectorAll(`[data-ohw-key="${CSS.escape(key)}"]`);
409
+ for (const el of Array.from(nodes)) {
410
+ if (override.color !== void 0) {
411
+ saveInline(el, "color");
412
+ el.style.setProperty("color", override.color, "important");
413
+ el.setAttribute(NODE_WROTE_ATTR, "");
414
+ }
415
+ if (override.fontFamily !== void 0) {
416
+ saveInline(el, "font-family");
417
+ el.style.setProperty("font-family", `'${override.fontFamily}'`, "important");
418
+ el.setAttribute(NODE_WROTE_ATTR, "");
419
+ }
420
+ if (override.fontSize !== void 0) {
421
+ saveInline(el, "font-size");
422
+ el.style.setProperty("font-size", `${override.fontSize}px`, "important");
423
+ el.setAttribute(NODE_WROTE_ATTR, "");
424
+ }
425
+ if (override.buttonBackground !== void 0 || override.buttonText !== void 0) {
426
+ const surface = buttonSurfaceOf(el);
427
+ if (override.buttonBackground !== void 0) {
428
+ saveInline(surface, "background");
429
+ surface.style.setProperty("background", override.buttonBackground, "important");
430
+ }
431
+ if (override.buttonText !== void 0) {
432
+ saveInline(surface, "color");
433
+ surface.style.setProperty("color", override.buttonText, "important");
434
+ }
435
+ surface.setAttribute(NODE_WROTE_ATTR, "");
436
+ }
437
+ }
438
+ }
439
+ }
440
+
121
441
  // src/ui/ai-tree/aiSectionsManager.tsx
122
442
  import { flushSync } from "react-dom";
123
443
  import { createRoot } from "react-dom/client";
@@ -132,7 +452,8 @@ function lucideByName(name) {
132
452
  }
133
453
  var typeStyle = (spec, font) => ({
134
454
  fontFamily: font,
135
- fontSize: spec.size,
455
+ // Headings shrink with the viewport (reaching full size around ~900px wide); body copy stays put.
456
+ 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
457
  lineHeight: spec.line,
137
458
  fontWeight: spec.weight
138
459
  });
@@ -141,12 +462,58 @@ var featureLines = (value) => value.split(/\n|<br\s*\/?>/i).map((line) => line.t
141
462
  var CHECK_MASK = `data:image/svg+xml,${encodeURIComponent(
142
463
  '<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
464
  )}`;
465
+ var AI_MOBILE_CSS = [
466
+ "@media (max-width: 768px){",
467
+ "[data-ai-section]{overflow-x:hidden}",
468
+ "[data-ai-container]{padding:0 20px !important}",
469
+ "[data-ai-row]{display:flex !important;flex-direction:column !important;align-items:stretch !important}",
470
+ "[data-ai-cell]{width:100%;min-width:0}",
471
+ "[data-ai-grid]{grid-template-columns:1fr !important}",
472
+ // Group containers flatten to a column on phones; span placements come along for free.
473
+ "[data-ai-group]{display:flex !important;flex-direction:column !important}",
474
+ "[data-ai-group] > *{grid-column:auto !important}",
475
+ "[data-ai-section] img{max-width:100%}",
476
+ "}",
477
+ "@media (min-width: 769px) and (max-width: 1024px){",
478
+ "[data-ai-grid]{grid-template-columns:repeat(2, 1fr) !important}",
479
+ "}"
480
+ ].join("");
144
481
  var FEATURE_LINE_CSS = [
145
482
  "[data-ai-features]>div{position:relative;padding-left:40px;min-height:24px}",
146
483
  '[data-ai-features]>div::before{content:"";position:absolute;left:0;top:1px;width:24px;height:24px;',
147
484
  `background-color:currentColor;-webkit-mask:url("${CHECK_MASK}") no-repeat 0 0/24px 24px;`,
148
485
  `mask:url("${CHECK_MASK}") no-repeat 0 0/24px 24px}`
149
486
  ].join("");
487
+ function hexLuminance(color) {
488
+ const m = /^#([0-9a-f]{6})$/i.exec(color.trim());
489
+ if (!m) return null;
490
+ const [r2, g, b] = [0, 2, 4].map((i) => {
491
+ const c = parseInt(m[1].slice(i, i + 2), 16) / 255;
492
+ return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
493
+ });
494
+ return 0.2126 * r2 + 0.7152 * g + 0.0722 * b;
495
+ }
496
+ function hexContrast(a, b) {
497
+ const la = hexLuminance(a);
498
+ const lb = hexLuminance(b);
499
+ if (la === null || lb === null) return null;
500
+ const [hi, lo] = la > lb ? [la, lb] : [lb, la];
501
+ return (hi + 0.05) / (lo + 0.05);
502
+ }
503
+ function accentBandContext(brand) {
504
+ const p = brand.palette;
505
+ const lightWins = (hexContrast(p.light, p.primary) ?? 99) >= (hexContrast(p.dark, p.primary) ?? 0);
506
+ if (lightWins) {
507
+ return {
508
+ brand: { ...brand, palette: { dark: p.light, primary: p.light, accent: p.light, light: p.primary } },
509
+ buttonLabel: p.primary
510
+ };
511
+ }
512
+ return {
513
+ brand: { ...brand, palette: { dark: p.dark, primary: p.dark, accent: p.dark, light: p.light } },
514
+ buttonLabel: p.light
515
+ };
516
+ }
150
517
  function textAttrs(ctx, path) {
151
518
  return ctx.keyFor ? { "data-ohw-key": ctx.keyFor(path), "data-ohw-editable": "text" } : {};
152
519
  }
@@ -158,7 +525,12 @@ var AI_RESPONSIVE_CSS = [
158
525
  "@media (max-width: 640px) {",
159
526
  " [data-ai-responsive] [data-ai-grid] { grid-template-columns: 1fr !important; }",
160
527
  " [data-ai-responsive] [data-ai-columns] > * { grid-column: 1 / -1 !important; }",
528
+ // Group containers flatten to a column on phones; span placements come along for free.
529
+ " [data-ai-responsive] [data-ai-group] { display: flex !important; flex-direction: column !important; }",
530
+ " [data-ai-responsive] [data-ai-group] > * { grid-column: auto !important; }",
161
531
  " [data-ai-responsive] [data-ai-section-inner] { padding-left: 20px !important; padding-right: 20px !important; }",
532
+ " [data-ai-responsive] { overflow-x: hidden; }",
533
+ " [data-ai-responsive] img { max-width: 100%; }",
162
534
  "}"
163
535
  ].join("\n");
164
536
  var TRANSPARENT_PX = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
@@ -236,7 +608,7 @@ function ButtonEl({
236
608
  }) {
237
609
  const secondary = slots.variant === "secondary";
238
610
  const href = str(slots.href);
239
- const linkAttrs = ctx.keyFor && path ? { "data-ohw-href-key": ctx.keyFor(`${path}.href`) } : {};
611
+ const linkAttrs = ctx.keyFor && path ? { "data-ohw-href-key": ctx.keyFor(`${path}.href`), "data-ohw-role": "button" } : {};
240
612
  return /* @__PURE__ */ jsx(
241
613
  "a",
242
614
  {
@@ -252,7 +624,7 @@ function ButtonEl({
252
624
  textDecoration: "none",
253
625
  cursor: "pointer",
254
626
  ...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 }
627
+ ...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
628
  },
257
629
  children: /* @__PURE__ */ jsx("span", { ...path ? textAttrs(ctx, `${path}.label`) : {}, children: str(slots.label) })
258
630
  }
@@ -758,7 +1130,24 @@ function CardBlock({ node, ctx, path }) {
758
1130
  minWidth: 0
759
1131
  },
760
1132
  children: [
761
- media && (horizontal ? /* @__PURE__ */ jsx("div", { style: { flex: 1, minWidth: 0, display: "flex", alignItems: "center", padding: mediaInset }, children: media }) : /* @__PURE__ */ jsx(
1133
+ media && (horizontal ? /* @__PURE__ */ jsx(
1134
+ "div",
1135
+ {
1136
+ style: (
1137
+ // An icon hugs its glyph — flex:1 gave a 48px icon half the card and pushed the
1138
+ // text to the far side. Photos keep the half-and-half split. The inset has no
1139
+ // inner padding (the photo split absorbed that), so the icon carries its own gap.
1140
+ /^(lucide|simple):/.test(mediaRef) ? {
1141
+ flexShrink: 0,
1142
+ display: "flex",
1143
+ alignItems: "center",
1144
+ padding: mediaInset,
1145
+ [mediaPosition === "right" ? "marginLeft" : "marginRight"]: 20
1146
+ } : { flex: 1, minWidth: 0, display: "flex", alignItems: "center", padding: mediaInset }
1147
+ ),
1148
+ children: media
1149
+ }
1150
+ ) : /* @__PURE__ */ jsx(
762
1151
  "div",
763
1152
  {
764
1153
  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 +1238,44 @@ function AccordionBlock({ node, ctx, path }) {
849
1238
  ) })
850
1239
  ] }, i)) });
851
1240
  }
1241
+ function useIsMobile() {
1242
+ const [mobile, setMobile] = React.useState(
1243
+ () => typeof window !== "undefined" && window.matchMedia("(max-width: 768px)").matches
1244
+ );
1245
+ React.useEffect(() => {
1246
+ const mq = window.matchMedia("(max-width: 768px)");
1247
+ const update = () => setMobile(mq.matches);
1248
+ update();
1249
+ mq.addEventListener("change", update);
1250
+ return () => mq.removeEventListener("change", update);
1251
+ }, []);
1252
+ return mobile;
1253
+ }
852
1254
  function Carousel({ items, itemsPerRow, ctx }) {
1255
+ const isMobile = useIsMobile();
1256
+ const perPage = isMobile ? 1 : itemsPerRow;
1257
+ const pages = Math.max(1, Math.ceil(items.length / perPage));
853
1258
  const [page, setPage] = React.useState(0);
854
- const pages = Math.max(1, Math.ceil(items.length / itemsPerRow));
855
1259
  const current = Math.min(page, pages - 1);
1260
+ if (pages <= 1) {
1261
+ const cols = Math.max(1, Math.min(items.length, itemsPerRow));
1262
+ return /* @__PURE__ */ jsx(
1263
+ "div",
1264
+ {
1265
+ "data-ai-grid": String(cols),
1266
+ style: {
1267
+ display: "grid",
1268
+ gridTemplateColumns: `repeat(${cols}, 1fr)`,
1269
+ gap: AI_TREE_TOKENS.spacing8,
1270
+ alignItems: "start"
1271
+ },
1272
+ children: items
1273
+ }
1274
+ );
1275
+ }
856
1276
  const pageGroups = Array.from(
857
1277
  { length: pages },
858
- (_, p) => items.slice(p * itemsPerRow, (p + 1) * itemsPerRow)
1278
+ (_, p) => items.slice(p * perPage, (p + 1) * perPage)
859
1279
  );
860
1280
  const chrome = (enabled) => ({
861
1281
  border: `1px solid ${ctx.brand.palette.dark}`,
@@ -880,55 +1300,69 @@ function Carousel({ items, itemsPerRow, ctx }) {
880
1300
  cursor: "pointer",
881
1301
  padding: 0
882
1302
  });
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(
1303
+ const viewport = /* @__PURE__ */ jsx("div", { style: { flex: isMobile ? "0 0 auto" : 1, minWidth: 0, width: "100%", overflow: "hidden" }, children: /* @__PURE__ */ jsx(
1304
+ "div",
1305
+ {
1306
+ style: {
1307
+ display: "flex",
1308
+ transform: `translateX(-${current * 100}%)`,
1309
+ transition: "transform 0.4s ease"
1310
+ },
1311
+ children: pageGroups.map((group, p) => /* @__PURE__ */ jsx(
896
1312
  "div",
897
1313
  {
1314
+ "data-ai-grid": String(perPage),
898
1315
  style: {
899
- display: "flex",
900
- transform: `translateX(-${current * 100}%)`,
901
- transition: "transform 0.4s ease"
1316
+ flex: "0 0 100%",
1317
+ display: "grid",
1318
+ gridTemplateColumns: `repeat(${perPage}, 1fr)`,
1319
+ gap: AI_TREE_TOKENS.spacing8,
1320
+ alignItems: "start"
902
1321
  },
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
- )
1322
+ children: group
1323
+ },
1324
+ p
1325
+ ))
1326
+ }
1327
+ ) });
1328
+ const prevBtn = /* @__PURE__ */ jsx(
1329
+ "button",
1330
+ {
1331
+ type: "button",
1332
+ "aria-label": "Previous",
1333
+ onClick: () => setPage((p) => Math.max(0, p - 1)),
1334
+ style: chrome(current > 0),
1335
+ children: /* @__PURE__ */ jsx(ArrowLeft, { size: 24, strokeWidth: 1.5, "aria-hidden": true })
1336
+ }
1337
+ );
1338
+ const nextBtn = /* @__PURE__ */ jsx(
1339
+ "button",
1340
+ {
1341
+ type: "button",
1342
+ "aria-label": "Next",
1343
+ onClick: () => setPage((p) => Math.min(pages - 1, p + 1)),
1344
+ style: chrome(current < pages - 1),
1345
+ children: /* @__PURE__ */ jsx(ArrowRight, { size: 24, strokeWidth: 1.5, "aria-hidden": true })
1346
+ }
1347
+ );
1348
+ 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)) });
1349
+ if (isMobile) {
1350
+ return /* @__PURE__ */ jsxs("div", { "data-ai-carousel": "", style: { display: "flex", flexDirection: "column", gap: AI_TREE_TOKENS.spacing6, alignItems: "center" }, children: [
1351
+ viewport,
1352
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: AI_TREE_TOKENS.spacing6, justifyContent: "center" }, children: [
1353
+ prevBtn,
1354
+ nextBtn
1355
+ ] }),
1356
+ dots
1357
+ ] });
1358
+ }
1359
+ return /* @__PURE__ */ jsxs("div", { "data-ai-carousel": "", style: { display: "flex", flexDirection: "column", gap: AI_TREE_TOKENS.spacing8, alignItems: "center" }, children: [
1360
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: AI_TREE_TOKENS.spacing6, width: "100%" }, children: [
1361
+ prevBtn,
1362
+ viewport,
1363
+ nextBtn
930
1364
  ] }),
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)) })
1365
+ dots
932
1366
  ] });
933
1367
  }
934
1368
  function CollectionBlock({ node, ctx, path }) {
@@ -1022,6 +1456,49 @@ function renderNode(node, ctx, path) {
1022
1456
  switch (node.type) {
1023
1457
  case "text":
1024
1458
  return /* @__PURE__ */ jsx(TextBlock, { slots, ctx, path });
1459
+ // Layout container: arranges child blocks, contributes no content of its own. `grid` is a
1460
+ // nested 12-column grid the children span (a collage is 3–5 media on it, bottom-aligned so
1461
+ // mixed aspects read as a composition); `split` is exactly two children at a ratio; `stack`
1462
+ // is a column. Children render through this same dispatcher, so edit markers, media
1463
+ // resolution, and copy paths all work unchanged inside a group.
1464
+ case "group": {
1465
+ const layout = str(slots.layout);
1466
+ const gap = slots.spacing === "tight" ? AI_TREE_TOKENS.spacing3 : slots.spacing === "airy" ? AI_TREE_TOKENS.spacing8 : AI_TREE_TOKENS.spacing6;
1467
+ const kids = (node.children ?? []).map((child, i) => /* @__PURE__ */ jsx(
1468
+ "div",
1469
+ {
1470
+ style: layout === "grid" ? {
1471
+ gridColumn: `span ${typeof child.span === "number" ? Math.min(12, Math.max(1, child.span)) : 12}`,
1472
+ minWidth: 0
1473
+ } : { minWidth: 0 },
1474
+ children: renderNode(child, ctx, `${path}.c${i}`)
1475
+ },
1476
+ i
1477
+ ));
1478
+ if (layout === "grid") {
1479
+ return /* @__PURE__ */ jsx(
1480
+ "div",
1481
+ {
1482
+ "data-ai-group": "grid",
1483
+ style: { display: "grid", gridTemplateColumns: "repeat(12, 1fr)", gap, alignItems: "end" },
1484
+ children: kids
1485
+ }
1486
+ );
1487
+ }
1488
+ if (layout === "split") {
1489
+ const ratio = str(slots.ratio);
1490
+ const cols = ratio === "3:5" ? "3fr 5fr" : ratio === "5:3" ? "5fr 3fr" : "1fr 1fr";
1491
+ return /* @__PURE__ */ jsx(
1492
+ "div",
1493
+ {
1494
+ "data-ai-group": "split",
1495
+ style: { display: "grid", gridTemplateColumns: cols, gap, alignItems: "center" },
1496
+ children: kids
1497
+ }
1498
+ );
1499
+ }
1500
+ return /* @__PURE__ */ jsx("div", { "data-ai-group": "stack", style: { display: "flex", flexDirection: "column", gap }, children: kids });
1501
+ }
1025
1502
  case "button":
1026
1503
  return /* @__PURE__ */ jsx(ButtonEl, { slots, ctx, path });
1027
1504
  case "button-row":
@@ -1102,33 +1579,111 @@ function renderNode(node, ctx, path) {
1102
1579
  }
1103
1580
  );
1104
1581
  }
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
- }) });
1582
+ case "form": {
1583
+ const formAttrs = ctx.keyFor ? {
1584
+ "data-ohw-editable": "form",
1585
+ "data-ohw-key": ctx.keyFor(`${path}.form`),
1586
+ "data-ohw-success-text": "Thanks \u2014 we'll be in touch shortly."
1587
+ } : {};
1588
+ const fieldStyle = {
1589
+ width: "100%",
1590
+ boxSizing: "border-box",
1591
+ border: `1px solid color-mix(in srgb, ${ctx.brand.palette.dark} 45%, #ffffff)`,
1592
+ borderRadius: 0,
1593
+ padding: 12,
1594
+ background: "#fff",
1595
+ color: ctx.brand.palette.dark,
1596
+ outline: "none",
1597
+ ...typeStyle(AI_TREE_TOKENS.type.bodyM, ctx.brand.fonts.body)
1598
+ };
1599
+ const labelStyle = {
1600
+ ...typeStyle(AI_TREE_TOKENS.type.bodyMBold, ctx.brand.fonts.body),
1601
+ color: ctx.brand.palette.dark,
1602
+ textAlign: "left",
1603
+ width: "100%"
1604
+ };
1605
+ const centered = ctx.sectionAlignment === "center";
1606
+ const submitAlign = centered ? "center" : "flex-start";
1607
+ const children = node.children ?? [];
1608
+ return (
1609
+ // 32px between the field group and the submit. In a stacked (centered) section the form is
1610
+ // capped at 780px and centered — the section's 12-col grid would otherwise leave it hugging
1611
+ // the left edge; a split section lets it fill its own column.
1612
+ /* @__PURE__ */ jsxs(
1613
+ "form",
1614
+ {
1615
+ ...formAttrs,
1616
+ "data-ai-form": "",
1617
+ style: {
1618
+ display: "flex",
1619
+ flexDirection: "column",
1620
+ gap: 32,
1621
+ width: "100%",
1622
+ ...centered ? { maxWidth: 780, marginLeft: "auto", marginRight: "auto" } : {}
1623
+ },
1624
+ children: [
1625
+ /* @__PURE__ */ jsx("div", { style: { display: "flex", flexDirection: "column", gap: 24, width: "100%", alignItems: "flex-start" }, children: children.map((child, i) => {
1626
+ if (child.type !== "input") return null;
1627
+ const cs = child.slots ?? {};
1628
+ const kind = str(cs.kind);
1629
+ const label = str(cs.label);
1630
+ const placeholder = str(cs.placeholder);
1631
+ const required = cs.required === true;
1632
+ const name = label.toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-+|-+$/gu, "") || `field-${i}`;
1633
+ const isTextarea = kind === "textarea";
1634
+ return /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 8, width: "100%" }, children: [
1635
+ /* @__PURE__ */ jsx("label", { ...textAttrs(ctx, `${path}.c${i}.label`), style: labelStyle, children: label }),
1636
+ isTextarea ? /* @__PURE__ */ jsx(
1637
+ "textarea",
1638
+ {
1639
+ name,
1640
+ placeholder,
1641
+ required,
1642
+ style: { ...fieldStyle, height: 180, resize: "vertical" }
1643
+ }
1644
+ ) : /* @__PURE__ */ jsx(
1645
+ "input",
1646
+ {
1647
+ name,
1648
+ type: kind === "email" ? "email" : "text",
1649
+ placeholder,
1650
+ required,
1651
+ style: { ...fieldStyle, height: 48 }
1652
+ }
1653
+ )
1654
+ ] }, i);
1655
+ }) }),
1656
+ children.map((child, i) => {
1657
+ if (child.type === "input") return null;
1658
+ const cs = child.slots ?? {};
1659
+ return /* @__PURE__ */ jsx(
1660
+ "button",
1661
+ {
1662
+ type: "submit",
1663
+ style: {
1664
+ alignSelf: submitAlign,
1665
+ border: "none",
1666
+ cursor: "pointer",
1667
+ padding: "12px 24px",
1668
+ // Corner radius follows the host template's own buttons (measured from a template
1669
+ // CTA); 8px only when the page has no template button to match.
1670
+ borderRadius: ctx.buttonRadius ?? 8,
1671
+ // Brand-styled: primary fill, brand-derived label colour (not a fixed token) so it
1672
+ // reads correctly on custom palettes.
1673
+ background: ctx.brand.palette.primary,
1674
+ color: ctx.buttonLabel ?? ctx.brand.palette.light,
1675
+ ...typeStyle(AI_TREE_TOKENS.type.button, ctx.brand.fonts.body)
1676
+ },
1677
+ children: /* @__PURE__ */ jsx("span", { ...textAttrs(ctx, `${path}.c${i}.label`), children: str(cs.label) })
1678
+ },
1679
+ i
1680
+ );
1681
+ })
1682
+ ]
1683
+ }
1684
+ )
1685
+ );
1686
+ }
1132
1687
  case "schedule-widget":
1133
1688
  return /* @__PURE__ */ jsx(
1134
1689
  "div",
@@ -1149,16 +1704,27 @@ function renderNode(node, ctx, path) {
1149
1704
  return null;
1150
1705
  }
1151
1706
  }
1152
- function AiTreeRenderer({ tree, brand, resolveMedia, editKeyPrefix }) {
1707
+ function AiTreeRenderer({
1708
+ tree,
1709
+ brand,
1710
+ buttonRadius,
1711
+ resolveMedia,
1712
+ editKeyPrefix
1713
+ }) {
1153
1714
  if (!isRenderableTree(tree)) {
1154
1715
  return null;
1155
1716
  }
1156
1717
  const resolvedBrand = brand ?? AI_DEFAULT_BRAND;
1718
+ const band = (tree.settings ?? {}).sectionBackground === "accent" ? accentBandContext(resolvedBrand) : null;
1719
+ const blockBrand = band?.brand ?? resolvedBrand;
1157
1720
  const ctx = {
1158
- brand: resolvedBrand,
1721
+ brand: blockBrand,
1159
1722
  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
1723
+ cardSurface: blockBrand.palette.light.toUpperCase() === AI_DEFAULT_BRAND.palette.light.toUpperCase() ? "#ECECEB" : `color-mix(in srgb, ${blockBrand.palette.light} 90%, ${blockBrand.palette.dark})`,
1724
+ keyFor: editKeyPrefix ? (path) => `${editKeyPrefix}.${path}` : null,
1725
+ sectionAlignment: (tree.settings ?? {}).alignment === "center" ? "center" : "left",
1726
+ buttonRadius,
1727
+ ...band ? { buttonLabel: band.buttonLabel } : {}
1162
1728
  };
1163
1729
  const settings = tree.settings ?? {};
1164
1730
  const pad = AI_TREE_TOKENS.sectionPadding[settings.spacing ?? "balanced"];
@@ -1166,6 +1732,20 @@ function AiTreeRenderer({ tree, brand, resolveMedia, editKeyPrefix }) {
1166
1732
  const overlayAlpha = { low: 0.25, medium: 0.45, high: 0.65 }[settings.overlayIntensity ?? "medium"];
1167
1733
  const backgroundUrl = isOverlay && settings.backgroundMedia ? ctx.resolveMedia(settings.backgroundMedia) : null;
1168
1734
  const bgAttrs = ctx.keyFor && isOverlay && settings.backgroundMedia ? { "data-ohw-key": ctx.keyFor("settings.backgroundMedia"), "data-ohw-editable": "bg-image" } : {};
1735
+ const toneBackground = (() => {
1736
+ const { dark, primary, light } = resolvedBrand.palette;
1737
+ switch (settings.sectionBackground) {
1738
+ case "surface":
1739
+ return `color-mix(in srgb, ${light} 94%, ${dark})`;
1740
+ case "accent":
1741
+ return primary;
1742
+ case "accent-soft":
1743
+ return `color-mix(in srgb, ${primary} 12%, ${light})`;
1744
+ default:
1745
+ return void 0;
1746
+ }
1747
+ })();
1748
+ const distributed = !isOverlay && settings.textDistribution;
1169
1749
  return /* @__PURE__ */ jsxs(
1170
1750
  "section",
1171
1751
  {
@@ -1175,13 +1755,15 @@ function AiTreeRenderer({ tree, brand, resolveMedia, editKeyPrefix }) {
1175
1755
  style: {
1176
1756
  position: "relative",
1177
1757
  padding: `${pad}px 0`,
1178
- background: isOverlay && !backgroundUrl ? "#DBEAFE" : void 0,
1758
+ background: toneBackground ?? (isOverlay && !backgroundUrl ? "#DBEAFE" : void 0),
1179
1759
  backgroundImage: backgroundUrl ? `url(${backgroundUrl})` : void 0,
1180
1760
  backgroundSize: "cover",
1181
- backgroundPosition: "center"
1761
+ backgroundPosition: "center",
1762
+ color: settings.sectionBackground === "accent" ? blockBrand.palette.dark : void 0
1182
1763
  },
1183
1764
  children: [
1184
1765
  /* @__PURE__ */ jsx("style", { children: AI_RESPONSIVE_CSS }),
1766
+ /* @__PURE__ */ jsx("style", { children: AI_MOBILE_CSS }),
1185
1767
  isOverlay && backgroundUrl && /* @__PURE__ */ jsx("div", { style: { position: "absolute", inset: 0, background: `rgba(255,255,255,${overlayAlpha})` } }),
1186
1768
  /* @__PURE__ */ jsx(
1187
1769
  "div",
@@ -1202,10 +1784,24 @@ function AiTreeRenderer({ tree, brand, resolveMedia, editKeyPrefix }) {
1202
1784
  display: "grid",
1203
1785
  gridTemplateColumns: "repeat(12, 1fr)",
1204
1786
  gap: AI_TREE_TOKENS.spacing6,
1205
- alignItems: settings.verticalPosition === "top" ? "start" : "center",
1787
+ alignItems: distributed === "space-between" ? "stretch" : (distributed ?? settings.verticalPosition) === "top" ? "start" : "center",
1206
1788
  marginTop: r2 > 0 ? AI_TREE_TOKENS.spacing8 : 0
1207
1789
  },
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))
1790
+ children: (row.blocks ?? []).map((block, b) => /* @__PURE__ */ jsx(
1791
+ "div",
1792
+ {
1793
+ "data-ai-cell": "",
1794
+ style: {
1795
+ gridColumn: `span ${Math.min(12, Math.max(1, block.span))}`,
1796
+ minWidth: 0,
1797
+ // space-between: each column becomes a flex column whose content spreads over
1798
+ // the full row height instead of clumping at the top.
1799
+ ...distributed === "space-between" ? { display: "flex", flexDirection: "column", justifyContent: "space-between" } : {}
1800
+ },
1801
+ children: renderNode(block, ctx, `r${r2}.b${b}`)
1802
+ },
1803
+ b
1804
+ ))
1209
1805
  },
1210
1806
  r2
1211
1807
  ))
@@ -1221,17 +1817,36 @@ import { jsx as jsx2 } from "react/jsx-runtime";
1221
1817
  var CONTAINER_ATTR = "data-ohw-ai-generated";
1222
1818
  var REPLACED_ATTR = "data-ohw-ai-replaced-by";
1223
1819
  var REMOVED_ATTR = "data-ohw-ai-removed";
1820
+ var TEMPLATE_HIDDEN_ATTR = "data-ohw-ai-template-hidden";
1821
+ var CHROME_SECTION_IDS = /* @__PURE__ */ new Set(["navbar", "footer"]);
1822
+ function readRootVar(name) {
1823
+ if (typeof document === "undefined") return "";
1824
+ return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
1825
+ }
1826
+ function deriveBrandOverride() {
1827
+ const dark = readRootVar("--ohw-brand-dark");
1828
+ const primary = readRootVar("--ohw-brand-primary");
1829
+ const light = readRootVar("--ohw-brand-light");
1830
+ if (!dark || !primary || !light) return null;
1831
+ const accent = readRootVar("--ohw-brand-accent");
1832
+ const heading = readRootVar("--font-heading") || readRootVar("--font-display");
1833
+ const body = readRootVar("--font-body");
1834
+ return {
1835
+ palette: { dark, primary, accent: accent || dark, light },
1836
+ fonts: {
1837
+ heading: heading || AI_DEFAULT_BRAND.fonts.heading,
1838
+ body: body || AI_DEFAULT_BRAND.fonts.body
1839
+ }
1840
+ };
1841
+ }
1224
1842
  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");
1843
+ const dark = readRootVar("--color-dark");
1844
+ const primary = readRootVar("--color-primary");
1845
+ const light = readRootVar("--color-light");
1231
1846
  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");
1847
+ const accent = readRootVar("--color-accent");
1848
+ const heading = readRootVar("--font-heading") || readRootVar("--font-display");
1849
+ const body = readRootVar("--font-body");
1235
1850
  return {
1236
1851
  palette: { dark, primary, accent: accent || dark, light },
1237
1852
  fonts: {
@@ -1240,6 +1855,13 @@ function deriveTemplateBrand() {
1240
1855
  }
1241
1856
  };
1242
1857
  }
1858
+ function deriveTemplateButtonRadius() {
1859
+ if (typeof document === "undefined") return null;
1860
+ const btn = document.querySelector('[data-ohw-role="button"]');
1861
+ if (!btn) return null;
1862
+ const radius = getComputedStyle(btn).borderTopLeftRadius;
1863
+ return radius || null;
1864
+ }
1243
1865
  var mounted = /* @__PURE__ */ new Map();
1244
1866
  function findTemplateSection(id) {
1245
1867
  for (const el of document.querySelectorAll(`[data-ohw-section="${CSS.escape(id)}"]`)) {
@@ -1303,6 +1925,24 @@ function syncRemovedSections(state) {
1303
1925
  }
1304
1926
  }
1305
1927
  }
1928
+ function syncTemplateHidden(state, pageHasSections) {
1929
+ const hide = state.hideTemplate === true && pageHasSections;
1930
+ for (const el of Array.from(document.querySelectorAll(`[${TEMPLATE_HIDDEN_ATTR}]`))) {
1931
+ if (!hide) {
1932
+ el.style.removeProperty("display");
1933
+ el.removeAttribute(TEMPLATE_HIDDEN_ATTR);
1934
+ }
1935
+ }
1936
+ if (!hide) return;
1937
+ for (const el of Array.from(document.querySelectorAll("[data-ohw-section]"))) {
1938
+ if (el.hasAttribute(CONTAINER_ATTR)) continue;
1939
+ if (CHROME_SECTION_IDS.has(el.getAttribute("data-ohw-section") ?? "")) continue;
1940
+ if (el.parentElement?.closest("[data-ohw-section]")) continue;
1941
+ if (el.hasAttribute(REPLACED_ATTR) || el.hasAttribute(REMOVED_ATTR)) continue;
1942
+ el.style.display = "none";
1943
+ el.setAttribute(TEMPLATE_HIDDEN_ATTR, "");
1944
+ }
1945
+ }
1306
1946
  function syncReplacedOriginals(state) {
1307
1947
  for (const el of document.querySelectorAll(`[${REPLACED_ATTR}]`)) {
1308
1948
  const byId = el.getAttribute(REPLACED_ATTR) ?? "";
@@ -1321,10 +1961,64 @@ function syncReplacedOriginals(state) {
1321
1961
  }
1322
1962
  }
1323
1963
  }
1964
+ var sectionOrderIndex = /* @__PURE__ */ new Map();
1965
+ function setAiSectionOrder(raw, currentPath) {
1966
+ const next = /* @__PURE__ */ new Map();
1967
+ if (raw) {
1968
+ try {
1969
+ const entries = JSON.parse(raw);
1970
+ for (const entry of entries) {
1971
+ if (!entry.pagePath || entry.pagePath === currentPath) next.set(entry.instanceId, entry.order);
1972
+ }
1973
+ } catch {
1974
+ }
1975
+ }
1976
+ sectionOrderIndex = next;
1977
+ }
1978
+ function applyExplicitOrder(entries) {
1979
+ if (sectionOrderIndex.size === 0) return entries;
1980
+ return entries.map((entry, index) => ({ entry, index, order: sectionOrderIndex.get(entry.id) })).sort((a, b) => {
1981
+ if (a.order === void 0 && b.order === void 0) return a.index - b.index;
1982
+ if (a.order === void 0) return 1;
1983
+ if (b.order === void 0) return -1;
1984
+ return a.order - b.order;
1985
+ }).map((item) => item.entry);
1986
+ }
1987
+ function orderByChain(sections) {
1988
+ const ids = new Set(sections.map((entry) => entry.id));
1989
+ const after = /* @__PURE__ */ new Map();
1990
+ const roots = [];
1991
+ for (const entry of sections) {
1992
+ const anchor = entry.replaces ?? entry.beforeSection ?? entry.afterSection ?? null;
1993
+ if (anchor && ids.has(anchor)) {
1994
+ const bucket = after.get(anchor);
1995
+ if (bucket) bucket.push(entry);
1996
+ else after.set(anchor, [entry]);
1997
+ } else {
1998
+ roots.push(entry);
1999
+ }
2000
+ }
2001
+ const out = [];
2002
+ const seen = /* @__PURE__ */ new Set();
2003
+ const visit = (entry) => {
2004
+ if (seen.has(entry.id)) return;
2005
+ seen.add(entry.id);
2006
+ out.push(entry);
2007
+ for (const child of after.get(entry.id) ?? []) visit(child);
2008
+ };
2009
+ for (const root of roots) visit(root);
2010
+ return out.length === sections.length ? out : sections;
2011
+ }
1324
2012
  function applyAiSectionsToDom(state, options) {
1325
2013
  if (typeof document === "undefined") return;
2014
+ const brandOverride = deriveBrandOverride();
1326
2015
  const templateBrand = deriveTemplateBrand();
1327
- const activeIds = new Set(state.sections.map((entry) => entry.id));
2016
+ const templateButtonRadius = deriveTemplateButtonRadius();
2017
+ const brandKey = brandOverride ? JSON.stringify(brandOverride) : "";
2018
+ const pagePath = window.location.pathname;
2019
+ const pageSections = state.sections.filter((entry) => (entry.path ?? "/") === pagePath);
2020
+ const activeIds = new Set(pageSections.map((entry) => entry.id));
2021
+ const ordered = state.hideTemplate === true ? applyExplicitOrder(orderByChain(pageSections)) : pageSections;
1328
2022
  for (const [id, section] of mounted) {
1329
2023
  if (!activeIds.has(id)) {
1330
2024
  section.root.unmount();
@@ -1332,8 +2026,8 @@ function applyAiSectionsToDom(state, options) {
1332
2026
  mounted.delete(id);
1333
2027
  }
1334
2028
  }
1335
- for (const entry of state.sections) {
1336
- const serialized = JSON.stringify(entry);
2029
+ for (const entry of ordered) {
2030
+ const serialized = JSON.stringify(entry) + brandKey;
1337
2031
  const existing = mounted.get(entry.id);
1338
2032
  if (existing && existing.serialized === serialized && existing.container.isConnected) {
1339
2033
  continue;
@@ -1358,7 +2052,8 @@ function applyAiSectionsToDom(state, options) {
1358
2052
  AiTreeRenderer,
1359
2053
  {
1360
2054
  tree: entry.tree,
1361
- brand: entry.brand ?? options?.brand ?? templateBrand ?? void 0,
2055
+ brand: brandOverride ?? entry.brand ?? options?.brand ?? templateBrand ?? void 0,
2056
+ buttonRadius: templateButtonRadius,
1362
2057
  resolveMedia,
1363
2058
  editKeyPrefix: `ai.${entry.id}`
1364
2059
  }
@@ -1367,8 +2062,20 @@ function applyAiSectionsToDom(state, options) {
1367
2062
  });
1368
2063
  mounted.set(entry.id, { root, container, serialized });
1369
2064
  }
2065
+ if (state.hideTemplate === true) {
2066
+ let prev = null;
2067
+ for (const entry of ordered) {
2068
+ const el = mounted.get(entry.id)?.container;
2069
+ if (!el) continue;
2070
+ if (prev && !(prev.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_FOLLOWING)) {
2071
+ prev.insertAdjacentElement("afterend", el);
2072
+ }
2073
+ prev = el;
2074
+ }
2075
+ }
1370
2076
  syncReplacedOriginals(state);
1371
2077
  syncRemovedSections(state);
2078
+ syncTemplateHidden(state, pageSections.length > 0);
1372
2079
  }
1373
2080
 
1374
2081
  // src/useLinkHrefGuardian.ts
@@ -1495,6 +2202,7 @@ function EmailCaptureModal({ title, subtitle, onSubmit, onClose }) {
1495
2202
  /* @__PURE__ */ jsx3(
1496
2203
  Dialog.Overlay,
1497
2204
  {
2205
+ "data-ohw-scheduling-modal": "",
1498
2206
  className: "fixed inset-0 z-50",
1499
2207
  style: { background: "rgba(0,0,0,0.45)" }
1500
2208
  }
@@ -1502,6 +2210,7 @@ function EmailCaptureModal({ title, subtitle, onSubmit, onClose }) {
1502
2210
  /* @__PURE__ */ jsxs2(
1503
2211
  Dialog.Content,
1504
2212
  {
2213
+ "data-ohw-scheduling-modal": "",
1505
2214
  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
2215
  style: { maxWidth: 400 },
1507
2216
  children: [
@@ -1975,7 +2684,7 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAf
1975
2684
  const autoId = useId();
1976
2685
  const insertAfter = insertAfterProp ?? autoId;
1977
2686
  const [schedule, setSchedule] = useState2(null);
1978
- const [loading, setLoading] = useState2(true);
2687
+ const [loading, setLoading] = useState2(initialScheduleId !== null);
1979
2688
  const [inEditor, setInEditor] = useState2(false);
1980
2689
  const [isHovered, setIsHovered] = useState2(false);
1981
2690
  const [modalState, setModalState] = useState2(null);
@@ -2149,8 +2858,10 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAf
2149
2858
  "*"
2150
2859
  );
2151
2860
  };
2152
- if (!inEditor && !loading && !schedule) return null;
2153
2861
  const sectionId = `scheduling-${insertAfter}`;
2862
+ if (!inEditor && !loading && !schedule) {
2863
+ return /* @__PURE__ */ jsx4("section", { "data-ohw-section": sectionId, "data-ohw-scheduling-anchor": insertAfter, style: { display: "none" } });
2864
+ }
2154
2865
  return /* @__PURE__ */ jsxs3(
2155
2866
  "section",
2156
2867
  {
@@ -7067,13 +7778,17 @@ function MediaOverlay({
7067
7778
  hover,
7068
7779
  isUploading,
7069
7780
  fadingOut = false,
7781
+ selected = false,
7782
+ hovered = false,
7070
7783
  onFadeOutComplete,
7071
7784
  onReplace,
7785
+ onSelect,
7072
7786
  onVideoSettingsChange
7073
7787
  }) {
7074
7788
  const { rect } = hover;
7075
7789
  const skeletonRef = React8.useRef(null);
7076
7790
  const isVideo = hover.elementType === "video";
7791
+ const showChrome = !selected || hovered;
7077
7792
  const autoplay = hover.videoAutoplay ?? true;
7078
7793
  const muted = hover.videoMuted ?? true;
7079
7794
  const probeRef = React8.useRef(null);
@@ -7087,6 +7802,7 @@ function MediaOverlay({
7087
7802
  (prev) => prev && prev.fullWidth === width && prev.height === height ? prev : { fullWidth: width, height }
7088
7803
  );
7089
7804
  }, [isVideo]);
7805
+ const replaceLabel = isVideo ? "Replace video" : hover.elementType === "bg-image" ? "Replace background" : "Replace image";
7090
7806
  const replaceMode = buttonSize ? replaceButtonMode({ width: rect.width, height: rect.height }, buttonSize) : "none";
7091
7807
  const box = {
7092
7808
  position: "fixed",
@@ -7120,7 +7836,7 @@ function MediaOverlay({
7120
7836
  }
7121
7837
  );
7122
7838
  }
7123
- const settingsBar = isVideo && !hover.isDragOver ? /* @__PURE__ */ jsxs7(
7839
+ const settingsBar = isVideo && !hover.isDragOver && showChrome ? /* @__PURE__ */ jsxs7(
7124
7840
  "div",
7125
7841
  {
7126
7842
  "data-ohw-bridge": "",
@@ -7190,10 +7906,12 @@ function MediaOverlay({
7190
7906
  // in-document, pointer-events does it natively. The button below opts back in, so
7191
7907
  // Replace still works.
7192
7908
  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)"
7909
+ // Selected: a firm component ring with no wash, so the image reads as chosen rather
7910
+ // than hovered. Hover keeps the existing tinted preview.
7911
+ boxShadow: selected ? "inset 0 0 0 2px var(--color-primary)" : "inset 0 0 0 1.5px var(--color-primary)",
7912
+ background: selected ? "transparent" : "color-mix(in srgb, var(--color-primary) 20%, transparent)"
7195
7913
  },
7196
- onClick: () => onReplace(hover.key),
7914
+ onClick: () => onSelect && !selected ? onSelect(hover.key) : onReplace(hover.key),
7197
7915
  children: [
7198
7916
  /* @__PURE__ */ jsxs7(
7199
7917
  Button,
@@ -7214,17 +7932,17 @@ function MediaOverlay({
7214
7932
  },
7215
7933
  children: [
7216
7934
  isVideo ? /* @__PURE__ */ jsx15(Film, { size: 14 }) : /* @__PURE__ */ jsx15(ImageIcon, { size: 14 }),
7217
- isVideo ? "Replace video" : "Replace image"
7935
+ replaceLabel
7218
7936
  ]
7219
7937
  }
7220
7938
  ),
7221
- replaceMode === "none" ? null : /* @__PURE__ */ jsxs7(
7939
+ showChrome && replaceMode !== "none" && /* @__PURE__ */ jsxs7(
7222
7940
  Button,
7223
7941
  {
7224
7942
  "data-ohw-media-overlay": "",
7225
7943
  variant: "outline",
7226
7944
  size: "sm",
7227
- "aria-label": isVideo ? "Replace video" : "Replace image",
7945
+ "aria-label": replaceLabel,
7228
7946
  className: "gap-1.5 cursor-pointer hover:bg-background",
7229
7947
  style: {
7230
7948
  ...OVERLAY_BUTTON_STYLE,
@@ -7247,7 +7965,7 @@ function MediaOverlay({
7247
7965
  },
7248
7966
  children: [
7249
7967
  isVideo ? /* @__PURE__ */ jsx15(Film, { size: 14 }) : /* @__PURE__ */ jsx15(ImageIcon, { size: 14 }),
7250
- replaceMode === "full" ? isVideo ? "Replace video" : "Replace image" : null
7968
+ replaceMode === "full" ? replaceLabel : null
7251
7969
  ]
7252
7970
  }
7253
7971
  )
@@ -7331,6 +8049,8 @@ function parseSectionsFromRoot(root) {
7331
8049
  const id = el.getAttribute("data-ohw-section") ?? "";
7332
8050
  if (!id || seen.has(id) || LINK_PICKER_EXCLUDED_IDS.has(id)) continue;
7333
8051
  if (el.parentElement?.closest("[data-ohw-section]")) continue;
8052
+ if (el.hasAttribute("data-ohw-ai-template-hidden") || el.hasAttribute("data-ohw-ai-removed") || el.hasAttribute("data-ohw-ai-replaced-by"))
8053
+ continue;
7334
8054
  seen.add(id);
7335
8055
  const label = el.getAttribute("data-ohw-section-label") ?? titleCaseSectionId(id);
7336
8056
  sections.push({ id, label });
@@ -7616,6 +8336,7 @@ function AiSectionOverlay({
7616
8336
  }) {
7617
8337
  const [selectedId, setSelectedId] = useState5(null);
7618
8338
  const [reviewId, setReviewId] = useState5(null);
8339
+ const [reviewButtonsHidden, setReviewButtonsHidden] = useState5(false);
7619
8340
  const reviewIdRef = useRef4(null);
7620
8341
  reviewIdRef.current = reviewId;
7621
8342
  const selectedIdRef = useRef4(null);
@@ -7677,6 +8398,7 @@ function AiSectionOverlay({
7677
8398
  }
7678
8399
  const found = readRect(sectionId) != null;
7679
8400
  setReviewId(found ? sectionId : null);
8401
+ setReviewButtonsHidden(e.data.hideButtons === true);
7680
8402
  postToParent2({ type: "ow:ai-review-started", sectionId, found });
7681
8403
  if (found) {
7682
8404
  document.querySelector(`[data-ohw-instance="${CSS.escape(sectionId)}"]`)?.scrollIntoView({ behavior: "smooth", block: "nearest" });
@@ -7804,13 +8526,16 @@ function AiSectionOverlay({
7804
8526
  border: `2px solid ${PRIMARY2}`,
7805
8527
  borderRadius: edgeAwareRadius(reviewRect),
7806
8528
  zIndex: 2147483200,
7807
- // The veil itself: swallows clicks so the section stays locked until decided.
8529
+ // The veil itself: swallows clicks so the section stays locked until decided. This
8530
+ // stopPropagation only guards the bubble phase; the bridge's capture-phase click
8531
+ // handlers additionally bail on `[data-ohw-ai-review]`, without which a click on
8532
+ // Accept/Discard resolves to the media beneath and opens the file picker.
7808
8533
  background: "rgba(8, 133, 254, 0.04)",
7809
8534
  pointerEvents: "auto",
7810
8535
  cursor: "default"
7811
8536
  },
7812
8537
  onClick: (e) => e.stopPropagation(),
7813
- children: /* @__PURE__ */ jsxs9(
8538
+ children: !reviewButtonsHidden && /* @__PURE__ */ jsxs9(
7814
8539
  "div",
7815
8540
  {
7816
8541
  style: {
@@ -10377,8 +11102,13 @@ function referenceBox(slot) {
10377
11102
  const neighbour = row ? Array.from(row.querySelectorAll('[data-ohw-editable="icon"]')).find(
10378
11103
  (el) => el !== slot && !el.hasAttribute("data-ohw-social-icon-placeholder")
10379
11104
  ) : null;
10380
- const source = (neighbour ?? slot).querySelector(GLYPH_SELECTOR);
10381
- const box = source?.getBoundingClientRect() ?? null;
11105
+ if (neighbour) {
11106
+ const box2 = neighbour.querySelector(GLYPH_SELECTOR)?.getBoundingClientRect();
11107
+ if (box2?.width && box2.height) return box2;
11108
+ }
11109
+ const own = slot.getBoundingClientRect();
11110
+ if (own.width && own.height) return own;
11111
+ const box = slot.querySelector(GLYPH_SELECTOR)?.getBoundingClientRect() ?? null;
10382
11112
  return box?.width && box.height ? box : null;
10383
11113
  }
10384
11114
  function iconMarkupSizedFor(slot, markup) {
@@ -12179,6 +12909,7 @@ function readLogoSizeState(content, placement) {
12179
12909
  function getLogoElement(el) {
12180
12910
  const marked = el.closest('[data-ohw-role="logo"], [data-ohw-logo]');
12181
12911
  if (marked) return marked;
12912
+ if (el.closest('[data-ohw-editable="icon"]')) return null;
12182
12913
  const root = el.closest("nav, [data-ohw-nav-root], footer");
12183
12914
  if (!root) return null;
12184
12915
  const anchor = el.closest("a");
@@ -13253,6 +13984,7 @@ function useSectionDrag({
13253
13984
  }
13254
13985
  const orderJson = JSON.stringify(entries);
13255
13986
  editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: orderJson };
13987
+ setAiSectionOrder(orderJson, window.location.pathname);
13256
13988
  postToParentRef.current({ type: "ow:change", nodes: [{ key: SECTION_ORDER_KEY, text: orderJson }] });
13257
13989
  applyPersistedOrder(entries);
13258
13990
  clearSectionDragVisuals();
@@ -14112,21 +14844,10 @@ function parseSchedulingInsertAfter(insertAfter) {
14112
14844
  insertBefore: insertAfter.slice(idx + INSERT_BEFORE_MARKER.length)
14113
14845
  };
14114
14846
  }
14115
- function resolveSchedulingInsert(insertAfter, explicitInsertBefore) {
14116
- const parsed = parseSchedulingInsertAfter(insertAfter);
14117
- const insertBefore = explicitInsertBefore ?? parsed.insertBefore;
14118
- const effectiveInsertAfter = insertBefore && parsed.insertBefore === null ? `${insertAfter}${INSERT_BEFORE_MARKER}${insertBefore}` : insertAfter;
14119
- return { effectiveInsertAfter, insertBefore };
14120
- }
14121
- function getSchedulingMountPoint(insertAfter) {
14122
- const { anchor } = parseSchedulingInsertAfter(insertAfter);
14123
- let anchorEl = document.querySelector(`[data-ohw-section="${anchor}"]`);
14124
- if (!anchorEl && anchor === "scheduling") {
14125
- const widgets = Array.from(document.querySelectorAll('[data-ohw-section^="scheduling-"]'));
14126
- anchorEl = widgets.at(-1) ?? null;
14127
- }
14128
- if (!anchorEl) return null;
14129
- return anchorEl.closest('[data-ohw-section-container="scheduling"]') ?? anchorEl;
14847
+ function resolveEntryAnchor(entry) {
14848
+ if (entry.anchorId !== void 0) return { anchorId: entry.anchorId, beforeId: entry.beforeId ?? null };
14849
+ const parsed = parseSchedulingInsertAfter(entry.insertAfter);
14850
+ return { anchorId: parsed.anchor, beforeId: parsed.insertBefore };
14130
14851
  }
14131
14852
  function schedulingMountDepth(insertAfter) {
14132
14853
  if (!insertAfter.includes(INSERT_BEFORE_MARKER)) return 0;
@@ -14143,8 +14864,7 @@ function getPageSchedulingEntries(raw) {
14143
14864
  }
14144
14865
  }
14145
14866
  function isSchedulingWidgetMissing(entry) {
14146
- const { effectiveInsertAfter } = resolveSchedulingInsert(entry.insertAfter);
14147
- return !document.querySelector(`[data-ohw-section="${schedulingSectionId(effectiveInsertAfter)}"]`);
14867
+ return !document.querySelector(`[data-ohw-section="${schedulingSectionId(entry.insertAfter)}"]`);
14148
14868
  }
14149
14869
  function hasMissingSchedulingWidgets(entries) {
14150
14870
  return entries.some(isSchedulingWidgetMissing);
@@ -14174,16 +14894,17 @@ function initSectionsFromContent(content, removeExisting = false) {
14174
14894
  } catch {
14175
14895
  }
14176
14896
  }
14177
- function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId, explicitInsertBefore) {
14178
- const { effectiveInsertAfter, insertBefore } = resolveSchedulingInsert(insertAfter, explicitInsertBefore);
14179
- const sectionId = schedulingSectionId(effectiveInsertAfter);
14897
+ function mountSchedulingWidget(anchorId, notifyOnConnect = false, scheduleId, beforeId, existingWidgetId) {
14898
+ const widgetId = existingWidgetId ?? (beforeId ? `${anchorId}${INSERT_BEFORE_MARKER}${beforeId}` : anchorId);
14899
+ const sectionId = schedulingSectionId(widgetId);
14180
14900
  if (document.querySelector(`[data-ohw-section="${sectionId}"]`)) return false;
14181
- const mountPoint = getSchedulingMountPoint(effectiveInsertAfter);
14182
- if (!mountPoint) return false;
14901
+ const anchorEl = document.querySelector(`[data-ohw-section="${anchorId}"]`);
14902
+ if (!anchorEl) return false;
14903
+ const mountPoint = anchorEl.closest('[data-ohw-section-container="scheduling"]') ?? anchorEl;
14183
14904
  const container = document.createElement("div");
14184
14905
  container.dataset.ohwSectionContainer = "scheduling";
14185
- if (insertBefore) {
14186
- const beforeAnchor = document.querySelector(`[data-ohw-section="${insertBefore}"]`);
14906
+ if (beforeId) {
14907
+ const beforeAnchor = document.querySelector(`[data-ohw-section="${beforeId}"]`);
14187
14908
  const beforePoint = beforeAnchor?.closest('[data-ohw-section-container="scheduling"]') ?? beforeAnchor;
14188
14909
  if (!beforePoint) return false;
14189
14910
  beforePoint.insertAdjacentElement("beforebegin", container);
@@ -14194,19 +14915,25 @@ function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId,
14194
14915
  }
14195
14916
  tail.insertAdjacentElement("afterend", container);
14196
14917
  }
14197
- const root = createRoot2(container);
14198
- flushSync2(() => {
14199
- root.render(
14200
- /* @__PURE__ */ jsx33(
14201
- SchedulingWidget,
14202
- {
14203
- notifyOnConnect,
14204
- initialScheduleId: scheduleId,
14205
- insertAfter: effectiveInsertAfter
14206
- }
14207
- )
14208
- );
14209
- });
14918
+ try {
14919
+ const root = createRoot2(container);
14920
+ flushSync2(() => {
14921
+ root.render(
14922
+ /* @__PURE__ */ jsx33(
14923
+ SchedulingWidget,
14924
+ {
14925
+ notifyOnConnect,
14926
+ initialScheduleId: scheduleId,
14927
+ insertAfter: widgetId
14928
+ }
14929
+ )
14930
+ );
14931
+ });
14932
+ } catch (err) {
14933
+ console.error("[ow:scheduling] render threw", err);
14934
+ container.remove();
14935
+ return false;
14936
+ }
14210
14937
  const tracker = getSectionsTracker();
14211
14938
  let sections = [];
14212
14939
  try {
@@ -14214,10 +14941,12 @@ function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId,
14214
14941
  } catch {
14215
14942
  }
14216
14943
  const inEditor = typeof window !== "undefined" && window.self !== window.top;
14217
- if (inEditor && !sections.find((s) => s.type === "scheduling" && s.insertAfter === effectiveInsertAfter)) {
14944
+ if (inEditor && !sections.find((s) => s.type === "scheduling" && s.insertAfter === widgetId)) {
14218
14945
  sections.push({
14219
14946
  type: "scheduling",
14220
- insertAfter: effectiveInsertAfter,
14947
+ insertAfter: widgetId,
14948
+ anchorId,
14949
+ beforeId: beforeId ?? null,
14221
14950
  pagePath: window.location.pathname,
14222
14951
  ...scheduleId ? { scheduleId } : {}
14223
14952
  });
@@ -14231,7 +14960,8 @@ function mountSchedulingEntries(entries, notifyOnConnect = false) {
14231
14960
  for (let i = pending.length - 1; i >= 0; i--) {
14232
14961
  const entry = pending[i];
14233
14962
  const shouldNotify = typeof notifyOnConnect === "function" ? notifyOnConnect(entry) : notifyOnConnect;
14234
- if (mountSchedulingWidget(entry.insertAfter, shouldNotify, entry.scheduleId)) {
14963
+ const { anchorId, beforeId } = resolveEntryAnchor(entry);
14964
+ if (mountSchedulingWidget(anchorId, shouldNotify, entry.scheduleId ?? null, beforeId, entry.insertAfter)) {
14235
14965
  pending.splice(i, 1);
14236
14966
  }
14237
14967
  }
@@ -14389,6 +15119,11 @@ function applyLinkByKey(key, val) {
14389
15119
  hrefAnchors.forEach((el) => applyLinkHref(el, val));
14390
15120
  }
14391
15121
  }
15122
+ function isInsideLinkEditor(target) {
15123
+ return Boolean(
15124
+ 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"]')
15125
+ );
15126
+ }
14392
15127
  function isInsideFloatingPanel(target) {
14393
15128
  return Boolean(target.closest("[data-ohw-floating-panel]"));
14394
15129
  }
@@ -14396,11 +15131,6 @@ function isPointOverFloatingPanel(clientX, clientY) {
14396
15131
  const el = document.elementFromPoint(clientX, clientY);
14397
15132
  return Boolean(el instanceof Element && el.closest("[data-ohw-floating-panel]"));
14398
15133
  }
14399
- function isInsideLinkEditor(target) {
14400
- return Boolean(
14401
- 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"]')
14402
- );
14403
- }
14404
15134
  function getHrefKeyFromElement(el) {
14405
15135
  if (!el) return null;
14406
15136
  const anchor = el.closest("[data-ohw-href-key]");
@@ -14659,7 +15389,7 @@ function getNavigationSelectionParent(el) {
14659
15389
  if (!isFooterLinksContainer(el) && (el.hasAttribute("data-ohw-footer-col") || el.hasAttribute("data-ohw-footer-column") || isInferredFooterGroup2(el))) {
14660
15390
  return getFooterLinksContainer();
14661
15391
  }
14662
- if (el.hasAttribute("data-ohw-nav-container") || el.hasAttribute("data-ohw-nav-drawer") || isFooterLinksContainer(el)) {
15392
+ 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)) {
14663
15393
  return getNavigationRoot(el);
14664
15394
  }
14665
15395
  return null;
@@ -14874,7 +15604,6 @@ var ICONS = {
14874
15604
  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"/>',
14875
15605
  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"/>'
14876
15606
  };
14877
- var HEIGHT_SETTLE_DELAYS = [150, 400, 800];
14878
15607
  var SELECTION_CHROME_GAP2 = 4;
14879
15608
  var TOOLBAR_STROKE_GAP2 = 4;
14880
15609
  var TOOLBAR_OFFSET_FROM_ELEMENT = SELECTION_CHROME_GAP2 + TOOLBAR_STROKE_GAP2;
@@ -15254,6 +15983,7 @@ function StateToggle({
15254
15983
  );
15255
15984
  }
15256
15985
  var contentCache = /* @__PURE__ */ new Map();
15986
+ var fetchedContentPaths = /* @__PURE__ */ new Set();
15257
15987
  var OHW_LOADER_STYLE = {
15258
15988
  position: "fixed",
15259
15989
  inset: 0,
@@ -15372,6 +16102,70 @@ function OhhwellsBridge() {
15372
16102
  const hoveredImageHasTextOverlapRef = useRef10(false);
15373
16103
  const dragOverElRef = useRef10(null);
15374
16104
  const [mediaHover, setMediaHover] = useState13(null);
16105
+ const [selectedMedia, setSelectedMedia] = useState13(null);
16106
+ const selectedMediaElRef = useRef10(null);
16107
+ const clearMediaSelection = useCallback8(() => {
16108
+ const prev = selectedMediaElRef.current;
16109
+ selectedMediaElRef.current = null;
16110
+ setSelectedMedia(null);
16111
+ const sectionEl = prev?.closest("[data-ohw-section]") ?? null;
16112
+ if (sectionEl) {
16113
+ postToParentRef.current({
16114
+ type: "ow:section-selected",
16115
+ sectionId: sectionEl.dataset.ohwSection ?? null,
16116
+ sectionLabel: sectionEl.dataset.ohwSectionLabel ?? titleCaseSectionId(sectionEl.dataset.ohwSection ?? ""),
16117
+ key: null
16118
+ });
16119
+ }
16120
+ }, []);
16121
+ const clearMediaSelectionRef = useRef10(clearMediaSelection);
16122
+ clearMediaSelectionRef.current = clearMediaSelection;
16123
+ const selectMediaElement = useCallback8((el) => {
16124
+ const r2 = el.getBoundingClientRect();
16125
+ const video = el.dataset.ohwEditable === "video" ? el.querySelector("video") : null;
16126
+ selectedMediaElRef.current = el;
16127
+ setSelectedMedia({
16128
+ key: el.dataset.ohwKey ?? "",
16129
+ rect: { top: r2.top, left: r2.left, width: r2.width, height: r2.height },
16130
+ elementType: el.dataset.ohwEditable ?? "image",
16131
+ hasTextOverlap: false,
16132
+ isDragOver: false,
16133
+ ...video ? { videoAutoplay: video.autoplay, videoMuted: video.muted } : {}
16134
+ });
16135
+ const sectionEl = el.closest("[data-ohw-section]");
16136
+ aiSectionApiRef.current?.selectFromElement(el, { report: false });
16137
+ postToParentRef.current({
16138
+ type: "ow:section-selected",
16139
+ sectionId: sectionEl?.dataset.ohwSection ?? null,
16140
+ sectionLabel: sectionEl ? sectionEl.dataset.ohwSectionLabel ?? titleCaseSectionId(sectionEl.dataset.ohwSection ?? "") : null,
16141
+ key: el.dataset.ohwKey ?? null,
16142
+ // Display name for the pill — the raw key prettifies into fragments ("Img"); the
16143
+ // bridge knows what the node IS, so it names it.
16144
+ keyLabel: el.dataset.ohwEditable === "video" ? "Video" : el.dataset.ohwEditable === "bg-image" ? "Background" : "Image"
16145
+ });
16146
+ }, []);
16147
+ const selectMediaElementRef = useRef10(selectMediaElement);
16148
+ selectMediaElementRef.current = selectMediaElement;
16149
+ useEffect13(() => {
16150
+ if (!selectedMedia) return;
16151
+ const update = () => {
16152
+ const el = selectedMediaElRef.current;
16153
+ if (!el || !el.isConnected) {
16154
+ clearMediaSelection();
16155
+ return;
16156
+ }
16157
+ const r2 = el.getBoundingClientRect();
16158
+ setSelectedMedia(
16159
+ (prev) => prev ? { ...prev, rect: { top: r2.top, left: r2.left, width: r2.width, height: r2.height } } : prev
16160
+ );
16161
+ };
16162
+ window.addEventListener("scroll", update, true);
16163
+ window.addEventListener("resize", update);
16164
+ return () => {
16165
+ window.removeEventListener("scroll", update, true);
16166
+ window.removeEventListener("resize", update);
16167
+ };
16168
+ }, [selectedMedia !== null]);
15375
16169
  const [carouselHover, setCarouselHover] = useState13(null);
15376
16170
  const [uploadingRects, setUploadingRects] = useState13({});
15377
16171
  const hoveredGapRef = useRef10(null);
@@ -15634,13 +16428,6 @@ function OhhwellsBridge() {
15634
16428
  const [isItemDragging, setIsItemDragging] = useState13(false);
15635
16429
  const [isFooterFrameSelection, setIsFooterFrameSelection] = useState13(false);
15636
16430
  isFooterFrameSelectionRef.current = isFooterFrameSelection;
15637
- const [floatingPanel, setFloatingPanel] = useState13(null);
15638
- const floatingPanelOpenRef = useRef10(false);
15639
- floatingPanelOpenRef.current = floatingPanel !== null;
15640
- const [floatingPanelPos, setFloatingPanelPos] = useState13(null);
15641
- const [logoSizeDraft, setLogoSizeDraft] = useState13(null);
15642
- const [editorViewport, setEditorViewport] = useState13("desktop");
15643
- const [parentScrollSnap, setParentScrollSnap] = useState13(null);
15644
16431
  const [navDropdownPreviewOpen, setNavDropdownPreviewOpen] = useState13(null);
15645
16432
  const [footerHeadingVisible, setFooterHeadingVisible] = useState13(null);
15646
16433
  const footerDragRef = useRef10(null);
@@ -15655,7 +16442,16 @@ function OhhwellsBridge() {
15655
16442
  const addNavAfterAnchorRef = useRef10(null);
15656
16443
  const editContentRef = useRef10({});
15657
16444
  const aiSectionsRef = useRef10("");
16445
+ const brandKitRef = useRef10("");
16446
+ const stylesRef = useRef10("");
15658
16447
  const pendingDeleteUndoRef = useRef10(null);
16448
+ const [floatingPanel, setFloatingPanel] = useState13(null);
16449
+ const floatingPanelOpenRef = useRef10(false);
16450
+ const setFloatingPanelRef = useRef10(setFloatingPanel);
16451
+ const [floatingPanelPos, setFloatingPanelPos] = useState13(null);
16452
+ const [logoSizeDraft, setLogoSizeDraft] = useState13(null);
16453
+ const [editorViewport, setEditorViewport] = useState13("desktop");
16454
+ const [parentScrollSnap, setParentScrollSnap] = useState13(null);
15659
16455
  const [sitePages, setSitePages] = useState13([]);
15660
16456
  const [sectionsByPath, setSectionsByPath] = useState13({});
15661
16457
  const sectionsPrefetchGenRef = useRef10(0);
@@ -15664,7 +16460,18 @@ function OhhwellsBridge() {
15664
16460
  const linkPopoverOpenRef = useRef10(false);
15665
16461
  const linkPopoverGraceUntilRef = useRef10(0);
15666
16462
  setLinkPopoverRef.current = setLinkPopover;
16463
+ setFloatingPanelRef.current = setFloatingPanel;
15667
16464
  linkPopoverSessionRef.current = linkPopover;
16465
+ floatingPanelOpenRef.current = Boolean(floatingPanel);
16466
+ useEffect13(() => {
16467
+ const syncViewport = () => {
16468
+ const next = window.innerWidth <= 480 ? "mobile" : "desktop";
16469
+ setEditorViewport((prev) => prev === next ? prev : next);
16470
+ };
16471
+ syncViewport();
16472
+ window.addEventListener("resize", syncViewport);
16473
+ return () => window.removeEventListener("resize", syncViewport);
16474
+ }, []);
15668
16475
  const {
15669
16476
  navDragRef,
15670
16477
  navDropSlots,
@@ -16975,15 +17782,31 @@ function OhhwellsBridge() {
16975
17782
  }
16976
17783
  const applyContent = (content) => {
16977
17784
  const imageLoads = [];
17785
+ if (typeof content[BRAND_KIT_KEY] === "string") {
17786
+ brandKitRef.current = content[BRAND_KIT_KEY];
17787
+ applyBrandToDom(parseBrandKit(content[BRAND_KIT_KEY]));
17788
+ } else {
17789
+ brandKitRef.current = "";
17790
+ applyBrandToDom(null);
17791
+ }
16978
17792
  if (typeof content[AI_SECTIONS_KEY] === "string") {
16979
17793
  aiSectionsRef.current = content[AI_SECTIONS_KEY];
17794
+ setAiSectionOrder(content[SECTION_ORDER_KEY], window.location.pathname);
16980
17795
  applyAiSectionsToDom(parseAiSectionsState(content[AI_SECTIONS_KEY]));
16981
17796
  }
17797
+ if (typeof content[STYLE_STORE_KEY] === "string") {
17798
+ stylesRef.current = content[STYLE_STORE_KEY];
17799
+ applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
17800
+ }
17801
+ applyBrandChrome(content);
16982
17802
  for (const [key, val] of Object.entries(content)) {
16983
17803
  if (key === "__ohw_sections") continue;
16984
17804
  if (key === AI_SECTIONS_KEY) continue;
16985
17805
  if (key === LOGO_PLACEHOLDER_KEY) continue;
16986
17806
  if (LOGO_IMAGE_KEYS.includes(key)) continue;
17807
+ if (key === BRAND_KIT_KEY) continue;
17808
+ if (key === STYLE_STORE_KEY) continue;
17809
+ if (BRAND_CHROME_KEYS.has(key)) continue;
16987
17810
  if (applyVideoSettingNode(key, val)) continue;
16988
17811
  if (applyCarouselNode(key, val)) continue;
16989
17812
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
@@ -17049,7 +17872,9 @@ function OhhwellsBridge() {
17049
17872
  let cancelled = false;
17050
17873
  setFetchState("loading");
17051
17874
  const apiUrl = process.env.NEXT_PUBLIC_FLOWOPS_API_URL ?? "https://flowops-backend-staging-2yfdh7pwpq-as.a.run.app";
17052
- fetch(`${apiUrl}/api/public/sites/${subdomain}/content`, { cache: "no-store" }).then((r2) => r2.ok ? r2.json() : null).then((data) => {
17875
+ const initialPath = pathname;
17876
+ fetchedContentPaths.add(`${subdomain}::${initialPath}`);
17877
+ fetch(`${apiUrl}/api/public/sites/${subdomain}/content?path=${encodeURIComponent(initialPath)}`, { cache: "no-store" }).then((r2) => r2.ok ? r2.json() : null).then((data) => {
17053
17878
  if (cancelled) return;
17054
17879
  const content = data?.content ?? {};
17055
17880
  contentCache.set(subdomain, content);
@@ -17169,10 +17994,28 @@ function OhhwellsBridge() {
17169
17994
  initSectionInstancesFromContent(content, window.location.pathname);
17170
17995
  observer?.disconnect();
17171
17996
  try {
17997
+ applyBrandChrome(content);
17998
+ if (typeof content[BRAND_KIT_KEY] === "string") {
17999
+ applyBrandToDom(parseBrandKit(content[BRAND_KIT_KEY]));
18000
+ } else {
18001
+ applyBrandToDom(null);
18002
+ }
18003
+ if (typeof content[AI_SECTIONS_KEY] === "string") {
18004
+ setAiSectionOrder(content[SECTION_ORDER_KEY], window.location.pathname);
18005
+ applyAiSectionsToDom(parseAiSectionsState(content[AI_SECTIONS_KEY]));
18006
+ }
18007
+ if (typeof content[STYLE_STORE_KEY] === "string") {
18008
+ applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
18009
+ }
17172
18010
  for (const [key, val] of Object.entries(content)) {
17173
18011
  if (key === "__ohw_sections") continue;
18012
+ if (key === AI_SECTIONS_KEY) continue;
17174
18013
  if (key === LOGO_PLACEHOLDER_KEY) continue;
17175
18014
  if (LOGO_IMAGE_KEYS.includes(key)) continue;
18015
+ if (key === BRAND_KIT_KEY) continue;
18016
+ if (key === STYLE_STORE_KEY) continue;
18017
+ if (key === STYLE_STORE_KEY) continue;
18018
+ if (BRAND_CHROME_KEYS.has(key)) continue;
17176
18019
  if (applyVideoSettingNode(key, val)) continue;
17177
18020
  if (applyCarouselNode(key, val)) continue;
17178
18021
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
@@ -17218,6 +18061,17 @@ function OhhwellsBridge() {
17218
18061
  debounceTimer = setTimeout(applyFromCache, 150);
17219
18062
  };
17220
18063
  applyFromCache();
18064
+ const pathCacheKey = `${subdomain}::${pathname}`;
18065
+ if (!fetchedContentPaths.has(pathCacheKey)) {
18066
+ fetchedContentPaths.add(pathCacheKey);
18067
+ const apiUrl = process.env.NEXT_PUBLIC_FLOWOPS_API_URL ?? "https://flowops-backend-staging-2yfdh7pwpq-as.a.run.app";
18068
+ fetch(`${apiUrl}/api/public/sites/${subdomain}/content?path=${encodeURIComponent(pathname)}`, { cache: "no-store" }).then((r2) => r2.ok ? r2.json() : null).then((data) => {
18069
+ if (!data?.content) return;
18070
+ contentCache.set(subdomain, data.content);
18071
+ applyFromCache();
18072
+ }).catch(() => {
18073
+ });
18074
+ }
17221
18075
  observer = new MutationObserver(scheduleApply);
17222
18076
  observer.observe(document.body, { childList: true, subtree: true });
17223
18077
  return () => {
@@ -17313,30 +18167,31 @@ function OhhwellsBridge() {
17313
18167
  }, [isEditMode, pathname, subdomain, fetchState, resyncSelectedNavigationItem]);
17314
18168
  useEffect13(() => {
17315
18169
  if (!isEditMode) return;
18170
+ let lastPosted = 0;
17316
18171
  const measure = () => {
17317
18172
  const h = document.body.scrollHeight;
17318
- if (h > 50) postToParent2({ type: "ow:height", height: h });
18173
+ if (h > 50 && Math.abs(h - lastPosted) > 1) {
18174
+ lastPosted = h;
18175
+ postToParent2({ type: "ow:height", height: h });
18176
+ }
18177
+ };
18178
+ let raf = null;
18179
+ const schedule = () => {
18180
+ if (raf != null) return;
18181
+ raf = requestAnimationFrame(() => {
18182
+ raf = null;
18183
+ measure();
18184
+ });
17319
18185
  };
17320
18186
  const t1 = setTimeout(measure, 50);
17321
18187
  const t2 = setTimeout(measure, 500);
17322
- let lastWidth = window.innerWidth;
17323
- let resizeTimers = [];
17324
- const clearResizeTimers = () => {
17325
- resizeTimers.forEach(clearTimeout);
17326
- resizeTimers = [];
17327
- };
17328
- const handleResize = () => {
17329
- if (window.innerWidth === lastWidth) return;
17330
- lastWidth = window.innerWidth;
17331
- clearResizeTimers();
17332
- resizeTimers = HEIGHT_SETTLE_DELAYS.map((delay) => setTimeout(measure, delay));
17333
- };
17334
- window.addEventListener("resize", handleResize);
18188
+ const ro = new ResizeObserver(schedule);
18189
+ ro.observe(document.body);
17335
18190
  return () => {
17336
18191
  clearTimeout(t1);
17337
18192
  clearTimeout(t2);
17338
- clearResizeTimers();
17339
- window.removeEventListener("resize", handleResize);
18193
+ if (raf != null) cancelAnimationFrame(raf);
18194
+ ro.disconnect();
17340
18195
  };
17341
18196
  }, [pathname, isEditMode, postToParent2]);
17342
18197
  useEffect13(() => {
@@ -17577,6 +18432,7 @@ function OhhwellsBridge() {
17577
18432
  return;
17578
18433
  }
17579
18434
  const target = e.target;
18435
+ if (target.closest("[data-ohw-ai-review]")) return;
17580
18436
  if (target.closest("[data-ohw-toolbar]")) return;
17581
18437
  if (target.closest("[data-ohw-state-toggle]")) return;
17582
18438
  if (target.closest("[data-ohw-max-badge]")) return;
@@ -17588,6 +18444,9 @@ function OhhwellsBridge() {
17588
18444
  )) {
17589
18445
  return;
17590
18446
  }
18447
+ if (selectedMediaElRef.current && !selectedMediaElRef.current.contains(target) && !target.closest("[data-ohw-media-overlay], [data-ohw-edit-chrome], [data-ohw-item-interaction]")) {
18448
+ clearMediaSelectionRef.current();
18449
+ }
17591
18450
  {
17592
18451
  const formEl = getFormElement(target);
17593
18452
  const onSuccessText = target.closest(`[${SUCCESS_TEXT_ATTR}]`);
@@ -17739,19 +18598,14 @@ function OhhwellsBridge() {
17739
18598
  }
17740
18599
  const clickedButton = findClosestButtonLike(target);
17741
18600
  const buttonOnMedia = Boolean(clickedButton && isMediaEditable(editable) && editable.contains(clickedButton));
17742
- console.log("[click-debug]", {
17743
- editableType: editable.dataset.ohwEditable,
17744
- editableTag: editable.tagName,
17745
- targetTag: target.tagName,
17746
- clickedButtonTag: clickedButton?.tagName ?? null,
17747
- buttonOnMedia,
17748
- isMediaEditableEditable: isMediaEditable(editable)
17749
- });
17750
18601
  if (isMediaEditable(editable) && !buttonOnMedia) {
17751
18602
  e.preventDefault();
17752
18603
  e.stopPropagation();
17753
- aiSectionApiRef.current?.selectFromElement(editable);
17754
- postToParentRef.current({ type: "ow:image-pick", key: editable.dataset.ohwKey ?? "", elementType: editable.dataset.ohwEditable ?? "image" });
18604
+ if (selectedMediaElRef.current === editable) {
18605
+ postToParentRef.current({ type: "ow:image-pick", key: editable.dataset.ohwKey ?? "", elementType: editable.dataset.ohwEditable ?? "image" });
18606
+ } else {
18607
+ selectMediaElementRef.current(editable);
18608
+ }
17755
18609
  return;
17756
18610
  }
17757
18611
  const socialItem = getSocialItem(editable);
@@ -17770,11 +18624,6 @@ function OhhwellsBridge() {
17770
18624
  const hrefLookupTarget = buttonOnMedia ? clickedButton : editable;
17771
18625
  const hrefCtx = getHrefKeyFromElement(hrefLookupTarget);
17772
18626
  const navAnchor = hrefCtx ? getNavigationItemAnchor(hrefCtx.anchor) : null;
17773
- console.log("[click-debug 2]", {
17774
- hrefLookupTargetTag: hrefLookupTarget.tagName,
17775
- hrefCtx: hrefCtx ? { key: hrefCtx.key } : null,
17776
- navAnchorTag: navAnchor?.tagName ?? null
17777
- });
17778
18627
  if (navAnchor) {
17779
18628
  e.preventDefault();
17780
18629
  e.stopPropagation();
@@ -17892,6 +18741,7 @@ function OhhwellsBridge() {
17892
18741
  };
17893
18742
  const handleDblClick = (e) => {
17894
18743
  const target = e.target;
18744
+ if (target.closest("[data-ohw-ai-review]")) return;
17895
18745
  if (target.closest("[data-ohw-toolbar]")) return;
17896
18746
  if (target.closest("[data-ohw-state-toggle]")) return;
17897
18747
  if (target.closest("[data-ohw-max-badge]")) return;
@@ -17943,6 +18793,9 @@ function OhhwellsBridge() {
17943
18793
  setHoveredItemRect(null);
17944
18794
  hoveredNavContainerRef.current = null;
17945
18795
  setHoveredNavContainerRect(null);
18796
+ siblingHintElRef.current = null;
18797
+ setSiblingHintRect(null);
18798
+ setSiblingHintRects([]);
17946
18799
  return;
17947
18800
  }
17948
18801
  {
@@ -18061,7 +18914,6 @@ function OhhwellsBridge() {
18061
18914
  hoveredNavContainerRef.current = null;
18062
18915
  setHoveredNavContainerRect(null);
18063
18916
  hoveredItemElRef.current = editable;
18064
- setHoveredItemRect(isSelectedForHoverRef.current(editable) ? null : editable.getBoundingClientRect());
18065
18917
  }
18066
18918
  }
18067
18919
  }
@@ -18358,7 +19210,7 @@ function OhhwellsBridge() {
18358
19210
  }
18359
19211
  };
18360
19212
  const probeImageAt = (clientX, clientY, isDragOver = false, fromParentViewport = false) => {
18361
- if (linkPopoverOpenRef.current) {
19213
+ if (linkPopoverOpenRef.current || floatingPanelOpenRef.current && isPointOverFloatingPanel(clientX, clientY)) {
18362
19214
  if (hoveredImageRef.current) {
18363
19215
  hoveredImageRef.current = null;
18364
19216
  hoveredImageHasTextOverlapRef.current = false;
@@ -18692,7 +19544,9 @@ function OhhwellsBridge() {
18692
19544
  return;
18693
19545
  }
18694
19546
  const { y } = toProbeCoords(clientX, clientY, fromParentViewport);
18695
- const sections = Array.from(document.querySelectorAll("[data-ohw-section]")).sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top);
19547
+ const sections = Array.from(document.querySelectorAll("[data-ohw-section]")).filter(
19548
+ (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
19549
+ ).sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top);
18696
19550
  const ZONE = 20;
18697
19551
  for (let i = 0; i < sections.length; i++) {
18698
19552
  const a = sections[i];
@@ -18721,8 +19575,7 @@ function OhhwellsBridge() {
18721
19575
  };
18722
19576
  const handleMouseMove = (e) => {
18723
19577
  const { clientX, clientY } = e;
18724
- if (pointOwnedByFloatingPanel(clientX, clientY)) return;
18725
- if (isOverEditorChrome(clientX, clientY)) {
19578
+ if (document.documentElement.hasAttribute("data-ohw-panel-dragging") || floatingPanelOpenRef.current && isPointOverFloatingPanel(clientX, clientY) || isOverEditorChrome(clientX, clientY)) {
18726
19579
  document.querySelectorAll("[data-ohw-hovered]").forEach((el) => el.removeAttribute("data-ohw-hovered"));
18727
19580
  formHoverElRef.current = null;
18728
19581
  setFormHoverRect(null);
@@ -18730,6 +19583,12 @@ function OhhwellsBridge() {
18730
19583
  setHoveredItemRect(null);
18731
19584
  hoveredNavContainerRef.current = null;
18732
19585
  setHoveredNavContainerRect(null);
19586
+ siblingHintElRef.current = null;
19587
+ setSiblingHintRect(null);
19588
+ setSiblingHintRects([]);
19589
+ dismissImageHover();
19590
+ clearImageHover();
19591
+ setSectionGap(null);
18733
19592
  return;
18734
19593
  }
18735
19594
  if (probeSocialsRowAt(clientX, clientY)) return;
@@ -18741,7 +19600,11 @@ function OhhwellsBridge() {
18741
19600
  if (e.data?.type !== "ow:pointer-sync") return;
18742
19601
  const { clientX, clientY } = e.data;
18743
19602
  if (typeof clientX !== "number" || typeof clientY !== "number") return;
18744
- if (pointOwnedByFloatingPanel(clientX, clientY)) return;
19603
+ if (document.documentElement.hasAttribute("data-ohw-panel-dragging") || floatingPanelOpenRef.current && isPointOverFloatingPanel(clientX, clientY)) {
19604
+ dismissImageHover();
19605
+ clearImageHover();
19606
+ return;
19607
+ }
18745
19608
  if (probeSocialsRowAt(clientX, clientY)) return;
18746
19609
  probeSectionGapAt(clientX, clientY);
18747
19610
  probeImageAt(clientX, clientY);
@@ -19024,10 +19887,23 @@ function OhhwellsBridge() {
19024
19887
  if (e.data?.type !== "ow:hydrate") return;
19025
19888
  const content = e.data.content;
19026
19889
  if (!content) return;
19890
+ if (typeof content[BRAND_KIT_KEY] === "string") {
19891
+ brandKitRef.current = content[BRAND_KIT_KEY];
19892
+ applyBrandToDom(parseBrandKit(content[BRAND_KIT_KEY]));
19893
+ } else {
19894
+ brandKitRef.current = "";
19895
+ applyBrandToDom(null);
19896
+ }
19027
19897
  if (typeof content[AI_SECTIONS_KEY] === "string") {
19028
19898
  aiSectionsRef.current = content[AI_SECTIONS_KEY];
19899
+ setAiSectionOrder(content[SECTION_ORDER_KEY], window.location.pathname);
19029
19900
  applyAiSectionsToDom(parseAiSectionsState(content[AI_SECTIONS_KEY]));
19030
19901
  }
19902
+ if (typeof content[STYLE_STORE_KEY] === "string") {
19903
+ stylesRef.current = content[STYLE_STORE_KEY];
19904
+ applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
19905
+ }
19906
+ applyBrandChrome(content);
19031
19907
  let sectionsJson = null;
19032
19908
  for (const [key, val] of Object.entries(content)) {
19033
19909
  if (key === "__ohw_sections") {
@@ -19037,6 +19913,9 @@ function OhhwellsBridge() {
19037
19913
  if (key === AI_SECTIONS_KEY) continue;
19038
19914
  if (key === LOGO_PLACEHOLDER_KEY) continue;
19039
19915
  if (LOGO_IMAGE_KEYS.includes(key) && !String(val ?? "").trim()) continue;
19916
+ if (key === BRAND_KIT_KEY) continue;
19917
+ if (key === STYLE_STORE_KEY) continue;
19918
+ if (BRAND_CHROME_KEYS.has(key)) continue;
19040
19919
  if (applyVideoSettingNode(key, val)) continue;
19041
19920
  if (applyCarouselNode(key, val)) continue;
19042
19921
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
@@ -19050,6 +19929,8 @@ function OhhwellsBridge() {
19050
19929
  if (video && video.src !== val) applyVideoSrc(video, val);
19051
19930
  } else if (el.dataset.ohwEditable === "link") {
19052
19931
  applyLinkHref(el, val);
19932
+ } else if (el.dataset.ohwEditable === "icon") {
19933
+ applyIconMarkup(el, val);
19053
19934
  } else if (isIconMarkupValue(val)) {
19054
19935
  } else {
19055
19936
  el.innerHTML = val;
@@ -19134,12 +20015,21 @@ function OhhwellsBridge() {
19134
20015
  nodes: collectEditableNodes(editContentRef.current)
19135
20016
  });
19136
20017
  };
20018
+ const clearInteractionChrome = () => {
20019
+ deactivateRef.current();
20020
+ deselectRef.current();
20021
+ clearMediaSelectionRef.current();
20022
+ };
19137
20023
  const handleAiApplyTree = (e) => {
19138
20024
  if (e.data?.type !== "ow:ai-apply-tree") return;
19139
20025
  const payload = e.data.payload;
19140
20026
  if (!payload || typeof payload.id !== "string" || !isRenderableTree(payload.tree)) return;
20027
+ clearInteractionChrome();
19141
20028
  const previous = aiSectionsRef.current;
19142
- const nextState = applyTreeToState(parseAiSectionsState(previous), payload);
20029
+ const nextState = applyTreeToState(parseAiSectionsState(previous), {
20030
+ ...payload,
20031
+ path: payload.path ?? window.location.pathname
20032
+ });
19143
20033
  const nextValue = serializeAiSectionsState(nextState);
19144
20034
  aiSectionsRef.current = nextValue;
19145
20035
  applyAiSectionsToDom(nextState);
@@ -19160,6 +20050,7 @@ function OhhwellsBridge() {
19160
20050
  const exists = document.querySelector(`[data-ohw-section="${CSS.escape(sectionId)}"]`);
19161
20051
  if (!exists) return;
19162
20052
  if (isPageFrameSection(exists)) return;
20053
+ clearInteractionChrome();
19163
20054
  const previous = aiSectionsRef.current;
19164
20055
  const nextState = deleteSectionFromState(parseAiSectionsState(previous), sectionId);
19165
20056
  const nextValue = serializeAiSectionsState(nextState);
@@ -19175,8 +20066,10 @@ function OhhwellsBridge() {
19175
20066
  const handleAiSetSections = (e) => {
19176
20067
  if (e.data?.type !== "ow:ai-set-sections") return;
19177
20068
  const value = typeof e.data.value === "string" ? e.data.value : "";
20069
+ clearInteractionChrome();
19178
20070
  aiSectionsRef.current = value;
19179
20071
  applyAiSectionsToDom(parseAiSectionsState(value));
20072
+ applyStylesToDom(parseStyleStore(stylesRef.current));
19180
20073
  const restoredHeight = document.body.scrollHeight;
19181
20074
  if (restoredHeight > 50) postToParentRef.current({ type: "ow:height", height: restoredHeight });
19182
20075
  postToParentRef.current({ type: "ow:change", nodes: [{ key: AI_SECTIONS_KEY, text: value }] });
@@ -19192,10 +20085,40 @@ function OhhwellsBridge() {
19192
20085
  if (!entries) return;
19193
20086
  const orderJson = JSON.stringify(entries);
19194
20087
  editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: orderJson };
20088
+ setAiSectionOrder(orderJson, window.location.pathname);
19195
20089
  postToParentRef.current({ type: "ow:change", nodes: [{ key: SECTION_ORDER_KEY, text: orderJson }] });
19196
20090
  window.dispatchEvent(new Event("resize"));
19197
20091
  };
19198
20092
  window.addEventListener("message", handleMoveSection);
20093
+ const handleAiSetBrand = (e) => {
20094
+ if (e.data?.type !== "ow:ai-set-brand") return;
20095
+ const value = typeof e.data.value === "string" ? e.data.value : "";
20096
+ const previous = brandKitRef.current;
20097
+ brandKitRef.current = value;
20098
+ applyBrandToDom(parseBrandKit(value));
20099
+ if (aiSectionsRef.current) applyAiSectionsToDom(parseAiSectionsState(aiSectionsRef.current));
20100
+ applyStylesToDom(parseStyleStore(stylesRef.current));
20101
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: BRAND_KIT_KEY, text: value }] });
20102
+ postToParentRef.current({ type: "ow:ai-brand-applied", previous, value });
20103
+ };
20104
+ window.addEventListener("message", handleAiSetBrand);
20105
+ const handleAiSetStyles = (e) => {
20106
+ if (e.data?.type !== "ow:ai-set-styles") return;
20107
+ const value = typeof e.data.value === "string" ? e.data.value : "";
20108
+ const previous = stylesRef.current;
20109
+ stylesRef.current = value;
20110
+ applyStylesToDom(parseStyleStore(value));
20111
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: STYLE_STORE_KEY, text: value }] });
20112
+ postToParentRef.current({ type: "ow:ai-styles-applied", previous, value });
20113
+ };
20114
+ window.addEventListener("message", handleAiSetStyles);
20115
+ const handleGetBrand = (e) => {
20116
+ if (e.data?.type !== "ow:get-brand") return;
20117
+ const template = deriveTemplateBrand();
20118
+ const value = brandKitRef.current || (template ? JSON.stringify(template) : "");
20119
+ postToParentRef.current({ type: "ow:brand-value", value });
20120
+ };
20121
+ window.addEventListener("message", handleGetBrand);
19199
20122
  const handlePanelDragging = (e) => {
19200
20123
  if (e.data?.type !== "ow:panel-dragging") return;
19201
20124
  if (e.data.dragging) document.documentElement.setAttribute("data-ohw-panel-dragging", "");
@@ -19253,8 +20176,15 @@ function OhhwellsBridge() {
19253
20176
  closeLinkPopoverRef.current();
19254
20177
  return;
19255
20178
  }
20179
+ if (floatingPanelOpenRef.current) {
20180
+ setFloatingPanelRef.current(null);
20181
+ deselectRef.current();
20182
+ deactivateRef.current();
20183
+ return;
20184
+ }
19256
20185
  deselectRef.current();
19257
20186
  deactivateRef.current();
20187
+ clearMediaSelectionRef.current();
19258
20188
  };
19259
20189
  window.addEventListener("message", handleDeactivate);
19260
20190
  const handleToastAction = (e) => {
@@ -19340,6 +20270,10 @@ function OhhwellsBridge() {
19340
20270
  const handleKeyDown = (e) => {
19341
20271
  if (e.key === "Escape" && document.querySelector("[data-ohw-section-picker]")) return;
19342
20272
  if (e.key === "Escape" && document.querySelector("[data-ohw-more-menu]")) return;
20273
+ if (e.key === "Escape" && selectedMediaElRef.current) {
20274
+ clearMediaSelectionRef.current();
20275
+ return;
20276
+ }
19343
20277
  if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "a" && activeElRef.current) {
19344
20278
  e.preventDefault();
19345
20279
  selectAllTextInEditable(activeElRef.current);
@@ -19499,6 +20433,12 @@ function OhhwellsBridge() {
19499
20433
  if (aiSectionsRef.current) {
19500
20434
  nodes.push({ key: AI_SECTIONS_KEY, type: "sections", text: aiSectionsRef.current });
19501
20435
  }
20436
+ if (stylesRef.current) {
20437
+ nodes.push({ key: STYLE_STORE_KEY, type: "styles", text: stylesRef.current });
20438
+ }
20439
+ if (brandKitRef.current) {
20440
+ nodes.push({ key: BRAND_KIT_KEY, type: "brand", text: brandKitRef.current });
20441
+ }
19502
20442
  document.querySelectorAll('[data-ohw-editable="form"]').forEach((form) => {
19503
20443
  const formKey = formKeyOf(form);
19504
20444
  if (!formKey) return;
@@ -19516,8 +20456,12 @@ function OhhwellsBridge() {
19516
20456
  if (inserted) {
19517
20457
  const tracker = getSectionsTracker();
19518
20458
  postToParentRef.current({ type: "ow:change", nodes: [{ key: "__ohw_sections", text: tracker.textContent ?? "[]" }] });
19519
- const h = document.body.scrollHeight;
19520
- if (h > 50) postToParentRef.current({ type: "ow:height", height: h });
20459
+ const reportHeight = () => {
20460
+ const h = document.body.scrollHeight;
20461
+ if (h > 50) postToParentRef.current({ type: "ow:height", height: h });
20462
+ };
20463
+ reportHeight();
20464
+ setTimeout(reportHeight, 500);
19521
20465
  }
19522
20466
  };
19523
20467
  const handleSwitchSchedule = (e) => {
@@ -19914,13 +20858,16 @@ function OhhwellsBridge() {
19914
20858
  window.removeEventListener("message", handleAiDeleteSection);
19915
20859
  window.removeEventListener("message", handleAiSetSections);
19916
20860
  window.removeEventListener("message", handleMoveSection);
20861
+ window.removeEventListener("message", handleAiSetBrand);
20862
+ window.removeEventListener("message", handleAiSetStyles);
20863
+ window.removeEventListener("message", handleGetBrand);
19917
20864
  window.removeEventListener("message", handlePanelDragging);
19918
20865
  window.removeEventListener("message", handleDeleteSection);
19919
20866
  window.removeEventListener("message", handleDeactivate);
19920
- document.documentElement.removeAttribute("data-ohw-panel-dragging");
19921
20867
  window.removeEventListener("message", handleToastAction);
19922
20868
  window.removeEventListener("message", handleFormCount);
19923
20869
  window.removeEventListener("message", handleUiEscape);
20870
+ document.documentElement.removeAttribute("data-ohw-panel-dragging");
19924
20871
  autoSaveTimers.current.forEach(clearTimeout);
19925
20872
  autoSaveTimers.current.clear();
19926
20873
  if (imageUnhoverTimerRef.current) clearTimeout(imageUnhoverTimerRef.current);
@@ -20123,7 +21070,7 @@ function OhhwellsBridge() {
20123
21070
  postToParent2({
20124
21071
  type: "ow:ready",
20125
21072
  version: "1",
20126
- bridgeVersion: "0.1.77",
21073
+ bridgeVersion: "0.1.79",
20127
21074
  path: pathname,
20128
21075
  nodes: collectEditableNodes(editContentRef.current),
20129
21076
  sections
@@ -20530,11 +21477,22 @@ function OhhwellsBridge() {
20530
21477
  const showEditLink = toolbarShowEditLink;
20531
21478
  const currentSections = sectionsByPath[pathname] ?? [];
20532
21479
  linkPopoverOpenRef.current = linkPopover !== null;
21480
+ const handleMediaSelect = useCallback8((key) => {
21481
+ const el = hoveredImageRef.current?.dataset.ohwKey === key ? hoveredImageRef.current : Array.from(document.querySelectorAll(MEDIA_SELECTOR)).find(
21482
+ (m) => (m.dataset.ohwKey ?? "") === key
21483
+ ) ?? null;
21484
+ if (!el) return;
21485
+ selectMediaElementRef.current(el);
21486
+ }, []);
20533
21487
  const handleMediaReplace = useCallback8(
20534
21488
  (key) => {
20535
- postToParent2({ type: "ow:image-pick", key, elementType: mediaHover?.elementType ?? "image" });
21489
+ postToParent2({
21490
+ type: "ow:image-pick",
21491
+ key,
21492
+ elementType: mediaHover?.elementType ?? selectedMedia?.elementType ?? "image"
21493
+ });
20536
21494
  },
20537
- [postToParent2, mediaHover?.elementType]
21495
+ [postToParent2, mediaHover?.elementType, selectedMedia?.elementType]
20538
21496
  );
20539
21497
  const handleEditCarousel = useCallback8(
20540
21498
  (key) => {
@@ -20606,12 +21564,25 @@ function OhhwellsBridge() {
20606
21564
  },
20607
21565
  `uploading-${key}`
20608
21566
  )),
20609
- mediaHover && !(mediaHover.key in uploadingRects) && /* @__PURE__ */ jsx33(
21567
+ mediaHover && !(mediaHover.key in uploadingRects) && mediaHover.key !== selectedMedia?.key && /* @__PURE__ */ jsx33(
20610
21568
  MediaOverlay,
20611
21569
  {
20612
21570
  hover: mediaHover,
20613
21571
  isUploading: false,
20614
21572
  onReplace: handleMediaReplace,
21573
+ onSelect: handleMediaSelect,
21574
+ onVideoSettingsChange: handleVideoSettingsChange
21575
+ }
21576
+ ),
21577
+ selectedMedia && !(selectedMedia.key in uploadingRects) && !linkPopover && /* @__PURE__ */ jsx33(
21578
+ MediaOverlay,
21579
+ {
21580
+ hover: selectedMedia,
21581
+ selected: true,
21582
+ hovered: mediaHover?.key === selectedMedia.key,
21583
+ isUploading: false,
21584
+ onReplace: handleMediaReplace,
21585
+ onSelect: handleMediaSelect,
20615
21586
  onVideoSettingsChange: handleVideoSettingsChange
20616
21587
  }
20617
21588
  ),
@@ -21017,6 +21988,59 @@ function OhhwellsBridge() {
21017
21988
  ) : null
21018
21989
  ] });
21019
21990
  }
21991
+
21992
+ // src/ui/EmptySection.tsx
21993
+ import Link3 from "next/link";
21994
+ import { Fragment as Fragment9, jsx as jsx34, jsxs as jsxs21 } from "react/jsx-runtime";
21995
+ function EmptySection({ title, homeHref = "/", eyebrowKey, titleKey, subtitleKey }) {
21996
+ return /* @__PURE__ */ jsxs21(Fragment9, { children: [
21997
+ /* @__PURE__ */ jsx34(
21998
+ "p",
21999
+ {
22000
+ style: {
22001
+ fontFamily: "var(--brand-font-body)",
22002
+ fontSize: "0.75rem",
22003
+ fontWeight: 500,
22004
+ letterSpacing: "0.15em",
22005
+ textTransform: "uppercase",
22006
+ color: "var(--brand-accent)",
22007
+ marginBottom: "1.5rem"
22008
+ },
22009
+ children: /* @__PURE__ */ jsx34(Link3, { href: homeHref, style: { color: "inherit" }, children: /* @__PURE__ */ jsx34("span", { ...eyebrowKey ? { "data-ohw-editable": "text", "data-ohw-key": eyebrowKey } : {}, children: "Home" }) })
22010
+ }
22011
+ ),
22012
+ /* @__PURE__ */ jsx34(
22013
+ "h1",
22014
+ {
22015
+ style: {
22016
+ fontFamily: "var(--brand-font-heading)",
22017
+ fontSize: "clamp(2rem, 3.5vw, 2.75rem)",
22018
+ lineHeight: 1.1,
22019
+ letterSpacing: "-0.025em",
22020
+ color: "var(--brand-text)",
22021
+ marginBottom: "1rem"
22022
+ },
22023
+ ...titleKey ? { "data-ohw-editable": "text", "data-ohw-key": titleKey, "data-ohw-max-length": 80 } : {},
22024
+ children: title
22025
+ }
22026
+ ),
22027
+ /* @__PURE__ */ jsx34(
22028
+ "p",
22029
+ {
22030
+ style: {
22031
+ fontFamily: "var(--brand-font-body)",
22032
+ fontSize: "1rem",
22033
+ lineHeight: 1.7,
22034
+ fontWeight: 300,
22035
+ color: "var(--brand-text-muted)",
22036
+ maxWidth: "340px"
22037
+ },
22038
+ ...subtitleKey ? { "data-ohw-editable": "text", "data-ohw-key": subtitleKey, "data-ohw-max-length": 160 } : {},
22039
+ children: "This page doesn't have any content yet."
22040
+ }
22041
+ )
22042
+ ] });
22043
+ }
21020
22044
  export {
21021
22045
  AI_DEFAULT_BRAND,
21022
22046
  AI_TREE_SCHEMA_VERSIONS,
@@ -21033,6 +22057,7 @@ export {
21033
22057
  DropdownMenuItem,
21034
22058
  DropdownMenuSeparator,
21035
22059
  DropdownMenuTrigger,
22060
+ EmptySection,
21036
22061
  ItemActionToolbar,
21037
22062
  ItemInteractionLayer,
21038
22063
  LinkEditorPanel,