@ohhwells/bridge 0.1.78 → 0.1.79

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -83,7 +83,12 @@ function parseAiSectionsState(raw) {
83
83
  media: entry.media && typeof entry.media === "object" ? entry.media : {}
84
84
  }));
85
85
  const removed = Array.isArray(parsed.removed) ? parsed.removed.filter((id) => typeof id === "string" && id.length > 0) : [];
86
- return { v: 1, sections, ...removed.length ? { removed } : {} };
86
+ return {
87
+ v: 1,
88
+ sections,
89
+ ...removed.length ? { removed } : {},
90
+ ...parsed.hideTemplate === true ? { hideTemplate: true } : {}
91
+ };
87
92
  } catch {
88
93
  return EMPTY_AI_SECTIONS;
89
94
  }
@@ -96,6 +101,7 @@ function applyTreeToState(state, payload) {
96
101
  const entry = {
97
102
  id: payload.id,
98
103
  label: payload.label ?? "Generated section",
104
+ ...typeof payload.path === "string" && payload.path ? { path: payload.path } : {},
99
105
  afterSection: payload.mode === "insert" && !insertBefore ? payload.targetSectionId ?? null : null,
100
106
  ...insertBefore && payload.targetSectionId ? { beforeSection: payload.targetSectionId } : {},
101
107
  ...payload.mode === "replace" && payload.targetSectionId ? { replaces: payload.targetSectionId } : {},
@@ -118,6 +124,317 @@ function deleteSectionFromState(state, sectionId) {
118
124
  return { ...state, v: 1, sections: state.sections, removed: [...removed, sectionId] };
119
125
  }
120
126
 
127
+ // src/lib/brand-chrome.ts
128
+ var BRAND_NAME_KEY = "__ohw_brand_name";
129
+ var BRAND_TITLE_KEY = "__ohw_site_title";
130
+ var BRAND_FAVICON_LETTER_KEY = "__ohw_favicon_letter";
131
+ var BRAND_CHROME_KEYS = /* @__PURE__ */ new Set([
132
+ BRAND_NAME_KEY,
133
+ BRAND_TITLE_KEY,
134
+ BRAND_FAVICON_LETTER_KEY
135
+ ]);
136
+ function upsertMeta(selector, attr, token, value) {
137
+ let el = document.head.querySelector(selector);
138
+ if (!el) {
139
+ el = document.createElement("meta");
140
+ el.setAttribute(attr, token);
141
+ document.head.appendChild(el);
142
+ }
143
+ if (el.getAttribute("content") !== value) el.setAttribute("content", value);
144
+ }
145
+ function escapeXml(value) {
146
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
147
+ }
148
+ function applyLetterFavicon(letter) {
149
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="12" fill="#111827"/><text x="32" y="46" font-family="system-ui,-apple-system,Segoe UI,sans-serif" font-size="40" font-weight="700" text-anchor="middle" fill="#ffffff">${escapeXml(letter)}</text></svg>`;
150
+ const href = `data:image/svg+xml,${encodeURIComponent(svg)}`;
151
+ let link = document.head.querySelector('link[rel="icon"]');
152
+ if (!link) {
153
+ link = document.createElement("link");
154
+ link.rel = "icon";
155
+ document.head.appendChild(link);
156
+ }
157
+ link.type = "image/svg+xml";
158
+ if (link.href !== href) link.href = href;
159
+ }
160
+ function applyBrandChrome(content) {
161
+ const name = content[BRAND_NAME_KEY];
162
+ if (typeof name === "string" && name.length > 0) {
163
+ document.querySelectorAll("[data-ohw-wordmark]").forEach((el) => {
164
+ if (el.textContent !== name) el.textContent = name;
165
+ if (el.getAttribute("title") !== name) el.setAttribute("title", name);
166
+ });
167
+ }
168
+ const title = content[BRAND_TITLE_KEY];
169
+ if (typeof title === "string" && title.length > 0) {
170
+ if (document.title !== title) document.title = title;
171
+ upsertMeta('meta[property="og:title"]', "property", "og:title", title);
172
+ upsertMeta('meta[name="twitter:title"]', "name", "twitter:title", title);
173
+ }
174
+ const letter = content[BRAND_FAVICON_LETTER_KEY];
175
+ if (typeof letter === "string" && letter.length > 0) {
176
+ applyLetterFavicon(letter);
177
+ }
178
+ }
179
+
180
+ // src/lib/brand-kit.ts
181
+ var BRAND_KIT_KEY = "__ohw_brand";
182
+ var BRAND_VAR_PREFIX = "--ohw-brand-";
183
+ var BRAND_VAR_NAMES = ["primary", "accent", "light", "dark", "surface", "border", "muted"].map(
184
+ (role) => `${BRAND_VAR_PREFIX}${role}`
185
+ );
186
+ var FONT_VARS = {
187
+ heading: ["--font-heading", "--font-display", "--brand-font-heading"],
188
+ body: ["--font-body", "--brand-font-body"]
189
+ };
190
+ var BRAND_FONT_LINK_ID = "ohw-brand-fonts";
191
+ function brandColorVars(kit) {
192
+ const { dark, primary, accent, light } = kit.palette;
193
+ const mix = (a, aPct, b) => `color-mix(in srgb, ${a} ${aPct}%, ${b})`;
194
+ return {
195
+ [`${BRAND_VAR_PREFIX}primary`]: primary,
196
+ [`${BRAND_VAR_PREFIX}accent`]: accent,
197
+ [`${BRAND_VAR_PREFIX}light`]: light,
198
+ [`${BRAND_VAR_PREFIX}dark`]: dark,
199
+ [`${BRAND_VAR_PREFIX}surface`]: mix(light, 95, dark),
200
+ [`${BRAND_VAR_PREFIX}border`]: mix(light, 85, dark),
201
+ [`${BRAND_VAR_PREFIX}muted`]: mix(dark, 62, light)
202
+ };
203
+ }
204
+ function parseBrandKit(raw) {
205
+ if (!raw) return null;
206
+ try {
207
+ const parsed = JSON.parse(raw);
208
+ const p = parsed?.palette;
209
+ const f = parsed?.fonts;
210
+ if (!p || !f || typeof p.dark !== "string" || typeof p.primary !== "string" || typeof p.accent !== "string" || typeof p.light !== "string" || typeof f.heading !== "string" || typeof f.body !== "string") {
211
+ return null;
212
+ }
213
+ return {
214
+ palette: { dark: p.dark, primary: p.primary, accent: p.accent, light: p.light },
215
+ fonts: { heading: f.heading, body: f.body }
216
+ };
217
+ } catch {
218
+ return null;
219
+ }
220
+ }
221
+ function familyOf(stack) {
222
+ const first = stack.split(",")[0]?.trim() ?? "";
223
+ return first.replace(/^['"]|['"]$/g, "");
224
+ }
225
+ function loadBrandFonts(families) {
226
+ const unique = [...new Set(families.map((f) => f.trim()).filter(Boolean))];
227
+ if (unique.length === 0) return;
228
+ const spec = unique.map((f) => `${f.replace(/ /g, "+")}:400,500,600,700`).join("|");
229
+ const href = `https://fonts.googleapis.com/css?family=${spec}&display=swap`;
230
+ let link = document.getElementById(BRAND_FONT_LINK_ID);
231
+ if (!link) {
232
+ link = document.createElement("link");
233
+ link.id = BRAND_FONT_LINK_ID;
234
+ link.rel = "stylesheet";
235
+ document.head.appendChild(link);
236
+ }
237
+ if (link.href !== href) link.href = href;
238
+ }
239
+ function applyBrandToDom(kit) {
240
+ const root = document.documentElement;
241
+ if (!kit) {
242
+ for (const name of BRAND_VAR_NAMES) root.style.removeProperty(name);
243
+ for (const name of [...FONT_VARS.heading, ...FONT_VARS.body]) root.style.removeProperty(name);
244
+ document.getElementById(BRAND_FONT_LINK_ID)?.remove();
245
+ return;
246
+ }
247
+ for (const [name, value] of Object.entries(brandColorVars(kit))) root.style.setProperty(name, value);
248
+ for (const name of FONT_VARS.heading) root.style.setProperty(name, kit.fonts.heading);
249
+ for (const name of FONT_VARS.body) root.style.setProperty(name, kit.fonts.body);
250
+ loadBrandFonts([familyOf(kit.fonts.heading), familyOf(kit.fonts.body)]);
251
+ }
252
+
253
+ // src/lib/section-styles.ts
254
+ var STYLE_STORE_KEY = "__ohw_styles";
255
+ var STYLE_SHEET_ID = "ohw-section-styles";
256
+ function parseStyleStore(raw) {
257
+ if (!raw) return null;
258
+ try {
259
+ const parsed = JSON.parse(raw);
260
+ if (parsed?.v !== 1) return null;
261
+ return {
262
+ v: 1,
263
+ sections: typeof parsed.sections === "object" && parsed.sections ? parsed.sections : {},
264
+ nodes: typeof parsed.nodes === "object" && parsed.nodes ? parsed.nodes : {}
265
+ };
266
+ } catch {
267
+ return null;
268
+ }
269
+ }
270
+ var BG_VALUES = {
271
+ surface: "color-mix(in srgb, var(--ohw-brand-light, var(--color-light, #ECECEB)) 94%, var(--ohw-brand-dark, var(--color-dark, #0C0A09)))",
272
+ accent: "var(--ohw-brand-primary, var(--color-primary, #0078E5))",
273
+ "accent-soft": "color-mix(in srgb, var(--ohw-brand-primary, var(--color-primary, #0078E5)) 12%, var(--ohw-brand-light, var(--color-light, #FFFFFF)))"
274
+ };
275
+ var HEADLINE_SIZES = { md: 24, lg: 32, xl: 40, display: 56 };
276
+ function styleSheetCss() {
277
+ const rules = [];
278
+ for (const [tone, value] of Object.entries(BG_VALUES)) {
279
+ rules.push(`[data-ohw-style-bg="${tone}"] { background: ${value} !important; }`);
280
+ }
281
+ rules.push(
282
+ `[data-ohw-style-bg="accent"] { color: var(--ohw-brand-light, var(--color-light, #FFFFFF)) !important; }`
283
+ );
284
+ rules.push(
285
+ `[data-ohw-style-distribution="space-between"] { display: flex !important; flex-direction: column; justify-content: space-between; }`,
286
+ `[data-ohw-style-distribution="center"] { display: flex !important; flex-direction: column; justify-content: center; }`
287
+ );
288
+ for (const [scale, size] of Object.entries(HEADLINE_SIZES)) {
289
+ rules.push(
290
+ `[data-ohw-style-headline="${scale}"] :is(h1, h2, h3) { font-size: ${size}px !important; line-height: 1.15 !important; }`
291
+ );
292
+ }
293
+ rules.push(
294
+ `[data-ohw-style-aspect="fill-height"] img { height: 100% !important; aspect-ratio: auto !important; object-fit: cover; }`
295
+ );
296
+ for (const aspect of ["1:1", "4:5", "3:4", "3:2", "16:9", "2:1", "3:1"]) {
297
+ rules.push(
298
+ `[data-ohw-style-aspect="${aspect.replace(":", "-")}"] img { aspect-ratio: ${aspect.replace(":", " / ")} !important; height: auto !important; object-fit: cover; }`
299
+ );
300
+ }
301
+ const pad = { tight: 40, balanced: 64, airy: 96 };
302
+ for (const [spacing, px] of Object.entries(pad)) {
303
+ rules.push(
304
+ `[data-ohw-style-spacing="${spacing}"] { padding-top: ${px}px !important; padding-bottom: ${px}px !important; }`
305
+ );
306
+ }
307
+ return rules.join("\n");
308
+ }
309
+ var STYLE_FONT_LINK_ID = "ohw-style-fonts";
310
+ function loadStyleFonts(families) {
311
+ const unique = [...new Set(families.map((f) => f.trim()).filter(Boolean))];
312
+ const existing = document.getElementById(STYLE_FONT_LINK_ID);
313
+ if (unique.length === 0) {
314
+ existing?.remove();
315
+ return;
316
+ }
317
+ const spec = unique.map((f) => `${f.replace(/ /g, "+")}:400,500,600,700`).join("|");
318
+ const href = `https://fonts.googleapis.com/css?family=${spec}&display=swap`;
319
+ let link = existing;
320
+ if (!link) {
321
+ link = document.createElement("link");
322
+ link.id = STYLE_FONT_LINK_ID;
323
+ link.rel = "stylesheet";
324
+ document.head.appendChild(link);
325
+ }
326
+ if (link.href !== href) link.href = href;
327
+ }
328
+ var SECTION_ATTRS = {
329
+ sectionBackground: "data-ohw-style-bg",
330
+ textDistribution: "data-ohw-style-distribution",
331
+ headlineScale: "data-ohw-style-headline",
332
+ imageAspect: "data-ohw-style-aspect",
333
+ spacing: "data-ohw-style-spacing"
334
+ };
335
+ var NODE_WROTE_ATTR = "data-ohw-style-node";
336
+ var NODE_PROPS = ["color", "font-family", "font-size", "background"];
337
+ function saveInline(el, prop) {
338
+ const attr = `data-ohw-style-prev-${prop}`;
339
+ if (!el.hasAttribute(attr)) el.setAttribute(attr, el.style.getPropertyValue(prop));
340
+ }
341
+ function restoreInline(el, prop) {
342
+ const attr = `data-ohw-style-prev-${prop}`;
343
+ if (!el.hasAttribute(attr)) return;
344
+ const prev = el.getAttribute(attr) ?? "";
345
+ if (prev) el.style.setProperty(prop, prev);
346
+ else el.style.removeProperty(prop);
347
+ el.removeAttribute(attr);
348
+ }
349
+ function ensureStyleSheet() {
350
+ let el = document.getElementById(STYLE_SHEET_ID);
351
+ if (!el) {
352
+ el = document.createElement("style");
353
+ el.id = STYLE_SHEET_ID;
354
+ document.head.appendChild(el);
355
+ }
356
+ const css = styleSheetCss();
357
+ if (el.textContent !== css) el.textContent = css;
358
+ }
359
+ function clearSectionAttrs(root) {
360
+ for (const attr of Object.values(SECTION_ATTRS)) {
361
+ for (const el of Array.from(root.querySelectorAll(`[${attr}]`))) el.removeAttribute(attr);
362
+ }
363
+ for (const el of Array.from(root.querySelectorAll("[data-ohw-style-bgcolor]"))) {
364
+ restoreInline(el, "background");
365
+ el.removeAttribute("data-ohw-style-bgcolor");
366
+ }
367
+ }
368
+ function clearNodeProps(root) {
369
+ for (const el of Array.from(root.querySelectorAll(`[${NODE_WROTE_ATTR}]`))) {
370
+ const h = el;
371
+ for (const prop of NODE_PROPS) restoreInline(h, prop);
372
+ h.removeAttribute(NODE_WROTE_ATTR);
373
+ }
374
+ }
375
+ function buttonSurfaceOf(el) {
376
+ return el.closest("a, button") ?? el;
377
+ }
378
+ function applyStylesToDom(store) {
379
+ ensureStyleSheet();
380
+ clearSectionAttrs(document);
381
+ clearNodeProps(document);
382
+ loadStyleFonts(
383
+ store ? Object.values(store.nodes).flatMap((n) => n.fontFamily ? [n.fontFamily] : []) : []
384
+ );
385
+ if (!store) return;
386
+ for (const [sectionId, override] of Object.entries(store.sections)) {
387
+ const sections = document.querySelectorAll(
388
+ `[data-ohw-section="${CSS.escape(sectionId)}"]`
389
+ );
390
+ for (const section of Array.from(sections)) {
391
+ for (const [prop, attr] of Object.entries(SECTION_ATTRS)) {
392
+ const value = override[prop];
393
+ if (value === void 0) continue;
394
+ if (prop === "sectionBackground" && override.sectionBackgroundColor !== void 0) continue;
395
+ section.setAttribute(attr, String(value).replace(":", "-"));
396
+ }
397
+ if (override.sectionBackgroundColor !== void 0) {
398
+ saveInline(section, "background");
399
+ section.style.setProperty("background", override.sectionBackgroundColor, "important");
400
+ section.setAttribute("data-ohw-style-bgcolor", "");
401
+ }
402
+ }
403
+ }
404
+ for (const [key, override] of Object.entries(store.nodes)) {
405
+ const nodes = document.querySelectorAll(`[data-ohw-key="${CSS.escape(key)}"]`);
406
+ for (const el of Array.from(nodes)) {
407
+ if (override.color !== void 0) {
408
+ saveInline(el, "color");
409
+ el.style.setProperty("color", override.color, "important");
410
+ el.setAttribute(NODE_WROTE_ATTR, "");
411
+ }
412
+ if (override.fontFamily !== void 0) {
413
+ saveInline(el, "font-family");
414
+ el.style.setProperty("font-family", `'${override.fontFamily}'`, "important");
415
+ el.setAttribute(NODE_WROTE_ATTR, "");
416
+ }
417
+ if (override.fontSize !== void 0) {
418
+ saveInline(el, "font-size");
419
+ el.style.setProperty("font-size", `${override.fontSize}px`, "important");
420
+ el.setAttribute(NODE_WROTE_ATTR, "");
421
+ }
422
+ if (override.buttonBackground !== void 0 || override.buttonText !== void 0) {
423
+ const surface = buttonSurfaceOf(el);
424
+ if (override.buttonBackground !== void 0) {
425
+ saveInline(surface, "background");
426
+ surface.style.setProperty("background", override.buttonBackground, "important");
427
+ }
428
+ if (override.buttonText !== void 0) {
429
+ saveInline(surface, "color");
430
+ surface.style.setProperty("color", override.buttonText, "important");
431
+ }
432
+ surface.setAttribute(NODE_WROTE_ATTR, "");
433
+ }
434
+ }
435
+ }
436
+ }
437
+
121
438
  // src/ui/ai-tree/aiSectionsManager.tsx
122
439
  import { flushSync } from "react-dom";
123
440
  import { createRoot } from "react-dom/client";
@@ -132,7 +449,8 @@ function lucideByName(name) {
132
449
  }
133
450
  var typeStyle = (spec, font) => ({
134
451
  fontFamily: font,
135
- fontSize: spec.size,
452
+ // Headings shrink with the viewport (reaching full size around ~900px wide); body copy stays put.
453
+ fontSize: spec.size >= 24 ? `clamp(${Math.max(18, Math.round(spec.size * 0.6))}px, ${(spec.size / 9).toFixed(2)}vw, ${spec.size}px)` : spec.size,
136
454
  lineHeight: spec.line,
137
455
  fontWeight: spec.weight
138
456
  });
@@ -141,26 +459,63 @@ var featureLines = (value) => value.split(/\n|<br\s*\/?>/i).map((line) => line.t
141
459
  var CHECK_MASK = `data:image/svg+xml,${encodeURIComponent(
142
460
  '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>'
143
461
  )}`;
462
+ var AI_MOBILE_CSS = [
463
+ "@media (max-width: 768px){",
464
+ "[data-ai-section]{overflow-x:hidden}",
465
+ "[data-ai-container]{padding:0 20px !important}",
466
+ "[data-ai-row]{display:flex !important;flex-direction:column !important;align-items:stretch !important}",
467
+ "[data-ai-cell]{width:100%;min-width:0}",
468
+ "[data-ai-grid]{grid-template-columns:1fr !important}",
469
+ // Group containers flatten to a column on phones; span placements come along for free.
470
+ "[data-ai-group]{display:flex !important;flex-direction:column !important}",
471
+ "[data-ai-group] > *{grid-column:auto !important}",
472
+ // The 50:50 form collapses to a single stacked column on phones.
473
+ "[data-ai-form]{grid-template-columns:1fr !important}",
474
+ "[data-ai-section] img{max-width:100%}",
475
+ "}",
476
+ "@media (min-width: 769px) and (max-width: 1024px){",
477
+ "[data-ai-grid]{grid-template-columns:repeat(2, 1fr) !important}",
478
+ "}"
479
+ ].join("");
144
480
  var FEATURE_LINE_CSS = [
145
481
  "[data-ai-features]>div{position:relative;padding-left:40px;min-height:24px}",
146
482
  '[data-ai-features]>div::before{content:"";position:absolute;left:0;top:1px;width:24px;height:24px;',
147
483
  `background-color:currentColor;-webkit-mask:url("${CHECK_MASK}") no-repeat 0 0/24px 24px;`,
148
484
  `mask:url("${CHECK_MASK}") no-repeat 0 0/24px 24px}`
149
485
  ].join("");
486
+ function hexLuminance(color) {
487
+ const m = /^#([0-9a-f]{6})$/i.exec(color.trim());
488
+ if (!m) return null;
489
+ const [r2, g, b] = [0, 2, 4].map((i) => {
490
+ const c = parseInt(m[1].slice(i, i + 2), 16) / 255;
491
+ return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
492
+ });
493
+ return 0.2126 * r2 + 0.7152 * g + 0.0722 * b;
494
+ }
495
+ function hexContrast(a, b) {
496
+ const la = hexLuminance(a);
497
+ const lb = hexLuminance(b);
498
+ if (la === null || lb === null) return null;
499
+ const [hi, lo] = la > lb ? [la, lb] : [lb, la];
500
+ return (hi + 0.05) / (lo + 0.05);
501
+ }
502
+ function accentBandContext(brand) {
503
+ const p = brand.palette;
504
+ const lightWins = (hexContrast(p.light, p.primary) ?? 99) >= (hexContrast(p.dark, p.primary) ?? 0);
505
+ if (lightWins) {
506
+ return {
507
+ brand: { ...brand, palette: { dark: p.light, primary: p.light, accent: p.light, light: p.primary } },
508
+ buttonLabel: p.primary
509
+ };
510
+ }
511
+ return {
512
+ brand: { ...brand, palette: { dark: p.dark, primary: p.dark, accent: p.dark, light: p.light } },
513
+ buttonLabel: p.light
514
+ };
515
+ }
150
516
  function textAttrs(ctx, path) {
151
517
  return ctx.keyFor ? { "data-ohw-key": ctx.keyFor(path), "data-ohw-editable": "text" } : {};
152
518
  }
153
- var AI_RESPONSIVE_CSS = [
154
- "@media (max-width: 960px) {",
155
- " [data-ai-responsive] [data-ai-grid] { grid-template-columns: repeat(2, 1fr) !important; }",
156
- ' [data-ai-responsive] [data-ai-grid="1"] { grid-template-columns: 1fr !important; }',
157
- "}",
158
- "@media (max-width: 640px) {",
159
- " [data-ai-responsive] [data-ai-grid] { grid-template-columns: 1fr !important; }",
160
- " [data-ai-responsive] [data-ai-columns] > * { grid-column: 1 / -1 !important; }",
161
- " [data-ai-responsive] [data-ai-section-inner] { padding-left: 20px !important; padding-right: 20px !important; }",
162
- "}"
163
- ].join("\n");
164
519
  var TRANSPARENT_PX = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
165
520
  function MediaBox({
166
521
  refValue,
@@ -173,17 +528,13 @@ function MediaBox({
173
528
  const url = refValue ? ctx.resolveMedia(refValue) : null;
174
529
  const isIcon = /^(lucide|simple):/.test(refValue);
175
530
  const aspectRatio = aspect && /^\d+:\d+$/.test(aspect) ? aspect.replace(":", " / ") : void 0;
176
- const editAttrs = ctx.keyFor && editPath ? {
177
- "data-ohw-key": ctx.keyFor(editPath),
178
- "data-ohw-editable": isIcon ? "icon" : "image"
179
- } : {};
531
+ const editAttrs = ctx.keyFor && editPath && !isIcon ? { "data-ohw-key": ctx.keyFor(editPath), "data-ohw-editable": "image" } : {};
180
532
  if (isIcon) {
181
533
  const Icon = refValue.startsWith("lucide:") ? lucideByName(refValue.slice(7)) : null;
182
534
  return /* @__PURE__ */ jsx(
183
535
  "span",
184
536
  {
185
537
  "data-ai-icon": refValue,
186
- ...editAttrs,
187
538
  style: {
188
539
  display: "inline-flex",
189
540
  width: 48,
@@ -236,7 +587,7 @@ function ButtonEl({
236
587
  }) {
237
588
  const secondary = slots.variant === "secondary";
238
589
  const href = str(slots.href);
239
- const linkAttrs = ctx.keyFor && path ? { "data-ohw-href-key": ctx.keyFor(`${path}.href`) } : {};
590
+ const linkAttrs = ctx.keyFor && path ? { "data-ohw-href-key": ctx.keyFor(`${path}.href`), "data-ohw-role": "button" } : {};
240
591
  return /* @__PURE__ */ jsx(
241
592
  "a",
242
593
  {
@@ -252,7 +603,7 @@ function ButtonEl({
252
603
  textDecoration: "none",
253
604
  cursor: "pointer",
254
605
  ...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 }
606
+ ...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
607
  },
257
608
  children: /* @__PURE__ */ jsx("span", { ...path ? textAttrs(ctx, `${path}.label`) : {}, children: str(slots.label) })
258
609
  }
@@ -758,7 +1109,24 @@ function CardBlock({ node, ctx, path }) {
758
1109
  minWidth: 0
759
1110
  },
760
1111
  children: [
761
- media && (horizontal ? /* @__PURE__ */ jsx("div", { style: { flex: 1, minWidth: 0, display: "flex", alignItems: "center", padding: mediaInset }, children: media }) : /* @__PURE__ */ jsx(
1112
+ media && (horizontal ? /* @__PURE__ */ jsx(
1113
+ "div",
1114
+ {
1115
+ style: (
1116
+ // An icon hugs its glyph — flex:1 gave a 48px icon half the card and pushed the
1117
+ // text to the far side. Photos keep the half-and-half split. The inset has no
1118
+ // inner padding (the photo split absorbed that), so the icon carries its own gap.
1119
+ /^(lucide|simple):/.test(mediaRef) ? {
1120
+ flexShrink: 0,
1121
+ display: "flex",
1122
+ alignItems: "center",
1123
+ padding: mediaInset,
1124
+ [mediaPosition === "right" ? "marginLeft" : "marginRight"]: 20
1125
+ } : { flex: 1, minWidth: 0, display: "flex", alignItems: "center", padding: mediaInset }
1126
+ ),
1127
+ children: media
1128
+ }
1129
+ ) : /* @__PURE__ */ jsx(
762
1130
  "div",
763
1131
  {
764
1132
  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 +1217,44 @@ function AccordionBlock({ node, ctx, path }) {
849
1217
  ) })
850
1218
  ] }, i)) });
851
1219
  }
1220
+ function useIsMobile() {
1221
+ const [mobile, setMobile] = React.useState(
1222
+ () => typeof window !== "undefined" && window.matchMedia("(max-width: 768px)").matches
1223
+ );
1224
+ React.useEffect(() => {
1225
+ const mq = window.matchMedia("(max-width: 768px)");
1226
+ const update = () => setMobile(mq.matches);
1227
+ update();
1228
+ mq.addEventListener("change", update);
1229
+ return () => mq.removeEventListener("change", update);
1230
+ }, []);
1231
+ return mobile;
1232
+ }
852
1233
  function Carousel({ items, itemsPerRow, ctx }) {
1234
+ const isMobile = useIsMobile();
1235
+ const perPage = isMobile ? 1 : itemsPerRow;
1236
+ const pages = Math.max(1, Math.ceil(items.length / perPage));
853
1237
  const [page, setPage] = React.useState(0);
854
- const pages = Math.max(1, Math.ceil(items.length / itemsPerRow));
855
1238
  const current = Math.min(page, pages - 1);
1239
+ if (pages <= 1) {
1240
+ const cols = Math.max(1, Math.min(items.length, itemsPerRow));
1241
+ return /* @__PURE__ */ jsx(
1242
+ "div",
1243
+ {
1244
+ "data-ai-grid": String(cols),
1245
+ style: {
1246
+ display: "grid",
1247
+ gridTemplateColumns: `repeat(${cols}, 1fr)`,
1248
+ gap: AI_TREE_TOKENS.spacing8,
1249
+ alignItems: "start"
1250
+ },
1251
+ children: items
1252
+ }
1253
+ );
1254
+ }
856
1255
  const pageGroups = Array.from(
857
1256
  { length: pages },
858
- (_, p) => items.slice(p * itemsPerRow, (p + 1) * itemsPerRow)
1257
+ (_, p) => items.slice(p * perPage, (p + 1) * perPage)
859
1258
  );
860
1259
  const chrome = (enabled) => ({
861
1260
  border: `1px solid ${ctx.brand.palette.dark}`,
@@ -880,55 +1279,69 @@ function Carousel({ items, itemsPerRow, ctx }) {
880
1279
  cursor: "pointer",
881
1280
  padding: 0
882
1281
  });
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(
1282
+ const viewport = /* @__PURE__ */ jsx("div", { style: { flex: isMobile ? "0 0 auto" : 1, minWidth: 0, width: "100%", overflow: "hidden" }, children: /* @__PURE__ */ jsx(
1283
+ "div",
1284
+ {
1285
+ style: {
1286
+ display: "flex",
1287
+ transform: `translateX(-${current * 100}%)`,
1288
+ transition: "transform 0.4s ease"
1289
+ },
1290
+ children: pageGroups.map((group, p) => /* @__PURE__ */ jsx(
896
1291
  "div",
897
1292
  {
1293
+ "data-ai-grid": String(perPage),
898
1294
  style: {
899
- display: "flex",
900
- transform: `translateX(-${current * 100}%)`,
901
- transition: "transform 0.4s ease"
1295
+ flex: "0 0 100%",
1296
+ display: "grid",
1297
+ gridTemplateColumns: `repeat(${perPage}, 1fr)`,
1298
+ gap: AI_TREE_TOKENS.spacing8,
1299
+ alignItems: "start"
902
1300
  },
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
- )
1301
+ children: group
1302
+ },
1303
+ p
1304
+ ))
1305
+ }
1306
+ ) });
1307
+ const prevBtn = /* @__PURE__ */ jsx(
1308
+ "button",
1309
+ {
1310
+ type: "button",
1311
+ "aria-label": "Previous",
1312
+ onClick: () => setPage((p) => Math.max(0, p - 1)),
1313
+ style: chrome(current > 0),
1314
+ children: /* @__PURE__ */ jsx(ArrowLeft, { size: 24, strokeWidth: 1.5, "aria-hidden": true })
1315
+ }
1316
+ );
1317
+ const nextBtn = /* @__PURE__ */ jsx(
1318
+ "button",
1319
+ {
1320
+ type: "button",
1321
+ "aria-label": "Next",
1322
+ onClick: () => setPage((p) => Math.min(pages - 1, p + 1)),
1323
+ style: chrome(current < pages - 1),
1324
+ children: /* @__PURE__ */ jsx(ArrowRight, { size: 24, strokeWidth: 1.5, "aria-hidden": true })
1325
+ }
1326
+ );
1327
+ 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)) });
1328
+ if (isMobile) {
1329
+ return /* @__PURE__ */ jsxs("div", { "data-ai-carousel": "", style: { display: "flex", flexDirection: "column", gap: AI_TREE_TOKENS.spacing6, alignItems: "center" }, children: [
1330
+ viewport,
1331
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: AI_TREE_TOKENS.spacing6, justifyContent: "center" }, children: [
1332
+ prevBtn,
1333
+ nextBtn
1334
+ ] }),
1335
+ dots
1336
+ ] });
1337
+ }
1338
+ return /* @__PURE__ */ jsxs("div", { "data-ai-carousel": "", style: { display: "flex", flexDirection: "column", gap: AI_TREE_TOKENS.spacing8, alignItems: "center" }, children: [
1339
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: AI_TREE_TOKENS.spacing6, width: "100%" }, children: [
1340
+ prevBtn,
1341
+ viewport,
1342
+ nextBtn
930
1343
  ] }),
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)) })
1344
+ dots
932
1345
  ] });
