@ohhwells/bridge 0.1.78 → 0.1.79-next.241

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -83,7 +83,12 @@ function parseAiSectionsState(raw) {
83
83
  media: entry.media && typeof entry.media === "object" ? entry.media : {}
84
84
  }));
85
85
  const removed = Array.isArray(parsed.removed) ? parsed.removed.filter((id) => typeof id === "string" && id.length > 0) : [];
86
- return { v: 1, sections, ...removed.length ? { removed } : {} };
86
+ return {
87
+ v: 1,
88
+ sections,
89
+ ...removed.length ? { removed } : {},
90
+ ...parsed.hideTemplate === true ? { hideTemplate: true } : {}
91
+ };
87
92
  } catch {
88
93
  return EMPTY_AI_SECTIONS;
89
94
  }
@@ -96,6 +101,7 @@ function applyTreeToState(state, payload) {
96
101
  const entry = {
97
102
  id: payload.id,
98
103
  label: payload.label ?? "Generated section",
104
+ ...typeof payload.path === "string" && payload.path ? { path: payload.path } : {},
99
105
  afterSection: payload.mode === "insert" && !insertBefore ? payload.targetSectionId ?? null : null,
100
106
  ...insertBefore && payload.targetSectionId ? { beforeSection: payload.targetSectionId } : {},
101
107
  ...payload.mode === "replace" && payload.targetSectionId ? { replaces: payload.targetSectionId } : {},
@@ -118,6 +124,320 @@ function deleteSectionFromState(state, sectionId) {
118
124
  return { ...state, v: 1, sections: state.sections, removed: [...removed, sectionId] };
119
125
  }
120
126
 
127
+ // src/lib/brand-chrome.ts
128
+ var BRAND_NAME_KEY = "__ohw_brand_name";
129
+ var BRAND_TITLE_KEY = "__ohw_site_title";
130
+ var BRAND_FAVICON_LETTER_KEY = "__ohw_favicon_letter";
131
+ var BRAND_CHROME_KEYS = /* @__PURE__ */ new Set([
132
+ BRAND_NAME_KEY,
133
+ BRAND_TITLE_KEY,
134
+ BRAND_FAVICON_LETTER_KEY
135
+ ]);
136
+ function upsertMeta(selector, attr, token, value) {
137
+ let el = document.head.querySelector(selector);
138
+ if (!el) {
139
+ el = document.createElement("meta");
140
+ el.setAttribute(attr, token);
141
+ document.head.appendChild(el);
142
+ }
143
+ if (el.getAttribute("content") !== value) el.setAttribute("content", value);
144
+ }
145
+ function escapeXml(value) {
146
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
147
+ }
148
+ function applyLetterFavicon(letter) {
149
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="12" fill="#111827"/><text x="32" y="46" font-family="system-ui,-apple-system,Segoe UI,sans-serif" font-size="40" font-weight="700" text-anchor="middle" fill="#ffffff">${escapeXml(letter)}</text></svg>`;
150
+ const href = `data:image/svg+xml,${encodeURIComponent(svg)}`;
151
+ let link = document.head.querySelector('link[rel="icon"]');
152
+ if (!link) {
153
+ link = document.createElement("link");
154
+ link.rel = "icon";
155
+ document.head.appendChild(link);
156
+ }
157
+ link.type = "image/svg+xml";
158
+ if (link.href !== href) link.href = href;
159
+ }
160
+ function applyBrandChrome(content) {
161
+ const name = content[BRAND_NAME_KEY];
162
+ if (typeof name === "string" && name.length > 0) {
163
+ document.querySelectorAll("[data-ohw-wordmark]").forEach((el) => {
164
+ if (el.textContent !== name) el.textContent = name;
165
+ if (el.getAttribute("title") !== name) el.setAttribute("title", name);
166
+ });
167
+ }
168
+ const title = content[BRAND_TITLE_KEY];
169
+ if (typeof title === "string" && title.length > 0) {
170
+ if (document.title !== title) document.title = title;
171
+ upsertMeta('meta[property="og:title"]', "property", "og:title", title);
172
+ upsertMeta('meta[name="twitter:title"]', "name", "twitter:title", title);
173
+ }
174
+ const letter = content[BRAND_FAVICON_LETTER_KEY];
175
+ if (typeof letter === "string" && letter.length > 0) {
176
+ applyLetterFavicon(letter);
177
+ }
178
+ }
179
+
180
+ // src/lib/brand-kit.ts
181
+ var BRAND_KIT_KEY = "__ohw_brand";
182
+ var BRAND_VAR_PREFIX = "--ohw-brand-";
183
+ var BRAND_VAR_NAMES = ["primary", "accent", "light", "dark", "surface", "border", "muted"].map(
184
+ (role) => `${BRAND_VAR_PREFIX}${role}`
185
+ );
186
+ var FONT_VARS = {
187
+ heading: ["--font-heading", "--font-display", "--brand-font-heading"],
188
+ body: ["--font-body", "--brand-font-body"]
189
+ };
190
+ var BRAND_FONT_LINK_ID = "ohw-brand-fonts";
191
+ function brandColorVars(kit) {
192
+ const { dark, primary, accent, light } = kit.palette;
193
+ const mix = (a, aPct, b) => `color-mix(in srgb, ${a} ${aPct}%, ${b})`;
194
+ return {
195
+ [`${BRAND_VAR_PREFIX}primary`]: primary,
196
+ [`${BRAND_VAR_PREFIX}accent`]: accent,
197
+ [`${BRAND_VAR_PREFIX}light`]: light,
198
+ [`${BRAND_VAR_PREFIX}dark`]: dark,
199
+ [`${BRAND_VAR_PREFIX}surface`]: mix(light, 95, dark),
200
+ [`${BRAND_VAR_PREFIX}border`]: mix(light, 85, dark),
201
+ [`${BRAND_VAR_PREFIX}muted`]: mix(dark, 62, light)
202
+ };
203
+ }
204
+ function parseBrandKit(raw) {
205
+ if (!raw) return null;
206
+ try {
207
+ const parsed = JSON.parse(raw);
208
+ const p = parsed?.palette;
209
+ const f = parsed?.fonts;
210
+ if (!p || !f || typeof p.dark !== "string" || typeof p.primary !== "string" || typeof p.accent !== "string" || typeof p.light !== "string" || typeof f.heading !== "string" || typeof f.body !== "string") {
211
+ return null;
212
+ }
213
+ return {
214
+ palette: { dark: p.dark, primary: p.primary, accent: p.accent, light: p.light },
215
+ fonts: { heading: f.heading, body: f.body }
216
+ };
217
+ } catch {
218
+ return null;
219
+ }
220
+ }
221
+ function familyOf(stack) {
222
+ const first = stack.split(",")[0]?.trim() ?? "";
223
+ return first.replace(/^['"]|['"]$/g, "");
224
+ }
225
+ function loadBrandFonts(families) {
226
+ const unique = [...new Set(families.map((f) => f.trim()).filter(Boolean))];
227
+ if (unique.length === 0) return;
228
+ const spec = unique.map((f) => `${f.replace(/ /g, "+")}:400,500,600,700`).join("|");
229
+ const href = `https://fonts.googleapis.com/css?family=${spec}&display=swap`;
230
+ let link = document.getElementById(BRAND_FONT_LINK_ID);
231
+ if (!link) {
232
+ link = document.createElement("link");
233
+ link.id = BRAND_FONT_LINK_ID;
234
+ link.rel = "stylesheet";
235
+ document.head.appendChild(link);
236
+ }
237
+ if (link.href !== href) link.href = href;
238
+ }
239
+ function applyBrandToDom(kit) {
240
+ const root = document.documentElement;
241
+ if (!kit) {
242
+ for (const name of BRAND_VAR_NAMES) root.style.removeProperty(name);
243
+ for (const name of [...FONT_VARS.heading, ...FONT_VARS.body]) root.style.removeProperty(name);
244
+ document.getElementById(BRAND_FONT_LINK_ID)?.remove();
245
+ return;
246
+ }
247
+ for (const [name, value] of Object.entries(brandColorVars(kit))) root.style.setProperty(name, value);
248
+ for (const name of FONT_VARS.heading) root.style.setProperty(name, kit.fonts.heading);
249
+ for (const name of FONT_VARS.body) root.style.setProperty(name, kit.fonts.body);
250
+ loadBrandFonts([familyOf(kit.fonts.heading), familyOf(kit.fonts.body)]);
251
+ }
252
+
253
+ // src/lib/section-styles.ts
254
+ var STYLE_STORE_KEY = "__ohw_styles";
255
+ var STYLE_SHEET_ID = "ohw-section-styles";
256
+ function parseStyleStore(raw) {
257
+ if (!raw) return null;
258
+ try {
259
+ const parsed = JSON.parse(raw);
260
+ if (parsed?.v !== 1) return null;
261
+ return {
262
+ v: 1,
263
+ sections: typeof parsed.sections === "object" && parsed.sections ? parsed.sections : {},
264
+ nodes: typeof parsed.nodes === "object" && parsed.nodes ? parsed.nodes : {}
265
+ };
266
+ } catch {
267
+ return null;
268
+ }
269
+ }
270
+ var BG_VALUES = {
271
+ surface: "color-mix(in srgb, var(--ohw-brand-light, var(--color-light, #ECECEB)) 94%, var(--ohw-brand-dark, var(--color-dark, #0C0A09)))",
272
+ accent: "var(--ohw-brand-primary, var(--color-primary, #0078E5))",
273
+ "accent-soft": "color-mix(in srgb, var(--ohw-brand-primary, var(--color-primary, #0078E5)) 12%, var(--ohw-brand-light, var(--color-light, #FFFFFF)))"
274
+ };
275
+ var HEADLINE_SIZES = { md: 24, lg: 32, xl: 40, display: 56 };
276
+ function styleSheetCss() {
277
+ const rules = [];
278
+ for (const [tone, value] of Object.entries(BG_VALUES)) {
279
+ rules.push(`[data-ohw-style-bg="${tone}"] { background: ${value} !important; }`);
280
+ }
281
+ rules.push(
282
+ `[data-ohw-style-bg="accent"] { color: var(--ohw-brand-light, var(--color-light, #FFFFFF)) !important; }`
283
+ );
284
+ rules.push(
285
+ `[data-ohw-style-distribution="space-between"] { display: flex !important; flex-direction: column; justify-content: space-between; }`,
286
+ `[data-ohw-style-distribution="center"] { display: flex !important; flex-direction: column; justify-content: center; }`
287
+ );
288
+ for (const [scale, size] of Object.entries(HEADLINE_SIZES)) {
289
+ rules.push(
290
+ `[data-ohw-style-headline="${scale}"] :is(h1, h2, h3) { font-size: ${size}px !important; line-height: 1.15 !important; }`
291
+ );
292
+ }
293
+ rules.push(
294
+ `[data-ohw-style-aspect="fill-height"] img { height: 100% !important; aspect-ratio: auto !important; object-fit: cover; }`
295
+ );
296
+ for (const aspect of ["1:1", "4:5", "3:4", "3:2", "16:9", "2:1", "3:1"]) {
297
+ rules.push(
298
+ `[data-ohw-style-aspect="${aspect.replace(":", "-")}"] img { aspect-ratio: ${aspect.replace(":", " / ")} !important; height: auto !important; object-fit: cover; }`
299
+ );
300
+ }
301
+ const pad = { tight: 40, balanced: 64, airy: 96 };
302
+ for (const [spacing, px] of Object.entries(pad)) {
303
+ rules.push(
304
+ `[data-ohw-style-spacing="${spacing}"] { padding-top: ${px}px !important; padding-bottom: ${px}px !important; }`
305
+ );
306
+ }
307
+ return rules.join("\n");
308
+ }
309
+ var STYLE_FONT_LINK_ID = "ohw-style-fonts";
310
+ function loadStyleFonts(families) {
311
+ const unique = [...new Set(families.map((f) => f.trim()).filter(Boolean))];
312
+ const existing = document.getElementById(STYLE_FONT_LINK_ID);
313
+ if (unique.length === 0) {
314
+ existing?.remove();
315
+ return;
316
+ }
317
+ const spec = unique.map((f) => `${f.replace(/ /g, "+")}:400,500,600,700`).join("|");
318
+ const href = `https://fonts.googleapis.com/css?family=${spec}&display=swap`;
319
+ let link = existing;
320
+ if (!link) {
321
+ link = document.createElement("link");
322
+ link.id = STYLE_FONT_LINK_ID;
323
+ link.rel = "stylesheet";
324
+ document.head.appendChild(link);
325
+ }
326
+ if (link.href !== href) link.href = href;
327
+ }
328
+ var SECTION_ATTRS = {
329
+ sectionBackground: "data-ohw-style-bg",
330
+ textDistribution: "data-ohw-style-distribution",
331
+ headlineScale: "data-ohw-style-headline",
332
+ imageAspect: "data-ohw-style-aspect",
333
+ spacing: "data-ohw-style-spacing"
334
+ };
335
+ var NODE_WROTE_ATTR = "data-ohw-style-node";
336
+ var NODE_PROPS = ["color", "font-family", "font-size", "background"];
337
+ function saveInline(el, prop) {
338
+ const attr = `data-ohw-style-prev-${prop}`;
339
+ if (el.hasAttribute(attr)) return;
340
+ const value = el.style.getPropertyValue(prop) || (prop === "background" ? el.style.getPropertyValue("background-color") : "");
341
+ el.setAttribute(attr, value);
342
+ }
343
+ function restoreInline(el, prop) {
344
+ const attr = `data-ohw-style-prev-${prop}`;
345
+ if (!el.hasAttribute(attr)) return;
346
+ const prev = el.getAttribute(attr) ?? "";
347
+ if (prev) el.style.setProperty(prop, prev);
348
+ else el.style.removeProperty(prop);
349
+ el.removeAttribute(attr);
350
+ }
351
+ function ensureStyleSheet() {
352
+ let el = document.getElementById(STYLE_SHEET_ID);
353
+ if (!el) {
354
+ el = document.createElement("style");
355
+ el.id = STYLE_SHEET_ID;
356
+ document.head.appendChild(el);
357
+ }
358
+ const css = styleSheetCss();
359
+ if (el.textContent !== css) el.textContent = css;
360
+ }
361
+ function clearSectionAttrs(root) {
362
+ for (const attr of Object.values(SECTION_ATTRS)) {
363
+ for (const el of Array.from(root.querySelectorAll(`[${attr}]`))) el.removeAttribute(attr);
364
+ }
365
+ for (const el of Array.from(root.querySelectorAll("[data-ohw-style-bgcolor]"))) {
366
+ restoreInline(el, "background");
367
+ el.removeAttribute("data-ohw-style-bgcolor");
368
+ }
369
+ }
370
+ function clearNodeProps(root) {
371
+ for (const el of Array.from(root.querySelectorAll(`[${NODE_WROTE_ATTR}]`))) {
372
+ const h = el;
373
+ for (const prop of NODE_PROPS) restoreInline(h, prop);
374
+ h.removeAttribute(NODE_WROTE_ATTR);
375
+ }
376
+ }
377
+ function buttonSurfaceOf(el) {
378
+ return el.closest("a, button") ?? el;
379
+ }
380
+ function applyStylesToDom(store) {
381
+ ensureStyleSheet();
382
+ clearSectionAttrs(document);
383
+ clearNodeProps(document);
384
+ loadStyleFonts(
385
+ store ? Object.values(store.nodes).flatMap((n) => n.fontFamily ? [n.fontFamily] : []) : []
386
+ );
387
+ if (!store) return;
388
+ for (const [sectionId, override] of Object.entries(store.sections)) {
389
+ const sections = document.querySelectorAll(
390
+ `[data-ohw-section="${CSS.escape(sectionId)}"]`
391
+ );
392
+ for (const marker of Array.from(sections)) {
393
+ const section = marker.querySelector(":scope > [data-ai-section]") ?? marker;
394
+ for (const [prop, attr] of Object.entries(SECTION_ATTRS)) {
395
+ const value = override[prop];
396
+ if (value === void 0) continue;
397
+ if (prop === "sectionBackground" && override.sectionBackgroundColor !== void 0) continue;
398
+ section.setAttribute(attr, String(value).replace(":", "-"));
399
+ }
400
+ if (override.sectionBackgroundColor !== void 0) {
401
+ saveInline(section, "background");
402
+ section.style.setProperty("background", override.sectionBackgroundColor, "important");
403
+ section.setAttribute("data-ohw-style-bgcolor", "");
404
+ }
405
+ }
406
+ }
407
+ for (const [key, override] of Object.entries(store.nodes)) {
408
+ const nodes = document.querySelectorAll(`[data-ohw-key="${CSS.escape(key)}"]`);
409
+ for (const el of Array.from(nodes)) {
410
+ if (override.color !== void 0) {
411
+ saveInline(el, "color");
412
+ el.style.setProperty("color", override.color, "important");
413
+ el.setAttribute(NODE_WROTE_ATTR, "");
414
+ }
415
+ if (override.fontFamily !== void 0) {
416
+ saveInline(el, "font-family");
417
+ el.style.setProperty("font-family", `'${override.fontFamily}'`, "important");
418
+ el.setAttribute(NODE_WROTE_ATTR, "");
419
+ }
420
+ if (override.fontSize !== void 0) {
421
+ saveInline(el, "font-size");
422
+ el.style.setProperty("font-size", `${override.fontSize}px`, "important");
423
+ el.setAttribute(NODE_WROTE_ATTR, "");
424
+ }
425
+ if (override.buttonBackground !== void 0 || override.buttonText !== void 0) {
426
+ const surface = buttonSurfaceOf(el);
427
+ if (override.buttonBackground !== void 0) {
428
+ saveInline(surface, "background");
429
+ surface.style.setProperty("background", override.buttonBackground, "important");
430
+ }
431
+ if (override.buttonText !== void 0) {
432
+ saveInline(surface, "color");
433
+ surface.style.setProperty("color", override.buttonText, "important");
434
+ }
435
+ surface.setAttribute(NODE_WROTE_ATTR, "");
436
+ }
437
+ }
438
+ }
439
+ }
440
+
121
441
  // src/ui/ai-tree/aiSectionsManager.tsx
122
442
  import { flushSync } from "react-dom";
123
443
  import { createRoot } from "react-dom/client";
@@ -132,7 +452,8 @@ function lucideByName(name) {
132
452
  }
133
453
  var typeStyle = (spec, font) => ({
134
454
  fontFamily: font,
135
- fontSize: spec.size,
455
+ // Headings shrink with the viewport (reaching full size around ~900px wide); body copy stays put.
456
+ fontSize: spec.size >= 24 ? `clamp(${Math.max(18, Math.round(spec.size * 0.6))}px, ${(spec.size / 9).toFixed(2)}vw, ${spec.size}px)` : spec.size,
136
457
  lineHeight: spec.line,
137
458
  fontWeight: spec.weight
138
459
  });
@@ -141,12 +462,58 @@ var featureLines = (value) => value.split(/\n|<br\s*\/?>/i).map((line) => line.t
141
462
  var CHECK_MASK = `data:image/svg+xml,${encodeURIComponent(
142
463
  '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>'
143
464
  )}`;
465
+ var AI_MOBILE_CSS = [
466
+ "@media (max-width: 768px){",
467
+ "[data-ai-section]{overflow-x:hidden}",
468
+ "[data-ai-container]{padding:0 20px !important}",
469
+ "[data-ai-row]{display:flex !important;flex-direction:column !important;align-items:stretch !important}",
470
+ "[data-ai-cell]{width:100%;min-width:0}",
471
+ "[data-ai-grid]{grid-template-columns:1fr !important}",
472
+ // Group containers flatten to a column on phones; span placements come along for free.
473
+ "[data-ai-group]{display:flex !important;flex-direction:column !important}",
474
+ "[data-ai-group] > *{grid-column:auto !important}",
475
+ "[data-ai-section] img{max-width:100%}",
476
+ "}",
477
+ "@media (min-width: 769px) and (max-width: 1024px){",
478
+ "[data-ai-grid]{grid-template-columns:repeat(2, 1fr) !important}",
479
+ "}"
480
+ ].join("");
144
481
  var FEATURE_LINE_CSS = [
145
482
  "[data-ai-features]>div{position:relative;padding-left:40px;min-height:24px}",
146
483
  '[data-ai-features]>div::before{content:"";position:absolute;left:0;top:1px;width:24px;height:24px;',
147
484
  `background-color:currentColor;-webkit-mask:url("${CHECK_MASK}") no-repeat 0 0/24px 24px;`,
148
485
  `mask:url("${CHECK_MASK}") no-repeat 0 0/24px 24px}`
149
486
  ].join("");
487
+ function hexLuminance(color) {
488
+ const m = /^#([0-9a-f]{6})$/i.exec(color.trim());
489
+ if (!m) return null;
490
+ const [r2, g, b] = [0, 2, 4].map((i) => {
491
+ const c = parseInt(m[1].slice(i, i + 2), 16) / 255;
492
+ return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
493
+ });
494
+ return 0.2126 * r2 + 0.7152 * g + 0.0722 * b;
495
+ }
496
+ function hexContrast(a, b) {
497
+ const la = hexLuminance(a);
498
+ const lb = hexLuminance(b);
499
+ if (la === null || lb === null) return null;
500
+ const [hi, lo] = la > lb ? [la, lb] : [lb, la];
501
+ return (hi + 0.05) / (lo + 0.05);
502
+ }
503
+ function accentBandContext(brand) {
504
+ const p = brand.palette;
505
+ const lightWins = (hexContrast(p.light, p.primary) ?? 99) >= (hexContrast(p.dark, p.primary) ?? 0);
506
+ if (lightWins) {
507
+ return {
508
+ brand: { ...brand, palette: { dark: p.light, primary: p.light, accent: p.light, light: p.primary } },
509
+ buttonLabel: p.primary
510
+ };
511
+ }
512
+ return {
513
+ brand: { ...brand, palette: { dark: p.dark, primary: p.dark, accent: p.dark, light: p.light } },
514
+ buttonLabel: p.light
515
+ };
516
+ }
150
517
  function textAttrs(ctx, path) {
151
518
  return ctx.keyFor ? { "data-ohw-key": ctx.keyFor(path), "data-ohw-editable": "text" } : {};
152
519
  }
@@ -158,7 +525,12 @@ var AI_RESPONSIVE_CSS = [
158
525
  "@media (max-width: 640px) {",
159
526
  " [data-ai-responsive] [data-ai-grid] { grid-template-columns: 1fr !important; }",
160
527
  " [data-ai-responsive] [data-ai-columns] > * { grid-column: 1 / -1 !important; }",
528
+ // Group containers flatten to a column on phones; span placements come along for free.
529
+ " [data-ai-responsive] [data-ai-group] { display: flex !important; flex-direction: column !important; }",
530
+ " [data-ai-responsive] [data-ai-group] > * { grid-column: auto !important; }",
161
531
  " [data-ai-responsive] [data-ai-section-inner] { padding-left: 20px !important; padding-right: 20px !important; }",
532
+ " [data-ai-responsive] { overflow-x: hidden; }",
533
+ " [data-ai-responsive] img { max-width: 100%; }",
162
534
  "}"
163
535
  ].join("\n");
164
536
  var TRANSPARENT_PX = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
@@ -236,7 +608,7 @@ function ButtonEl({
236
608
  }) {
237
609
  const secondary = slots.variant === "secondary";
238
610
  const href = str(slots.href);
239
- const linkAttrs = ctx.keyFor && path ? { "data-ohw-href-key": ctx.keyFor(`${path}.href`) } : {};
611
+ const linkAttrs = ctx.keyFor && path ? { "data-ohw-href-key": ctx.keyFor(`${path}.href`), "data-ohw-role": "button" } : {};
240
612
  return /* @__PURE__ */ jsx(
241
613
  "a",
242
614
  {
@@ -252,7 +624,7 @@ function ButtonEl({
252
624
  textDecoration: "none",
253
625
  cursor: "pointer",
254
626
  ...typeStyle(AI_TREE_TOKENS.type.button, ctx.brand.fonts.body),
255
- ...secondary ? { border: `1px solid ${ctx.brand.palette.dark}`, color: ctx.brand.palette.dark } : { background: ctx.brand.palette.primary, color: AI_TREE_TOKENS.textPrimaryForeground }
627
+ ...secondary ? { border: `1px solid ${ctx.brand.palette.dark}`, color: ctx.brand.palette.dark } : { background: ctx.brand.palette.primary, color: ctx.buttonLabel ?? AI_TREE_TOKENS.textPrimaryForeground }
256
628
  },
257
629
  children: /* @__PURE__ */ jsx("span", { ...path ? textAttrs(ctx, `${path}.label`) : {}, children: str(slots.label) })
258
630
  }
@@ -758,7 +1130,24 @@ function CardBlock({ node, ctx, path }) {
758
1130
  minWidth: 0
759
1131
  },
760
1132
  children: [
761
- media && (horizontal ? /* @__PURE__ */ jsx("div", { style: { flex: 1, minWidth: 0, display: "flex", alignItems: "center", padding: mediaInset }, children: media }) : /* @__PURE__ */ jsx(
1133
+ media && (horizontal ? /* @__PURE__ */ jsx(
1134
+ "div",
1135
+ {
1136
+ style: (
1137
+ // An icon hugs its glyph — flex:1 gave a 48px icon half the card and pushed the
1138
+ // text to the far side. Photos keep the half-and-half split. The inset has no
1139
+ // inner padding (the photo split absorbed that), so the icon carries its own gap.
1140
+ /^(lucide|simple):/.test(mediaRef) ? {
1141
+ flexShrink: 0,
1142
+ display: "flex",
1143
+ alignItems: "center",
1144
+ padding: mediaInset,
1145
+ [mediaPosition === "right" ? "marginLeft" : "marginRight"]: 20
1146
+ } : { flex: 1, minWidth: 0, display: "flex", alignItems: "center", padding: mediaInset }
1147
+ ),
1148
+ children: media
1149
+ }
1150
+ ) : /* @__PURE__ */ jsx(
762
1151
  "div",
763
1152
  {
764
1153
  style: /^(lucide|simple):/.test(mediaRef) ? { padding: `${AI_TREE_TOKENS.paddingBlock}px ${AI_TREE_TOKENS.paddingBlock}px 0` } : hasBg ? void 0 : { borderRadius: AI_TREE_TOKENS.radiusCard, overflow: "hidden" },
@@ -849,13 +1238,44 @@ function AccordionBlock({ node, ctx, path }) {
849
1238
  ) })
850
1239
  ] }, i)) });
851
1240
  }
1241
+ function useIsMobile() {
1242
+ const [mobile, setMobile] = React.useState(
1243
+ () => typeof window !== "undefined" && window.matchMedia("(max-width: 768px)").matches
1244
+ );
1245
+ React.useEffect(() => {
1246
+ const mq = window.matchMedia("(max-width: 768px)");
1247
+ const update = () => setMobile(mq.matches);
1248
+ update();
1249
+ mq.addEventListener("change", update);
1250
+ return () => mq.removeEventListener("change", update);
1251
+ }, []);
1252
+ return mobile;
1253
+ }
852
1254
  function Carousel({ items, itemsPerRow, ctx }) {
1255
+ const isMobile = useIsMobile();
1256
+ const perPage = isMobile ? 1 : itemsPerRow;
1257
+ const pages = Math.max(1, Math.ceil(items.length / perPage));
853
1258
  const [page, setPage] = React.useState(0);
854
- const pages = Math.max(1, Math.ceil(items.length / itemsPerRow));
855
1259
  const current = Math.min(page, pages - 1);
1260
+ if (pages <= 1) {
1261
+ const cols = Math.max(1, Math.min(items.length, itemsPerRow));
1262
+ return /* @__PURE__ */ jsx(
1263
+ "div",
1264
+ {
1265
+ "data-ai-grid": String(cols),
1266
+ style: {
1267
+ display: "grid",
1268
+ gridTemplateColumns: `repeat(${cols}, 1fr)`,
1269
+ gap: AI_TREE_TOKENS.spacing8,
1270
+ alignItems: "start"
1271
+ },
1272
+ children: items
1273
+ }
1274
+ );
1275
+ }
856
1276
  const pageGroups = Array.from(
857
1277
  { length: pages },
858
- (_, p) => items.slice(p * itemsPerRow, (p + 1) * itemsPerRow)
1278
+ (_, p) => items.slice(p * perPage, (p + 1) * perPage)
859
1279
  );
860
1280
  const chrome = (enabled) => ({
861
1281
  border: `1px solid ${ctx.brand.palette.dark}`,
@@ -880,55 +1300,69 @@ function Carousel({ items, itemsPerRow, ctx }) {
880
1300
  cursor: "pointer",
881
1301
  padding: 0
882
1302
  });
883
- return /* @__PURE__ */ jsxs("div", { "data-ai-carousel": "", style: { display: "flex", flexDirection: "column", gap: AI_TREE_TOKENS.spacing8, alignItems: "center" }, children: [
884
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: AI_TREE_TOKENS.spacing6, width: "100%" }, children: [
885
- /* @__PURE__ */ jsx(
886
- "button",
887
- {
888
- type: "button",
889
- "aria-label": "Previous",
890
- onClick: () => setPage((p) => Math.max(0, p - 1)),
891
- style: chrome(current > 0),
892
- children: /* @__PURE__ */ jsx(ArrowLeft, { size: 24, strokeWidth: 1.5, "aria-hidden": true })
893
- }
894
- ),
895
- /* @__PURE__ */ jsx("div", { style: { flex: 1, minWidth: 0, overflow: "hidden" }, children: /* @__PURE__ */ jsx(
1303
+ const viewport = /* @__PURE__ */ jsx("div", { style: { flex: isMobile ? "0 0 auto" : 1, minWidth: 0, width: "100%", overflow: "hidden" }, children: /* @__PURE__ */ jsx(
1304
+ "div",
1305
+ {
1306
+ style: {
1307
+ display: "flex",
1308
+ transform: `translateX(-${current * 100}%)`,
1309
+ transition: "transform 0.4s ease"
1310
+ },
1311
+ children: pageGroups.map((group, p) => /* @__PURE__ */ jsx(
896
1312
  "div",
897
1313
  {
1314
+ "data-ai-grid": String(perPage),
898
1315
  style: {
899
- display: "flex",
900
- transform: `translateX(-${current * 100}%)`,
901
- transition: "transform 0.4s ease"
1316
+ flex: "0 0 100%",
1317
+ display: "grid",
1318
+ gridTemplateColumns: `repeat(${perPage}, 1fr)`,
1319
+ gap: AI_TREE_TOKENS.spacing8,
1320
+ alignItems: "start"
902
1321
  },
903
- children: pageGroups.map((group, p) => /* @__PURE__ */ jsx(
904
- "div",
905
- {
906
- "data-ai-grid": String(itemsPerRow),
907
- style: {
908
- flex: "0 0 100%",
909
- display: "grid",
910
- gridTemplateColumns: `repeat(${itemsPerRow}, 1fr)`,
911
- gap: AI_TREE_TOKENS.spacing8,
912
- alignItems: "start"
913
- },
914
- children: group
915
- },
916
- p
917
- ))
918
- }
919
- ) }),
920
- /* @__PURE__ */ jsx(
921
- "button",
922
- {
923
- type: "button",
924
- "aria-label": "Next",
925
- onClick: () => setPage((p) => Math.min(pages - 1, p + 1)),
926
- style: chrome(current < pages - 1),
927
- children: /* @__PURE__ */ jsx(ArrowRight, { size: 24, strokeWidth: 1.5, "aria-hidden": true })
928
- }
929
- )
1322
+ children: group
1323
+ },
1324
+ p
1325
+ ))
1326
+ }
1327
+ ) });
1328
+ const prevBtn = /* @__PURE__ */ jsx(
1329
+ "button",
1330
+ {
1331
+ type: "button",
1332
+ "aria-label": "Previous",
1333
+ onClick: () => setPage((p) => Math.max(0, p - 1)),
1334
+ style: chrome(current > 0),
1335
+ children: /* @__PURE__ */ jsx(ArrowLeft, { size: 24, strokeWidth: 1.5, "aria-hidden": true })
1336
+ }
1337
+ );
1338
+ const nextBtn = /* @__PURE__ */ jsx(
1339
+ "button",
1340
+ {
1341
+ type: "button",
1342
+ "aria-label": "Next",
1343
+ onClick: () => setPage((p) => Math.min(pages - 1, p + 1)),
1344
+ style: chrome(current < pages - 1),
1345
+ children: /* @__PURE__ */ jsx(ArrowRight, { size: 24, strokeWidth: 1.5, "aria-hidden": true })
1346
+ }
1347
+ );
1348
+ const dots = /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: 9, justifyContent: "center" }, children: pageGroups.map((_, p) => /* @__PURE__ */ jsx("button", { type: "button", "aria-label": `Page ${p + 1}`, onClick: () => setPage(p), style: dot(p === current) }, p)) });
1349
+ if (isMobile) {
1350
+ return /* @__PURE__ */ jsxs("div", { "data-ai-carousel": "", style: { display: "flex", flexDirection: "column", gap: AI_TREE_TOKENS.spacing6, alignItems: "center" }, children: [
1351
+ viewport,
1352
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: AI_TREE_TOKENS.spacing6, justifyContent: "center" }, children: [
1353
+ prevBtn,
1354
+ nextBtn
1355
+ ] }),
1356
+ dots
1357
+ ] });
1358
+ }
1359
+ return /* @__PURE__ */ jsxs("div", { "data-ai-carousel": "", style: { display: "flex", flexDirection: "column", gap: AI_TREE_TOKENS.spacing8, alignItems: "center" }, children: [
1360
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: AI_TREE_TOKENS.spacing6, width: "100%" }, children: [
1361
+ prevBtn,
1362
+ viewport,
1363
+ nextBtn
930
1364
  ] }),
931
- pages > 1 && /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: 9, justifyContent: "center" }, children: pageGroups.map((_, p) => /* @__PURE__ */ jsx("button", { type: "button", "aria-label": `Page ${p + 1}`, onClick: () => setPage(p), style: dot(p === current) }, p)) })
1365
+ dots
932
1366
  ] });
