@ohhwells/bridge 0.1.76 → 0.1.77-next.232

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