933
1346
  }
934
1347
  function CollectionBlock({ node, ctx, path }) {
@@ -1007,7 +1420,7 @@ function CollectionBlock({ node, ctx, path }) {
1007
1420
  return /* @__PURE__ */ jsx(
1008
1421
  "div",
1009
1422
  {
1010
- "data-ai-grid": String(itemsPerRow),
1423
+ "data-ai-grid": "",
1011
1424
  style: {
1012
1425
  display: "grid",
1013
1426
  gridTemplateColumns: `repeat(${itemsPerRow}, 1fr)`,
@@ -1022,6 +1435,49 @@ function renderNode(node, ctx, path) {
1022
1435
  switch (node.type) {
1023
1436
  case "text":
1024
1437
  return /* @__PURE__ */ jsx(TextBlock, { slots, ctx, path });
1438
+ // Layout container: arranges child blocks, contributes no content of its own. `grid` is a
1439
+ // nested 12-column grid the children span (a collage is 3–5 media on it, bottom-aligned so
1440
+ // mixed aspects read as a composition); `split` is exactly two children at a ratio; `stack`
1441
+ // is a column. Children render through this same dispatcher, so edit markers, media
1442
+ // resolution, and copy paths all work unchanged inside a group.
1443
+ case "group": {
1444
+ const layout = str(slots.layout);
1445
+ const gap = slots.spacing === "tight" ? AI_TREE_TOKENS.spacing3 : slots.spacing === "airy" ? AI_TREE_TOKENS.spacing8 : AI_TREE_TOKENS.spacing6;
1446
+ const kids = (node.children ?? []).map((child, i) => /* @__PURE__ */ jsx(
1447
+ "div",
1448
+ {
1449
+ style: layout === "grid" ? {
1450
+ gridColumn: `span ${typeof child.span === "number" ? Math.min(12, Math.max(1, child.span)) : 12}`,
1451
+ minWidth: 0
1452
+ } : { minWidth: 0 },
1453
+ children: renderNode(child, ctx, `${path}.c${i}`)
1454
+ },
1455
+ i
1456
+ ));
1457
+ if (layout === "grid") {
1458
+ return /* @__PURE__ */ jsx(
1459
+ "div",
1460
+ {
1461
+ "data-ai-group": "grid",
1462
+ style: { display: "grid", gridTemplateColumns: "repeat(12, 1fr)", gap, alignItems: "end" },
1463
+ children: kids
1464
+ }
1465
+ );
1466
+ }
1467
+ if (layout === "split") {
1468
+ const ratio = str(slots.ratio);
1469
+ const cols = ratio === "3:5" ? "3fr 5fr" : ratio === "5:3" ? "5fr 3fr" : "1fr 1fr";
1470
+ return /* @__PURE__ */ jsx(
1471
+ "div",
1472
+ {
1473
+ "data-ai-group": "split",
1474
+ style: { display: "grid", gridTemplateColumns: cols, gap, alignItems: "center" },
1475
+ children: kids
1476
+ }
1477
+ );
1478
+ }
1479
+ return /* @__PURE__ */ jsx("div", { "data-ai-group": "stack", style: { display: "flex", flexDirection: "column", gap }, children: kids });
1480
+ }
1025
1481
  case "button":
1026
1482
  return /* @__PURE__ */ jsx(ButtonEl, { slots, ctx, path });
1027
1483
  case "button-row":
@@ -1102,33 +1558,102 @@ function renderNode(node, ctx, path) {
1102
1558
  }
1103
1559
  );
1104
1560
  }
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",
1561
+ case "form": {
1562
+ const formAttrs = ctx.keyFor ? {
1563
+ "data-ohw-editable": "form",
1564
+ "data-ohw-key": ctx.keyFor(`${path}.form`),
1565
+ "data-ohw-success-text": "Thanks \u2014 we'll be in touch shortly."
1566
+ } : {};
1567
+ const fieldStyle = {
1568
+ width: "100%",
1569
+ boxSizing: "border-box",
1570
+ border: `1px solid ${ctx.brand.palette.accent}`,
1571
+ borderRadius: AI_TREE_TOKENS.radiusButton,
1572
+ padding: "12px 14px",
1573
+ background: "#fff",
1574
+ color: ctx.brand.palette.dark,
1575
+ outline: "none",
1576
+ ...typeStyle(AI_TREE_TOKENS.type.bodyM, ctx.brand.fonts.body)
1577
+ };
1578
+ const labelStyle = {
1579
+ ...typeStyle(AI_TREE_TOKENS.type.bodyMBold, ctx.brand.fonts.body),
1580
+ color: ctx.brand.palette.dark
1581
+ };
1582
+ return /* @__PURE__ */ jsx(
1583
+ "form",
1584
+ {
1585
+ ...formAttrs,
1586
+ "data-ai-form": "",
1587
+ style: {
1588
+ display: "grid",
1589
+ gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
1590
+ columnGap: 64,
1591
+ rowGap: AI_TREE_TOKENS.spacing4
1592
+ },
1593
+ children: (node.children ?? []).map((child, i) => {
1594
+ if (child.type === "input") {
1595
+ const cs2 = child.slots ?? {};
1596
+ const kind = str(cs2.kind);
1597
+ const label = str(cs2.label);
1598
+ const placeholder = str(cs2.placeholder);
1599
+ const name = label.toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-+|-+$/gu, "") || `field-${i}`;
1600
+ const isTextarea = kind === "textarea";
1601
+ return /* @__PURE__ */ jsxs(
1602
+ "div",
1603
+ {
1604
+ style: {
1605
+ display: "flex",
1606
+ flexDirection: "column",
1607
+ gap: 8,
1608
+ ...isTextarea ? { gridColumn: "1 / -1", maxWidth: 780 } : {}
1609
+ },
1610
+ children: [
1611
+ /* @__PURE__ */ jsx("label", { ...textAttrs(ctx, `${path}.c${i}.label`), style: labelStyle, children: label }),
1612
+ isTextarea ? /* @__PURE__ */ jsx(
1613
+ "textarea",
1614
+ {
1615
+ name,
1616
+ placeholder,
1617
+ style: { ...fieldStyle, height: 140, resize: "vertical" }
1618
+ }
1619
+ ) : /* @__PURE__ */ jsx(
1620
+ "input",
1621
+ {
1622
+ name,
1623
+ type: kind === "email" ? "email" : "text",
1624
+ placeholder,
1625
+ style: { ...fieldStyle, height: 48 }
1626
+ }
1627
+ )
1628
+ ]
1629
+ },
1630
+ i
1631
+ );
1632
+ }
1633
+ const cs = child.slots ?? {};
1634
+ return /* @__PURE__ */ jsx(
1635
+ "button",
1120
1636
  {
1637
+ type: "submit",
1121
1638
  style: {
1122
- border: `1px solid ${ctx.brand.palette.accent}`,
1639
+ gridColumn: "1 / -1",
1640
+ justifySelf: "start",
1641
+ border: "none",
1642
+ cursor: "pointer",
1643
+ padding: `${AI_TREE_TOKENS.spacing3}px ${AI_TREE_TOKENS.spacing6}px`,
1123
1644
  borderRadius: AI_TREE_TOKENS.radiusButton,
1124
- height: cs.kind === "textarea" ? 96 : 42
1125
- }
1126
- }
1127
- )
1128
- ] }, i);
1645
+ background: ctx.brand.palette.primary,
1646
+ color: AI_TREE_TOKENS.textPrimaryForeground,
1647
+ ...typeStyle(AI_TREE_TOKENS.type.button, ctx.brand.fonts.body)
1648
+ },
1649
+ children: /* @__PURE__ */ jsx("span", { ...textAttrs(ctx, `${path}.c${i}.label`), children: str(cs.label) })
1650
+ },
1651
+ i
1652
+ );
1653
+ })
1129
1654
  }