933
1367
  }
934
1368
  function CollectionBlock({ node, ctx, path }) {
@@ -1022,6 +1456,49 @@ function renderNode(node, ctx, path) {
1022
1456
  switch (node.type) {
1023
1457
  case "text":
1024
1458
  return /* @__PURE__ */ jsx(TextBlock, { slots, ctx, path });
1459
+ // Layout container: arranges child blocks, contributes no content of its own. `grid` is a
1460
+ // nested 12-column grid the children span (a collage is 3–5 media on it, bottom-aligned so
1461
+ // mixed aspects read as a composition); `split` is exactly two children at a ratio; `stack`
1462
+ // is a column. Children render through this same dispatcher, so edit markers, media
1463
+ // resolution, and copy paths all work unchanged inside a group.
1464
+ case "group": {
1465
+ const layout = str(slots.layout);
1466
+ const gap = slots.spacing === "tight" ? AI_TREE_TOKENS.spacing3 : slots.spacing === "airy" ? AI_TREE_TOKENS.spacing8 : AI_TREE_TOKENS.spacing6;
1467
+ const kids = (node.children ?? []).map((child, i) => /* @__PURE__ */ jsx(
1468
+ "div",
1469
+ {
1470
+ style: layout === "grid" ? {
1471
+ gridColumn: `span ${typeof child.span === "number" ? Math.min(12, Math.max(1, child.span)) : 12}`,
1472
+ minWidth: 0
1473
+ } : { minWidth: 0 },
1474
+ children: renderNode(child, ctx, `${path}.c${i}`)
1475
+ },
1476
+ i
1477
+ ));
1478
+ if (layout === "grid") {
1479
+ return /* @__PURE__ */ jsx(
1480
+ "div",
1481
+ {
1482
+ "data-ai-group": "grid",
1483
+ style: { display: "grid", gridTemplateColumns: "repeat(12, 1fr)", gap, alignItems: "end" },
1484
+ children: kids
1485
+ }
1486
+ );
1487
+ }
1488
+ if (layout === "split") {
1489
+ const ratio = str(slots.ratio);
1490
+ const cols = ratio === "3:5" ? "3fr 5fr" : ratio === "5:3" ? "5fr 3fr" : "1fr 1fr";
1491
+ return /* @__PURE__ */ jsx(
1492
+ "div",
1493
+ {
1494
+ "data-ai-group": "split",
1495
+ style: { display: "grid", gridTemplateColumns: cols, gap, alignItems: "center" },
1496
+ children: kids
1497
+ }
1498
+ );
1499
+ }
1500
+ return /* @__PURE__ */ jsx("div", { "data-ai-group": "stack", style: { display: "flex", flexDirection: "column", gap }, children: kids });
1501
+ }
1025
1502
  case "button":
1026
1503
  return /* @__PURE__ */ jsx(ButtonEl, { slots, ctx, path });
1027
1504
  case "button-row":
@@ -1102,33 +1579,111 @@ function renderNode(node, ctx, path) {
1102
1579
  }
1103
1580
  );
1104
1581
  }
1105
- case "form":
1106
- return /* @__PURE__ */ jsx("div", { style: { display: "flex", flexDirection: "column", gap: AI_TREE_TOKENS.spacing4 }, children: (node.children ?? []).map((child, i) => {
1107
- if (child.type === "input") {
1108
- const cs = child.slots ?? {};
1109
- return /* @__PURE__ */ jsxs("div", { children: [
1110
- /* @__PURE__ */ jsx(
1111
- "div",
1112
- {
1113
- ...textAttrs(ctx, `${path}.c${i}.label`),
1114
- style: { ...typeStyle(AI_TREE_TOKENS.type.bodyMBold, ctx.brand.fonts.body), color: ctx.brand.palette.dark, marginBottom: 6 },
1115
- children: str(cs.label)
1116
- }
1117
- ),
1118
- /* @__PURE__ */ jsx(
1119
- "div",
1120
- {
1121
- style: {
1122
- border: `1px solid ${ctx.brand.palette.accent}`,
1123
- borderRadius: AI_TREE_TOKENS.radiusButton,
1124
- height: cs.kind === "textarea" ? 96 : 42
1125
- }
1126
- }
1127
- )
1128
- ] }, i);
1129
- }
1130
- return /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(ButtonEl, { slots: child.slots ?? {}, ctx, path: `${path}.c${i}` }) }, i);
1131
- }) });
1582
+ case "form": {
1583
+ const formAttrs = ctx.keyFor ? {
1584
+ "data-ohw-editable": "form",
1585
+ "data-ohw-key": ctx.keyFor(`${path}.form`),
1586
+ "data-ohw-success-text": "Thanks \u2014 we'll be in touch shortly."
1587
+ } : {};
1588
+ const fieldStyle = {
1589
+ width: "100%",
1590
+ boxSizing: "border-box",
1591
+ border: `1px solid color-mix(in srgb, ${ctx.brand.palette.dark} 45%, #ffffff)`,
1592
+ borderRadius: 0,
1593
+ padding: 12,
1594
+ background: "#fff",
1595
+ color: ctx.brand.palette.dark,
1596
+ outline: "none",
1597
+ ...typeStyle(AI_TREE_TOKENS.type.bodyM, ctx.brand.fonts.body)
1598
+ };
1599
+ const labelStyle = {
1600
+ ...typeStyle(AI_TREE_TOKENS.type.bodyMBold, ctx.brand.fonts.body),
1601
+ color: ctx.brand.palette.dark,
1602
+ textAlign: "left",
1603
+ width: "100%"
1604
+ };
1605
+ const centered = ctx.sectionAlignment === "center";
1606
+ const submitAlign = centered ? "center" : "flex-start";
1607
+ const children = node.children ?? [];
1608
+ return (
1609
+ // 32px between the field group and the submit. In a stacked (centered) section the form is
1610
+ // capped at 780px and centered — the section's 12-col grid would otherwise leave it hugging
1611
+ // the left edge; a split section lets it fill its own column.
1612
+ /* @__PURE__ */ jsxs(
1613
+ "form",
1614
+ {
1615
+ ...formAttrs,
1616
+ "data-ai-form": "",
1617
+ style: {
1618
+ display: "flex",
1619
+ flexDirection: "column",
1620
+ gap: 32,
1621
+ width: "100%",
1622
+ ...centered ? { maxWidth: 780, marginLeft: "auto", marginRight: "auto" } : {}
1623
+ },
1624
+ children: [
1625
+ /* @__PURE__ */ jsx("div", { style: { display: "flex", flexDirection: "column", gap: 24, width: "100%", alignItems: "flex-start" }, children: children.map((child, i) => {
1626
+ if (child.type !== "input") return null;
1627
+ const cs = child.slots ?? {};
1628
+ const kind = str(cs.kind);
1629
+ const label = str(cs.label);
1630
+ const placeholder = str(cs.placeholder);
1631
+ const required = cs.required === true;
1632
+ const name = label.toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-+|-+$/gu, "") || `field-${i}`;
1633
+ const isTextarea = kind === "textarea";
1634
+ return /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 8, width: "100%" }, children: [
1635
+ /* @__PURE__ */ jsx("label", { ...textAttrs(ctx, `${path}.c${i}.label`), style: labelStyle, children: label }),
1636
+ isTextarea ? /* @__PURE__ */ jsx(
1637
+ "textarea",
1638
+ {
1639
+ name,
1640
+ placeholder,
1641
+ required,
1642
+ style: { ...fieldStyle, height: 180, resize: "vertical" }
1643
+ }
1644
+ ) : /* @__PURE__ */ jsx(
1645
+ "input",
1646
+ {
1647
+ name,
1648
+ type: kind === "email" ? "email" : "text",
1649
+ placeholder,
1650
+ required,
1651
+ style: { ...fieldStyle, height: 48 }
1652
+ }
1653
+ )
1654
+ ] }, i);
1655
+ }) }),
1656
+ children.map((child, i) => {
1657
+ if (child.type === "input") return null;
1658
+ const cs = child.slots ?? {};
1659
+ return /* @__PURE__ */ jsx(
1660
+ "button",
1661
+ {
1662
+ type: "submit",
1663
+ style: {
1664
+ alignSelf: submitAlign,
1665
+ border: "none",
1666
+ cursor: "pointer",
1667
+ padding: "12px 24px",
1668
+ // Corner radius follows the host template's own buttons (measured from a template
1669
+ // CTA); 8px only when the page has no template button to match.
1670
+ borderRadius: ctx.buttonRadius ?? 8,
1671
+ // Brand-styled: primary fill, brand-derived label colour (not a fixed token) so it
1672
+ // reads correctly on custom palettes.
1673
+ background: ctx.brand.palette.primary,
1674
+ color: ctx.buttonLabel ?? ctx.brand.palette.light,
1675
+ ...typeStyle(AI_TREE_TOKENS.type.button, ctx.brand.fonts.body)
1676
+ },
1677
+ children: /* @__PURE__ */ jsx("span", { ...textAttrs(ctx, `${path}.c${i}.label`), children: str(cs.label) })
1678
+ },
1679
+ i
1680
+ );
1681
+ })
1682
+ ]
1683
+ }
1684
+ )
1685
+ );
1686
+ }
1132
1687
  case "schedule-widget":
1133
1688
  return /* @__PURE__ */ jsx(
1134
1689
  "div",
@@ -1149,16 +1704,27 @@ function renderNode(node, ctx, path) {
1149
1704
  return null;
1150
1705
  }
1151
1706
  }
1152
- function AiTreeRenderer({ tree, brand, resolveMedia, editKeyPrefix }) {
1707
+ function AiTreeRenderer({
1708
+ tree,
1709
+ brand,
1710
+ buttonRadius,
1711
+ resolveMedia,
1712
+ editKeyPrefix
1713
+ }) {
1153
1714
  if (!isRenderableTree(tree)) {
1154
1715
  return null;
1155
1716
  }
1156
1717
  const resolvedBrand = brand ?? AI_DEFAULT_BRAND;
1718
+ const band = (tree.settings ?? {}).sectionBackground === "accent" ? accentBandContext(resolvedBrand) : null;
1719
+ const blockBrand = band?.brand ?? resolvedBrand;
1157
1720
  const ctx = {
1158
- brand: resolvedBrand,
1721
+ brand: blockBrand,
1159
1722
  resolveMedia: resolveMedia ?? (() => null),
1160
- cardSurface: resolvedBrand.palette.light.toUpperCase() === AI_DEFAULT_BRAND.palette.light.toUpperCase() ? "#ECECEB" : `color-mix(in srgb, ${resolvedBrand.palette.light} 90%, ${resolvedBrand.palette.dark})`,
1161
- keyFor: editKeyPrefix ? (path) => `${editKeyPrefix}.${path}` : null
1723
+ cardSurface: blockBrand.palette.light.toUpperCase() === AI_DEFAULT_BRAND.palette.light.toUpperCase() ? "#ECECEB" : `color-mix(in srgb, ${blockBrand.palette.light} 90%, ${blockBrand.palette.dark})`,
1724
+ keyFor: editKeyPrefix ? (path) => `${editKeyPrefix}.${path}` : null,
1725
+ sectionAlignment: (tree.settings ?? {}).alignment === "center" ? "center" : "left",
1726
+ buttonRadius,
1727
+ ...band ? { buttonLabel: band.buttonLabel } : {}
1162
1728
  };
1163
1729
  const settings = tree.settings ?? {};
1164
1730
  const pad = AI_TREE_TOKENS.sectionPadding[settings.spacing ?? "balanced"];
@@ -1166,6 +1732,20 @@ function AiTreeRenderer({ tree, brand, resolveMedia, editKeyPrefix }) {
1166
1732
  const overlayAlpha = { low: 0.25, medium: 0.45, high: 0.65 }[settings.overlayIntensity ?? "medium"];
1167
1733
  const backgroundUrl = isOverlay && settings.backgroundMedia ? ctx.resolveMedia(settings.backgroundMedia) : null;
1168
1734
  const bgAttrs = ctx.keyFor && isOverlay && settings.backgroundMedia ? { "data-ohw-key": ctx.keyFor("settings.backgroundMedia"), "data-ohw-editable": "bg-image" } : {};
1735
+ const toneBackground = (() => {
1736
+ const { dark, primary, light } = resolvedBrand.palette;
1737
+ switch (settings.sectionBackground) {
1738
+ case "surface":
1739
+ return `color-mix(in srgb, ${light} 94%, ${dark})`;
1740
+ case "accent":
1741
+ return primary;
1742
+ case "accent-soft":
1743
+ return `color-mix(in srgb, ${primary} 12%, ${light})`;
1744
+ default:
1745
+ return void 0;
1746
+ }
1747
+ })();
1748
+ const distributed = !isOverlay && settings.textDistribution;
1169
1749
  return /* @__PURE__ */ jsxs(
1170
1750
  "section",
1171
1751
  {
@@ -1175,13 +1755,15 @@ function AiTreeRenderer({ tree, brand, resolveMedia, editKeyPrefix }) {
1175
1755
  style: {
1176
1756
  position: "relative",
1177
1757
  padding: `${pad}px 0`,
1178
- background: isOverlay && !backgroundUrl ? "#DBEAFE" : void 0,
1758
+ background: toneBackground ?? (isOverlay && !backgroundUrl ? "#DBEAFE" : void 0),
1179
1759
  backgroundImage: backgroundUrl ? `url(${backgroundUrl})` : void 0,
1180
1760
  backgroundSize: "cover",
1181
- backgroundPosition: "center"
1761
+ backgroundPosition: "center",
1762
+ color: settings.sectionBackground === "accent" ? blockBrand.palette.dark : void 0
1182
1763
  },
1183
1764
  children: [
1184
1765
  /* @__PURE__ */ jsx("style", { children: AI_RESPONSIVE_CSS }),
1766
+ /* @__PURE__ */ jsx("style", { children: AI_MOBILE_CSS }),
1185
1767
  isOverlay && backgroundUrl && /* @__PURE__ */ jsx("div", { style: { position: "absolute", inset: 0, background: `rgba(255,255,255,${overlayAlpha})` } }),
1186
1768
  /* @__PURE__ */ jsx(
1187
1769
  "div",
@@ -1202,10 +1784,24 @@ function AiTreeRenderer({ tree, brand, resolveMedia, editKeyPrefix }) {
1202
1784
  display: "grid",
1203
1785
  gridTemplateColumns: "repeat(12, 1fr)",
1204
1786
  gap: AI_TREE_TOKENS.spacing6,
1205
- alignItems: settings.verticalPosition === "top" ? "start" : "center",
1787
+ alignItems: distributed === "space-between" ? "stretch" : (distributed ?? settings.verticalPosition) === "top" ? "start" : "center",
1206
1788
  marginTop: r2 > 0 ? AI_TREE_TOKENS.spacing8 : 0
1207
1789
  },
1208
- children: (row.blocks ?? []).map((block, b) => /* @__PURE__ */ jsx("div", { style: { gridColumn: `span ${Math.min(12, Math.max(1, block.span))}`, minWidth: 0 }, children: renderNode(block, ctx, `r${r2}.b${b}`) }, b))
1790
+ children: (row.blocks ?? []).map((block, b) => /* @__PURE__ */ jsx(
1791
+ "div",
1792
+ {
1793
+ "data-ai-cell": "",
1794
+ style: {
1795
+ gridColumn: `span ${Math.min(12, Math.max(1, block.span))}`,
1796
+ minWidth: 0,
1797
+ // space-between: each column becomes a flex column whose content spreads over
1798
+ // the full row height instead of clumping at the top.
1799
+ ...distributed === "space-between" ? { display: "flex", flexDirection: "column", justifyContent: "space-between" } : {}
1800
+ },
1801
+ children: renderNode(block, ctx, `r${r2}.b${b}`)
1802
+ },
1803
+ b
1804
+ ))
1209
1805
  },
1210
1806
  r2
1211
1807
  ))
@@ -1221,17 +1817,36 @@ import { jsx as jsx2 } from "react/jsx-runtime";
1221
1817
  var CONTAINER_ATTR = "data-ohw-ai-generated";
1222
1818
  var REPLACED_ATTR = "data-ohw-ai-replaced-by";
1223
1819
  var REMOVED_ATTR = "data-ohw-ai-removed";
1820
+ var TEMPLATE_HIDDEN_ATTR = "data-ohw-ai-template-hidden";
1821
+ var CHROME_SECTION_IDS = /* @__PURE__ */ new Set(["navbar", "footer"]);
1822
+ function readRootVar(name) {
1823
+ if (typeof document === "undefined") return "";
1824
+ return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
1825
+ }
1826
+ function deriveBrandOverride() {
1827
+ const dark = readRootVar("--ohw-brand-dark");
1828
+ const primary = readRootVar("--ohw-brand-primary");
1829
+ const light = readRootVar("--ohw-brand-light");
1830
+ if (!dark || !primary || !light) return null;
1831
+ const accent = readRootVar("--ohw-brand-accent");
1832
+ const heading = readRootVar("--font-heading") || readRootVar("--font-display");
1833
+ const body = readRootVar("--font-body");
1834
+ return {
1835
+ palette: { dark, primary, accent: accent || dark, light },
1836
+ fonts: {
1837
+ heading: heading || AI_DEFAULT_BRAND.fonts.heading,
1838
+ body: body || AI_DEFAULT_BRAND.fonts.body
1839
+ }
1840
+ };
1841
+ }
1224
1842
  function deriveTemplateBrand() {
1225
- if (typeof document === "undefined") return null;
1226
- const cs = getComputedStyle(document.documentElement);
1227
- const read = (name) => cs.getPropertyValue(name).trim();
1228
- const dark = read("--color-dark");
1229
- const primary = read("--color-primary");
1230
- const light = read("--color-light");
1843
+ const dark = readRootVar("--color-dark");
1844
+ const primary = readRootVar("--color-primary");
1845
+ const light = readRootVar("--color-light");
1231
1846
  if (!dark || !primary || !light) return null;
1232
- const accent = read("--color-accent");
1233
- const heading = read("--font-heading") || read("--font-display");
1234
- const body = read("--font-body");
1847
+ const accent = readRootVar("--color-accent");
1848
+ const heading = readRootVar("--font-heading") || readRootVar("--font-display");
1849
+ const body = readRootVar("--font-body");
1235
1850
  return {
1236
1851
  palette: { dark, primary, accent: accent || dark, light },
1237
1852
  fonts: {
@@ -1240,6 +1855,13 @@ function deriveTemplateBrand() {
1240
1855
  }
1241
1856
  };
1242
1857
  }
1858
+ function deriveTemplateButtonRadius() {
1859
+ if (typeof document === "undefined") return null;
1860
+ const btn = document.querySelector('[data-ohw-role="button"]');
1861
+ if (!btn) return null;
1862
+ const radius = getComputedStyle(btn).borderTopLeftRadius;
1863
+ return radius || null;
1864
+ }
1243
1865
  var mounted = /* @__PURE__ */ new Map();
1244
1866
  function findTemplateSection(id) {
1245
1867
  for (const el of document.querySelectorAll(`[data-ohw-section="${CSS.escape(id)}"]`)) {
@@ -1303,6 +1925,24 @@ function syncRemovedSections(state) {
1303
1925
  }
1304
1926
  }
1305
1927
  }
1928
+ function syncTemplateHidden(state, pageHasSections) {
1929
+ const hide = state.hideTemplate === true && pageHasSections;
1930
+ for (const el of Array.from(document.querySelectorAll(`[${TEMPLATE_HIDDEN_ATTR}]`))) {
1931
+ if (!hide) {
1932
+ el.style.removeProperty("display");
1933
+ el.removeAttribute(TEMPLATE_HIDDEN_ATTR);
1934
+ }
1935
+ }
1936
+ if (!hide) return;
1937
+ for (const el of Array.from(document.querySelectorAll("[data-ohw-section]"))) {
1938
+ if (el.hasAttribute(CONTAINER_ATTR)) continue;
1939
+ if (CHROME_SECTION_IDS.has(el.getAttribute("data-ohw-section") ?? "")) continue;
1940
+ if (el.parentElement?.closest("[data-ohw-section]")) continue;
1941
+ if (el.hasAttribute(REPLACED_ATTR) || el.hasAttribute(REMOVED_ATTR)) continue;
1942
+ el.style.display = "none";
1943
+ el.setAttribute(TEMPLATE_HIDDEN_ATTR, "");
1944
+ }
1945
+ }
1306
1946
  function syncReplacedOriginals(state) {
1307
1947
  for (const el of document.querySelectorAll(`[${REPLACED_ATTR}]`)) {
1308
1948
  const byId = el.getAttribute(REPLACED_ATTR) ?? "";
@@ -1321,10 +1961,64 @@ function syncReplacedOriginals(state) {
1321
1961
  }
1322
1962
  }
1323
1963
  }
1964
+ var sectionOrderIndex = /* @__PURE__ */ new Map();
1965
+ function setAiSectionOrder(raw, currentPath) {
1966
+ const next = /* @__PURE__ */ new Map();
1967
+ if (raw) {
1968
+ try {
1969
+ const entries = JSON.parse(raw);
1970
+ for (const entry of entries) {
1971
+ if (!entry.pagePath || entry.pagePath === currentPath) next.set(entry.instanceId, entry.order);
1972
+ }
1973
+ } catch {
1974
+ }
1975
+ }
1976
+ sectionOrderIndex = next;
1977
+ }
1978
+ function applyExplicitOrder(entries) {
1979
+ if (sectionOrderIndex.size === 0) return entries;
1980
+ return entries.map((entry, index) => ({ entry, index, order: sectionOrderIndex.get(entry.id) })).sort((a, b) => {
1981
+ if (a.order === void 0 && b.order === void 0) return a.index - b.index;
1982
+ if (a.order === void 0) return 1;
1983
+ if (b.order === void 0) return -1;
1984
+ return a.order - b.order;
1985
+ }).map((item) => item.entry);
1986
+ }
1987
+ function orderByChain(sections) {
1988
+ const ids = new Set(sections.map((entry) => entry.id));
1989
+ const after = /* @__PURE__ */ new Map();
1990
+ const roots = [];
1991
+ for (const entry of sections) {
1992
+ const anchor = entry.replaces ?? entry.beforeSection ?? entry.afterSection ?? null;
1993
+ if (anchor && ids.has(anchor)) {
1994
+ const bucket = after.get(anchor);
1995
+ if (bucket) bucket.push(entry);
1996
+ else after.set(anchor, [entry]);
1997
+ } else {
1998
+ roots.push(entry);
1999
+ }
2000
+ }
2001
+ const out = [];
2002
+ const seen = /* @__PURE__ */ new Set();
2003
+ const visit = (entry) => {
2004
+ if (seen.has(entry.id)) return;
2005
+ seen.add(entry.id);
2006
+ out.push(entry);
2007
+ for (const child of after.get(entry.id) ?? []) visit(child);
2008
+ };
2009
+ for (const root of roots) visit(root);
2010
+ return out.length === sections.length ? out : sections;
2011
+ }
1324
2012
  function applyAiSectionsToDom(state, options) {
1325
2013
  if (typeof document === "undefined") return;
2014
+ const brandOverride = deriveBrandOverride();
1326
2015
  const templateBrand = deriveTemplateBrand();
1327
- const activeIds = new Set(state.sections.map((entry) => entry.id));
2016
+ const templateButtonRadius = deriveTemplateButtonRadius();
2017
+ const brandKey = brandOverride ? JSON.stringify(brandOverride) : "";
2018
+ const pagePath = window.location.pathname;
2019
+ const pageSections = state.sections.filter((entry) => (entry.path ?? "/") === pagePath);
2020
+ const activeIds = new Set(pageSections.map((entry) => entry.id));
2021
+ const ordered = state.hideTemplate === true ? applyExplicitOrder(orderByChain(pageSections)) : pageSections;
1328
2022
  for (const [id, section] of mounted) {
1329
2023
  if (!activeIds.has(id)) {
1330
2024
  section.root.unmount();
@@ -1332,8 +2026,8 @@ function applyAiSectionsToDom(state, options) {
1332
2026
  mounted.delete(id);
1333
2027
  }
1334
2028
  }
1335
- for (const entry of state.sections) {
1336
- const serialized = JSON.stringify(entry);
2029
+ for (const entry of ordered) {
2030
+ const serialized = JSON.stringify(entry) + brandKey;
1337
2031
  const existing = mounted.get(entry.id);
1338
2032
  if (existing && existing.serialized === serialized && existing.container.isConnected) {
1339
2033
  continue;
@@ -1358,7 +2052,8 @@ function applyAiSectionsToDom(state, options) {
1358
2052
  AiTreeRenderer,
1359
2053
  {
1360
2054
  tree: entry.tree,
1361
- brand: entry.brand ?? options?.brand ?? templateBrand ?? void 0,
2055
+ brand: brandOverride ?? entry.brand ?? options?.brand ?? templateBrand ?? void 0,
2056
+ buttonRadius: templateButtonRadius,
1362
2057
  resolveMedia,
1363
2058
  editKeyPrefix: `ai.${entry.id}`
1364
2059
  }
@@ -1367,8 +2062,20 @@ function applyAiSectionsToDom(state, options) {
1367
2062
  });
1368
2063
  mounted.set(entry.id, { root, container, serialized });
1369
2064
  }
2065
+ if (state.hideTemplate === true) {
2066
+ let prev = null;
2067
+ for (const entry of ordered) {
2068
+ const el = mounted.get(entry.id)?.container;
2069
+ if (!el) continue;
2070
+ if (prev && !(prev.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_FOLLOWING)) {
2071
+ prev.insertAdjacentElement("afterend", el);
2072
+ }
2073
+ prev = el;
2074
+ }
2075
+ }
1370
2076
  syncReplacedOriginals(state);
1371
2077
  syncRemovedSections(state);
2078
+ syncTemplateHidden(state, pageSections.length > 0);
1372
2079
  }
1373
2080
 
1374
2081
  // src/useLinkHrefGuardian.ts
@@ -1495,6 +2202,7 @@ function EmailCaptureModal({ title, subtitle, onSubmit, onClose }) {
1495
2202
  /* @__PURE__ */ jsx3(
1496
2203
  Dialog.Overlay,
1497
2204
  {
2205
+ "data-ohw-scheduling-modal": "",
1498
2206
  className: "fixed inset-0 z-50",
1499
2207
  style: { background: "rgba(0,0,0,0.45)" }
1500
2208
  }
@@ -1502,6 +2210,7 @@ function EmailCaptureModal({ title, subtitle, onSubmit, onClose }) {
1502
2210
  /* @__PURE__ */ jsxs2(
1503
2211
  Dialog.Content,
1504
2212
  {
2213
+ "data-ohw-scheduling-modal": "",
1505
2214
  className: "fixed left-1/2 top-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2 bg-white rounded-xl shadow-xl outline-none font-body box-border overflow-hidden",
1506
2215
  style: { maxWidth: 400 },
1507
2216
  children: [
@@ -1975,7 +2684,7 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAf
1975
2684
  const autoId = useId();
1976
2685
  const insertAfter = insertAfterProp ?? autoId;
1977
2686
  const [schedule, setSchedule] = useState2(null);
1978
- const [loading, setLoading] = useState2(true);
2687
+ const [loading, setLoading] = useState2(initialScheduleId !== null);
1979
2688
  const [inEditor, setInEditor] = useState2(false);
1980
2689
  const [isHovered, setIsHovered] = useState2(false);
1981
2690
  const [modalState, setModalState] = useState2(null);
@@ -2149,8 +2858,10 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAf
2149
2858
  "*"
2150
2859
  );
2151
2860
  };
2152
- if (!inEditor && !loading && !schedule) return null;
2153
2861
  const sectionId = `scheduling-${insertAfter}`;
2862
+ if (!inEditor && !loading && !schedule) {
2863
+ return /* @__PURE__ */ jsx4("section", { "data-ohw-section": sectionId, "data-ohw-scheduling-anchor": insertAfter, style: { display: "none" } });
2864
+ }
2154
2865
  return /* @__PURE__ */ jsxs3(
2155
2866
  "section",
2156
2867
  {
@@ -7067,13 +7778,17 @@ function MediaOverlay({
7067
7778
  hover,
7068
7779
  isUploading,
7069
7780
  fadingOut = false,
7781
+ selected = false,
7782
+ hovered = false,
7070
7783
  onFadeOutComplete,
7071
7784
  onReplace,
7785
+ onSelect,
7072
7786
  onVideoSettingsChange
7073
7787
  }) {
7074
7788
  const { rect } = hover;
7075
7789
  const skeletonRef = React8.useRef(null);
7076
7790
  const isVideo = hover.elementType === "video";
7791
+ const showChrome = !selected || hovered;
7077
7792
  const autoplay = hover.videoAutoplay ?? true;
7078
7793
  const muted = hover.videoMuted ?? true;
7079
7794
  const probeRef = React8.useRef(null);
@@ -7087,6 +7802,7 @@ function MediaOverlay({
7087
7802
  (prev) => prev && prev.fullWidth === width && prev.height === height ? prev : { fullWidth: width, height }
7088
7803
  );
7089
7804
  }, [isVideo]);
7805
+ const replaceLabel = isVideo ? "Replace video" : hover.elementType === "bg-image" ? "Replace background" : "Replace image";
7090
7806
  const replaceMode = buttonSize ? replaceButtonMode({ width: rect.width, height: rect.height }, buttonSize) : "none";
7091
7807
  const box = {
7092
7808
  position: "fixed",
@@ -7120,7 +7836,7 @@ function MediaOverlay({
7120
7836
  }
7121
7837
  );
7122
7838
  }
7123
- const settingsBar = isVideo && !hover.isDragOver ? /* @__PURE__ */ jsxs7(
7839
+ const settingsBar = isVideo && !hover.isDragOver && showChrome ? /* @__PURE__ */ jsxs7(
7124
7840
  "div",
7125
7841
  {
7126
7842
  "data-ohw-bridge": "",
@@ -7190,10 +7906,12 @@ function MediaOverlay({
7190
7906
  // in-document, pointer-events does it natively. The button below opts back in, so
7191
7907
  // Replace still works.
7192
7908
  pointerEvents: hover.hasTextOverlap ? "none" : "auto",
7193
- boxShadow: "inset 0 0 0 1.5px var(--color-primary)",
7194
- background: "color-mix(in srgb, var(--color-primary) 20%, transparent)"
7909
+ // Selected: a firm component ring with no wash, so the image reads as chosen rather
7910
+ // than hovered. Hover keeps the existing tinted preview.
7911
+ boxShadow: selected ? "inset 0 0 0 2px var(--color-primary)" : "inset 0 0 0 1.5px var(--color-primary)",
7912
+ background: selected ? "transparent" : "color-mix(in srgb, var(--color-primary) 20%, transparent)"
7195
7913
  },
7196
- onClick: () => onReplace(hover.key),
7914
+ onClick: () => onSelect && !selected ? onSelect(hover.key) : onReplace(hover.key),
7197
7915
  children: [
7198
7916
  /* @__PURE__ */ jsxs7(
7199
7917
  Button,
@@ -7214,17 +7932,17 @@ function MediaOverlay({
7214
7932
  },
7215
7933
  children: [
7216
7934
  isVideo ? /* @__PURE__ */ jsx15(Film, { size: 14 }) : /* @__PURE__ */ jsx15(ImageIcon, { size: 14 }),
7217
- isVideo ? "Replace video" : "Replace image"
7935
+ replaceLabel
7218
7936
  ]
7219
7937
  }
7220
7938
  ),
7221
- replaceMode === "none" ? null : /* @__PURE__ */ jsxs7(
7939
+ showChrome && replaceMode !== "none" && /* @__PURE__ */ jsxs7(
7222
7940
  Button,
7223
7941
  {
7224
7942
  "data-ohw-media-overlay": "",
7225
7943
  variant: "outline",
7226
7944
  size: "sm",
7227
- "aria-label": isVideo ? "Replace video" : "Replace image",
7945
+ "aria-label": replaceLabel,
7228
7946
  className: "gap-1.5 cursor-pointer hover:bg-background",
7229
7947
  style: {
7230
7948
  ...OVERLAY_BUTTON_STYLE,
@@ -7247,7 +7965,7 @@ function MediaOverlay({
7247
7965
  },
7248
7966
  children: [
7249
7967
  isVideo ? /* @__PURE__ */ jsx15(Film, { size: 14 }) : /* @__PURE__ */ jsx15(ImageIcon, { size: 14 }),
7250
- replaceMode === "full" ? isVideo ? "Replace video" : "Replace image" : null
7968
+ replaceMode === "full" ? replaceLabel : null
7251
7969
  ]
7252
7970
  }
7253
7971
  )
@@ -7331,6 +8049,8 @@ function parseSectionsFromRoot(root) {
7331
8049
  const id = el.getAttribute("data-ohw-section") ?? "";
7332
8050
  if (!id || seen.has(id) || LINK_PICKER_EXCLUDED_IDS.has(id)) continue;
7333
8051
  if (el.parentElement?.closest("[data-ohw-section]")) continue;
8052
+ if (el.hasAttribute("data-ohw-ai-template-hidden") || el.hasAttribute("data-ohw-ai-removed") || el.hasAttribute("data-ohw-ai-replaced-by"))
8053
+ continue;
7334
8054
  seen.add(id);
7335
8055
  const label = el.getAttribute("data-ohw-section-label") ?? titleCaseSectionId(id);
7336
8056
  sections.push({ id, label });
@@ -7616,6 +8336,7 @@ function AiSectionOverlay({
7616
8336
  }) {
7617
8337
  const [selectedId, setSelectedId] = useState5(null);
7618
8338
  const [reviewId, setReviewId] = useState5(null);
8339
+ const [reviewButtonsHidden, setReviewButtonsHidden] = useState5(false);
7619
8340
  const reviewIdRef = useRef4(null);
7620
8341
  reviewIdRef.current = reviewId;
7621
8342
  const selectedIdRef = useRef4(null);
@@ -7677,6 +8398,7 @@ function AiSectionOverlay({
7677
8398
  }
7678
8399
  const found = readRect(sectionId) != null;
7679
8400
  setReviewId(found ? sectionId : null);
8401
+ setReviewButtonsHidden(e.data.hideButtons === true);
7680
8402
  postToParent2({ type: "ow:ai-review-started", sectionId, found });
7681
8403
  if (found) {
7682
8404
  document.querySelector(`[data-ohw-instance="${CSS.escape(sectionId)}"]`)?.scrollIntoView({ behavior: "smooth", block: "nearest" });
@@ -7804,13 +8526,16 @@ function AiSectionOverlay({
7804
8526
  border: `2px solid ${PRIMARY2}`,
7805
8527
  borderRadius: edgeAwareRadius(reviewRect),
7806
8528
  zIndex: 2147483200,
7807
- // The veil itself: swallows clicks so the section stays locked until decided.
8529
+ // The veil itself: swallows clicks so the section stays locked until decided. This
8530
+ // stopPropagation only guards the bubble phase; the bridge's capture-phase click
8531
+ // handlers additionally bail on `[data-ohw-ai-review]`, without which a click on
8532
+ // Accept/Discard resolves to the media beneath and opens the file picker.
7808
8533
  background: "rgba(8, 133, 254, 0.04)",
7809
8534
  pointerEvents: "auto",
7810
8535
  cursor: "default"
7811
8536
  },
7812
8537
  onClick: (e) => e.stopPropagation(),
7813
- children: /* @__PURE__ */ jsxs9(
8538
+ children: !reviewButtonsHidden && /* @__PURE__ */ jsxs9(
7814
8539
  "div",
7815
8540
  {
7816
8541
  style: {
@@ -10377,8 +11102,13 @@ function referenceBox(slot) {
10377
11102
  const neighbour = row ? Array.from(row.querySelectorAll('[data-ohw-editable="icon"]')).find(
10378
11103
  (el) => el !== slot && !el.hasAttribute("data-ohw-social-icon-placeholder")
10379
11104
  ) : null;
10380
- const source = (neighbour ?? slot).querySelector(GLYPH_SELECTOR);
10381
- const box = source?.getBoundingClientRect() ?? null;
11105
+ if (neighbour) {
11106
+ const box2 = neighbour.querySelector(GLYPH_SELECTOR)?.getBoundingClientRect();
11107
+ if (box2?.width && box2.height) return box2;
11108
+ }
11109
+ const own = slot.getBoundingClientRect();
11110
+ if (own.width && own.height) return own;
11111
+ const box = slot.querySelector(GLYPH_SELECTOR)?.getBoundingClientRect() ?? null;
10382
11112
  return box?.width && box.height ? box : null;
10383
11113
  }
10384
11114
  function iconMarkupSizedFor(slot, markup) {
@@ -12179,6 +12909,7 @@ function readLogoSizeState(content, placement) {
12179
12909
  function getLogoElement(el) {
12180
12910
  const marked = el.closest('[data-ohw-role="logo"], [data-ohw-logo]');
12181
12911
  if (marked) return marked;
12912
+ if (el.closest('[data-ohw-editable="icon"]')) return null;
12182
12913
  const root = el.closest("nav, [data-ohw-nav-root], footer");
12183
12914
  if (!root) return null;
12184
12915
  const anchor = el.closest("a");
@@ -13253,6 +13984,7 @@ function useSectionDrag({
13253
13984
  }
13254
13985
  const orderJson = JSON.stringify(entries);
13255
13986
  editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: orderJson };
13987
+ setAiSectionOrder(orderJson, window.location.pathname);
13256
13988
  postToParentRef.current({ type: "ow:change", nodes: [{ key: SECTION_ORDER_KEY, text: orderJson }] });
13257
13989
  applyPersistedOrder(entries);
13258
13990
  clearSectionDragVisuals();
@@ -14112,21 +14844,10 @@ function parseSchedulingInsertAfter(insertAfter) {
14112
14844
  insertBefore: insertAfter.slice(idx + INSERT_BEFORE_MARKER.length)
14113
14845
  };
14114
14846
  }
14115
- function resolveSchedulingInsert(insertAfter, explicitInsertBefore) {
14116
- const parsed = parseSchedulingInsertAfter(insertAfter);
14117
- const insertBefore = explicitInsertBefore ?? parsed.insertBefore;
14118
- const effectiveInsertAfter = insertBefore && parsed.insertBefore === null ? `${insertAfter}${INSERT_BEFORE_MARKER}${insertBefore}` : insertAfter;
14119
- return { effectiveInsertAfter, insertBefore };
14120
- }
14121
- function getSchedulingMountPoint(insertAfter) {
14122
- const { anchor } = parseSchedulingInsertAfter(insertAfter);
14123
- let anchorEl = document.querySelector(`[data-ohw-section="${anchor}"]`);
14124
- if (!anchorEl && anchor === "scheduling") {
14125
- const widgets = Array.from(document.querySelectorAll('[data-ohw-section^="scheduling-"]'));
14126
- anchorEl = widgets.at(-1) ?? null;
14127
- }
14128
- if (!anchorEl) return null;
14129
- return anchorEl.closest('[data-ohw-section-container="scheduling"]') ?? anchorEl;
14847
+ function resolveEntryAnchor(entry) {
14848
+ if (entry.anchorId !== void 0) return { anchorId: entry.anchorId, beforeId: entry.beforeId ?? null };
14849
+ const parsed = parseSchedulingInsertAfter(entry.insertAfter);
14850
+ return { anchorId: parsed.anchor, beforeId: parsed.insertBefore };
14130
14851
  }
14131
14852
  function schedulingMountDepth(insertAfter) {
14132
14853
  if (!insertAfter.includes(INSERT_BEFORE_MARKER)) return 0;
@@ -14143,8 +14864,7 @@ function getPageSchedulingEntries(raw) {
14143
14864
  }
14144
14865
  }
14145
14866
  function isSchedulingWidgetMissing(entry) {
14146
- const { effectiveInsertAfter } = resolveSchedulingInsert(entry.insertAfter);
14147
- return !document.querySelector(`[data-ohw-section="${schedulingSectionId(effectiveInsertAfter)}"]`);
14867
+ return !document.querySelector(`[data-ohw-section="${schedulingSectionId(entry.insertAfter)}"]`);
14148
14868
  }
14149
14869
  function hasMissingSchedulingWidgets(entries) {
14150
14870
  return entries.some(isSchedulingWidgetMissing);
@@ -14174,16 +14894,17 @@ function initSectionsFromContent(content, removeExisting = false) {
14174
14894
  } catch {
14175
14895
  }
14176
14896
  }
14177
- function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId, explicitInsertBefore) {
14178
- const { effectiveInsertAfter, insertBefore } = resolveSchedulingInsert(insertAfter, explicitInsertBefore);
14179
- const sectionId = schedulingSectionId(effectiveInsertAfter);
14897
+ function mountSchedulingWidget(anchorId, notifyOnConnect = false, scheduleId, beforeId, existingWidgetId) {
14898
+ const widgetId = existingWidgetId ?? (beforeId ? `${anchorId}${INSERT_BEFORE_MARKER}${beforeId}` : anchorId);
14899
+ const sectionId = schedulingSectionId(widgetId);
14180
14900
  if (document.querySelector(`[data-ohw-section="${sectionId}"]`)) return false;
14181
- const mountPoint = getSchedulingMountPoint(effectiveInsertAfter);
14182
- if (!mountPoint) return false;
14901
+ const anchorEl = document.querySelector(`[data-ohw-section="${anchorId}"]`);
14902
+ if (!anchorEl) return false;
14903
+ const mountPoint = anchorEl.closest('[data-ohw-section-container="scheduling"]') ?? anchorEl;
14183
14904
  const container = document.createElement("div");
14184
14905
  container.dataset.ohwSectionContainer = "scheduling";
14185
- if (insertBefore) {
14186
- const beforeAnchor = document.querySelector(`[data-ohw-section="${insertBefore}"]`);
14906
+ if (beforeId) {
14907
+ const beforeAnchor = document.querySelector(`[data-ohw-section="${beforeId}"]`);
14187
14908
  const beforePoint = beforeAnchor?.closest('[data-ohw-section-container="scheduling"]') ?? beforeAnchor;
14188
14909
  if (!beforePoint) return false;
14189
14910
  beforePoint.insertAdjacentElement("beforebegin", container);
@@ -14194,19 +14915,25 @@ function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId,
14194
14915
  }
14195
14916
  tail.insertAdjacentElement("afterend", container);
14196
14917
  }
14197
- const root = createRoot2(container);
14198
- flushSync2(() => {
14199
- root.render(
14200
- /* @__PURE__ */ jsx33(
14201
- SchedulingWidget,
14202
- {
14203
- notifyOnConnect,
14204
- initialScheduleId: scheduleId,
14205
- insertAfter: effectiveInsertAfter
14206
- }
14207
- )
14208
- );
14209
- });
14918
+ try {
14919
+ const root = createRoot2(container);
14920
+ flushSync2(() => {
14921
+ root.render(
14922
+ /* @__PURE__ */ jsx33(
14923
+ SchedulingWidget,
14924
+ {
14925
+ notifyOnConnect,
14926
+ initialScheduleId: scheduleId,
14927
+ insertAfter: widgetId
14928
+ }
14929
+ )
14930
+ );
14931
+ });
14932
+ } catch (err) {
14933
+ console.error("[ow:scheduling] render threw", err);
14934
+ container.remove();
14935
+ return false;
14936
+ }
14210
14937
  const tracker = getSectionsTracker();
14211
14938
  let sections = [];
14212
14939
  try {
@@ -14214,10 +14941,12 @@ function mountSchedulingWidget(insertAfter, notifyOnConnect = false, scheduleId,
14214
14941
  } catch {
14215
14942
  }
14216
14943
  const inEditor = typeof window !== "undefined" && window.self !== window.top;
14217
- if (inEditor && !sections.find((s) => s.type === "scheduling" && s.insertAfter === effectiveInsertAfter)) {
14944
+ if (inEditor && !sections.find((s) => s.type === "scheduling" && s.insertAfter === widgetId)) {
14218
14945
  sections.push({
14219
14946
  type: "scheduling",
14220
- insertAfter: effectiveInsertAfter,
14947
+ insertAfter: widgetId,
14948
+ anchorId,
14949
+ beforeId: beforeId ?? null,
14221
14950
  pagePath: window.location.pathname,
14222
14951
  ...scheduleId ? { scheduleId } : {}
14223
14952
  });
@@ -14231,7 +14960,8 @@ function mountSchedulingEntries(entries, notifyOnConnect = false) {
14231
14960
  for (let i = pending.length - 1; i >= 0; i--) {
14232
14961
  const entry = pending[i];
14233
14962
  const shouldNotify = typeof notifyOnConnect === "function" ? notifyOnConnect(entry) : notifyOnConnect;
14234
- if (mountSchedulingWidget(entry.insertAfter, shouldNotify, entry.scheduleId)) {
14963
+ const { anchorId, beforeId } = resolveEntryAnchor(entry);
14964
+ if (mountSchedulingWidget(anchorId, shouldNotify, entry.scheduleId ?? null, beforeId, entry.insertAfter)) {
14235
14965
  pending.splice(i, 1);
14236
14966
  }
14237
14967
  }
@@ -14389,6 +15119,11 @@ function applyLinkByKey(key, val) {
14389
15119
  hrefAnchors.forEach((el) => applyLinkHref(el, val));
14390
15120
  }
14391
15121
  }
15122
+ function isInsideLinkEditor(target) {
15123
+ return Boolean(
15124
+ target.closest("[data-ohw-link-popover-root]") || target.closest("[data-ohw-link-modal-root]") || target.closest("[data-ohw-link-page-dropdown]") || target.closest("[data-ohw-section-picker]") || target.closest("[data-ohw-navbar-container-chrome]") || target.closest("[data-ohw-navbar-add-button]") || target.closest("[data-ohw-footer-container-chrome]") || target.closest("[data-ohw-footer-add-button]") || target.closest("[data-ohw-more-menu]") || target.closest('[data-slot="dropdown-menu-content"]') || target.closest('[data-slot="popover-content"]') || target.closest('[data-slot="dialog-content"]') || target.closest('[data-slot="dialog-overlay"]')
15125
+ );
15126
+ }
14392
15127
  function isInsideFloatingPanel(target) {
14393
15128
  return Boolean(target.closest("[data-ohw-floating-panel]"));
14394
15129
  }
@@ -14396,11 +15131,6 @@ function isPointOverFloatingPanel(clientX, clientY) {
14396
15131
  const el = document.elementFromPoint(clientX, clientY);
14397
15132
  return Boolean(el instanceof Element && el.closest("[data-ohw-floating-panel]"));
14398
15133
  }
14399
- function isInsideLinkEditor(target) {
14400
- return Boolean(
14401
- target.closest("[data-ohw-link-popover-root]") || target.closest("[data-ohw-link-modal-root]") || target.closest("[data-ohw-link-page-dropdown]") || target.closest("[data-ohw-section-picker]") || target.closest("[data-ohw-navbar-container-chrome]") || target.closest("[data-ohw-navbar-add-button]") || target.closest("[data-ohw-footer-container-chrome]") || target.closest("[data-ohw-footer-add-button]") || target.closest("[data-ohw-more-menu]") || target.closest('[data-slot="dropdown-menu-content"]') || target.closest('[data-slot="popover-content"]') || target.closest('[data-slot="dialog-content"]') || target.closest('[data-slot="dialog-overlay"]')
14402
- );
14403
- }
14404
15134
  function getHrefKeyFromElement(el) {
14405
15135
  if (!el) return null;
14406
15136
  const anchor = el.closest("[data-ohw-href-key]");
@@ -14659,7 +15389,7 @@ function getNavigationSelectionParent(el) {
14659
15389
  if (!isFooterLinksContainer(el) && (el.hasAttribute("data-ohw-footer-col") || el.hasAttribute("data-ohw-footer-column") || isInferredFooterGroup2(el))) {
14660
15390
  return getFooterLinksContainer();
14661
15391
  }
14662
- if (el.hasAttribute("data-ohw-nav-container") || el.hasAttribute("data-ohw-nav-drawer") || isFooterLinksContainer(el)) {
15392
+ if (el.hasAttribute("data-ohw-nav-container") || el.hasAttribute("data-ohw-nav-drawer") || el.hasAttribute("data-ohw-footer-col") || el.hasAttribute("data-ohw-footer-column") || isFooterLinksContainer(el) || isInferredFooterGroup2(el)) {
14663
15393
  return getNavigationRoot(el);
14664
15394
  }
14665
15395
  return null;
@@ -14874,7 +15604,6 @@ var ICONS = {
14874
15604
  insertUnorderedList: '<line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/>',
14875
15605
  insertOrderedList: '<line x1="10" y1="6" x2="21" y2="6"/><line x1="10" y1="12" x2="21" y2="12"/><line x1="10" y1="18" x2="21" y2="18"/><path d="M4 6h1v4"/><path d="M4 10h2"/><path d="M6 18H4c0-1 2-2 2-3s-1-1.5-2-1"/>'
14876
15606
  };
14877
- var HEIGHT_SETTLE_DELAYS = [150, 400, 800];
14878
15607
  var SELECTION_CHROME_GAP2 = 4;
14879
15608
  var TOOLBAR_STROKE_GAP2 = 4;
14880
15609
  var TOOLBAR_OFFSET_FROM_ELEMENT = SELECTION_CHROME_GAP2 + TOOLBAR_STROKE_GAP2;
@@ -15254,6 +15983,8 @@ function StateToggle({
15254
15983
  );
15255
15984
  }
15256
15985
  var contentCache = /* @__PURE__ */ new Map();
15986
+ var fetchedContentPaths = /* @__PURE__ */ new Set();
15987
+ var brandingCache = /* @__PURE__ */ new Map();
15257
15988
  var OHW_LOADER_STYLE = {
15258
15989
  position: "fixed",
15259
15990
  inset: 0,
@@ -15291,6 +16022,89 @@ function OhwLoaderSpinner() {
15291
16022
  )
15292
16023
  ] });
15293
16024
  }
16025
+ function OhwBrandMark() {
16026
+ return /* @__PURE__ */ jsxs20(
16027
+ "svg",
16028
+ {
16029
+ width: "16",
16030
+ height: "16",
16031
+ viewBox: "0 0 48 48",
16032
+ fill: "none",
16033
+ "aria-hidden": true,
16034
+ style: { display: "block", flexShrink: 0 },
16035
+ xmlns: "http://www.w3.org/2000/svg",
16036
+ children: [
16037
+ /* @__PURE__ */ jsx33(
16038
+ "mask",
16039
+ {
16040
+ id: "ohw-badge-mark",
16041
+ style: { maskType: "luminance" },
16042
+ maskUnits: "userSpaceOnUse",
16043
+ x: "0",
16044
+ y: "0",
16045
+ width: "48",
16046
+ height: "48",
16047
+ children: /* @__PURE__ */ jsx33("path", { d: "M23.8741 48C37.0594 48 47.7481 37.2548 47.7481 24C47.7481 10.7452 37.0594 0 23.8741 0C10.6888 0 0 10.7452 0 24C0 37.2548 10.6888 48 23.8741 48Z", fill: "white" })
16048
+ }
16049
+ ),
16050
+ /* @__PURE__ */ jsxs20("g", { mask: "url(#ohw-badge-mark)", children: [
16051
+ /* @__PURE__ */ jsx33("path", { d: "M23.8731 48.0497C37.0584 48.0497 47.7472 37.3046 47.7472 24.0497C47.7472 10.7949 37.0584 0.0497208 23.8731 0.0497208C10.6878 0.0497208 -0.000976562 10.7949 -0.000976562 24.0497C-0.000976562 37.3046 10.6878 48.0497 23.8731 48.0497Z", fill: "#0078E5" }),
16052
+ /* @__PURE__ */ jsx33("path", { d: "M17.1307 14.7172C13.1687 14.7172 9.38102 18.1154 8.65114 22.34C8.38885 23.8488 8.5598 25.2581 9.06929 26.4451C6.20005 29.1677 1.77216 27.8721 -1.40212 26.1536C-2.73618 25.4317 -3.92695 27.4745 -2.59037 28.1981C1.33389 30.3226 6.86037 31.6621 10.4402 28.4188C11.4718 29.3859 12.867 29.9621 14.4894 29.9621C18.4161 29.9621 22.2038 26.5318 22.9337 22.34C23.6636 18.1162 21.0566 14.7172 17.1298 14.7172H17.1307ZM19.9798 22.34C19.5281 25.0399 17.2689 27.231 14.9754 27.231C12.6466 27.231 11.1877 25.0399 11.6394 22.34C12.1262 19.6401 14.3151 17.4482 16.6438 17.4482C18.9374 17.4482 20.4667 19.6401 19.9798 22.34Z", fill: "white" }),
16053
+ /* @__PURE__ */ jsx33("path", { d: "M40.0017 27.0262C39.1797 27.081 38.2721 26.995 37.4668 26.7415C37.3344 26.6993 37.28 26.5401 37.3529 26.4205C37.5959 26.0212 37.8255 25.6152 38.009 25.1889C38.1071 24.9918 38.2018 24.793 38.2897 24.5908C38.3274 24.5041 38.4163 24.451 38.5101 24.4619C38.63 24.4754 38.7054 24.4821 38.8881 24.4821L39.1529 24.4796L39.8283 24.4543C45.9229 24.0492 50.4765 20.4319 54.8466 16.9014C56.0172 15.9554 57.6932 17.6208 56.5116 18.5752C51.7687 22.4065 47.1966 26.5081 40.9319 26.9689", fill: "white" }),
16054
+ /* @__PURE__ */ jsx33("path", { d: "M37.9687 24.27C38.4472 23.1319 38.7656 21.9045 38.9609 20.6991C39.5927 16.76 38.5058 14.2193 36.1553 14.2193C34.1835 14.2193 33.1469 17.2427 32.9199 18.8694C32.743 19.9872 32.5914 22.1219 33.6028 23.9524C33.7553 24.259 34.15 24.7712 34.471 25.1259C34.5447 25.2067 34.6746 25.2 34.7349 25.1082C34.9528 24.7788 35.1615 24.4039 35.3584 24.0257C35.5444 23.6677 35.5888 23.6138 35.8587 23.0207C35.8838 22.966 35.8813 22.9002 35.8478 22.8505C35.5888 22.4597 35.2168 21.9787 35.1204 21.4614C34.9184 20.4455 34.9436 19.2257 35.2218 18.1078C35.4413 17.3118 35.7195 16.7844 35.9039 16.5106C35.9466 16.4466 36.0279 16.4129 36.0975 16.4449C36.369 16.5671 36.5827 16.8838 36.7385 17.396C37.0167 18.2089 37.0167 19.3782 36.814 20.6999C36.6991 21.5271 36.4771 22.3729 36.1746 23.1673C36.1293 23.308 36.0757 23.4461 36.0187 23.5826C36.0187 23.5868 36.0187 23.591 36.0187 23.5961C35.9911 23.6946 35.9207 23.8067 35.8846 23.901C35.5536 24.5497 35.2344 25.1697 34.8439 25.7838C34.8388 25.7863 34.8346 25.7914 34.8296 25.7931C34.6528 26.0525 34.4718 26.2901 34.2866 26.4965C34.2774 26.5099 34.2682 26.5234 34.2589 26.5369C34.2405 26.5638 34.2179 26.5815 34.1944 26.595C33.5064 27.3212 32.7665 27.6893 32.0123 27.6893C31.8606 27.6893 31.6838 27.664 31.507 27.4349C31.3042 27.1299 30.85 26.0879 31.2539 22.9112C31.4785 21.3949 31.8011 20.0268 31.931 19.5408C31.9579 19.4413 31.8908 19.3419 31.7886 19.3293L30.0062 19.1162C29.9241 19.1061 29.8478 19.1566 29.8252 19.2366C29.2704 21.1615 27.0305 27.6885 24.9599 27.6885C24.328 27.6885 24.1512 26.6211 24.1001 26.2909C23.775 23.6264 25.528 18.5492 29.5302 16.2267C29.6048 16.1838 29.635 16.0928 29.5998 16.0136L28.9042 14.467C28.8632 14.3752 28.7501 14.3389 28.6638 14.3886C25.8079 16.0414 24.1847 18.5231 23.2915 20.3436C22.2046 22.5793 21.6993 25.0189 21.9515 26.8738C22.1795 28.7794 23.1398 29.872 24.6054 29.872C26.0543 29.872 27.4369 28.9066 28.7098 27.0187C28.7953 26.8915 28.9889 26.9311 29.0149 27.0819C29.2646 28.5283 29.9811 29.872 31.6579 29.872C33.5282 29.872 35.3232 28.7288 36.7134 26.6447C36.7712 26.5874 36.7972 26.5411 36.8181 26.4906C36.8232 26.4931 36.8282 26.4948 36.8332 26.4973C37.01 26.2025 37.181 25.9051 37.3444 25.6027C37.4508 25.4005 37.5572 25.1992 37.6586 24.9945C37.7181 24.874 37.7743 24.7527 37.8262 24.6289C37.838 24.6002 37.8547 24.5665 37.8706 24.5337", fill: "white" }),
16055
+ /* @__PURE__ */ jsx33("path", { d: "M30.5839 31.6397C25.7546 34.8577 19.4773 34.7853 14.6907 31.5243C13.5368 30.7384 12.5044 32.6498 13.6474 33.4281C19.034 37.0985 26.3077 37.096 31.7218 33.488C32.8791 32.7172 31.7478 30.8639 30.5839 31.6397Z", fill: "white" })
16056
+ ] })
16057
+ ]
16058
+ }
16059
+ );
16060
+ }
16061
+ var OHW_BADGE_STYLE = {
16062
+ position: "fixed",
16063
+ left: 20,
16064
+ bottom: 20,
16065
+ zIndex: 2147483e3,
16066
+ boxSizing: "border-box",
16067
+ display: "inline-flex",
16068
+ alignItems: "center",
16069
+ gap: 0,
16070
+ padding: "6px 8px",
16071
+ margin: 0,
16072
+ background: "#ffffff",
16073
+ border: "1px solid #e7e5e4",
16074
+ borderRadius: 9999,
16075
+ boxShadow: "0 1px 3px rgba(0, 0, 0, 0.1)",
16076
+ color: "#0c0a09",
16077
+ textDecoration: "none",
16078
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif"
16079
+ };
16080
+ var OHW_BADGE_LABEL_STYLE = {
16081
+ padding: "0 4px",
16082
+ fontSize: 14,
16083
+ lineHeight: "24px",
16084
+ fontWeight: 500,
16085
+ fontStyle: "normal",
16086
+ letterSpacing: "normal",
16087
+ textTransform: "none",
16088
+ color: "#0c0a09",
16089
+ whiteSpace: "nowrap"
16090
+ };
16091
+ function MadeWithOhhWells() {
16092
+ return /* @__PURE__ */ jsxs20(
16093
+ "a",
16094
+ {
16095
+ href: "https://ohhwells.com",
16096
+ target: "_blank",
16097
+ rel: "noopener noreferrer",
16098
+ "aria-label": "Made with OhhWells",
16099
+ "data-ohw-badge": "",
16100
+ style: OHW_BADGE_STYLE,
16101
+ children: [
16102
+ /* @__PURE__ */ jsx33(OhwBrandMark, {}),
16103
+ /* @__PURE__ */ jsx33("span", { style: OHW_BADGE_LABEL_STYLE, children: "Made with OhhWells" })
16104
+ ]
16105
+ }
16106
+ );
16107
+ }
15294
16108
  var OHW_LOADER_PREHYDRATE_SCRIPT = `(function(){try{var p=location.hostname.split(".");var fromHost=p.length>=3&&p[0]!=="www"?p[0]:"";var fromQuery=new URLSearchParams(location.search).get("subdomain")||"";if(!fromHost&&!fromQuery)return;var e=document.getElementById("ohw-loader");if(e)e.style.display="flex"}catch(e){}})();`;
15295
16109
  function resolveSubdomain(subdomainFromQuery) {
15296
16110
  if (subdomainFromQuery) return subdomainFromQuery;
@@ -15350,6 +16164,7 @@ function OhhwellsBridge() {
15350
16164
  }
15351
16165
  }, []);
15352
16166
  const [fetchState, setFetchState] = useState13("idle");
16167
+ const [showBranding, setShowBranding] = useState13(false);
15353
16168
  const autoSaveTimers = useRef10(/* @__PURE__ */ new Map());
15354
16169
  const activeElRef = useRef10(null);
15355
16170
  const pointerHeldRef = useRef10(false);
@@ -15372,6 +16187,70 @@ function OhhwellsBridge() {
15372
16187
  const hoveredImageHasTextOverlapRef = useRef10(false);
15373
16188
  const dragOverElRef = useRef10(null);
15374
16189
  const [mediaHover, setMediaHover] = useState13(null);
16190
+ const [selectedMedia, setSelectedMedia] = useState13(null);
16191
+ const selectedMediaElRef = useRef10(null);
16192
+ const clearMediaSelection = useCallback8(() => {
16193
+ const prev = selectedMediaElRef.current;
16194
+ selectedMediaElRef.current = null;
16195
+ setSelectedMedia(null);
16196
+ const sectionEl = prev?.closest("[data-ohw-section]") ?? null;
16197
+ if (sectionEl) {
16198
+ postToParentRef.current({
16199
+ type: "ow:section-selected",
16200
+ sectionId: sectionEl.dataset.ohwSection ?? null,
16201
+ sectionLabel: sectionEl.dataset.ohwSectionLabel ?? titleCaseSectionId(sectionEl.dataset.ohwSection ?? ""),
16202
+ key: null
16203
+ });
16204
+ }
16205
+ }, []);
16206
+ const clearMediaSelectionRef = useRef10(clearMediaSelection);
16207
+ clearMediaSelectionRef.current = clearMediaSelection;
16208
+ const selectMediaElement = useCallback8((el) => {
16209
+ const r2 = el.getBoundingClientRect();
16210
+ const video = el.dataset.ohwEditable === "video" ? el.querySelector("video") : null;
16211
+ selectedMediaElRef.current = el;
16212
+ setSelectedMedia({
16213
+ key: el.dataset.ohwKey ?? "",
16214
+ rect: { top: r2.top, left: r2.left, width: r2.width, height: r2.height },
16215
+ elementType: el.dataset.ohwEditable ?? "image",
16216
+ hasTextOverlap: false,
16217
+ isDragOver: false,
16218
+ ...video ? { videoAutoplay: video.autoplay, videoMuted: video.muted } : {}
16219
+ });
16220
+ const sectionEl = el.closest("[data-ohw-section]");
16221
+ aiSectionApiRef.current?.selectFromElement(el, { report: false });
16222
+ postToParentRef.current({
16223
+ type: "ow:section-selected",
16224
+ sectionId: sectionEl?.dataset.ohwSection ?? null,
16225
+ sectionLabel: sectionEl ? sectionEl.dataset.ohwSectionLabel ?? titleCaseSectionId(sectionEl.dataset.ohwSection ?? "") : null,
16226
+ key: el.dataset.ohwKey ?? null,
16227
+ // Display name for the pill — the raw key prettifies into fragments ("Img"); the
16228
+ // bridge knows what the node IS, so it names it.
16229
+ keyLabel: el.dataset.ohwEditable === "video" ? "Video" : el.dataset.ohwEditable === "bg-image" ? "Background" : "Image"
16230
+ });
16231
+ }, []);
16232
+ const selectMediaElementRef = useRef10(selectMediaElement);
16233
+ selectMediaElementRef.current = selectMediaElement;
16234
+ useEffect13(() => {
16235
+ if (!selectedMedia) return;
16236
+ const update = () => {
16237
+ const el = selectedMediaElRef.current;
16238
+ if (!el || !el.isConnected) {
16239
+ clearMediaSelection();
16240
+ return;
16241
+ }
16242
+ const r2 = el.getBoundingClientRect();
16243
+ setSelectedMedia(
16244
+ (prev) => prev ? { ...prev, rect: { top: r2.top, left: r2.left, width: r2.width, height: r2.height } } : prev
16245
+ );
16246
+ };
16247
+ window.addEventListener("scroll", update, true);
16248
+ window.addEventListener("resize", update);
16249
+ return () => {
16250
+ window.removeEventListener("scroll", update, true);
16251
+ window.removeEventListener("resize", update);
16252
+ };
16253
+ }, [selectedMedia !== null]);
15375
16254
  const [carouselHover, setCarouselHover] = useState13(null);
15376
16255
  const [uploadingRects, setUploadingRects] = useState13({});
15377
16256
  const hoveredGapRef = useRef10(null);
@@ -15634,13 +16513,6 @@ function OhhwellsBridge() {
15634
16513
  const [isItemDragging, setIsItemDragging] = useState13(false);
15635
16514
  const [isFooterFrameSelection, setIsFooterFrameSelection] = useState13(false);
15636
16515
  isFooterFrameSelectionRef.current = isFooterFrameSelection;
15637
- const [floatingPanel, setFloatingPanel] = useState13(null);
15638
- const floatingPanelOpenRef = useRef10(false);
15639
- floatingPanelOpenRef.current = floatingPanel !== null;
15640
- const [floatingPanelPos, setFloatingPanelPos] = useState13(null);
15641
- const [logoSizeDraft, setLogoSizeDraft] = useState13(null);
15642
- const [editorViewport, setEditorViewport] = useState13("desktop");
15643
- const [parentScrollSnap, setParentScrollSnap] = useState13(null);
15644
16516
  const [navDropdownPreviewOpen, setNavDropdownPreviewOpen] = useState13(null);
15645
16517
  const [footerHeadingVisible, setFooterHeadingVisible] = useState13(null);
15646
16518
  const footerDragRef = useRef10(null);
@@ -15655,7 +16527,16 @@ function OhhwellsBridge() {
15655
16527
  const addNavAfterAnchorRef = useRef10(null);
15656
16528
  const editContentRef = useRef10({});
15657
16529
  const aiSectionsRef = useRef10("");
16530
+ const brandKitRef = useRef10("");
16531
+ const stylesRef = useRef10("");
15658
16532
  const pendingDeleteUndoRef = useRef10(null);
16533
+ const [floatingPanel, setFloatingPanel] = useState13(null);
16534
+ const floatingPanelOpenRef = useRef10(false);
16535
+ const setFloatingPanelRef = useRef10(setFloatingPanel);
16536
+ const [floatingPanelPos, setFloatingPanelPos] = useState13(null);
16537
+ const [logoSizeDraft, setLogoSizeDraft] = useState13(null);
16538
+ const [editorViewport, setEditorViewport] = useState13("desktop");
16539
+ const [parentScrollSnap, setParentScrollSnap] = useState13(null);
15659
16540
  const [sitePages, setSitePages] = useState13([]);
15660
16541
  const [sectionsByPath, setSectionsByPath] = useState13({});
15661
16542
  const sectionsPrefetchGenRef = useRef10(0);
@@ -15664,7 +16545,18 @@ function OhhwellsBridge() {
15664
16545
  const linkPopoverOpenRef = useRef10(false);
15665
16546
  const linkPopoverGraceUntilRef = useRef10(0);
15666
16547
  setLinkPopoverRef.current = setLinkPopover;
16548
+ setFloatingPanelRef.current = setFloatingPanel;
15667
16549
  linkPopoverSessionRef.current = linkPopover;
16550
+ floatingPanelOpenRef.current = Boolean(floatingPanel);
16551
+ useEffect13(() => {
16552
+ const syncViewport = () => {
16553
+ const next = window.innerWidth <= 480 ? "mobile" : "desktop";
16554
+ setEditorViewport((prev) => prev === next ? prev : next);
16555
+ };
16556
+ syncViewport();
16557
+ window.addEventListener("resize", syncViewport);
16558
+ return () => window.removeEventListener("resize", syncViewport);
16559
+ }, []);
15668
16560
  const {
15669
16561
  navDragRef,
15670
16562
  navDropSlots,
@@ -16975,15 +17867,31 @@ function OhhwellsBridge() {
16975
17867
  }
16976
17868
  const applyContent = (content) => {
16977
17869
  const imageLoads = [];
17870
+ if (typeof content[BRAND_KIT_KEY] === "string") {
17871
+ brandKitRef.current = content[BRAND_KIT_KEY];
17872
+ applyBrandToDom(parseBrandKit(content[BRAND_KIT_KEY]));
17873
+ } else {
17874
+ brandKitRef.current = "";
17875
+ applyBrandToDom(null);
17876
+ }
16978
17877
  if (typeof content[AI_SECTIONS_KEY] === "string") {
16979
17878
  aiSectionsRef.current = content[AI_SECTIONS_KEY];
17879
+ setAiSectionOrder(content[SECTION_ORDER_KEY], window.location.pathname);
16980
17880
  applyAiSectionsToDom(parseAiSectionsState(content[AI_SECTIONS_KEY]));
16981
17881
  }
17882
+ if (typeof content[STYLE_STORE_KEY] === "string") {
17883
+ stylesRef.current = content[STYLE_STORE_KEY];
17884
+ applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
17885
+ }
17886
+ applyBrandChrome(content);
16982
17887
  for (const [key, val] of Object.entries(content)) {
16983
17888
  if (key === "__ohw_sections") continue;
16984
17889
  if (key === AI_SECTIONS_KEY) continue;
16985
17890
  if (key === LOGO_PLACEHOLDER_KEY) continue;
16986
17891
  if (LOGO_IMAGE_KEYS.includes(key)) continue;
17892
+ if (key === BRAND_KIT_KEY) continue;
17893
+ if (key === STYLE_STORE_KEY) continue;
17894
+ if (BRAND_CHROME_KEYS.has(key)) continue;
16987
17895
  if (applyVideoSettingNode(key, val)) continue;
16988
17896
  if (applyCarouselNode(key, val)) continue;
16989
17897
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
@@ -17043,16 +17951,22 @@ function OhhwellsBridge() {
17043
17951
  };
17044
17952
  const cached = contentCache.get(subdomain);
17045
17953
  if (cached) {
17954
+ setShowBranding(brandingCache.get(subdomain) ?? false);
17046
17955
  applyContent(cached).finally(() => setFetchState("done"));
17047
17956
  return;
17048
17957
  }
17049
17958
  let cancelled = false;
17050
17959
  setFetchState("loading");
17051
17960
  const apiUrl = process.env.NEXT_PUBLIC_FLOWOPS_API_URL ?? "https://flowops-backend-staging-2yfdh7pwpq-as.a.run.app";
17052
- fetch(`${apiUrl}/api/public/sites/${subdomain}/content`, { cache: "no-store" }).then((r2) => r2.ok ? r2.json() : null).then((data) => {
17961
+ const initialPath = pathname;
17962
+ fetchedContentPaths.add(`${subdomain}::${initialPath}`);
17963
+ fetch(`${apiUrl}/api/public/sites/${subdomain}/content?path=${encodeURIComponent(initialPath)}`, { cache: "no-store" }).then((r2) => r2.ok ? r2.json() : null).then((data) => {
17053
17964
  if (cancelled) return;
17054
17965
  const content = data?.content ?? {};
17966
+ const branding = Boolean(data?.showBranding);
17055
17967
  contentCache.set(subdomain, content);
17968
+ brandingCache.set(subdomain, branding);
17969
+ setShowBranding(branding);
17056
17970
  return applyContent(content);
17057
17971
  }).catch(() => {
17058
17972
  }).finally(() => {
@@ -17169,10 +18083,28 @@ function OhhwellsBridge() {
17169
18083
  initSectionInstancesFromContent(content, window.location.pathname);
17170
18084
  observer?.disconnect();
17171
18085
  try {
18086
+ applyBrandChrome(content);
18087
+ if (typeof content[BRAND_KIT_KEY] === "string") {
18088
+ applyBrandToDom(parseBrandKit(content[BRAND_KIT_KEY]));
18089
+ } else {
18090
+ applyBrandToDom(null);
18091
+ }
18092
+ if (typeof content[AI_SECTIONS_KEY] === "string") {
18093
+ setAiSectionOrder(content[SECTION_ORDER_KEY], window.location.pathname);
18094
+ applyAiSectionsToDom(parseAiSectionsState(content[AI_SECTIONS_KEY]));
18095
+ }
18096
+ if (typeof content[STYLE_STORE_KEY] === "string") {
18097
+ applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
18098
+ }
17172
18099
  for (const [key, val] of Object.entries(content)) {
17173
18100
  if (key === "__ohw_sections") continue;
18101
+ if (key === AI_SECTIONS_KEY) continue;
17174
18102
  if (key === LOGO_PLACEHOLDER_KEY) continue;
17175
18103
  if (LOGO_IMAGE_KEYS.includes(key)) continue;
18104
+ if (key === BRAND_KIT_KEY) continue;
18105
+ if (key === STYLE_STORE_KEY) continue;
18106
+ if (key === STYLE_STORE_KEY) continue;
18107
+ if (BRAND_CHROME_KEYS.has(key)) continue;
17176
18108
  if (applyVideoSettingNode(key, val)) continue;
17177
18109
  if (applyCarouselNode(key, val)) continue;
17178
18110
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
@@ -17218,6 +18150,17 @@ function OhhwellsBridge() {
17218
18150
  debounceTimer = setTimeout(applyFromCache, 150);
17219
18151
  };
17220
18152
  applyFromCache();
18153
+ const pathCacheKey = `${subdomain}::${pathname}`;
18154
+ if (!fetchedContentPaths.has(pathCacheKey)) {
18155
+ fetchedContentPaths.add(pathCacheKey);
18156
+ const apiUrl = process.env.NEXT_PUBLIC_FLOWOPS_API_URL ?? "https://flowops-backend-staging-2yfdh7pwpq-as.a.run.app";
18157
+ fetch(`${apiUrl}/api/public/sites/${subdomain}/content?path=${encodeURIComponent(pathname)}`, { cache: "no-store" }).then((r2) => r2.ok ? r2.json() : null).then((data) => {
18158
+ if (!data?.content) return;
18159
+ contentCache.set(subdomain, data.content);
18160
+ applyFromCache();
18161
+ }).catch(() => {
18162
+ });
18163
+ }
17221
18164
  observer = new MutationObserver(scheduleApply);
17222
18165
  observer.observe(document.body, { childList: true, subtree: true });
17223
18166
  return () => {
@@ -17313,30 +18256,31 @@ function OhhwellsBridge() {
17313
18256
  }, [isEditMode, pathname, subdomain, fetchState, resyncSelectedNavigationItem]);
17314
18257
  useEffect13(() => {
17315
18258
  if (!isEditMode) return;
18259
+ let lastPosted = 0;
17316
18260
  const measure = () => {
17317
18261
  const h = document.body.scrollHeight;
17318
- if (h > 50) postToParent2({ type: "ow:height", height: h });
18262
+ if (h > 50 && Math.abs(h - lastPosted) > 1) {
18263
+ lastPosted = h;
18264
+ postToParent2({ type: "ow:height", height: h });
18265
+ }
18266
+ };
18267
+ let raf = null;
18268
+ const schedule = () => {
18269
+ if (raf != null) return;
18270
+ raf = requestAnimationFrame(() => {
18271
+ raf = null;
18272
+ measure();
18273
+ });
17319
18274
  };
17320
18275
  const t1 = setTimeout(measure, 50);
17321
18276
  const t2 = setTimeout(measure, 500);
17322
- let lastWidth = window.innerWidth;
17323
- let resizeTimers = [];
17324
- const clearResizeTimers = () => {
17325
- resizeTimers.forEach(clearTimeout);
17326
- resizeTimers = [];
17327
- };
17328
- const handleResize = () => {
17329
- if (window.innerWidth === lastWidth) return;
17330
- lastWidth = window.innerWidth;
17331
- clearResizeTimers();
17332
- resizeTimers = HEIGHT_SETTLE_DELAYS.map((delay) => setTimeout(measure, delay));
17333
- };
17334
- window.addEventListener("resize", handleResize);
18277
+ const ro = new ResizeObserver(schedule);
18278
+ ro.observe(document.body);
17335
18279
  return () => {
17336
18280
  clearTimeout(t1);
17337
18281
  clearTimeout(t2);
17338
- clearResizeTimers();
17339
- window.removeEventListener("resize", handleResize);
18282
+ if (raf != null) cancelAnimationFrame(raf);
18283
+ ro.disconnect();
17340
18284
  };
17341
18285
  }, [pathname, isEditMode, postToParent2]);
17342
18286
  useEffect13(() => {
@@ -17577,6 +18521,7 @@ function OhhwellsBridge() {
17577
18521
  return;
17578
18522
  }
17579
18523
  const target = e.target;
18524
+ if (target.closest("[data-ohw-ai-review]")) return;
17580
18525
  if (target.closest("[data-ohw-toolbar]")) return;
17581
18526
  if (target.closest("[data-ohw-state-toggle]")) return;
17582
18527
  if (target.closest("[data-ohw-max-badge]")) return;
@@ -17588,6 +18533,9 @@ function OhhwellsBridge() {
17588
18533
  )) {
17589
18534
  return;
17590
18535
  }
18536
+ if (selectedMediaElRef.current && !selectedMediaElRef.current.contains(target) && !target.closest("[data-ohw-media-overlay], [data-ohw-edit-chrome], [data-ohw-item-interaction]")) {
18537
+ clearMediaSelectionRef.current();
18538
+ }
17591
18539
  {
17592
18540
  const formEl = getFormElement(target);
17593
18541
  const onSuccessText = target.closest(`[${SUCCESS_TEXT_ATTR}]`);
@@ -17739,19 +18687,14 @@ function OhhwellsBridge() {
17739
18687
  }
17740
18688
  const clickedButton = findClosestButtonLike(target);
17741
18689
  const buttonOnMedia = Boolean(clickedButton && isMediaEditable(editable) && editable.contains(clickedButton));
17742
- console.log("[click-debug]", {
17743
- editableType: editable.dataset.ohwEditable,
17744
- editableTag: editable.tagName,
17745
- targetTag: target.tagName,
17746
- clickedButtonTag: clickedButton?.tagName ?? null,
17747
- buttonOnMedia,
17748
- isMediaEditableEditable: isMediaEditable(editable)
17749
- });
17750
18690
  if (isMediaEditable(editable) && !buttonOnMedia) {
17751
18691
  e.preventDefault();
17752
18692
  e.stopPropagation();
17753
- aiSectionApiRef.current?.selectFromElement(editable);
17754
- postToParentRef.current({ type: "ow:image-pick", key: editable.dataset.ohwKey ?? "", elementType: editable.dataset.ohwEditable ?? "image" });
18693
+ if (selectedMediaElRef.current === editable) {
18694
+ postToParentRef.current({ type: "ow:image-pick", key: editable.dataset.ohwKey ?? "", elementType: editable.dataset.ohwEditable ?? "image" });
18695
+ } else {
18696
+ selectMediaElementRef.current(editable);
18697
+ }
17755
18698
  return;
17756
18699
  }
17757
18700
  const socialItem = getSocialItem(editable);
@@ -17770,11 +18713,6 @@ function OhhwellsBridge() {
17770
18713
  const hrefLookupTarget = buttonOnMedia ? clickedButton : editable;
17771
18714
  const hrefCtx = getHrefKeyFromElement(hrefLookupTarget);
17772
18715
  const navAnchor = hrefCtx ? getNavigationItemAnchor(hrefCtx.anchor) : null;
17773
- console.log("[click-debug 2]", {
17774
- hrefLookupTargetTag: hrefLookupTarget.tagName,
17775
- hrefCtx: hrefCtx ? { key: hrefCtx.key } : null,
17776
- navAnchorTag: navAnchor?.tagName ?? null
17777
- });
17778
18716
  if (navAnchor) {
17779
18717
  e.preventDefault();
17780
18718
  e.stopPropagation();
@@ -17892,6 +18830,7 @@ function OhhwellsBridge() {
17892
18830
  };
17893
18831
  const handleDblClick = (e) => {
17894
18832
  const target = e.target;
18833
+ if (target.closest("[data-ohw-ai-review]")) return;
17895
18834
  if (target.closest("[data-ohw-toolbar]")) return;
17896
18835
  if (target.closest("[data-ohw-state-toggle]")) return;
17897
18836
  if (target.closest("[data-ohw-max-badge]")) return;
@@ -17943,6 +18882,9 @@ function OhhwellsBridge() {
17943
18882
  setHoveredItemRect(null);
17944
18883
  hoveredNavContainerRef.current = null;
17945
18884
  setHoveredNavContainerRect(null);
18885
+ siblingHintElRef.current = null;
18886
+ setSiblingHintRect(null);
18887
+ setSiblingHintRects([]);
17946
18888
  return;
17947
18889
  }
17948
18890
  {
@@ -18061,7 +19003,6 @@ function OhhwellsBridge() {
18061
19003
  hoveredNavContainerRef.current = null;
18062
19004
  setHoveredNavContainerRect(null);
18063
19005
  hoveredItemElRef.current = editable;
18064
- setHoveredItemRect(isSelectedForHoverRef.current(editable) ? null : editable.getBoundingClientRect());
18065
19006
  }
18066
19007
  }
18067
19008
  }
@@ -18358,7 +19299,7 @@ function OhhwellsBridge() {
18358
19299
  }
18359
19300
  };
18360
19301
  const probeImageAt = (clientX, clientY, isDragOver = false, fromParentViewport = false) => {
18361
- if (linkPopoverOpenRef.current) {
19302
+ if (linkPopoverOpenRef.current || floatingPanelOpenRef.current && isPointOverFloatingPanel(clientX, clientY)) {
18362
19303
  if (hoveredImageRef.current) {
18363
19304
  hoveredImageRef.current = null;
18364
19305
  hoveredImageHasTextOverlapRef.current = false;
@@ -18692,7 +19633,9 @@ function OhhwellsBridge() {
18692
19633
  return;
18693
19634
  }
18694
19635
  const { y } = toProbeCoords(clientX, clientY, fromParentViewport);
18695
- const sections = Array.from(document.querySelectorAll("[data-ohw-section]")).sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top);
19636
+ const sections = Array.from(document.querySelectorAll("[data-ohw-section]")).filter(
19637
+ (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
19638
+ ).sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top);
18696
19639
  const ZONE = 20;
18697
19640
  for (let i = 0; i < sections.length; i++) {
18698
19641
  const a = sections[i];
@@ -18721,8 +19664,7 @@ function OhhwellsBridge() {
18721
19664
  };
18722
19665
  const handleMouseMove = (e) => {
18723
19666
  const { clientX, clientY } = e;
18724
- if (pointOwnedByFloatingPanel(clientX, clientY)) return;
18725
- if (isOverEditorChrome(clientX, clientY)) {
19667
+ if (document.documentElement.hasAttribute("data-ohw-panel-dragging") || floatingPanelOpenRef.current && isPointOverFloatingPanel(clientX, clientY) || isOverEditorChrome(clientX, clientY)) {
18726
19668
  document.querySelectorAll("[data-ohw-hovered]").forEach((el) => el.removeAttribute("data-ohw-hovered"));
18727
19669
  formHoverElRef.current = null;
18728
19670
  setFormHoverRect(null);
@@ -18730,6 +19672,12 @@ function OhhwellsBridge() {
18730
19672
  setHoveredItemRect(null);
18731
19673
  hoveredNavContainerRef.current = null;
18732
19674
  setHoveredNavContainerRect(null);
19675
+ siblingHintElRef.current = null;
19676
+ setSiblingHintRect(null);
19677
+ setSiblingHintRects([]);
19678
+ dismissImageHover();
19679
+ clearImageHover();
19680
+ setSectionGap(null);
18733
19681
  return;
18734
19682
  }
18735
19683
  if (probeSocialsRowAt(clientX, clientY)) return;
@@ -18741,7 +19689,11 @@ function OhhwellsBridge() {
18741
19689
  if (e.data?.type !== "ow:pointer-sync") return;
18742
19690
  const { clientX, clientY } = e.data;
18743
19691
  if (typeof clientX !== "number" || typeof clientY !== "number") return;
18744
- if (pointOwnedByFloatingPanel(clientX, clientY)) return;
19692
+ if (document.documentElement.hasAttribute("data-ohw-panel-dragging") || floatingPanelOpenRef.current && isPointOverFloatingPanel(clientX, clientY)) {
19693
+ dismissImageHover();
19694
+ clearImageHover();
19695
+ return;
19696
+ }
18745
19697
  if (probeSocialsRowAt(clientX, clientY)) return;
18746
19698
  probeSectionGapAt(clientX, clientY);
18747
19699
  probeImageAt(clientX, clientY);
@@ -19024,10 +19976,23 @@ function OhhwellsBridge() {
19024
19976
  if (e.data?.type !== "ow:hydrate") return;
19025
19977
  const content = e.data.content;
19026
19978
  if (!content) return;
19979
+ if (typeof content[BRAND_KIT_KEY] === "string") {
19980
+ brandKitRef.current = content[BRAND_KIT_KEY];
19981
+ applyBrandToDom(parseBrandKit(content[BRAND_KIT_KEY]));
19982
+ } else {
19983
+ brandKitRef.current = "";
19984
+ applyBrandToDom(null);
19985
+ }
19027
19986
  if (typeof content[AI_SECTIONS_KEY] === "string") {
19028
19987
  aiSectionsRef.current = content[AI_SECTIONS_KEY];
19988
+ setAiSectionOrder(content[SECTION_ORDER_KEY], window.location.pathname);
19029
19989
  applyAiSectionsToDom(parseAiSectionsState(content[AI_SECTIONS_KEY]));
19030
19990
  }
19991
+ if (typeof content[STYLE_STORE_KEY] === "string") {
19992
+ stylesRef.current = content[STYLE_STORE_KEY];
19993
+ applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
19994
+ }
19995
+ applyBrandChrome(content);
19031
19996
  let sectionsJson = null;
19032
19997
  for (const [key, val] of Object.entries(content)) {
19033
19998
  if (key === "__ohw_sections") {
@@ -19037,6 +20002,9 @@ function OhhwellsBridge() {
19037
20002
  if (key === AI_SECTIONS_KEY) continue;
19038
20003
  if (key === LOGO_PLACEHOLDER_KEY) continue;
19039
20004
  if (LOGO_IMAGE_KEYS.includes(key) && !String(val ?? "").trim()) continue;
20005
+ if (key === BRAND_KIT_KEY) continue;
20006
+ if (key === STYLE_STORE_KEY) continue;
20007
+ if (BRAND_CHROME_KEYS.has(key)) continue;
19040
20008
  if (applyVideoSettingNode(key, val)) continue;
19041
20009
  if (applyCarouselNode(key, val)) continue;
19042
20010
  document.querySelectorAll(`[data-ohw-key="${key}"]`).forEach((el) => {
@@ -19050,6 +20018,8 @@ function OhhwellsBridge() {
19050
20018
  if (video && video.src !== val) applyVideoSrc(video, val);
19051
20019
  } else if (el.dataset.ohwEditable === "link") {
19052
20020
  applyLinkHref(el, val);
20021
+ } else if (el.dataset.ohwEditable === "icon") {
20022
+ applyIconMarkup(el, val);
19053
20023
  } else if (isIconMarkupValue(val)) {
19054
20024
  } else {
19055
20025
  el.innerHTML = val;
@@ -19134,12 +20104,21 @@ function OhhwellsBridge() {
19134
20104
  nodes: collectEditableNodes(editContentRef.current)
19135
20105
  });
19136
20106
  };
20107
+ const clearInteractionChrome = () => {
20108
+ deactivateRef.current();
20109
+ deselectRef.current();
20110
+ clearMediaSelectionRef.current();
20111
+ };
19137
20112
  const handleAiApplyTree = (e) => {
19138
20113
  if (e.data?.type !== "ow:ai-apply-tree") return;
19139
20114
  const payload = e.data.payload;
19140
20115
  if (!payload || typeof payload.id !== "string" || !isRenderableTree(payload.tree)) return;
20116
+ clearInteractionChrome();
19141
20117
  const previous = aiSectionsRef.current;
19142
- const nextState = applyTreeToState(parseAiSectionsState(previous), payload);
20118
+ const nextState = applyTreeToState(parseAiSectionsState(previous), {
20119
+ ...payload,
20120
+ path: payload.path ?? window.location.pathname
20121
+ });
19143
20122
  const nextValue = serializeAiSectionsState(nextState);
19144
20123
  aiSectionsRef.current = nextValue;
19145
20124
  applyAiSectionsToDom(nextState);
@@ -19160,6 +20139,7 @@ function OhhwellsBridge() {
19160
20139
  const exists = document.querySelector(`[data-ohw-section="${CSS.escape(sectionId)}"]`);
19161
20140
  if (!exists) return;
19162
20141
  if (isPageFrameSection(exists)) return;
20142
+ clearInteractionChrome();
19163
20143
  const previous = aiSectionsRef.current;
19164
20144
  const nextState = deleteSectionFromState(parseAiSectionsState(previous), sectionId);
19165
20145
  const nextValue = serializeAiSectionsState(nextState);
@@ -19175,8 +20155,10 @@ function OhhwellsBridge() {
19175
20155
  const handleAiSetSections = (e) => {
19176
20156
  if (e.data?.type !== "ow:ai-set-sections") return;
19177
20157
  const value = typeof e.data.value === "string" ? e.data.value : "";
20158
+ clearInteractionChrome();
19178
20159
  aiSectionsRef.current = value;
19179
20160
  applyAiSectionsToDom(parseAiSectionsState(value));
20161
+ applyStylesToDom(parseStyleStore(stylesRef.current));
19180
20162
  const restoredHeight = document.body.scrollHeight;
19181
20163
  if (restoredHeight > 50) postToParentRef.current({ type: "ow:height", height: restoredHeight });
19182
20164
  postToParentRef.current({ type: "ow:change", nodes: [{ key: AI_SECTIONS_KEY, text: value }] });
@@ -19192,10 +20174,40 @@ function OhhwellsBridge() {
19192
20174
  if (!entries) return;
19193
20175
  const orderJson = JSON.stringify(entries);
19194
20176
  editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: orderJson };
20177
+ setAiSectionOrder(orderJson, window.location.pathname);
19195
20178
  postToParentRef.current({ type: "ow:change", nodes: [{ key: SECTION_ORDER_KEY, text: orderJson }] });
19196
20179
  window.dispatchEvent(new Event("resize"));
19197
20180
  };
19198
20181
  window.addEventListener("message", handleMoveSection);
20182
+ const handleAiSetBrand = (e) => {
20183
+ if (e.data?.type !== "ow:ai-set-brand") return;
20184
+ const value = typeof e.data.value === "string" ? e.data.value : "";
20185
+ const previous = brandKitRef.current;
20186
+ brandKitRef.current = value;
20187
+ applyBrandToDom(parseBrandKit(value));
20188
+ if (aiSectionsRef.current) applyAiSectionsToDom(parseAiSectionsState(aiSectionsRef.current));
20189
+ applyStylesToDom(parseStyleStore(stylesRef.current));
20190
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: BRAND_KIT_KEY, text: value }] });
20191
+ postToParentRef.current({ type: "ow:ai-brand-applied", previous, value });
20192
+ };
20193
+ window.addEventListener("message", handleAiSetBrand);
20194
+ const handleAiSetStyles = (e) => {
20195
+ if (e.data?.type !== "ow:ai-set-styles") return;
20196
+ const value = typeof e.data.value === "string" ? e.data.value : "";
20197
+ const previous = stylesRef.current;
20198
+ stylesRef.current = value;
20199
+ applyStylesToDom(parseStyleStore(value));
20200
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: STYLE_STORE_KEY, text: value }] });
20201
+ postToParentRef.current({ type: "ow:ai-styles-applied", previous, value });
20202
+ };
20203
+ window.addEventListener("message", handleAiSetStyles);
20204
+ const handleGetBrand = (e) => {
20205
+ if (e.data?.type !== "ow:get-brand") return;
20206
+ const template = deriveTemplateBrand();
20207
+ const value = brandKitRef.current || (template ? JSON.stringify(template) : "");
20208
+ postToParentRef.current({ type: "ow:brand-value", value });
20209
+ };
20210
+ window.addEventListener("message", handleGetBrand);
19199
20211
  const handlePanelDragging = (e) => {
19200
20212
  if (e.data?.type !== "ow:panel-dragging") return;
19201
20213
  if (e.data.dragging) document.documentElement.setAttribute("data-ohw-panel-dragging", "");
@@ -19253,8 +20265,15 @@ function OhhwellsBridge() {
19253
20265
  closeLinkPopoverRef.current();
19254
20266
  return;
19255
20267
  }
20268
+ if (floatingPanelOpenRef.current) {
20269
+ setFloatingPanelRef.current(null);
20270
+ deselectRef.current();
20271
+ deactivateRef.current();
20272
+ return;
20273
+ }
19256
20274
  deselectRef.current();
19257
20275
  deactivateRef.current();
20276
+ clearMediaSelectionRef.current();
19258
20277
  };
19259
20278
  window.addEventListener("message", handleDeactivate);
19260
20279
  const handleToastAction = (e) => {
@@ -19340,6 +20359,10 @@ function OhhwellsBridge() {
19340
20359
  const handleKeyDown = (e) => {
19341
20360
  if (e.key === "Escape" && document.querySelector("[data-ohw-section-picker]")) return;
19342
20361
  if (e.key === "Escape" && document.querySelector("[data-ohw-more-menu]")) return;
20362
+ if (e.key === "Escape" && selectedMediaElRef.current) {
20363
+ clearMediaSelectionRef.current();
20364
+ return;
20365
+ }
19343
20366
  if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "a" && activeElRef.current) {
19344
20367
  e.preventDefault();
19345
20368
  selectAllTextInEditable(activeElRef.current);
@@ -19499,6 +20522,12 @@ function OhhwellsBridge() {
19499
20522
  if (aiSectionsRef.current) {
19500
20523
  nodes.push({ key: AI_SECTIONS_KEY, type: "sections", text: aiSectionsRef.current });
19501
20524
  }
20525
+ if (stylesRef.current) {
20526
+ nodes.push({ key: STYLE_STORE_KEY, type: "styles", text: stylesRef.current });
20527
+ }
20528
+ if (brandKitRef.current) {
20529
+ nodes.push({ key: BRAND_KIT_KEY, type: "brand", text: brandKitRef.current });
20530
+ }
19502
20531
  document.querySelectorAll('[data-ohw-editable="form"]').forEach((form) => {
19503
20532
  const formKey = formKeyOf(form);
19504
20533
  if (!formKey) return;
@@ -19516,8 +20545,12 @@ function OhhwellsBridge() {
19516
20545
  if (inserted) {
19517
20546
  const tracker = getSectionsTracker();
19518
20547
  postToParentRef.current({ type: "ow:change", nodes: [{ key: "__ohw_sections", text: tracker.textContent ?? "[]" }] });
19519
- const h = document.body.scrollHeight;
19520
- if (h > 50) postToParentRef.current({ type: "ow:height", height: h });
20548
+ const reportHeight = () => {
20549
+ const h = document.body.scrollHeight;
20550
+ if (h > 50) postToParentRef.current({ type: "ow:height", height: h });
20551
+ };
20552
+ reportHeight();
20553
+ setTimeout(reportHeight, 500);
19521
20554
  }
19522
20555
  };
19523
20556
  const handleSwitchSchedule = (e) => {
@@ -19914,13 +20947,16 @@ function OhhwellsBridge() {
19914
20947
  window.removeEventListener("message", handleAiDeleteSection);
19915
20948
  window.removeEventListener("message", handleAiSetSections);
19916
20949
  window.removeEventListener("message", handleMoveSection);
20950
+ window.removeEventListener("message", handleAiSetBrand);
20951
+ window.removeEventListener("message", handleAiSetStyles);
20952
+ window.removeEventListener("message", handleGetBrand);
19917
20953
  window.removeEventListener("message", handlePanelDragging);
19918
20954
  window.removeEventListener("message", handleDeleteSection);
19919
20955
  window.removeEventListener("message", handleDeactivate);
19920
- document.documentElement.removeAttribute("data-ohw-panel-dragging");
19921
20956
  window.removeEventListener("message", handleToastAction);
19922
20957
  window.removeEventListener("message", handleFormCount);
19923
20958
  window.removeEventListener("message", handleUiEscape);
20959
+ document.documentElement.removeAttribute("data-ohw-panel-dragging");
19924
20960
  autoSaveTimers.current.forEach(clearTimeout);
19925
20961
  autoSaveTimers.current.clear();
19926
20962
  if (imageUnhoverTimerRef.current) clearTimeout(imageUnhoverTimerRef.current);
@@ -20123,7 +21159,7 @@ function OhhwellsBridge() {
20123
21159
  postToParent2({
20124
21160
  type: "ow:ready",
20125
21161
  version: "1",
20126
- bridgeVersion: "0.1.77",
21162
+ bridgeVersion: "0.1.79",
20127
21163
  path: pathname,
20128
21164
  nodes: collectEditableNodes(editContentRef.current),
20129
21165
  sections
@@ -20530,11 +21566,22 @@ function OhhwellsBridge() {
20530
21566
  const showEditLink = toolbarShowEditLink;
20531
21567
  const currentSections = sectionsByPath[pathname] ?? [];
20532
21568
  linkPopoverOpenRef.current = linkPopover !== null;
21569
+ const handleMediaSelect = useCallback8((key) => {
21570
+ const el = hoveredImageRef.current?.dataset.ohwKey === key ? hoveredImageRef.current : Array.from(document.querySelectorAll(MEDIA_SELECTOR)).find(
21571
+ (m) => (m.dataset.ohwKey ?? "") === key
21572
+ ) ?? null;
21573
+ if (!el) return;
21574
+ selectMediaElementRef.current(el);
21575
+ }, []);
20533
21576
  const handleMediaReplace = useCallback8(
20534
21577
  (key) => {
20535
- postToParent2({ type: "ow:image-pick", key, elementType: mediaHover?.elementType ?? "image" });
21578
+ postToParent2({
21579
+ type: "ow:image-pick",
21580
+ key,
21581
+ elementType: mediaHover?.elementType ?? selectedMedia?.elementType ?? "image"
21582
+ });
20536
21583
  },
20537
- [postToParent2, mediaHover?.elementType]
21584
+ [postToParent2, mediaHover?.elementType, selectedMedia?.elementType]
20538
21585
  );
20539
21586
  const handleEditCarousel = useCallback8(
20540
21587
  (key) => {
@@ -20575,6 +21622,7 @@ function OhhwellsBridge() {
20575
21622
  return /* @__PURE__ */ jsxs20(Fragment8, { children: [
20576
21623
  /* @__PURE__ */ jsx33("div", { id: "ohw-loader", suppressHydrationWarning: true, style: { ...OHW_LOADER_STYLE, display: "none" }, children: /* @__PURE__ */ jsx33(OhwLoaderSpinner, {}) }),
20577
21624
  /* @__PURE__ */ jsx33("script", { suppressHydrationWarning: true, dangerouslySetInnerHTML: { __html: OHW_LOADER_PREHYDRATE_SCRIPT } }),
21625
+ subdomain && !isEditMode && showBranding && /* @__PURE__ */ jsx33(MadeWithOhhWells, {}),
20578
21626
  bridgeRoot ? createPortal2(
20579
21627
  /* @__PURE__ */ jsxs20(Fragment8, { children: [
20580
21628
  /* @__PURE__ */ jsx33("div", { ref: attachVisibleViewport, "data-ohw-visible-viewport": "", "aria-hidden": true }),
@@ -20606,12 +21654,25 @@ function OhhwellsBridge() {
20606
21654
  },
20607
21655
  `uploading-${key}`
20608
21656
  )),
20609
- mediaHover && !(mediaHover.key in uploadingRects) && /* @__PURE__ */ jsx33(
21657
+ mediaHover && !(mediaHover.key in uploadingRects) && mediaHover.key !== selectedMedia?.key && /* @__PURE__ */ jsx33(
20610
21658
  MediaOverlay,
20611
21659
  {
20612
21660
  hover: mediaHover,
20613
21661
  isUploading: false,
20614
21662
  onReplace: handleMediaReplace,
21663
+ onSelect: handleMediaSelect,
21664
+ onVideoSettingsChange: handleVideoSettingsChange
21665
+ }
21666
+ ),
21667
+ selectedMedia && !(selectedMedia.key in uploadingRects) && !linkPopover && /* @__PURE__ */ jsx33(
21668
+ MediaOverlay,
21669
+ {
21670
+ hover: selectedMedia,
21671
+ selected: true,
21672
+ hovered: mediaHover?.key === selectedMedia.key,
21673
+ isUploading: false,
21674
+ onReplace: handleMediaReplace,
21675
+ onSelect: handleMediaSelect,
20615
21676
  onVideoSettingsChange: handleVideoSettingsChange
20616
21677
  }
20617
21678
  ),
@@ -21017,6 +22078,59 @@ function OhhwellsBridge() {
21017
22078
  ) : null
21018
22079
  ] });
21019
22080
  }
22081
+
22082
+ // src/ui/EmptySection.tsx
22083
+ import Link3 from "next/link";
22084
+ import { Fragment as Fragment9, jsx as jsx34, jsxs as jsxs21 } from "react/jsx-runtime";
22085
+ function EmptySection({ title, homeHref = "/", eyebrowKey, titleKey, subtitleKey }) {
22086
+ return /* @__PURE__ */ jsxs21(Fragment9, { children: [
22087
+ /* @__PURE__ */ jsx34(
22088
+ "p",
22089
+ {
22090
+ style: {
22091
+ fontFamily: "var(--brand-font-body)",
22092
+ fontSize: "0.75rem",
22093
+ fontWeight: 500,
22094
+ letterSpacing: "0.15em",
22095
+ textTransform: "uppercase",
22096
+ color: "var(--brand-accent)",
22097
+ marginBottom: "1.5rem"
22098
+ },
22099
+ children: /* @__PURE__ */ jsx34(Link3, { href: homeHref, style: { color: "inherit" }, children: /* @__PURE__ */ jsx34("span", { ...eyebrowKey ? { "data-ohw-editable": "text", "data-ohw-key": eyebrowKey } : {}, children: "Home" }) })
22100
+ }
22101
+ ),
22102
+ /* @__PURE__ */ jsx34(
22103
+ "h1",
22104
+ {
22105
+ style: {
22106
+ fontFamily: "var(--brand-font-heading)",
22107
+ fontSize: "clamp(2rem, 3.5vw, 2.75rem)",
22108
+ lineHeight: 1.1,
22109
+ letterSpacing: "-0.025em",
22110
+ color: "var(--brand-text)",
22111
+ marginBottom: "1rem"
22112
+ },
22113
+ ...titleKey ? { "data-ohw-editable": "text", "data-ohw-key": titleKey, "data-ohw-max-length": 80 } : {},
22114
+ children: title
22115
+ }
22116
+ ),
22117
+ /* @__PURE__ */ jsx34(
22118
+ "p",
22119
+ {
22120
+ style: {
22121
+ fontFamily: "var(--brand-font-body)",
22122
+ fontSize: "1rem",
22123
+ lineHeight: 1.7,
22124
+ fontWeight: 300,
22125
+ color: "var(--brand-text-muted)",
22126
+ maxWidth: "340px"
22127
+ },
22128
+ ...subtitleKey ? { "data-ohw-editable": "text", "data-ohw-key": subtitleKey, "data-ohw-max-length": 160 } : {},
22129
+ children: "This page doesn't have any content yet."
22130
+ }
22131
+ )
22132
+ ] });
22133
+ }
21020
22134
  export {
21021
22135
  AI_DEFAULT_BRAND,
21022
22136
  AI_TREE_SCHEMA_VERSIONS,
@@ -21033,6 +22147,7 @@ export {
21033
22147
  DropdownMenuItem,
21034
22148
  DropdownMenuSeparator,
21035
22149
  DropdownMenuTrigger,
22150
+ EmptySection,
21036
22151
  ItemActionToolbar,
21037
22152
  ItemInteractionLayer,
21038
22153
  LinkEditorPanel,