1130
- return /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(ButtonEl, { slots: child.slots ?? {}, ctx, path: `${path}.c${i}` }) }, i);
1131
- }) });
1655
+ );
1656
+ }
1132
1657
  case "schedule-widget":
1133
1658
  return /* @__PURE__ */ jsx(
1134
1659
  "div",
@@ -1154,11 +1679,14 @@ function AiTreeRenderer({ tree, brand, resolveMedia, editKeyPrefix }) {
1154
1679
  return null;
1155
1680
  }
1156
1681
  const resolvedBrand = brand ?? AI_DEFAULT_BRAND;
1682
+ const band = (tree.settings ?? {}).sectionBackground === "accent" ? accentBandContext(resolvedBrand) : null;
1683
+ const blockBrand = band?.brand ?? resolvedBrand;
1157
1684
  const ctx = {
1158
- brand: resolvedBrand,
1685
+ brand: blockBrand,
1159
1686
  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
1687
+ cardSurface: blockBrand.palette.light.toUpperCase() === AI_DEFAULT_BRAND.palette.light.toUpperCase() ? "#ECECEB" : `color-mix(in srgb, ${blockBrand.palette.light} 90%, ${blockBrand.palette.dark})`,
1688
+ keyFor: editKeyPrefix ? (path) => `${editKeyPrefix}.${path}` : null,
1689
+ ...band ? { buttonLabel: band.buttonLabel } : {}
1162
1690
  };
1163
1691
  const settings = tree.settings ?? {};
1164
1692
  const pad = AI_TREE_TOKENS.sectionPadding[settings.spacing ?? "balanced"];
@@ -1166,27 +1694,41 @@ function AiTreeRenderer({ tree, brand, resolveMedia, editKeyPrefix }) {
1166
1694
  const overlayAlpha = { low: 0.25, medium: 0.45, high: 0.65 }[settings.overlayIntensity ?? "medium"];
1167
1695
  const backgroundUrl = isOverlay && settings.backgroundMedia ? ctx.resolveMedia(settings.backgroundMedia) : null;
1168
1696
  const bgAttrs = ctx.keyFor && isOverlay && settings.backgroundMedia ? { "data-ohw-key": ctx.keyFor("settings.backgroundMedia"), "data-ohw-editable": "bg-image" } : {};
1697
+ const toneBackground = (() => {
1698
+ const { dark, primary, light } = resolvedBrand.palette;
1699
+ switch (settings.sectionBackground) {
1700
+ case "surface":
1701
+ return `color-mix(in srgb, ${light} 94%, ${dark})`;
1702
+ case "accent":
1703
+ return primary;
1704
+ case "accent-soft":
1705
+ return `color-mix(in srgb, ${primary} 12%, ${light})`;
1706
+ default:
1707
+ return void 0;
1708
+ }
1709
+ })();
1710
+ const distributed = !isOverlay && settings.textDistribution;
1169
1711
  return /* @__PURE__ */ jsxs(
1170
1712
  "section",
1171
1713
  {
1172
1714
  "data-ai-section": tree.tag ?? "",
1173
1715
  ...bgAttrs,
1174
- "data-ai-responsive": "",
1175
1716
  style: {
1176
1717
  position: "relative",
1177
1718
  padding: `${pad}px 0`,
1178
- background: isOverlay && !backgroundUrl ? "#DBEAFE" : void 0,
1719
+ background: toneBackground ?? (isOverlay && !backgroundUrl ? "#DBEAFE" : void 0),
1179
1720
  backgroundImage: backgroundUrl ? `url(${backgroundUrl})` : void 0,
1180
1721
  backgroundSize: "cover",
1181
- backgroundPosition: "center"
1722
+ backgroundPosition: "center",
1723
+ color: settings.sectionBackground === "accent" ? blockBrand.palette.dark : void 0
1182
1724
  },
1183
1725
  children: [
1184
- /* @__PURE__ */ jsx("style", { children: AI_RESPONSIVE_CSS }),
1185
1726
  isOverlay && backgroundUrl && /* @__PURE__ */ jsx("div", { style: { position: "absolute", inset: 0, background: `rgba(255,255,255,${overlayAlpha})` } }),
1727
+ /* @__PURE__ */ jsx("style", { children: AI_MOBILE_CSS }),
1186
1728
  /* @__PURE__ */ jsx(
1187
1729
  "div",
1188
1730
  {
1189
- "data-ai-section-inner": "",
1731
+ "data-ai-container": "",
1190
1732
  style: {
1191
1733
  position: "relative",
1192
1734
  maxWidth: AI_TREE_TOKENS.sectionMaxWidth,
@@ -1197,15 +1739,29 @@ function AiTreeRenderer({ tree, brand, resolveMedia, editKeyPrefix }) {
1197
1739
  children: tree.rows.map((row, r2) => /* @__PURE__ */ jsx(
1198
1740
  "div",
1199
1741
  {
1200
- "data-ai-columns": "",
1742
+ "data-ai-row": "",
1201
1743
  style: {
1202
1744
  display: "grid",
1203
1745
  gridTemplateColumns: "repeat(12, 1fr)",
1204
1746
  gap: AI_TREE_TOKENS.spacing6,
1205
- alignItems: settings.verticalPosition === "top" ? "start" : "center",
1747
+ alignItems: distributed === "space-between" ? "stretch" : (distributed ?? settings.verticalPosition) === "top" ? "start" : "center",
1206
1748
  marginTop: r2 > 0 ? AI_TREE_TOKENS.spacing8 : 0
1207
1749
  },
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))
1750
+ children: (row.blocks ?? []).map((block, b) => /* @__PURE__ */ jsx(
1751
+ "div",
1752
+ {
1753
+ "data-ai-cell": "",
1754
+ style: {
1755
+ gridColumn: `span ${Math.min(12, Math.max(1, block.span))}`,
1756
+ minWidth: 0,
1757
+ // space-between: each column becomes a flex column whose content spreads over
1758
+ // the full row height instead of clumping at the top.
1759
+ ...distributed === "space-between" ? { display: "flex", flexDirection: "column", justifyContent: "space-between" } : {}
1760
+ },
1761
+ children: renderNode(block, ctx, `r${r2}.b${b}`)
1762
+ },
1763
+ b
1764
+ ))
1209
1765
  },
1210
1766
  r2
1211
1767
  ))
@@ -1221,17 +1777,36 @@ import { jsx as jsx2 } from "react/jsx-runtime";
1221
1777
  var CONTAINER_ATTR = "data-ohw-ai-generated";
1222
1778
  var REPLACED_ATTR = "data-ohw-ai-replaced-by";
1223
1779
  var REMOVED_ATTR = "data-ohw-ai-removed";
1780
+ var TEMPLATE_HIDDEN_ATTR = "data-ohw-ai-template-hidden";
1781
+ var CHROME_SECTION_IDS = /* @__PURE__ */ new Set(["navbar", "footer"]);
1782
+ function readRootVar(name) {
1783
+ if (typeof document === "undefined") return "";
1784
+ return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
1785
+ }
1786
+ function deriveBrandOverride() {
1787
+ const dark = readRootVar("--ohw-brand-dark");
1788
+ const primary = readRootVar("--ohw-brand-primary");
1789
+ const light = readRootVar("--ohw-brand-light");
1790
+ if (!dark || !primary || !light) return null;
1791
+ const accent = readRootVar("--ohw-brand-accent");
1792
+ const heading = readRootVar("--font-heading") || readRootVar("--font-display");
1793
+ const body = readRootVar("--font-body");
1794
+ return {
1795
+ palette: { dark, primary, accent: accent || dark, light },
1796
+ fonts: {
1797
+ heading: heading || AI_DEFAULT_BRAND.fonts.heading,
1798
+ body: body || AI_DEFAULT_BRAND.fonts.body
1799
+ }
1800
+ };
1801
+ }
1224
1802
  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");
1803
+ const dark = readRootVar("--color-dark");
1804
+ const primary = readRootVar("--color-primary");
1805
+ const light = readRootVar("--color-light");
1231
1806
  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");
1807
+ const accent = readRootVar("--color-accent");
1808
+ const heading = readRootVar("--font-heading") || readRootVar("--font-display");
1809
+ const body = readRootVar("--font-body");
1235
1810
  return {
1236
1811
  palette: { dark, primary, accent: accent || dark, light },
1237
1812
  fonts: {
@@ -1303,6 +1878,24 @@ function syncRemovedSections(state) {
1303
1878
  }
1304
1879
  }
1305
1880
  }
1881
+ function syncTemplateHidden(state, pageHasSections) {
1882
+ const hide = state.hideTemplate === true && pageHasSections;
1883
+ for (const el of Array.from(document.querySelectorAll(`[${TEMPLATE_HIDDEN_ATTR}]`))) {
1884
+ if (!hide) {
1885
+ el.style.removeProperty("display");
1886
+ el.removeAttribute(TEMPLATE_HIDDEN_ATTR);
1887
+ }
1888
+ }
1889
+ if (!hide) return;
1890
+ for (const el of Array.from(document.querySelectorAll("[data-ohw-section]"))) {
1891
+ if (el.hasAttribute(CONTAINER_ATTR)) continue;
1892
+ if (CHROME_SECTION_IDS.has(el.getAttribute("data-ohw-section") ?? "")) continue;
1893
+ if (el.parentElement?.closest("[data-ohw-section]")) continue;
1894
+ if (el.hasAttribute(REPLACED_ATTR) || el.hasAttribute(REMOVED_ATTR)) continue;
1895
+ el.style.display = "none";
1896
+ el.setAttribute(TEMPLATE_HIDDEN_ATTR, "");
1897
+ }
1898
+ }
1306
1899
  function syncReplacedOriginals(state) {
1307
1900
  for (const el of document.querySelectorAll(`[${REPLACED_ATTR}]`)) {
1308
1901
  const byId = el.getAttribute(REPLACED_ATTR) ?? "";
@@ -1321,10 +1914,63 @@ function syncReplacedOriginals(state) {
1321
1914
  }
1322
1915
  }
1323
1916
  }
1917
+ var sectionOrderIndex = /* @__PURE__ */ new Map();
1918
+ function setAiSectionOrder(raw, currentPath) {
1919
+ const next = /* @__PURE__ */ new Map();
1920
+ if (raw) {
1921
+ try {
1922
+ const entries = JSON.parse(raw);
1923
+ for (const entry of entries) {
1924
+ if (!entry.pagePath || entry.pagePath === currentPath) next.set(entry.instanceId, entry.order);
1925
+ }
1926
+ } catch {
1927
+ }
1928
+ }
1929
+ sectionOrderIndex = next;
1930
+ }
1931
+ function applyExplicitOrder(entries) {
1932
+ if (sectionOrderIndex.size === 0) return entries;
1933
+ return entries.map((entry, index) => ({ entry, index, order: sectionOrderIndex.get(entry.id) })).sort((a, b) => {
1934
+ if (a.order === void 0 && b.order === void 0) return a.index - b.index;
1935
+ if (a.order === void 0) return 1;
1936
+ if (b.order === void 0) return -1;
1937
+ return a.order - b.order;
1938
+ }).map((item) => item.entry);
1939
+ }
1940
+ function orderByChain(sections) {
1941
+ const ids = new Set(sections.map((entry) => entry.id));
1942
+ const after = /* @__PURE__ */ new Map();
1943
+ const roots = [];
1944
+ for (const entry of sections) {
1945
+ const anchor = entry.replaces ?? entry.beforeSection ?? entry.afterSection ?? null;
1946
+ if (anchor && ids.has(anchor)) {
1947
+ const bucket = after.get(anchor);
1948
+ if (bucket) bucket.push(entry);
1949
+ else after.set(anchor, [entry]);
1950
+ } else {
1951
+ roots.push(entry);
1952
+ }
1953
+ }
1954
+ const out = [];
1955
+ const seen = /* @__PURE__ */ new Set();
1956
+ const visit = (entry) => {
1957
+ if (seen.has(entry.id)) return;
1958
+ seen.add(entry.id);
1959
+ out.push(entry);
1960
+ for (const child of after.get(entry.id) ?? []) visit(child);
1961
+ };
1962
+ for (const root of roots) visit(root);
1963
+ return out.length === sections.length ? out : sections;
1964
+ }
1324
1965
  function applyAiSectionsToDom(state, options) {
1325
1966
  if (typeof document === "undefined") return;
1967
+ const brandOverride = deriveBrandOverride();
1326
1968
  const templateBrand = deriveTemplateBrand();
1327
- const activeIds = new Set(state.sections.map((entry) => entry.id));
1969
+ const brandKey = brandOverride ? JSON.stringify(brandOverride) : "";
1970
+ const pagePath = window.location.pathname;
1971
+ const pageSections = state.sections.filter((entry) => (entry.path ?? "/") === pagePath);
1972
+ const activeIds = new Set(pageSections.map((entry) => entry.id));
1973
+ const ordered = state.hideTemplate === true ? applyExplicitOrder(orderByChain(pageSections)) : pageSections;
1328
1974
  for (const [id, section] of mounted) {
1329
1975
  if (!activeIds.has(id)) {
1330
1976
  section.root.unmount();
@@ -1332,8 +1978,8 @@ function applyAiSectionsToDom(state, options) {
1332
1978
  mounted.delete(id);
1333
1979
  }
1334
1980
  }
1335
- for (const entry of state.sections) {
1336
- const serialized = JSON.stringify(entry);
1981
+ for (const entry of ordered) {
1982
+ const serialized = JSON.stringify(entry) + brandKey;
1337
1983
  const existing = mounted.get(entry.id);
1338
1984
  if (existing && existing.serialized === serialized && existing.container.isConnected) {
1339
1985
  continue;
@@ -1358,7 +2004,7 @@ function applyAiSectionsToDom(state, options) {
1358
2004
  AiTreeRenderer,
1359
2005
  {
1360
2006
  tree: entry.tree,
1361
- brand: entry.brand ?? options?.brand ?? templateBrand ?? void 0,
2007
+ brand: brandOverride ?? entry.brand ?? options?.brand ?? templateBrand ?? void 0,
1362
2008
  resolveMedia,
1363
2009
  editKeyPrefix: `ai.${entry.id}`
1364
2010
  }
@@ -1367,8 +2013,20 @@ function applyAiSectionsToDom(state, options) {
1367
2013
  });
1368
2014
  mounted.set(entry.id, { root, container, serialized });
1369
2015
  }
2016
+ if (state.hideTemplate === true) {
2017
+ let prev = null;
2018
+ for (const entry of ordered) {
2019
+ const el = mounted.get(entry.id)?.container;
2020
+ if (!el) continue;
2021
+ if (prev && !(prev.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_FOLLOWING)) {
2022
+ prev.insertAdjacentElement("afterend", el);
2023
+ }
2024
+ prev = el;
2025
+ }
2026
+ }
1370
2027
  syncReplacedOriginals(state);
1371
2028
  syncRemovedSections(state);
2029
+ syncTemplateHidden(state, pageSections.length > 0);
1372
2030
  }
1373
2031
 
1374
2032
  // src/useLinkHrefGuardian.ts
@@ -7067,13 +7725,17 @@ function MediaOverlay({
7067
7725
  hover,
7068
7726
  isUploading,
7069
7727
  fadingOut = false,
7728
+ selected = false,
7729
+ hovered = false,
7070
7730
  onFadeOutComplete,
7071
7731
  onReplace,
7732
+ onSelect,
7072
7733
  onVideoSettingsChange
7073
7734
  }) {
7074
7735
  const { rect } = hover;
7075
7736
  const skeletonRef = React8.useRef(null);
7076
7737
  const isVideo = hover.elementType === "video";
7738
+ const showChrome = !selected || hovered;
7077
7739
  const autoplay = hover.videoAutoplay ?? true;
7078
7740
  const muted = hover.videoMuted ?? true;
7079
7741
  const probeRef = React8.useRef(null);
@@ -7120,7 +7782,7 @@ function MediaOverlay({
7120
7782
  }
7121
7783
  );
7122
7784
  }
7123
- const settingsBar = isVideo && !hover.isDragOver ? /* @__PURE__ */ jsxs7(
7785
+ const settingsBar = isVideo && !hover.isDragOver && showChrome ? /* @__PURE__ */ jsxs7(
7124
7786
  "div",
7125
7787
  {
7126
7788
  "data-ohw-bridge": "",
@@ -7190,10 +7852,12 @@ function MediaOverlay({
7190
7852
  // in-document, pointer-events does it natively. The button below opts back in, so
7191
7853
  // Replace still works.
7192
7854
  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)"
7855
+ // Selected: a firm component ring with no wash, so the image reads as chosen rather
7856
+ // than hovered. Hover keeps the existing tinted preview.
7857
+ boxShadow: selected ? "inset 0 0 0 2px var(--color-primary)" : "inset 0 0 0 1.5px var(--color-primary)",
7858
+ background: selected ? "transparent" : "color-mix(in srgb, var(--color-primary) 20%, transparent)"
7195
7859
  },
7196
- onClick: () => onReplace(hover.key),
7860
+ onClick: () => onSelect && !selected ? onSelect(hover.key) : onReplace(hover.key),
7197
7861
  children: [
7198
7862
  /* @__PURE__ */ jsxs7(
7199
7863
  Button,
@@ -7247,7 +7911,7 @@ function MediaOverlay({
7247
7911
  },
7248
7912
  children: [
7249
7913
  isVideo ? /* @__PURE__ */ jsx15(Film, { size: 14 }) : /* @__PURE__ */ jsx15(ImageIcon, { size: 14 }),
7250
- replaceMode === "full" ? isVideo ? "Replace video" : "Replace image" : null
7914
+ replaceMode === "full" ? isVideo ? "Replace video" : hover.elementType === "bg-image" ? "Replace background" : "Replace image" : null
7251
7915
  ]
7252
7916
  }
7253
7917
  )
@@ -7331,6 +7995,8 @@ function parseSectionsFromRoot(root) {
7331
7995
  const id = el.getAttribute("data-ohw-section") ?? "";
7332
7996
  if (!id || seen.has(id) || LINK_PICKER_EXCLUDED_IDS.has(id)) continue;
7333
7997
  if (el.parentElement?.closest("[data-ohw-section]")) continue;
7998
+ if (el.hasAttribute("data-ohw-ai-template-hidden") || el.hasAttribute("data-ohw-ai-removed") || el.hasAttribute("data-ohw-ai-replaced-by"))
7999
+ continue;
7334
8000
  seen.add(id);
7335
8001
  const label = el.getAttribute("data-ohw-section-label") ?? titleCaseSectionId(id);
7336
8002
  sections.push({ id, label });
@@ -7616,6 +8282,7 @@ function AiSectionOverlay({
7616
8282
  }) {
7617
8283
  const [selectedId, setSelectedId] = useState5(null);
7618
8284
  const [reviewId, setReviewId] = useState5(null);
8285
+ const [reviewButtonsHidden, setReviewButtonsHidden] = useState5(false);
7619
8286
  const reviewIdRef = useRef4(null);
7620
8287
  reviewIdRef.current = reviewId;
7621
8288
  const selectedIdRef = useRef4(null);
@@ -7677,6 +8344,7 @@ function AiSectionOverlay({
7677
8344
  }
7678
8345
  const found = readRect(sectionId) != null;
7679
8346
  setReviewId(found ? sectionId : null);
8347
+ setReviewButtonsHidden(e.data.hideButtons === true);
7680
8348
  postToParent2({ type: "ow:ai-review-started", sectionId, found });
7681
8349
  if (found) {
7682
8350
  document.querySelector(`[data-ohw-instance="${CSS.escape(sectionId)}"]`)?.scrollIntoView({ behavior: "smooth", block: "nearest" });
@@ -7804,13 +8472,16 @@ function AiSectionOverlay({
7804
8472
  border: `2px solid ${PRIMARY2}`,
7805
8473
  borderRadius: edgeAwareRadius(reviewRect),
7806
8474
  zIndex: 2147483200,
7807
- // The veil itself: swallows clicks so the section stays locked until decided.
8475
+ // The veil itself: swallows clicks so the section stays locked until decided. This
8476
+ // stopPropagation only guards the bubble phase; the bridge's capture-phase click
8477
+ // handlers additionally bail on `[data-ohw-ai-review]`, without which a click on
8478
+ // Accept/Discard resolves to the media beneath and opens the file picker.
7808
8479
  background: "rgba(8, 133, 254, 0.04)",
7809
8480
  pointerEvents: "auto",
7810
8481
  cursor: "default"
7811
8482
  },
7812
8483
  onClick: (e) => e.stopPropagation(),
7813
- children: /* @__PURE__ */ jsxs9(
8484
+ children: !reviewButtonsHidden && /* @__PURE__ */ jsxs9(
7814
8485
  "div",
7815
8486
  {
7816
8487
  style: {
@@ -10377,8 +11048,13 @@ function referenceBox(slot) {
10377
11048
  const neighbour = row ? Array.from(row.querySelectorAll('[data-ohw-editable="icon"]')).find(
10378
11049
  (el) => el !== slot && !el.hasAttribute("data-ohw-social-icon-placeholder")
10379
11050
  ) : null;
10380
- const source = (neighbour ?? slot).querySelector(GLYPH_SELECTOR);
10381
- const box = source?.getBoundingClientRect() ?? null;
11051
+ if (neighbour) {
11052
+ const box2 = neighbour.querySelector(GLYPH_SELECTOR)?.getBoundingClientRect();
11053
+ if (box2?.width && box2.height) return box2;
11054
+ }
11055
+ const own = slot.getBoundingClientRect();
11056
+ if (own.width && own.height) return own;
11057
+ const box = slot.querySelector(GLYPH_SELECTOR)?.getBoundingClientRect() ?? null;
10382
11058
  return box?.width && box.height ? box : null;
10383
11059
  }
10384
11060
  function iconMarkupSizedFor(slot, markup) {
@@ -13253,6 +13929,7 @@ function useSectionDrag({
13253
13929
  }
13254
13930
  const orderJson = JSON.stringify(entries);
13255
13931
  editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: orderJson };
13932
+ setAiSectionOrder(orderJson, window.location.pathname);
13256
13933
  postToParentRef.current({ type: "ow:change", nodes: [{ key: SECTION_ORDER_KEY, text: orderJson }] });
13257
13934
  applyPersistedOrder(entries);
13258
13935
  clearSectionDragVisuals();
@@ -15372,6 +16049,70 @@ function OhhwellsBridge() {
15372
16049
  const hoveredImageHasTextOverlapRef = useRef10(false);
15373
16050
  const dragOverElRef = useRef10(null);
15374
16051
  const [mediaHover, setMediaHover] = useState13(null);
16052
+ const [selectedMedia, setSelectedMedia] = useState13(null);
16053
+ const selectedMediaElRef = useRef10(null);
16054
+ const clearMediaSelection = useCallback8(() => {
16055
+ const prev = selectedMediaElRef.current;
16056
+ selectedMediaElRef.current = null;
16057
+ setSelectedMedia(null);
16058
+ const sectionEl = prev?.closest("[data-ohw-section]") ?? null;
16059
+ if (sectionEl) {
16060
+ postToParentRef.current({
16061
+ type: "ow:section-selected",
16062
+ sectionId: sectionEl.dataset.ohwSection ?? null,
16063
+ sectionLabel: sectionEl.dataset.ohwSectionLabel ?? titleCaseSectionId(sectionEl.dataset.ohwSection ?? ""),
16064
+ key: null
16065
+ });
16066
+ }
16067
+ }, []);
16068
+ const clearMediaSelectionRef = useRef10(clearMediaSelection);
16069
+ clearMediaSelectionRef.current = clearMediaSelection;
16070
+ const selectMediaElement = useCallback8((el) => {
16071
+ const r2 = el.getBoundingClientRect();
16072
+ const video = el.dataset.ohwEditable === "video" ? el.querySelector("video") : null;
16073
+ selectedMediaElRef.current = el;
16074
+ setSelectedMedia({
16075
+ key: el.dataset.ohwKey ?? "",
16076
+ rect: { top: r2.top, left: r2.left, width: r2.width, height: r2.height },
16077
+ elementType: el.dataset.ohwEditable ?? "image",
16078
+ hasTextOverlap: false,
16079
+ isDragOver: false,
16080
+ ...video ? { videoAutoplay: video.autoplay, videoMuted: video.muted } : {}
16081
+ });
16082
+ const sectionEl = el.closest("[data-ohw-section]");
16083
+ aiSectionApiRef.current?.selectFromElement(el, { report: false });
16084
+ postToParentRef.current({
16085
+ type: "ow:section-selected",
16086
+ sectionId: sectionEl?.dataset.ohwSection ?? null,
16087
+ sectionLabel: sectionEl ? sectionEl.dataset.ohwSectionLabel ?? titleCaseSectionId(sectionEl.dataset.ohwSection ?? "") : null,
16088
+ key: el.dataset.ohwKey ?? null,
16089
+ // Display name for the pill — the raw key prettifies into fragments ("Img"); the
16090
+ // bridge knows what the node IS, so it names it.
16091
+ keyLabel: el.dataset.ohwEditable === "video" ? "Video" : el.dataset.ohwEditable === "bg-image" ? "Background" : "Image"
16092
+ });
16093
+ }, []);
16094
+ const selectMediaElementRef = useRef10(selectMediaElement);
16095
+ selectMediaElementRef.current = selectMediaElement;
16096
+ useEffect13(() => {
16097
+ if (!selectedMedia) return;
16098
+ const update = () => {
16099
+ const el = selectedMediaElRef.current;
16100
+ if (!el || !el.isConnected) {
16101
+ clearMediaSelection();
16102
+ return;
16103
+ }
16104
+ const r2 = el.getBoundingClientRect();
16105
+ setSelectedMedia(
16106
+ (prev) => prev ? { ...prev, rect: { top: r2.top, left: r2.left, width: r2.width, height: r2.height } } : prev
16107
+ );
16108
+ };
16109
+ window.addEventListener("scroll", update, true);
16110
+ window.addEventListener("resize", update);
16111
+ return () => {
16112
+ window.removeEventListener("scroll", update, true);
16113
+ window.removeEventListener("resize", update);
16114
+ };
16115
+ }, [selectedMedia !== null]);
15375
16116
  const [carouselHover, setCarouselHover] = useState13(null);
15376
16117
  const [uploadingRects, setUploadingRects] = useState13({});
15377
16118
  const hoveredGapRef = useRef10(null);
@@ -15655,6 +16396,8 @@ function OhhwellsBridge() {
15655
16396
  const addNavAfterAnchorRef = useRef10(null);
15656
16397
  const editContentRef = useRef10({});
15657
16398
  const aiSectionsRef = useRef10("");
16399
+ const brandKitRef = useRef10("");
16400
+ const stylesRef = useRef10("");
15658
16401
  const pendingDeleteUndoRef = useRef10(null);
15659
16402
  const [sitePages, setSitePages] = useState13([]);
15660
16403
  const [sectionsByPath, setSectionsByPath] = useState13({});
@@ -16975,13 +17718,29 @@ function OhhwellsBridge() {
16975
17718
  }
16976
17719
  const applyContent = (content) => {
16977
17720
  const imageLoads = [];
17721
+ if (typeof content[BRAND_KIT_KEY] === "string") {
17722
+ brandKitRef.current = content[BRAND_KIT_KEY];
17723
+ applyBrandToDom(parseBrandKit(content[BRAND_KIT_KEY]));
17724
+ } else {
17725
+ brandKitRef.current = "";
17726
+ applyBrandToDom(null);
17727
+ }
16978
17728
  if (typeof content[AI_SECTIONS_KEY] === "string") {
16979
17729
  aiSectionsRef.current = content[AI_SECTIONS_KEY];
17730
+ setAiSectionOrder(content[SECTION_ORDER_KEY], window.location.pathname);
16980
17731
  applyAiSectionsToDom(parseAiSectionsState(content[AI_SECTIONS_KEY]));
16981
17732
  }
17733
+ if (typeof content[STYLE_STORE_KEY] === "string") {
17734
+ stylesRef.current = content[STYLE_STORE_KEY];
17735
+ applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
17736
+ }
17737
+ applyBrandChrome(content);
16982
17738
  for (const [key, val] of Object.entries(content)) {
16983
17739
  if (key === "__ohw_sections") continue;
16984
17740
  if (key === AI_SECTIONS_KEY) continue;
17741
+ if (key === BRAND_KIT_KEY) continue;
17742
+ if (key === STYLE_STORE_KEY) continue;
17743
+ if (BRAND_CHROME_KEYS.has(key)) continue;
16985
17744
  if (key === LOGO_PLACEHOLDER_KEY) continue;
16986
17745
  if (LOGO_IMAGE_KEYS.includes(key)) continue;
16987
17746
  if (applyVideoSettingNode(key, val)) continue;
@@ -17169,8 +17928,25 @@ function OhhwellsBridge() {
17169
17928
  initSectionInstancesFromContent(content, window.location.pathname);
17170
17929
  observer?.disconnect();
17171
17930
  try {
17931
+ applyBrandChrome(content);
17932
+ if (typeof content[BRAND_KIT_KEY] === "string") {
17933
+ applyBrandToDom(parseBrandKit(content[BRAND_KIT_KEY]));
17934
+ } else {
17935
+ applyBrandToDom(null);
17936
+ }
17937
+ if (typeof content[AI_SECTIONS_KEY] === "string") {
17938
+ setAiSectionOrder(content[SECTION_ORDER_KEY], window.location.pathname);
17939
+ applyAiSectionsToDom(parseAiSectionsState(content[AI_SECTIONS_KEY]));
17940
+ }
17941
+ if (typeof content[STYLE_STORE_KEY] === "string") {
17942
+ applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
17943
+ }
17172
17944
  for (const [key, val] of Object.entries(content)) {
17173
17945
  if (key === "__ohw_sections") continue;
17946
+ if (key === AI_SECTIONS_KEY) continue;
17947
+ if (key === BRAND_KIT_KEY) continue;
17948
+ if (key === STYLE_STORE_KEY) continue;
17949
+ if (BRAND_CHROME_KEYS.has(key)) continue;
17174
17950
  if (key === LOGO_PLACEHOLDER_KEY) continue;
17175
17951
  if (LOGO_IMAGE_KEYS.includes(key)) continue;
17176
17952
  if (applyVideoSettingNode(key, val)) continue;
@@ -17313,9 +18089,21 @@ function OhhwellsBridge() {
17313
18089
  }, [isEditMode, pathname, subdomain, fetchState, resyncSelectedNavigationItem]);
17314
18090
  useEffect13(() => {
17315
18091
  if (!isEditMode) return;
18092
+ let lastPosted = 0;
17316
18093
  const measure = () => {
17317
18094
  const h = document.body.scrollHeight;
17318
- if (h > 50) postToParent2({ type: "ow:height", height: h });
18095
+ if (h > 50 && Math.abs(h - lastPosted) > 1) {
18096
+ lastPosted = h;
18097
+ postToParent2({ type: "ow:height", height: h });
18098
+ }
18099
+ };
18100
+ let raf = null;
18101
+ const schedule = () => {
18102
+ if (raf != null) return;
18103
+ raf = requestAnimationFrame(() => {
18104
+ raf = null;
18105
+ measure();
18106
+ });
17319
18107
  };
17320
18108
  const t1 = setTimeout(measure, 50);
17321
18109
  const t2 = setTimeout(measure, 500);
@@ -17335,6 +18123,7 @@ function OhhwellsBridge() {
17335
18123
  return () => {
17336
18124
  clearTimeout(t1);
17337
18125
  clearTimeout(t2);
18126
+ if (raf != null) cancelAnimationFrame(raf);
17338
18127
  clearResizeTimers();
17339
18128
  window.removeEventListener("resize", handleResize);
17340
18129
  };
@@ -17577,10 +18366,14 @@ function OhhwellsBridge() {
17577
18366
  return;
17578
18367
  }
17579
18368
  const target = e.target;
18369
+ if (target.closest("[data-ohw-ai-review]")) return;
17580
18370
  if (target.closest("[data-ohw-toolbar]")) return;
17581
18371
  if (target.closest("[data-ohw-state-toggle]")) return;
17582
18372
  if (target.closest("[data-ohw-max-badge]")) return;
17583
18373
  if (isInsideLinkEditor(target)) return;
18374
+ if (selectedMediaElRef.current && !selectedMediaElRef.current.contains(target) && !target.closest("[data-ohw-media-overlay], [data-ohw-edit-chrome], [data-ohw-item-interaction]")) {
18375
+ clearMediaSelectionRef.current();
18376
+ }
17584
18377
  if (isInsideFloatingPanel(target)) return;
17585
18378
  if (target.closest("[data-ohw-form-toolbar]")) return;
17586
18379
  if (target.closest(
@@ -17750,8 +18543,11 @@ function OhhwellsBridge() {
17750
18543
  if (isMediaEditable(editable) && !buttonOnMedia) {
17751
18544
  e.preventDefault();
17752
18545
  e.stopPropagation();
17753
- aiSectionApiRef.current?.selectFromElement(editable);
17754
- postToParentRef.current({ type: "ow:image-pick", key: editable.dataset.ohwKey ?? "", elementType: editable.dataset.ohwEditable ?? "image" });
18546
+ if (selectedMediaElRef.current === editable) {
18547
+ postToParentRef.current({ type: "ow:image-pick", key: editable.dataset.ohwKey ?? "", elementType: editable.dataset.ohwEditable ?? "image" });
18548
+ } else {
18549
+ selectMediaElementRef.current(editable);
18550
+ }
17755
18551
  return;
17756
18552
  }
17757
18553
  const socialItem = getSocialItem(editable);
@@ -17892,6 +18688,7 @@ function OhhwellsBridge() {
17892
18688
  };
17893
18689
  const handleDblClick = (e) => {
17894
18690
  const target = e.target;
18691
+ if (target.closest("[data-ohw-ai-review]")) return;
17895
18692
  if (target.closest("[data-ohw-toolbar]")) return;
17896
18693
  if (target.closest("[data-ohw-state-toggle]")) return;
17897
18694
  if (target.closest("[data-ohw-max-badge]")) return;
@@ -18692,7 +19489,9 @@ function OhhwellsBridge() {
18692
19489
  return;
18693
19490
  }
18694
19491
  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);
19492
+ const sections = Array.from(document.querySelectorAll("[data-ohw-section]")).filter(
19493
+ (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
19494
+ ).sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top);
18696
19495
  const ZONE = 20;
18697
19496
  for (let i = 0; i < sections.length; i++) {
18698
19497
  const a = sections[i];
@@ -19024,10 +19823,23 @@ function OhhwellsBridge() {
19024
19823
  if (e.data?.type !== "ow:hydrate") return;
19025
19824
  const content = e.data.content;
19026
19825
  if (!content) return;
19826
+ if (typeof content[BRAND_KIT_KEY] === "string") {
19827
+ brandKitRef.current = content[BRAND_KIT_KEY];
19828
+ applyBrandToDom(parseBrandKit(content[BRAND_KIT_KEY]));
19829
+ } else {
19830
+ brandKitRef.current = "";
19831
+ applyBrandToDom(null);
19832
+ }
19027
19833
  if (typeof content[AI_SECTIONS_KEY] === "string") {
19028
19834
  aiSectionsRef.current = content[AI_SECTIONS_KEY];
19835
+ setAiSectionOrder(content[SECTION_ORDER_KEY], window.location.pathname);
19029
19836
  applyAiSectionsToDom(parseAiSectionsState(content[AI_SECTIONS_KEY]));
19030
19837
  }
19838
+ if (typeof content[STYLE_STORE_KEY] === "string") {
19839
+ stylesRef.current = content[STYLE_STORE_KEY];
19840
+ applyStylesToDom(parseStyleStore(content[STYLE_STORE_KEY]));
19841
+ }
19842
+ applyBrandChrome(content);
19031
19843
  let sectionsJson = null;
19032
19844
  for (const [key, val] of Object.entries(content)) {
19033
19845
  if (key === "__ohw_sections") {
@@ -19035,6 +19847,9 @@ function OhhwellsBridge() {
19035
19847
  continue;
19036
19848
  }
19037
19849
  if (key === AI_SECTIONS_KEY) continue;
19850
+ if (key === BRAND_KIT_KEY) continue;
19851
+ if (key === STYLE_STORE_KEY) continue;
19852
+ if (BRAND_CHROME_KEYS.has(key)) continue;
19038
19853
  if (key === LOGO_PLACEHOLDER_KEY) continue;
19039
19854
  if (LOGO_IMAGE_KEYS.includes(key) && !String(val ?? "").trim()) continue;
19040
19855
  if (applyVideoSettingNode(key, val)) continue;
@@ -19050,6 +19865,8 @@ function OhhwellsBridge() {
19050
19865
  if (video && video.src !== val) applyVideoSrc(video, val);
19051
19866
  } else if (el.dataset.ohwEditable === "link") {
19052
19867
  applyLinkHref(el, val);
19868
+ } else if (el.dataset.ohwEditable === "icon") {
19869
+ applyIconMarkup(el, val);
19053
19870
  } else if (isIconMarkupValue(val)) {
19054
19871
  } else {
19055
19872
  el.innerHTML = val;
@@ -19134,12 +19951,21 @@ function OhhwellsBridge() {
19134
19951
  nodes: collectEditableNodes(editContentRef.current)
19135
19952
  });
19136
19953
  };
19954
+ const clearInteractionChrome = () => {
19955
+ deactivateRef.current();
19956
+ deselectRef.current();
19957
+ clearMediaSelectionRef.current();
19958
+ };
19137
19959
  const handleAiApplyTree = (e) => {
19138
19960
  if (e.data?.type !== "ow:ai-apply-tree") return;
19139
19961
  const payload = e.data.payload;
19140
19962
  if (!payload || typeof payload.id !== "string" || !isRenderableTree(payload.tree)) return;
19963
+ clearInteractionChrome();
19141
19964
  const previous = aiSectionsRef.current;
19142
- const nextState = applyTreeToState(parseAiSectionsState(previous), payload);
19965
+ const nextState = applyTreeToState(parseAiSectionsState(previous), {
19966
+ ...payload,
19967
+ path: payload.path ?? window.location.pathname
19968
+ });
19143
19969
  const nextValue = serializeAiSectionsState(nextState);
19144
19970
  aiSectionsRef.current = nextValue;
19145
19971
  applyAiSectionsToDom(nextState);
@@ -19160,6 +19986,7 @@ function OhhwellsBridge() {
19160
19986
  const exists = document.querySelector(`[data-ohw-section="${CSS.escape(sectionId)}"]`);
19161
19987
  if (!exists) return;
19162
19988
  if (isPageFrameSection(exists)) return;
19989
+ clearInteractionChrome();
19163
19990
  const previous = aiSectionsRef.current;
19164
19991
  const nextState = deleteSectionFromState(parseAiSectionsState(previous), sectionId);
19165
19992
  const nextValue = serializeAiSectionsState(nextState);
@@ -19175,14 +20002,45 @@ function OhhwellsBridge() {
19175
20002
  const handleAiSetSections = (e) => {
19176
20003
  if (e.data?.type !== "ow:ai-set-sections") return;
19177
20004
  const value = typeof e.data.value === "string" ? e.data.value : "";
20005
+ clearInteractionChrome();
19178
20006
  aiSectionsRef.current = value;
19179
20007
  applyAiSectionsToDom(parseAiSectionsState(value));
20008
+ applyStylesToDom(parseStyleStore(stylesRef.current));
19180
20009
  const restoredHeight = document.body.scrollHeight;
19181
20010
  if (restoredHeight > 50) postToParentRef.current({ type: "ow:height", height: restoredHeight });
19182
20011
  postToParentRef.current({ type: "ow:change", nodes: [{ key: AI_SECTIONS_KEY, text: value }] });
19183
20012
  postAiSectionsChanged();
19184
20013
  };
19185
20014
  window.addEventListener("message", handleAiSetSections);
20015
+ const handleAiSetBrand = (e) => {
20016
+ if (e.data?.type !== "ow:ai-set-brand") return;
20017
+ const value = typeof e.data.value === "string" ? e.data.value : "";
20018
+ const previous = brandKitRef.current;
20019
+ brandKitRef.current = value;
20020
+ applyBrandToDom(parseBrandKit(value));
20021
+ if (aiSectionsRef.current) applyAiSectionsToDom(parseAiSectionsState(aiSectionsRef.current));
20022
+ applyStylesToDom(parseStyleStore(stylesRef.current));
20023
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: BRAND_KIT_KEY, text: value }] });
20024
+ postToParentRef.current({ type: "ow:ai-brand-applied", previous, value });
20025
+ };
20026
+ window.addEventListener("message", handleAiSetBrand);
20027
+ const handleAiSetStyles = (e) => {
20028
+ if (e.data?.type !== "ow:ai-set-styles") return;
20029
+ const value = typeof e.data.value === "string" ? e.data.value : "";
20030
+ const previous = stylesRef.current;
20031
+ stylesRef.current = value;
20032
+ applyStylesToDom(parseStyleStore(value));
20033
+ postToParentRef.current({ type: "ow:change", nodes: [{ key: STYLE_STORE_KEY, text: value }] });
20034
+ postToParentRef.current({ type: "ow:ai-styles-applied", previous, value });
20035
+ };
20036
+ window.addEventListener("message", handleAiSetStyles);
20037
+ const handleGetBrand = (e) => {
20038
+ if (e.data?.type !== "ow:get-brand") return;
20039
+ const template = deriveTemplateBrand();
20040
+ const value = brandKitRef.current || (template ? JSON.stringify(template) : "");
20041
+ postToParentRef.current({ type: "ow:brand-value", value });
20042
+ };
20043
+ window.addEventListener("message", handleGetBrand);
19186
20044
  const handleMoveSection = (e) => {
19187
20045
  if (e.data?.type !== "ow:move-section") return;
19188
20046
  const instanceId = typeof e.data.instanceId === "string" ? e.data.instanceId : "";
@@ -19192,6 +20050,7 @@ function OhhwellsBridge() {
19192
20050
  if (!entries) return;
19193
20051
  const orderJson = JSON.stringify(entries);
19194
20052
  editContentRef.current = { ...editContentRef.current, [SECTION_ORDER_KEY]: orderJson };
20053
+ setAiSectionOrder(orderJson, window.location.pathname);
19195
20054
  postToParentRef.current({ type: "ow:change", nodes: [{ key: SECTION_ORDER_KEY, text: orderJson }] });
19196
20055
  window.dispatchEvent(new Event("resize"));
19197
20056
  };
@@ -19255,6 +20114,7 @@ function OhhwellsBridge() {
19255
20114
  }
19256
20115
  deselectRef.current();
19257
20116
  deactivateRef.current();
20117
+ clearMediaSelectionRef.current();
19258
20118
  };
19259
20119
  window.addEventListener("message", handleDeactivate);
19260
20120
  const handleToastAction = (e) => {
@@ -19340,6 +20200,10 @@ function OhhwellsBridge() {
19340
20200
  const handleKeyDown = (e) => {
19341
20201
  if (e.key === "Escape" && document.querySelector("[data-ohw-section-picker]")) return;
19342
20202
  if (e.key === "Escape" && document.querySelector("[data-ohw-more-menu]")) return;
20203
+ if (e.key === "Escape" && selectedMediaElRef.current) {
20204
+ clearMediaSelectionRef.current();
20205
+ return;
20206
+ }
19343
20207
  if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "a" && activeElRef.current) {
19344
20208
  e.preventDefault();
19345
20209
  selectAllTextInEditable(activeElRef.current);
@@ -19499,6 +20363,12 @@ function OhhwellsBridge() {
19499
20363
  if (aiSectionsRef.current) {
19500
20364
  nodes.push({ key: AI_SECTIONS_KEY, type: "sections", text: aiSectionsRef.current });
19501
20365
  }
20366
+ if (stylesRef.current) {
20367
+ nodes.push({ key: STYLE_STORE_KEY, type: "styles", text: stylesRef.current });
20368
+ }
20369
+ if (brandKitRef.current) {
20370
+ nodes.push({ key: BRAND_KIT_KEY, type: "brand", text: brandKitRef.current });
20371
+ }
19502
20372
  document.querySelectorAll('[data-ohw-editable="form"]').forEach((form) => {
19503
20373
  const formKey = formKeyOf(form);
19504
20374
  if (!formKey) return;
@@ -19913,6 +20783,9 @@ function OhhwellsBridge() {
19913
20783
  window.removeEventListener("message", handleAiApplyTree);
19914
20784
  window.removeEventListener("message", handleAiDeleteSection);
19915
20785
  window.removeEventListener("message", handleAiSetSections);
20786
+ window.removeEventListener("message", handleAiSetBrand);
20787
+ window.removeEventListener("message", handleAiSetStyles);
20788
+ window.removeEventListener("message", handleGetBrand);
19916
20789
  window.removeEventListener("message", handleMoveSection);
19917
20790
  window.removeEventListener("message", handlePanelDragging);
19918
20791
  window.removeEventListener("message", handleDeleteSection);
@@ -20123,7 +20996,7 @@ function OhhwellsBridge() {
20123
20996
  postToParent2({
20124
20997
  type: "ow:ready",
20125
20998
  version: "1",
20126
- bridgeVersion: "0.1.77",
20999
+ bridgeVersion: "0.1.78",
20127
21000
  path: pathname,
20128
21001
  nodes: collectEditableNodes(editContentRef.current),
20129
21002
  sections
@@ -20530,11 +21403,22 @@ function OhhwellsBridge() {
20530
21403
  const showEditLink = toolbarShowEditLink;
20531
21404
  const currentSections = sectionsByPath[pathname] ?? [];
20532
21405
  linkPopoverOpenRef.current = linkPopover !== null;
21406
+ const handleMediaSelect = useCallback8((key) => {
21407
+ const el = hoveredImageRef.current?.dataset.ohwKey === key ? hoveredImageRef.current : Array.from(document.querySelectorAll(MEDIA_SELECTOR)).find(
21408
+ (m) => (m.dataset.ohwKey ?? "") === key
21409
+ ) ?? null;
21410
+ if (!el) return;
21411
+ selectMediaElementRef.current(el);
21412
+ }, []);
20533
21413
  const handleMediaReplace = useCallback8(
20534
21414
  (key) => {
20535
- postToParent2({ type: "ow:image-pick", key, elementType: mediaHover?.elementType ?? "image" });
21415
+ postToParent2({
21416
+ type: "ow:image-pick",
21417
+ key,
21418
+ elementType: mediaHover?.elementType ?? selectedMedia?.elementType ?? "image"
21419
+ });
20536
21420
  },
20537
- [postToParent2, mediaHover?.elementType]
21421
+ [postToParent2, mediaHover?.elementType, selectedMedia?.elementType]
20538
21422
  );
20539
21423
  const handleEditCarousel = useCallback8(
20540
21424
  (key) => {
@@ -20606,12 +21490,25 @@ function OhhwellsBridge() {
20606
21490
  },
20607
21491
  `uploading-${key}`
20608
21492
  )),
20609
- mediaHover && !(mediaHover.key in uploadingRects) && /* @__PURE__ */ jsx33(
21493
+ mediaHover && !(mediaHover.key in uploadingRects) && mediaHover.key !== selectedMedia?.key && /* @__PURE__ */ jsx33(
20610
21494
  MediaOverlay,
20611
21495
  {
20612
21496
  hover: mediaHover,
20613
21497
  isUploading: false,
20614
21498
  onReplace: handleMediaReplace,
21499
+ onSelect: handleMediaSelect,
21500
+ onVideoSettingsChange: handleVideoSettingsChange
21501
+ }
21502
+ ),
21503
+ selectedMedia && !(selectedMedia.key in uploadingRects) && !linkPopover && /* @__PURE__ */ jsx33(
21504
+ MediaOverlay,
21505
+ {
21506
+ hover: selectedMedia,
21507
+ selected: true,
21508
+ hovered: mediaHover?.key === selectedMedia.key,
21509
+ isUploading: false,
21510
+ onReplace: handleMediaReplace,
21511
+ onSelect: handleMediaSelect,
20615
21512
  onVideoSettingsChange: handleVideoSettingsChange
20616
21513
  }
20617
21514
  ),