@ohhwells/bridge 0.1.126-next.349 → 0.1.127-next.351

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.cjs CHANGED
@@ -120,6 +120,12 @@ function enforceLinkHrefs() {
120
120
 
121
121
  // src/lib/ai-tree.ts
122
122
  var AI_TREE_SCHEMA_VERSIONS = [1];
123
+ function legacyVerticalPaddingAlias(spacing) {
124
+ if (spacing === "tight") return "s";
125
+ if (spacing === "balanced") return "m";
126
+ if (spacing === "airy") return "l";
127
+ return void 0;
128
+ }
123
129
  var BUTTON_FILL_TOKENS = ["dark", "primary", "accent", "light"];
124
130
  var BUTTON_STYLES = ["solid", "outline", "ghost"];
125
131
  var BUTTON_RADII = ["square", "rounded", "pill"];
@@ -142,7 +148,10 @@ var AI_TREE_TOKENS = {
142
148
  radiusButton: 10,
143
149
  radiusCard: 18,
144
150
  sectionMaxWidth: 1280,
145
- sectionPadding: { tight: 40, balanced: 64, airy: 96 },
151
+ /** 6-step vertical-padding scale (OHH-789) — replaces the deprecated tight/balanced/airy
152
+ * map; `m` keeps the old `balanced` value (64) so already-generated sites render unchanged.
153
+ * `none` is the true zero floor for an explicit "remove the padding" ask. */
154
+ sectionPaddingByScale: { none: 0, xs: 24, s: 40, m: 64, l: 96, xl: 128 },
146
155
  type: {
147
156
  display: { size: 56, line: 1.1, weight: 700 },
148
157
  h2: { size: 40, line: 1.15, weight: 700 },
@@ -160,177 +169,538 @@ function isRenderableTree(value) {
160
169
  return AI_TREE_SCHEMA_VERSIONS.includes(tree.schemaVersion) && tree.type === "section" && Array.isArray(tree.rows);
161
170
  }
162
171
 
163
- // src/lib/ai-sections-store.ts
164
- var AI_SECTIONS_KEY = "__ohw_ai_sections";
165
- var AI_SLOT_KEY_PREFIX = "ai.";
166
- function aiSlotKeyPrefixFor(sectionId) {
167
- return `${AI_SLOT_KEY_PREFIX}${sectionId}.`;
168
- }
169
- var EMPTY_AI_SECTIONS = { v: 1, sections: [] };
170
- function parseAiSectionsState(raw) {
171
- if (!raw) return EMPTY_AI_SECTIONS;
172
+ // src/lib/section-styles.ts
173
+ var STYLE_STORE_KEY = "__ohw_styles";
174
+ var STYLE_SHEET_ID = "ohw-section-styles";
175
+ function parseStyleStore(raw) {
176
+ if (!raw) return null;
172
177
  try {
173
178
  const parsed = JSON.parse(raw);
174
- if (parsed?.v !== 1 || !Array.isArray(parsed.sections)) return EMPTY_AI_SECTIONS;
175
- const sections = parsed.sections.filter(
176
- (entry) => typeof entry === "object" && entry !== null && typeof entry.id === "string" && entry.id.length > 0 && isRenderableTree(entry.tree)
177
- ).map((entry) => ({
178
- ...entry,
179
- label: typeof entry.label === "string" ? entry.label : "Generated section",
180
- media: entry.media && typeof entry.media === "object" ? entry.media : {}
181
- }));
182
- const removed = Array.isArray(parsed.removed) ? parsed.removed.filter((id) => typeof id === "string" && id.length > 0) : [];
179
+ if (parsed?.v !== 1) return null;
183
180
  return {
184
181
  v: 1,
185
- sections,
186
- ...removed.length ? { removed } : {},
187
- ...parsed.hideTemplate === true ? { hideTemplate: true } : {}
182
+ sections: typeof parsed.sections === "object" && parsed.sections ? parsed.sections : {},
183
+ nodes: typeof parsed.nodes === "object" && parsed.nodes ? parsed.nodes : {}
188
184
  };
189
185
  } catch {
190
- return EMPTY_AI_SECTIONS;
186
+ return null;
191
187
  }
192
188
  }
193
- function serializeAiSectionsState(state) {
194
- return JSON.stringify(state);
195
- }
196
- function applyTreeToState(state, payload) {
197
- const insertBefore = payload.mode === "insert" && payload.position === "before";
198
- const priorEntry = state.sections.find((existing) => existing.id === payload.id);
199
- const selfEdit = payload.mode === "replace" && payload.targetSectionId === payload.id;
200
- const entry = {
201
- id: payload.id,
202
- label: payload.label ?? "Generated section",
203
- ...typeof payload.path === "string" && payload.path ? { path: payload.path } : {},
204
- afterSection: selfEdit ? priorEntry?.afterSection ?? null : payload.mode === "insert" && !insertBefore ? payload.targetSectionId ?? null : null,
205
- ...selfEdit ? priorEntry?.beforeSection ? { beforeSection: priorEntry.beforeSection } : {} : insertBefore && payload.targetSectionId ? { beforeSection: payload.targetSectionId } : {},
206
- ...selfEdit ? priorEntry?.replaces ? { replaces: priorEntry.replaces } : {} : payload.mode === "replace" && payload.targetSectionId ? { replaces: payload.targetSectionId } : {},
207
- tree: payload.tree,
208
- media: payload.media ?? {},
209
- ...payload.brand && typeof payload.brand === "object" ? { brand: payload.brand } : {}
189
+ var BG_VALUES = {
190
+ surface: "color-mix(in srgb, var(--ohw-brand-light, var(--color-light, #ECECEB)) 94%, var(--ohw-brand-dark, var(--color-dark, #0C0A09)))",
191
+ accent: "var(--ohw-brand-primary, var(--color-primary, #0078E5))",
192
+ "accent-soft": "color-mix(in srgb, var(--ohw-brand-primary, var(--color-primary, #0078E5)) 12%, var(--ohw-brand-light, var(--color-light, #FFFFFF)))"
193
+ };
194
+ var NAVBAR_COLOR_TOKENS = ["dark", "primary", "accent", "light"];
195
+ function navbarColorVar(token) {
196
+ const fallback = {
197
+ dark: "#0C0A09",
198
+ primary: "#0078E5",
199
+ accent: "#0078E5",
200
+ light: "#FFFFFF"
210
201
  };
211
- const others = state.sections.filter((existing) => existing.id !== entry.id);
212
- return { ...state, v: 1, sections: [...others, entry] };
202
+ return `var(--ohw-brand-${token}, var(--color-${token}, ${fallback[token]}))`;
213
203
  }
214
- function foldAlignIntoTrees(state, store) {
215
- const byId = new Map(state.sections.map((entry) => [entry.id, entry]));
216
- const nextTrees = /* @__PURE__ */ new Map();
217
- const treeFor = (id) => {
218
- const cloned = nextTrees.get(id);
219
- if (cloned) return cloned;
220
- const entry = byId.get(id);
221
- if (!entry) return void 0;
222
- const fresh = {
223
- ...entry.tree,
224
- rows: entry.tree.rows.map((row) => ({ ...row, blocks: row.blocks.map((block) => ({ ...block })) }))
225
- };
226
- nextTrees.set(id, fresh);
227
- return fresh;
228
- };
229
- const nodes = {};
230
- for (const [key, override] of Object.entries(store.nodes)) {
231
- const match = override.align !== void 0 && key.startsWith(AI_SLOT_KEY_PREFIX) ? key.match(/^ai\.(.+?)\.r(\d+)\.b(\d+)(?:\.|$)/) : null;
232
- const tree = match ? treeFor(match[1]) : void 0;
233
- const block = match && tree ? tree.rows[Number(match[2])]?.blocks[Number(match[3])] : void 0;
234
- if (!block) {
235
- nodes[key] = override;
236
- continue;
237
- }
238
- block.align = override.align;
239
- const rest = { ...override };
240
- delete rest.align;
241
- if (Object.keys(rest).length > 0) nodes[key] = rest;
204
+ function resolveNavbarValue(value) {
205
+ if (value === void 0) return void 0;
206
+ return NAVBAR_COLOR_TOKENS.includes(value) ? navbarColorVar(value) : value;
207
+ }
208
+ var HEADLINE_SIZES = { md: 24, lg: 32, xl: 40, display: 56 };
209
+ function styleSheetCss() {
210
+ const rules = [];
211
+ for (const [tone, value] of Object.entries(BG_VALUES)) {
212
+ rules.push(`[data-ohw-style-bg="${tone}"] { background: ${value} !important; }`);
242
213
  }
243
- const sections = {};
244
- for (const [sectionId, override] of Object.entries(store.sections)) {
245
- const tree = override.align !== void 0 ? treeFor(sectionId) : void 0;
246
- if (!tree) {
247
- sections[sectionId] = override;
248
- continue;
249
- }
250
- for (const row of tree.rows) {
251
- for (const block of row.blocks) block.align = override.align;
252
- }
253
- const rest = { ...override };
254
- delete rest.align;
255
- if (Object.keys(rest).length > 0) sections[sectionId] = rest;
214
+ rules.push(
215
+ `[data-ohw-style-bg="accent"] { color: var(--ohw-brand-light, var(--color-light, #FFFFFF)) !important; }`
216
+ );
217
+ rules.push(
218
+ `[data-ohw-style-distribution="space-between"] { display: flex !important; flex-direction: column; justify-content: space-between; }`,
219
+ `[data-ohw-style-distribution="center"] { display: flex !important; flex-direction: column; justify-content: center; }`
220
+ );
221
+ for (const [scale, size] of Object.entries(HEADLINE_SIZES)) {
222
+ rules.push(
223
+ `[data-ohw-style-headline="${scale}"] :is(h1, h2, h3) { font-size: ${size}px !important; line-height: 1.15 !important; }`
224
+ );
256
225
  }
257
- if (nextTrees.size === 0) return { state, store, changed: false };
258
- return {
259
- state: {
260
- ...state,
261
- v: 1,
262
- sections: state.sections.map((entry) => {
263
- const tree = nextTrees.get(entry.id);
264
- return tree ? { ...entry, tree } : entry;
265
- })
266
- },
267
- store: { v: 1, sections, nodes },
268
- changed: true
269
- };
270
- }
271
- function removeFromState(state, id) {
272
- return { ...state, v: 1, sections: state.sections.filter((entry) => entry.id !== id) };
273
- }
274
- function deleteSectionFromState(state, sectionId) {
275
- if (state.sections.some((entry) => entry.id === sectionId)) {
276
- return removeFromState(state, sectionId);
226
+ rules.push(
227
+ `[data-ohw-style-aspect="fill-height"] img { height: 100% !important; aspect-ratio: auto !important; object-fit: cover; }`
228
+ );
229
+ for (const aspect of ["1:1", "4:5", "3:4", "3:2", "16:9", "2:1", "3:1"]) {
230
+ rules.push(
231
+ `[data-ohw-style-aspect="${aspect.replace(":", "-")}"] img { aspect-ratio: ${aspect.replace(":", " / ")} !important; height: auto !important; object-fit: cover; }`
232
+ );
277
233
  }
278
- const removed = state.removed ?? [];
279
- if (removed.includes(sectionId)) return state;
280
- return { ...state, v: 1, sections: state.sections, removed: [...removed, sectionId] };
234
+ const pad = { tight: 40, balanced: 64, airy: 96 };
235
+ for (const [spacing, px2] of Object.entries(pad)) {
236
+ rules.push(
237
+ `[data-ohw-style-spacing="${spacing}"] { padding-top: ${px2}px !important; padding-bottom: ${px2}px !important; }`
238
+ );
239
+ }
240
+ for (const [step, px2] of Object.entries(AI_TREE_TOKENS.sectionPaddingByScale)) {
241
+ const vpCss = px2 === 0 ? "0px" : `clamp(${Math.max(16, Math.round(px2 * 0.5))}px, ${(px2 / 9).toFixed(2)}vw, ${px2}px)`;
242
+ rules.push(
243
+ `[data-ohw-style-vpadding="${step}"] { padding-top: ${vpCss} !important; padding-bottom: ${vpCss} !important; }`
244
+ );
245
+ }
246
+ rules.push(
247
+ `[data-ohw-style-corners="sharp"] :is(.card, [data-ohw-card], img, picture, figure, [data-ohw-editable="bg-image"]) { border-radius: 0 !important; }`,
248
+ `[data-ohw-style-corners="sharp"] :has(> img) { border-radius: 0 !important; }`,
249
+ `[data-ohw-style-corners="rounded"] :is(.card, [data-ohw-card], img, picture, figure, [data-ohw-editable="bg-image"]) { border-radius: max(0.75rem, var(--radius, 0px)) !important; }`,
250
+ `[data-ohw-style-corners="rounded"] :has(> img) { border-radius: max(0.75rem, var(--radius, 0px)) !important; }`
251
+ );
252
+ for (const align of ["left", "center", "right"]) {
253
+ rules.push(`[data-ohw-style-align="${align}"] { text-align: ${align} !important; }`);
254
+ }
255
+ return rules.join("\n");
281
256
  }
282
- function reapRemovedAiSections(state, store, removedIds, excludeIds = /* @__PURE__ */ new Set()) {
283
- const generated = new Set(state.sections.map((entry) => entry.id));
284
- const reapedIds = [...new Set(removedIds)].filter((id) => generated.has(id) && !excludeIds.has(id));
285
- if (reapedIds.length === 0) {
286
- return { state, store, reapedIds: [], slotPrefixes: [], changed: false };
257
+ var STYLE_FONT_LINK_ID = "ohw-style-fonts";
258
+ function loadStyleFonts(families) {
259
+ const unique = [...new Set(families.map((f) => f.trim()).filter(Boolean))];
260
+ const existing = document.getElementById(STYLE_FONT_LINK_ID);
261
+ if (unique.length === 0) {
262
+ existing?.remove();
263
+ return;
287
264
  }
288
- const reaped = new Set(reapedIds);
289
- const slotPrefixes = reapedIds.map(aiSlotKeyPrefixFor);
290
- const nextState = {
291
- ...state,
292
- v: 1,
293
- sections: state.sections.filter((entry) => !reaped.has(entry.id))
294
- };
295
- let nextStore = store;
296
- if (store) {
297
- const sections = {};
298
- for (const [key, override] of Object.entries(store.sections)) {
299
- if (!reaped.has(key)) sections[key] = override;
300
- }
301
- const nodes = {};
302
- for (const [key, override] of Object.entries(store.nodes)) {
303
- if (!slotPrefixes.some((prefix) => key.startsWith(prefix))) nodes[key] = override;
304
- }
305
- nextStore = { v: 1, sections, nodes };
265
+ const spec = unique.map((f) => `${f.replace(/ /g, "+")}:400,500,600,700`).join("|");
266
+ const href = `https://fonts.googleapis.com/css?family=${spec}&display=swap`;
267
+ let link = existing;
268
+ if (!link) {
269
+ link = document.createElement("link");
270
+ link.id = STYLE_FONT_LINK_ID;
271
+ link.rel = "stylesheet";
272
+ document.head.appendChild(link);
306
273
  }
307
- return { state: nextState, store: nextStore, reapedIds, slotPrefixes, changed: true };
274
+ if (link.href !== href) link.href = href;
308
275
  }
309
-
310
- // src/lib/ai-footer-tree.ts
311
- var FOOTER_COLUMN_SPANS = [12, 6, 4, 3];
312
- var FOOTER_COLUMN_BLOCKS = [
313
- "link-list",
314
- "text",
315
- "hours",
316
- "logo",
317
- "social-row",
318
- "media-strip"
319
- ];
320
- var FOOTER_SOCIAL_PLATFORMS = [
321
- "facebook",
322
- "instagram",
323
- "youtube",
324
- "linkedin",
325
- "twitter",
326
- "tiktok",
327
- "threads",
328
- "pinterest",
329
- "whatsapp"
276
+ var SECTION_ATTRS = {
277
+ sectionBackground: "data-ohw-style-bg",
278
+ textDistribution: "data-ohw-style-distribution",
279
+ headlineScale: "data-ohw-style-headline",
280
+ imageAspect: "data-ohw-style-aspect",
281
+ spacing: "data-ohw-style-spacing",
282
+ verticalPadding: "data-ohw-style-vpadding",
283
+ cornerStyle: "data-ohw-style-corners",
284
+ align: "data-ohw-style-align"
285
+ };
286
+ var NODE_WROTE_ATTR = "data-ohw-style-node";
287
+ var NODE_PROPS = [
288
+ "color",
289
+ "font-family",
290
+ "font-size",
291
+ "text-align",
292
+ "justify-content",
293
+ "align-items"
330
294
  ];
331
- function resolveFooterCopyright(copyright, year = (/* @__PURE__ */ new Date()).getFullYear()) {
332
- return copyright.replace(/\{year\}/g, String(year));
333
- }
295
+ var ALIGN_JUSTIFY = {
296
+ left: "flex-start",
297
+ center: "center",
298
+ right: "flex-end"
299
+ };
300
+ function saveInline(el, prop) {
301
+ const attr = `data-ohw-style-prev-${prop}`;
302
+ if (el.hasAttribute(attr)) return;
303
+ const value = el.style.getPropertyValue(prop) || (prop === "background" ? el.style.getPropertyValue("background-color") : "");
304
+ el.setAttribute(attr, value);
305
+ }
306
+ function restoreInline(el, prop) {
307
+ const attr = `data-ohw-style-prev-${prop}`;
308
+ if (!el.hasAttribute(attr)) return;
309
+ const prev = el.getAttribute(attr) ?? "";
310
+ if (prev) el.style.setProperty(prop, prev);
311
+ else el.style.removeProperty(prop);
312
+ el.removeAttribute(attr);
313
+ }
314
+ function ensureStyleSheet() {
315
+ let el = document.getElementById(STYLE_SHEET_ID);
316
+ if (!el) {
317
+ el = document.createElement("style");
318
+ el.id = STYLE_SHEET_ID;
319
+ document.head.appendChild(el);
320
+ }
321
+ const css = styleSheetCss();
322
+ if (el.textContent !== css) el.textContent = css;
323
+ }
324
+ function clearSectionAttrs(root) {
325
+ for (const attr of Object.values(SECTION_ATTRS)) {
326
+ for (const el of Array.from(root.querySelectorAll(`[${attr}]`))) el.removeAttribute(attr);
327
+ }
328
+ for (const el of Array.from(root.querySelectorAll("[data-ohw-style-bgcolor]"))) {
329
+ restoreInline(el, "background");
330
+ el.removeAttribute("data-ohw-style-bgcolor");
331
+ }
332
+ for (const el of Array.from(root.querySelectorAll("[data-ohw-style-minheight]"))) {
333
+ restoreInline(el, "min-height");
334
+ el.removeAttribute("data-ohw-style-minheight");
335
+ }
336
+ }
337
+ function clearNodeProps(root) {
338
+ for (const el of Array.from(root.querySelectorAll(`[${NODE_WROTE_ATTR}]`))) {
339
+ const h = el;
340
+ for (const prop of NODE_PROPS) restoreInline(h, prop);
341
+ h.removeAttribute(NODE_WROTE_ATTR);
342
+ }
343
+ }
344
+ function alignSubjectOf(el) {
345
+ const button = el.closest('[data-ohw-role="button"]');
346
+ if (button?.parentElement) return button.parentElement;
347
+ let subject = el;
348
+ while (subject.parentElement && !subject.parentElement.hasAttribute("data-ohw-section") && /^inline/.test(getComputedStyle(subject).display)) {
349
+ subject = subject.parentElement;
350
+ }
351
+ const TEXT_ROW = /^(P|LI|H[1-6]|BLOCKQUOTE|FIGCAPTION|DT|DD)$/;
352
+ const parent = subject.parentElement;
353
+ if (parent && TEXT_ROW.test(parent.tagName) && /^(inline-)?flex$/.test(getComputedStyle(parent).display)) {
354
+ return parent;
355
+ }
356
+ if (parent && !parent.hasAttribute("data-ohw-section")) {
357
+ const pcs = getComputedStyle(parent);
358
+ const children = Array.from(parent.children);
359
+ const textRowChild = (c) => c.hasAttribute("data-ohw-editable") || (c.textContent ?? "").trim().length === 0 || c.getAttribute("aria-hidden") === "true";
360
+ if (/^(inline-)?flex$/.test(pcs.display) && pcs.flexDirection.startsWith("row") && children.length > 0 && children.some((c) => c.hasAttribute("data-ohw-editable")) && children.every(textRowChild)) {
361
+ return parent;
362
+ }
363
+ }
364
+ const row = subject.closest("li");
365
+ if (row && row !== subject && /^(inline-)?flex$/.test(getComputedStyle(row).display)) {
366
+ return row;
367
+ }
368
+ return subject;
369
+ }
370
+ function applyStylesToDom(store) {
371
+ ensureStyleSheet();
372
+ clearSectionAttrs(document);
373
+ clearNodeProps(document);
374
+ loadStyleFonts(
375
+ store ? [
376
+ ...Object.values(store.nodes).flatMap((n) => n.fontFamily ? [n.fontFamily] : []),
377
+ ...Object.values(store.sections).flatMap((s) => s.fontFamily ? [s.fontFamily] : [])
378
+ ] : []
379
+ );
380
+ if (!store) return;
381
+ for (const [sectionId, override] of Object.entries(store.sections)) {
382
+ const escaped = CSS.escape(sectionId);
383
+ const byInstance = document.querySelectorAll(`[data-ohw-instance="${escaped}"]`);
384
+ const sections = Array.from(
385
+ byInstance.length ? byInstance : document.querySelectorAll(`[data-ohw-section="${escaped}"]`)
386
+ );
387
+ const isNavbar = sectionId === "navbar";
388
+ if (isNavbar) {
389
+ const drawer = document.querySelector("[data-ohw-nav-drawer]");
390
+ if (drawer && !sections.includes(drawer)) sections.push(drawer);
391
+ const header = document.querySelector("[data-ohw-nav-root]");
392
+ if (header && !sections.includes(header)) sections.push(header);
393
+ }
394
+ for (const marker of Array.from(sections)) {
395
+ const isDrawerMarker = isNavbar && marker.hasAttribute("data-ohw-nav-drawer");
396
+ const section = marker.querySelector(":scope > [data-ai-section]") ?? marker;
397
+ for (const [prop, attr] of Object.entries(SECTION_ATTRS)) {
398
+ const value = override[prop];
399
+ if (value === void 0) continue;
400
+ if (prop === "sectionBackground" && override.sectionBackgroundColor !== void 0) continue;
401
+ section.setAttribute(attr, String(value).replace(":", "-"));
402
+ }
403
+ const bgOverride = isDrawerMarker ? override.navbarMobileBackground : override.sectionBackgroundColor;
404
+ const textOverride = isDrawerMarker ? override.navbarMobileTextColor : override.textColor;
405
+ const bgValue = isNavbar ? resolveNavbarValue(bgOverride) : bgOverride;
406
+ const textValue = isNavbar ? resolveNavbarValue(textOverride) : textOverride;
407
+ if (bgValue !== void 0) {
408
+ saveInline(section, "background");
409
+ section.style.setProperty("background", bgValue, "important");
410
+ section.setAttribute("data-ohw-style-bgcolor", "");
411
+ }
412
+ if (override.minHeight !== void 0 && !isDrawerMarker) {
413
+ const minHeightCss = override.minHeight === "full" ? "var(--ohw-canvas-h, calc(100vh - 80px))" : override.minHeight === "half" ? "calc(var(--ohw-canvas-h, 100vh) / 2)" : void 0;
414
+ saveInline(section, "min-height");
415
+ if (minHeightCss) section.style.setProperty("min-height", minHeightCss, "important");
416
+ else section.style.removeProperty("min-height");
417
+ section.setAttribute("data-ohw-style-minheight", "");
418
+ }
419
+ if (textValue !== void 0 || override.fontFamily !== void 0) {
420
+ const textEls = marker.querySelectorAll(
421
+ '[data-ohw-editable="text"], [data-ohw-editable="plain"], [data-ohw-field-label]'
422
+ );
423
+ for (const el of Array.from(textEls)) {
424
+ if (textValue !== void 0) {
425
+ saveInline(el, "color");
426
+ el.style.setProperty("color", textValue, "important");
427
+ el.setAttribute(NODE_WROTE_ATTR, "");
428
+ }
429
+ if (override.fontFamily !== void 0) {
430
+ saveInline(el, "font-family");
431
+ el.style.setProperty("font-family", `'${override.fontFamily}'`, "important");
432
+ el.setAttribute(NODE_WROTE_ATTR, "");
433
+ }
434
+ }
435
+ }
436
+ }
437
+ }
438
+ for (const [key, override] of Object.entries(store.nodes)) {
439
+ const nodes = document.querySelectorAll(`[data-ohw-key="${CSS.escape(key)}"]`);
440
+ for (const el of Array.from(nodes)) {
441
+ if (override.color !== void 0) {
442
+ saveInline(el, "color");
443
+ el.style.setProperty("color", override.color, "important");
444
+ el.setAttribute(NODE_WROTE_ATTR, "");
445
+ }
446
+ if (override.fontFamily !== void 0) {
447
+ saveInline(el, "font-family");
448
+ el.style.setProperty("font-family", `'${override.fontFamily}'`, "important");
449
+ el.setAttribute(NODE_WROTE_ATTR, "");
450
+ }
451
+ if (override.fontSize !== void 0) {
452
+ saveInline(el, "font-size");
453
+ el.style.setProperty("font-size", `${override.fontSize}px`, "important");
454
+ el.setAttribute(NODE_WROTE_ATTR, "");
455
+ }
456
+ if (override.align !== void 0) {
457
+ const subject = alignSubjectOf(el);
458
+ saveInline(subject, "text-align");
459
+ saveInline(subject, "justify-content");
460
+ subject.style.setProperty("text-align", override.align, "important");
461
+ subject.style.setProperty(
462
+ "justify-content",
463
+ ALIGN_JUSTIFY[override.align] ?? "flex-start",
464
+ "important"
465
+ );
466
+ subject.setAttribute(NODE_WROTE_ATTR, "");
467
+ }
468
+ }
469
+ }
470
+ for (const [sectionId, override] of Object.entries(store.sections)) {
471
+ if (override.headingColor === void 0) continue;
472
+ const escaped = CSS.escape(sectionId);
473
+ const byInstance = document.querySelectorAll(`[data-ohw-instance="${escaped}"]`);
474
+ const sections = byInstance.length ? byInstance : document.querySelectorAll(`[data-ohw-section="${escaped}"]`);
475
+ for (const marker of Array.from(sections)) {
476
+ const headings = marker.querySelectorAll(
477
+ ":is(h1, h2, h3, h4, h5, h6)[data-ohw-editable], :is(h1, h2, h3, h4, h5, h6) > [data-ohw-editable]"
478
+ );
479
+ for (const el of Array.from(headings)) {
480
+ saveInline(el, "color");
481
+ el.style.setProperty("color", override.headingColor, "important");
482
+ el.setAttribute(NODE_WROTE_ATTR, "");
483
+ }
484
+ }
485
+ }
486
+ }
487
+
488
+ // src/lib/ai-sections-store.ts
489
+ var AI_SECTIONS_KEY = "__ohw_ai_sections";
490
+ var AI_SLOT_KEY_PREFIX = "ai.";
491
+ function aiSlotKeyPrefixFor(sectionId) {
492
+ return `${AI_SLOT_KEY_PREFIX}${sectionId}.`;
493
+ }
494
+ var EMPTY_AI_SECTIONS = { v: 1, sections: [] };
495
+ function parseAiSectionsState(raw) {
496
+ if (!raw) return EMPTY_AI_SECTIONS;
497
+ try {
498
+ const parsed = JSON.parse(raw);
499
+ if (parsed?.v !== 1 || !Array.isArray(parsed.sections)) return EMPTY_AI_SECTIONS;
500
+ const sections = parsed.sections.filter(
501
+ (entry) => typeof entry === "object" && entry !== null && typeof entry.id === "string" && entry.id.length > 0 && isRenderableTree(entry.tree)
502
+ ).map((entry) => ({
503
+ ...entry,
504
+ label: typeof entry.label === "string" ? entry.label : "Generated section",
505
+ media: entry.media && typeof entry.media === "object" ? entry.media : {}
506
+ }));
507
+ const removed = Array.isArray(parsed.removed) ? parsed.removed.filter((id) => typeof id === "string" && id.length > 0) : [];
508
+ return {
509
+ v: 1,
510
+ sections,
511
+ ...removed.length ? { removed } : {},
512
+ ...parsed.hideTemplate === true ? { hideTemplate: true } : {}
513
+ };
514
+ } catch {
515
+ return EMPTY_AI_SECTIONS;
516
+ }
517
+ }
518
+ function serializeAiSectionsState(state) {
519
+ return JSON.stringify(state);
520
+ }
521
+ function resolveSectionSettingsContext(sectionId, aiSectionsRaw, stylesRaw) {
522
+ const genSettings = parseAiSectionsState(aiSectionsRaw).sections.find(
523
+ (entry) => entry.id === sectionId
524
+ )?.tree.settings;
525
+ if (genSettings) return genSettings;
526
+ const styleOverride = parseStyleStore(stylesRaw)?.sections[sectionId];
527
+ if (!styleOverride?.verticalPadding && !styleOverride?.minHeight && !styleOverride?.spacing) {
528
+ return void 0;
529
+ }
530
+ return {
531
+ verticalPadding: styleOverride.verticalPadding ?? legacyVerticalPaddingAlias(styleOverride.spacing),
532
+ minHeight: styleOverride.minHeight
533
+ };
534
+ }
535
+ function applyTreeToState(state, payload) {
536
+ const insertBefore = payload.mode === "insert" && payload.position === "before";
537
+ const priorEntry = state.sections.find((existing) => existing.id === payload.id);
538
+ const selfEdit = payload.mode === "replace" && payload.targetSectionId === payload.id;
539
+ const entry = {
540
+ id: payload.id,
541
+ label: payload.label ?? "Generated section",
542
+ ...typeof payload.path === "string" && payload.path ? { path: payload.path } : {},
543
+ afterSection: selfEdit ? priorEntry?.afterSection ?? null : payload.mode === "insert" && !insertBefore ? payload.targetSectionId ?? null : null,
544
+ ...selfEdit ? priorEntry?.beforeSection ? { beforeSection: priorEntry.beforeSection } : {} : insertBefore && payload.targetSectionId ? { beforeSection: payload.targetSectionId } : {},
545
+ ...selfEdit ? priorEntry?.replaces ? { replaces: priorEntry.replaces } : {} : payload.mode === "replace" && payload.targetSectionId ? { replaces: payload.targetSectionId } : {},
546
+ tree: payload.tree,
547
+ media: payload.media ?? {},
548
+ ...payload.brand && typeof payload.brand === "object" ? { brand: payload.brand } : {}
549
+ };
550
+ const others = state.sections.filter((existing) => existing.id !== entry.id);
551
+ return { ...state, v: 1, sections: [...others, entry] };
552
+ }
553
+ function foldAlignIntoTrees(state, store) {
554
+ const byId = new Map(state.sections.map((entry) => [entry.id, entry]));
555
+ const nextTrees = /* @__PURE__ */ new Map();
556
+ const treeFor = (id) => {
557
+ const cloned = nextTrees.get(id);
558
+ if (cloned) return cloned;
559
+ const entry = byId.get(id);
560
+ if (!entry) return void 0;
561
+ const fresh = {
562
+ ...entry.tree,
563
+ rows: entry.tree.rows.map((row) => ({ ...row, blocks: row.blocks.map((block) => ({ ...block })) }))
564
+ };
565
+ nextTrees.set(id, fresh);
566
+ return fresh;
567
+ };
568
+ const nodes = {};
569
+ for (const [key, override] of Object.entries(store.nodes)) {
570
+ const match = override.align !== void 0 && key.startsWith(AI_SLOT_KEY_PREFIX) ? key.match(/^ai\.(.+?)\.r(\d+)\.b(\d+)(?:\.|$)/) : null;
571
+ const tree = match ? treeFor(match[1]) : void 0;
572
+ const block = match && tree ? tree.rows[Number(match[2])]?.blocks[Number(match[3])] : void 0;
573
+ if (!block) {
574
+ nodes[key] = override;
575
+ continue;
576
+ }
577
+ block.align = override.align;
578
+ const rest = { ...override };
579
+ delete rest.align;
580
+ if (Object.keys(rest).length > 0) nodes[key] = rest;
581
+ }
582
+ const sections = {};
583
+ for (const [sectionId, override] of Object.entries(store.sections)) {
584
+ const tree = override.align !== void 0 ? treeFor(sectionId) : void 0;
585
+ if (!tree) {
586
+ sections[sectionId] = override;
587
+ continue;
588
+ }
589
+ for (const row of tree.rows) {
590
+ for (const block of row.blocks) block.align = override.align;
591
+ }
592
+ const rest = { ...override };
593
+ delete rest.align;
594
+ if (Object.keys(rest).length > 0) sections[sectionId] = rest;
595
+ }
596
+ if (nextTrees.size === 0) return { state, store, changed: false };
597
+ return {
598
+ state: {
599
+ ...state,
600
+ v: 1,
601
+ sections: state.sections.map((entry) => {
602
+ const tree = nextTrees.get(entry.id);
603
+ return tree ? { ...entry, tree } : entry;
604
+ })
605
+ },
606
+ store: { v: 1, sections, nodes },
607
+ changed: true
608
+ };
609
+ }
610
+ function foldSectionHeightIntoTrees(state, store) {
611
+ const byId = new Map(state.sections.map((entry) => [entry.id, entry]));
612
+ const nextEntries = /* @__PURE__ */ new Map();
613
+ const sections = {};
614
+ for (const [sectionId, override] of Object.entries(store.sections)) {
615
+ const entry = byId.get(sectionId);
616
+ const touchesHeight = override.verticalPadding !== void 0 || override.minHeight !== void 0;
617
+ if (!entry || !touchesHeight) {
618
+ sections[sectionId] = override;
619
+ continue;
620
+ }
621
+ const nextSettings = { ...entry.tree.settings ?? {} };
622
+ if (override.verticalPadding !== void 0) nextSettings.verticalPadding = override.verticalPadding;
623
+ if (override.minHeight !== void 0) nextSettings.minHeight = override.minHeight;
624
+ nextEntries.set(sectionId, { ...entry, tree: { ...entry.tree, settings: nextSettings } });
625
+ const rest = { ...override };
626
+ delete rest.verticalPadding;
627
+ delete rest.minHeight;
628
+ if (Object.keys(rest).length > 0) sections[sectionId] = rest;
629
+ }
630
+ if (nextEntries.size === 0) return { state, store, changed: false };
631
+ return {
632
+ state: {
633
+ ...state,
634
+ v: 1,
635
+ sections: state.sections.map((entry) => nextEntries.get(entry.id) ?? entry)
636
+ },
637
+ store: { v: 1, sections, nodes: store.nodes },
638
+ changed: true
639
+ };
640
+ }
641
+ function removeFromState(state, id) {
642
+ return { ...state, v: 1, sections: state.sections.filter((entry) => entry.id !== id) };
643
+ }
644
+ function deleteSectionFromState(state, sectionId) {
645
+ if (state.sections.some((entry) => entry.id === sectionId)) {
646
+ return removeFromState(state, sectionId);
647
+ }
648
+ const removed = state.removed ?? [];
649
+ if (removed.includes(sectionId)) return state;
650
+ return { ...state, v: 1, sections: state.sections, removed: [...removed, sectionId] };
651
+ }
652
+ function reapRemovedAiSections(state, store, removedIds, excludeIds = /* @__PURE__ */ new Set()) {
653
+ const generated = new Set(state.sections.map((entry) => entry.id));
654
+ const reapedIds = [...new Set(removedIds)].filter((id) => generated.has(id) && !excludeIds.has(id));
655
+ if (reapedIds.length === 0) {
656
+ return { state, store, reapedIds: [], slotPrefixes: [], changed: false };
657
+ }
658
+ const reaped = new Set(reapedIds);
659
+ const slotPrefixes = reapedIds.map(aiSlotKeyPrefixFor);
660
+ const nextState = {
661
+ ...state,
662
+ v: 1,
663
+ sections: state.sections.filter((entry) => !reaped.has(entry.id))
664
+ };
665
+ let nextStore = store;
666
+ if (store) {
667
+ const sections = {};
668
+ for (const [key, override] of Object.entries(store.sections)) {
669
+ if (!reaped.has(key)) sections[key] = override;
670
+ }
671
+ const nodes = {};
672
+ for (const [key, override] of Object.entries(store.nodes)) {
673
+ if (!slotPrefixes.some((prefix) => key.startsWith(prefix))) nodes[key] = override;
674
+ }
675
+ nextStore = { v: 1, sections, nodes };
676
+ }
677
+ return { state: nextState, store: nextStore, reapedIds, slotPrefixes, changed: true };
678
+ }
679
+
680
+ // src/lib/ai-footer-tree.ts
681
+ var FOOTER_COLUMN_SPANS = [12, 6, 4, 3];
682
+ var FOOTER_COLUMN_BLOCKS = [
683
+ "link-list",
684
+ "text",
685
+ "hours",
686
+ "logo",
687
+ "social-row",
688
+ "media-strip"
689
+ ];
690
+ var FOOTER_SOCIAL_PLATFORMS = [
691
+ "facebook",
692
+ "instagram",
693
+ "youtube",
694
+ "linkedin",
695
+ "twitter",
696
+ "tiktok",
697
+ "threads",
698
+ "pinterest",
699
+ "whatsapp"
700
+ ];
701
+ function resolveFooterCopyright(copyright, year = (/* @__PURE__ */ new Date()).getFullYear()) {
702
+ return copyright.replace(/\{year\}/g, String(year));
703
+ }
334
704
  function isRenderableFooterTree(value) {
335
705
  if (typeof value !== "object" || value === null) return false;
336
706
  const tree = value;
@@ -613,520 +983,222 @@ function isButtonStyle(value) {
613
983
  const v = value;
614
984
  return BUTTON_FILL_TOKENS.includes(v.fill) && BUTTON_FILL_TOKENS.includes(v.textColor) && BUTTON_STYLES.includes(v.style) && BUTTON_RADII.includes(v.radius) && BUTTON_SIZES.includes(v.size);
615
985
  }
616
- function parseCustomFontWeight(raw) {
617
- if (typeof raw !== "object" || raw === null) return null;
618
- const w = raw;
619
- if (typeof w.weight !== "number" || !Number.isFinite(w.weight)) return null;
620
- if (typeof w.url !== "string" || !w.url) return null;
621
- if (typeof w.format !== "string" || !CUSTOM_FONT_FORMATS.has(w.format)) return null;
622
- const weightEnd = typeof w.weightEnd === "number" && Number.isFinite(w.weightEnd) && w.weightEnd > w.weight ? w.weightEnd : void 0;
623
- return { weight: w.weight, ...weightEnd !== void 0 ? { weightEnd } : {}, url: w.url, format: w.format };
624
- }
625
- function parseCustomFont(raw) {
626
- if (typeof raw !== "object" || raw === null) return null;
627
- const f = raw;
628
- if (typeof f.family !== "string" || !f.family) return null;
629
- if (typeof f.label !== "string" || !f.label) return null;
630
- if (!Array.isArray(f.weights)) return null;
631
- const weights = f.weights.map(parseCustomFontWeight).filter((w) => w !== null);
632
- if (weights.length === 0) return null;
633
- return { family: f.family, label: f.label, weights };
634
- }
635
- var BUTTON_FIELDS = Object.keys(ROLES_BY_FIELD);
636
- function parseTouchedButtons(raw) {
637
- const out = {};
638
- for (const variant of ["primary", "secondary"]) {
639
- const list = raw?.[variant];
640
- if (Array.isArray(list)) out[variant] = BUTTON_FIELDS.filter((f) => list.includes(f));
641
- }
642
- return out;
643
- }
644
- function parseBrandKit(raw) {
645
- if (!raw) return null;
646
- try {
647
- const parsed = JSON.parse(raw);
648
- const p = parsed?.palette;
649
- const f = parsed?.fonts;
650
- const palette = p && typeof p.dark === "string" && typeof p.primary === "string" && typeof p.accent === "string" && typeof p.light === "string" ? { dark: p.dark, primary: p.primary, accent: p.accent, light: p.light } : void 0;
651
- const fonts = f && typeof f.heading === "string" && typeof f.body === "string" ? { heading: f.heading, body: f.body } : void 0;
652
- const customFonts = Array.isArray(parsed?.customFonts) ? parsed.customFonts.map(parseCustomFont).filter((c) => c !== null) : void 0;
653
- const b = parsed?.buttons;
654
- const buttons = b && isButtonStyle(b.primary) && isButtonStyle(b.secondary) ? { primary: b.primary, secondary: b.secondary } : void 0;
655
- if (!palette && !fonts && !customFonts && !buttons) return null;
656
- return {
657
- ...palette ? { palette } : {},
658
- ...fonts ? { fonts } : {},
659
- ...customFonts ? { customFonts } : {},
660
- ...buttons ? { buttons } : {},
661
- ...parsed.touched && typeof parsed.touched === "object" ? {
662
- touched: {
663
- palette: parsed.touched.palette === true,
664
- fonts: parsed.touched.fonts === true,
665
- buttons: parseTouchedButtons(parsed.touched.buttons)
666
- }
667
- } : {}
668
- };
669
- } catch {
670
- return null;
671
- }
672
- }
673
- function familyOf(stack) {
674
- const first = stack.split(",")[0]?.trim() ?? "";
675
- return first.replace(/^['"]|['"]$/g, "");
676
- }
677
- function quoteFamily(family) {
678
- return `'${family.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
679
- }
680
- var WEIGHT_SCALE = [100, 200, 300, 400, 500, 600, 700, 800, 900];
681
- function loadBrandFonts(families) {
682
- const unique = [...new Set(families.map((f) => f.trim()).filter(Boolean))];
683
- if (unique.length === 0) return;
684
- const weights = WEIGHT_SCALE.join(";");
685
- const spec = unique.map((f) => `family=${f.replace(/ /g, "+")}:wght@${weights}`).join("&");
686
- const href = `https://fonts.googleapis.com/css2?${spec}&display=swap`;
687
- let link = document.getElementById(BRAND_FONT_LINK_ID);
688
- if (!link) {
689
- link = document.createElement("link");
690
- link.id = BRAND_FONT_LINK_ID;
691
- link.rel = "stylesheet";
692
- document.head.appendChild(link);
693
- }
694
- if (link.href !== href) link.href = href;
695
- }
696
- var BUTTON_RULES_STYLE_ID = "ohw-brand-button-rules";
697
- function buttonRulesCss(kit) {
698
- const rule = (variant) => {
699
- const roles = touchedButtonRoles(variant, kit);
700
- const v = (role) => `var(--brand-button-${variant}-${role})`;
701
- const decls = [];
702
- if (roles.has("bg")) decls.push(`background:${v("bg")} !important`);
703
- if (roles.has("text")) decls.push(`color:${v("text")} !important`);
704
- if (roles.has("border")) decls.push(`border:1.5px solid ${v("border")} !important`);
705
- if (roles.has("radius")) decls.push(`border-radius:${v("radius")} !important`);
706
- if (roles.has("padding-x")) decls.push(`padding:var(--brand-button-${variant}-padding, ${v("padding-y")} ${v("padding-x")}) !important`);
707
- if (roles.has("font-size")) decls.push(`font-size:${v("font-size")} !important`);
708
- if (decls.length === 0) return "";
709
- const selectors = [`html .btn--${variant}`, `html .btn-${variant}`];
710
- const labelRule = roles.has("text") ? `${selectors.map((x) => `${x} *`).join(",")}{color:inherit !important}` : "";
711
- return `${selectors.join(",")}{${decls.join(";")}}` + labelRule + (roles.has("bg") ? `${selectors.map((x) => `${x}:hover`).join(",")}{opacity:0.88}` : "");
712
- };
713
- return rule("primary") + rule("secondary");
714
- }
715
- function syncButtonRules(kit) {
716
- let el = document.getElementById(BUTTON_RULES_STYLE_ID);
717
- const css = kit ? buttonRulesCss(kit) : "";
718
- if (!css) {
719
- el?.remove();
720
- return;
721
- }
722
- if (!el) {
723
- el = document.createElement("style");
724
- el.id = BUTTON_RULES_STYLE_ID;
725
- document.head.appendChild(el);
726
- }
727
- if (el.textContent !== css) el.textContent = css;
728
- }
729
- function escapeCssString(value) {
730
- return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
731
- }
732
- function loadCustomFonts(fonts) {
733
- const list = fonts ?? [];
734
- let style = document.getElementById(CUSTOM_FONT_STYLE_ID);
735
- if (list.length === 0) {
736
- style?.remove();
737
- return;
738
- }
739
- if (!style) {
740
- style = document.createElement("style");
741
- style.id = CUSTOM_FONT_STYLE_ID;
742
- document.head.appendChild(style);
743
- }
744
- style.textContent = list.flatMap(
745
- (font) => font.weights.map((w) => {
746
- const weightDescriptor = w.weightEnd ? `${w.weight} ${w.weightEnd}` : `${w.weight}`;
747
- return `@font-face { font-family: '${escapeCssString(font.family)}'; src: url('${escapeCssString(w.url)}') format('${w.format}'); font-weight: ${weightDescriptor}; font-display: swap; }`;
748
- })
749
- ).join("\n");
750
- }
751
- function applyBrandToDom(kit) {
752
- const root = document.documentElement;
753
- if (!kit) {
754
- for (const name of [...BRAND_VAR_NAMES, ...LEGACY_BRAND_VAR_NAMES, ...BUTTON_VAR_NAMES]) {
755
- root.style.removeProperty(name);
756
- }
757
- for (const name of [...FONT_VARS.heading, ...FONT_VARS.body]) root.style.removeProperty(name);
758
- document.getElementById(BRAND_FONT_LINK_ID)?.remove();
759
- document.getElementById(CUSTOM_FONT_STYLE_ID)?.remove();
760
- syncButtonRules(null);
761
- return;
762
- }
763
- const applyPalette = kit.touched ? kit.touched.palette === true : true;
764
- const applyFonts = kit.touched ? kit.touched.fonts === true : true;
765
- if (kit.palette) {
766
- if (applyPalette) {
767
- for (const [name, value] of Object.entries(brandColorVars(kit.palette))) root.style.setProperty(name, value);
768
- } else {
769
- for (const name of [...BRAND_VAR_NAMES, ...LEGACY_BRAND_VAR_NAMES]) root.style.removeProperty(name);
770
- }
771
- }
772
- if (kit.buttons) {
773
- for (const name of BUTTON_VAR_NAMES) root.style.removeProperty(name);
774
- for (const [name, value] of Object.entries(buttonStyleVars({ ...kit, buttons: kit.buttons }))) root.style.setProperty(name, value);
775
- }
776
- syncButtonRules(kit.buttons ? kit : null);
777
- if (kit.customFonts) {
778
- loadCustomFonts(kit.customFonts);
779
- }
780
- if (kit.fonts) {
781
- if (applyFonts) {
782
- const heading = quoteFamily(kit.fonts.heading);
783
- const body = quoteFamily(kit.fonts.body);
784
- for (const name of FONT_VARS.heading) root.style.setProperty(name, heading);
785
- for (const name of FONT_VARS.body) root.style.setProperty(name, body);
786
- const customFamilies = new Set((kit.customFonts ?? []).map((f) => f.family));
787
- const googleFamilies = [familyOf(kit.fonts.heading), familyOf(kit.fonts.body)].filter(
788
- (f) => !customFamilies.has(f)
789
- );
790
- loadBrandFonts(googleFamilies);
791
- } else {
792
- for (const name of [...FONT_VARS.heading, ...FONT_VARS.body]) root.style.removeProperty(name);
793
- document.getElementById(BRAND_FONT_LINK_ID)?.remove();
794
- }
795
- }
986
+ function parseCustomFontWeight(raw) {
987
+ if (typeof raw !== "object" || raw === null) return null;
988
+ const w = raw;
989
+ if (typeof w.weight !== "number" || !Number.isFinite(w.weight)) return null;
990
+ if (typeof w.url !== "string" || !w.url) return null;
991
+ if (typeof w.format !== "string" || !CUSTOM_FONT_FORMATS.has(w.format)) return null;
992
+ const weightEnd = typeof w.weightEnd === "number" && Number.isFinite(w.weightEnd) && w.weightEnd > w.weight ? w.weightEnd : void 0;
993
+ return { weight: w.weight, ...weightEnd !== void 0 ? { weightEnd } : {}, url: w.url, format: w.format };
796
994
  }
797
-
798
- // src/lib/custom-code.ts
799
- var ALLOWED_HEAD_TAGS = /* @__PURE__ */ new Set(["style", "meta", "link", "script", "noscript", "base"]);
800
- var CUSTOM_CODE_AFTER_HEAD_KEY = "__ohw_custom_code_after_head";
801
- var CUSTOM_CODE_BEFORE_HEAD_KEY = "__ohw_custom_code_before_head";
802
- function clearExisting(slot) {
803
- document.querySelectorAll(`[data-ohw-custom-code="${slot}"]`).forEach((el) => el.remove());
995
+ function parseCustomFont(raw) {
996
+ if (typeof raw !== "object" || raw === null) return null;
997
+ const f = raw;
998
+ if (typeof f.family !== "string" || !f.family) return null;
999
+ if (typeof f.label !== "string" || !f.label) return null;
1000
+ if (!Array.isArray(f.weights)) return null;
1001
+ const weights = f.weights.map(parseCustomFontWeight).filter((w) => w !== null);
1002
+ if (weights.length === 0) return null;
1003
+ return { family: f.family, label: f.label, weights };
804
1004
  }
805
- function buildNodes(raw, slot) {
806
- if (!raw || !raw.trim()) return [];
807
- const parsed = new DOMParser().parseFromString(raw, "text/html");
808
- const source = [...parsed.head.childNodes, ...parsed.body.childNodes];
809
- const nodes = [];
810
- for (const node of source) {
811
- if (node.nodeType !== Node.ELEMENT_NODE) continue;
812
- const el = node;
813
- const tag = el.tagName.toLowerCase();
814
- if (!ALLOWED_HEAD_TAGS.has(tag)) continue;
815
- const built = document.createElement(tag);
816
- for (const attr of Array.from(el.attributes)) built.setAttribute(attr.name, attr.value);
817
- if (tag === "script" || tag === "style" || tag === "noscript") built.textContent = el.textContent;
818
- built.dataset.ohwCustomCode = slot;
819
- nodes.push(built);
1005
+ var BUTTON_FIELDS = Object.keys(ROLES_BY_FIELD);
1006
+ function parseTouchedButtons(raw) {
1007
+ const out = {};
1008
+ for (const variant of ["primary", "secondary"]) {
1009
+ const list = raw?.[variant];
1010
+ if (Array.isArray(list)) out[variant] = BUTTON_FIELDS.filter((f) => list.includes(f));
820
1011
  }
821
- return nodes;
822
- }
823
- function applyCustomCodeToHead(content) {
824
- clearExisting("after");
825
- clearExisting("before");
826
- const afterNodes = buildNodes(content[CUSTOM_CODE_AFTER_HEAD_KEY], "after");
827
- if (afterNodes.length > 0) document.head.prepend(...afterNodes);
828
- const beforeNodes = buildNodes(content[CUSTOM_CODE_BEFORE_HEAD_KEY], "before");
829
- if (beforeNodes.length > 0) document.head.append(...beforeNodes);
1012
+ return out;
830
1013
  }
831
-
832
- // src/lib/section-styles.ts
833
- var STYLE_STORE_KEY = "__ohw_styles";
834
- var STYLE_SHEET_ID = "ohw-section-styles";
835
- function parseStyleStore(raw) {
1014
+ function parseBrandKit(raw) {
836
1015
  if (!raw) return null;
837
1016
  try {
838
1017
  const parsed = JSON.parse(raw);
839
- if (parsed?.v !== 1) return null;
1018
+ const p = parsed?.palette;
1019
+ const f = parsed?.fonts;
1020
+ const palette = p && typeof p.dark === "string" && typeof p.primary === "string" && typeof p.accent === "string" && typeof p.light === "string" ? { dark: p.dark, primary: p.primary, accent: p.accent, light: p.light } : void 0;
1021
+ const fonts = f && typeof f.heading === "string" && typeof f.body === "string" ? { heading: f.heading, body: f.body } : void 0;
1022
+ const customFonts = Array.isArray(parsed?.customFonts) ? parsed.customFonts.map(parseCustomFont).filter((c) => c !== null) : void 0;
1023
+ const b = parsed?.buttons;
1024
+ const buttons = b && isButtonStyle(b.primary) && isButtonStyle(b.secondary) ? { primary: b.primary, secondary: b.secondary } : void 0;
1025
+ if (!palette && !fonts && !customFonts && !buttons) return null;
840
1026
  return {
841
- v: 1,
842
- sections: typeof parsed.sections === "object" && parsed.sections ? parsed.sections : {},
843
- nodes: typeof parsed.nodes === "object" && parsed.nodes ? parsed.nodes : {}
1027
+ ...palette ? { palette } : {},
1028
+ ...fonts ? { fonts } : {},
1029
+ ...customFonts ? { customFonts } : {},
1030
+ ...buttons ? { buttons } : {},
1031
+ ...parsed.touched && typeof parsed.touched === "object" ? {
1032
+ touched: {
1033
+ palette: parsed.touched.palette === true,
1034
+ fonts: parsed.touched.fonts === true,
1035
+ buttons: parseTouchedButtons(parsed.touched.buttons)
1036
+ }
1037
+ } : {}
844
1038
  };
845
1039
  } catch {
846
1040
  return null;
847
1041
  }
848
1042
  }
849
- var BG_VALUES = {
850
- surface: "color-mix(in srgb, var(--ohw-brand-light, var(--color-light, #ECECEB)) 94%, var(--ohw-brand-dark, var(--color-dark, #0C0A09)))",
851
- accent: "var(--ohw-brand-primary, var(--color-primary, #0078E5))",
852
- "accent-soft": "color-mix(in srgb, var(--ohw-brand-primary, var(--color-primary, #0078E5)) 12%, var(--ohw-brand-light, var(--color-light, #FFFFFF)))"
853
- };
854
- var NAVBAR_COLOR_TOKENS = ["dark", "primary", "accent", "light"];
855
- function navbarColorVar(token) {
856
- const fallback = {
857
- dark: "#0C0A09",
858
- primary: "#0078E5",
859
- accent: "#0078E5",
860
- light: "#FFFFFF"
861
- };
862
- return `var(--ohw-brand-${token}, var(--color-${token}, ${fallback[token]}))`;
863
- }
864
- function resolveNavbarValue(value) {
865
- if (value === void 0) return void 0;
866
- return NAVBAR_COLOR_TOKENS.includes(value) ? navbarColorVar(value) : value;
1043
+ function familyOf(stack) {
1044
+ const first = stack.split(",")[0]?.trim() ?? "";
1045
+ return first.replace(/^['"]|['"]$/g, "");
867
1046
  }
868
- var HEADLINE_SIZES = { md: 24, lg: 32, xl: 40, display: 56 };
869
- function styleSheetCss() {
870
- const rules = [];
871
- for (const [tone, value] of Object.entries(BG_VALUES)) {
872
- rules.push(`[data-ohw-style-bg="${tone}"] { background: ${value} !important; }`);
873
- }
874
- rules.push(
875
- `[data-ohw-style-bg="accent"] { color: var(--ohw-brand-light, var(--color-light, #FFFFFF)) !important; }`
876
- );
877
- rules.push(
878
- `[data-ohw-style-distribution="space-between"] { display: flex !important; flex-direction: column; justify-content: space-between; }`,
879
- `[data-ohw-style-distribution="center"] { display: flex !important; flex-direction: column; justify-content: center; }`
880
- );
881
- for (const [scale, size] of Object.entries(HEADLINE_SIZES)) {
882
- rules.push(
883
- `[data-ohw-style-headline="${scale}"] :is(h1, h2, h3) { font-size: ${size}px !important; line-height: 1.15 !important; }`
884
- );
885
- }
886
- rules.push(
887
- `[data-ohw-style-aspect="fill-height"] img { height: 100% !important; aspect-ratio: auto !important; object-fit: cover; }`
888
- );
889
- for (const aspect of ["1:1", "4:5", "3:4", "3:2", "16:9", "2:1", "3:1"]) {
890
- rules.push(
891
- `[data-ohw-style-aspect="${aspect.replace(":", "-")}"] img { aspect-ratio: ${aspect.replace(":", " / ")} !important; height: auto !important; object-fit: cover; }`
892
- );
893
- }
894
- const pad = { tight: 40, balanced: 64, airy: 96 };
895
- for (const [spacing, px2] of Object.entries(pad)) {
896
- rules.push(
897
- `[data-ohw-style-spacing="${spacing}"] { padding-top: ${px2}px !important; padding-bottom: ${px2}px !important; }`
898
- );
899
- }
900
- rules.push(
901
- `[data-ohw-style-corners="sharp"] :is(.card, [data-ohw-card], img, picture, figure, [data-ohw-editable="bg-image"]) { border-radius: 0 !important; }`,
902
- `[data-ohw-style-corners="sharp"] :has(> img) { border-radius: 0 !important; }`,
903
- `[data-ohw-style-corners="rounded"] :is(.card, [data-ohw-card], img, picture, figure, [data-ohw-editable="bg-image"]) { border-radius: max(0.75rem, var(--radius, 0px)) !important; }`,
904
- `[data-ohw-style-corners="rounded"] :has(> img) { border-radius: max(0.75rem, var(--radius, 0px)) !important; }`
905
- );
906
- for (const align of ["left", "center", "right"]) {
907
- rules.push(`[data-ohw-style-align="${align}"] { text-align: ${align} !important; }`);
908
- }
909
- return rules.join("\n");
1047
+ function quoteFamily(family) {
1048
+ return `'${family.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
910
1049
  }
911
- var STYLE_FONT_LINK_ID = "ohw-style-fonts";
912
- function loadStyleFonts(families) {
1050
+ var WEIGHT_SCALE = [100, 200, 300, 400, 500, 600, 700, 800, 900];
1051
+ function loadBrandFonts(families) {
913
1052
  const unique = [...new Set(families.map((f) => f.trim()).filter(Boolean))];
914
- const existing = document.getElementById(STYLE_FONT_LINK_ID);
915
- if (unique.length === 0) {
916
- existing?.remove();
917
- return;
918
- }
919
- const spec = unique.map((f) => `${f.replace(/ /g, "+")}:400,500,600,700`).join("|");
920
- const href = `https://fonts.googleapis.com/css?family=${spec}&display=swap`;
921
- let link = existing;
1053
+ if (unique.length === 0) return;
1054
+ const weights = WEIGHT_SCALE.join(";");
1055
+ const spec = unique.map((f) => `family=${f.replace(/ /g, "+")}:wght@${weights}`).join("&");
1056
+ const href = `https://fonts.googleapis.com/css2?${spec}&display=swap`;
1057
+ let link = document.getElementById(BRAND_FONT_LINK_ID);
922
1058
  if (!link) {
923
1059
  link = document.createElement("link");
924
- link.id = STYLE_FONT_LINK_ID;
1060
+ link.id = BRAND_FONT_LINK_ID;
925
1061
  link.rel = "stylesheet";
926
1062
  document.head.appendChild(link);
927
1063
  }
928
1064
  if (link.href !== href) link.href = href;
929
1065
  }
930
- var SECTION_ATTRS = {
931
- sectionBackground: "data-ohw-style-bg",
932
- textDistribution: "data-ohw-style-distribution",
933
- headlineScale: "data-ohw-style-headline",
934
- imageAspect: "data-ohw-style-aspect",
935
- spacing: "data-ohw-style-spacing",
936
- cornerStyle: "data-ohw-style-corners",
937
- align: "data-ohw-style-align"
938
- };
939
- var NODE_WROTE_ATTR = "data-ohw-style-node";
940
- var NODE_PROPS = [
941
- "color",
942
- "font-family",
943
- "font-size",
944
- "text-align",
945
- "justify-content",
946
- "align-items"
947
- ];
948
- var ALIGN_JUSTIFY = {
949
- left: "flex-start",
950
- center: "center",
951
- right: "flex-end"
952
- };
953
- function saveInline(el, prop) {
954
- const attr = `data-ohw-style-prev-${prop}`;
955
- if (el.hasAttribute(attr)) return;
956
- const value = el.style.getPropertyValue(prop) || (prop === "background" ? el.style.getPropertyValue("background-color") : "");
957
- el.setAttribute(attr, value);
958
- }
959
- function restoreInline(el, prop) {
960
- const attr = `data-ohw-style-prev-${prop}`;
961
- if (!el.hasAttribute(attr)) return;
962
- const prev = el.getAttribute(attr) ?? "";
963
- if (prev) el.style.setProperty(prop, prev);
964
- else el.style.removeProperty(prop);
965
- el.removeAttribute(attr);
1066
+ var BUTTON_RULES_STYLE_ID = "ohw-brand-button-rules";
1067
+ function buttonRulesCss(kit) {
1068
+ const rule = (variant) => {
1069
+ const roles = touchedButtonRoles(variant, kit);
1070
+ const v = (role) => `var(--brand-button-${variant}-${role})`;
1071
+ const decls = [];
1072
+ if (roles.has("bg")) decls.push(`background:${v("bg")} !important`);
1073
+ if (roles.has("text")) decls.push(`color:${v("text")} !important`);
1074
+ if (roles.has("border")) decls.push(`border:1.5px solid ${v("border")} !important`);
1075
+ if (roles.has("radius")) decls.push(`border-radius:${v("radius")} !important`);
1076
+ if (roles.has("padding-x")) decls.push(`padding:var(--brand-button-${variant}-padding, ${v("padding-y")} ${v("padding-x")}) !important`);
1077
+ if (roles.has("font-size")) decls.push(`font-size:${v("font-size")} !important`);
1078
+ if (decls.length === 0) return "";
1079
+ const selectors = [`html .btn--${variant}`, `html .btn-${variant}`];
1080
+ const labelRule = roles.has("text") ? `${selectors.map((x) => `${x} *`).join(",")}{color:inherit !important}` : "";
1081
+ return `${selectors.join(",")}{${decls.join(";")}}` + labelRule + (roles.has("bg") ? `${selectors.map((x) => `${x}:hover`).join(",")}{opacity:0.88}` : "");
1082
+ };
1083
+ return rule("primary") + rule("secondary");
966
1084
  }
967
- function ensureStyleSheet() {
968
- let el = document.getElementById(STYLE_SHEET_ID);
1085
+ function syncButtonRules(kit) {
1086
+ let el = document.getElementById(BUTTON_RULES_STYLE_ID);
1087
+ const css = kit ? buttonRulesCss(kit) : "";
1088
+ if (!css) {
1089
+ el?.remove();
1090
+ return;
1091
+ }
969
1092
  if (!el) {
970
1093
  el = document.createElement("style");
971
- el.id = STYLE_SHEET_ID;
1094
+ el.id = BUTTON_RULES_STYLE_ID;
972
1095
  document.head.appendChild(el);
973
1096
  }
974
- const css = styleSheetCss();
975
1097
  if (el.textContent !== css) el.textContent = css;
976
1098
  }
977
- function clearSectionAttrs(root) {
978
- for (const attr of Object.values(SECTION_ATTRS)) {
979
- for (const el of Array.from(root.querySelectorAll(`[${attr}]`))) el.removeAttribute(attr);
980
- }
981
- for (const el of Array.from(root.querySelectorAll("[data-ohw-style-bgcolor]"))) {
982
- restoreInline(el, "background");
983
- el.removeAttribute("data-ohw-style-bgcolor");
984
- }
985
- }
986
- function clearNodeProps(root) {
987
- for (const el of Array.from(root.querySelectorAll(`[${NODE_WROTE_ATTR}]`))) {
988
- const h = el;
989
- for (const prop of NODE_PROPS) restoreInline(h, prop);
990
- h.removeAttribute(NODE_WROTE_ATTR);
991
- }
1099
+ function escapeCssString(value) {
1100
+ return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
992
1101
  }
993
- function alignSubjectOf(el) {
994
- const button = el.closest('[data-ohw-role="button"]');
995
- if (button?.parentElement) return button.parentElement;
996
- let subject = el;
997
- while (subject.parentElement && !subject.parentElement.hasAttribute("data-ohw-section") && /^inline/.test(getComputedStyle(subject).display)) {
998
- subject = subject.parentElement;
999
- }
1000
- const TEXT_ROW = /^(P|LI|H[1-6]|BLOCKQUOTE|FIGCAPTION|DT|DD)$/;
1001
- const parent = subject.parentElement;
1002
- if (parent && TEXT_ROW.test(parent.tagName) && /^(inline-)?flex$/.test(getComputedStyle(parent).display)) {
1003
- return parent;
1004
- }
1005
- if (parent && !parent.hasAttribute("data-ohw-section")) {
1006
- const pcs = getComputedStyle(parent);
1007
- const children = Array.from(parent.children);
1008
- const textRowChild = (c) => c.hasAttribute("data-ohw-editable") || (c.textContent ?? "").trim().length === 0 || c.getAttribute("aria-hidden") === "true";
1009
- if (/^(inline-)?flex$/.test(pcs.display) && pcs.flexDirection.startsWith("row") && children.length > 0 && children.some((c) => c.hasAttribute("data-ohw-editable")) && children.every(textRowChild)) {
1010
- return parent;
1011
- }
1102
+ function loadCustomFonts(fonts) {
1103
+ const list = fonts ?? [];
1104
+ let style = document.getElementById(CUSTOM_FONT_STYLE_ID);
1105
+ if (list.length === 0) {
1106
+ style?.remove();
1107
+ return;
1012
1108
  }
1013
- const row = subject.closest("li");
1014
- if (row && row !== subject && /^(inline-)?flex$/.test(getComputedStyle(row).display)) {
1015
- return row;
1109
+ if (!style) {
1110
+ style = document.createElement("style");
1111
+ style.id = CUSTOM_FONT_STYLE_ID;
1112
+ document.head.appendChild(style);
1016
1113
  }
1017
- return subject;
1114
+ style.textContent = list.flatMap(
1115
+ (font) => font.weights.map((w) => {
1116
+ const weightDescriptor = w.weightEnd ? `${w.weight} ${w.weightEnd}` : `${w.weight}`;
1117
+ return `@font-face { font-family: '${escapeCssString(font.family)}'; src: url('${escapeCssString(w.url)}') format('${w.format}'); font-weight: ${weightDescriptor}; font-display: swap; }`;
1118
+ })
1119
+ ).join("\n");
1018
1120
  }
1019
- function applyStylesToDom(store) {
1020
- ensureStyleSheet();
1021
- clearSectionAttrs(document);
1022
- clearNodeProps(document);
1023
- loadStyleFonts(
1024
- store ? [
1025
- ...Object.values(store.nodes).flatMap((n) => n.fontFamily ? [n.fontFamily] : []),
1026
- ...Object.values(store.sections).flatMap((s) => s.fontFamily ? [s.fontFamily] : [])
1027
- ] : []
1028
- );
1029
- if (!store) return;
1030
- for (const [sectionId, override] of Object.entries(store.sections)) {
1031
- const escaped = CSS.escape(sectionId);
1032
- const byInstance = document.querySelectorAll(`[data-ohw-instance="${escaped}"]`);
1033
- const sections = Array.from(
1034
- byInstance.length ? byInstance : document.querySelectorAll(`[data-ohw-section="${escaped}"]`)
1035
- );
1036
- const isNavbar = sectionId === "navbar";
1037
- if (isNavbar) {
1038
- const drawer = document.querySelector("[data-ohw-nav-drawer]");
1039
- if (drawer && !sections.includes(drawer)) sections.push(drawer);
1040
- const header = document.querySelector("[data-ohw-nav-root]");
1041
- if (header && !sections.includes(header)) sections.push(header);
1042
- }
1043
- for (const marker of Array.from(sections)) {
1044
- const isDrawerMarker = isNavbar && marker.hasAttribute("data-ohw-nav-drawer");
1045
- const section = marker.querySelector(":scope > [data-ai-section]") ?? marker;
1046
- for (const [prop, attr] of Object.entries(SECTION_ATTRS)) {
1047
- const value = override[prop];
1048
- if (value === void 0) continue;
1049
- if (prop === "sectionBackground" && override.sectionBackgroundColor !== void 0) continue;
1050
- section.setAttribute(attr, String(value).replace(":", "-"));
1051
- }
1052
- const bgOverride = isDrawerMarker ? override.navbarMobileBackground : override.sectionBackgroundColor;
1053
- const textOverride = isDrawerMarker ? override.navbarMobileTextColor : override.textColor;
1054
- const bgValue = isNavbar ? resolveNavbarValue(bgOverride) : bgOverride;
1055
- const textValue = isNavbar ? resolveNavbarValue(textOverride) : textOverride;
1056
- if (bgValue !== void 0) {
1057
- saveInline(section, "background");
1058
- section.style.setProperty("background", bgValue, "important");
1059
- section.setAttribute("data-ohw-style-bgcolor", "");
1060
- }
1061
- if (textValue !== void 0 || override.fontFamily !== void 0) {
1062
- const textEls = marker.querySelectorAll(
1063
- '[data-ohw-editable="text"], [data-ohw-editable="plain"], [data-ohw-field-label]'
1064
- );
1065
- for (const el of Array.from(textEls)) {
1066
- if (textValue !== void 0) {
1067
- saveInline(el, "color");
1068
- el.style.setProperty("color", textValue, "important");
1069
- el.setAttribute(NODE_WROTE_ATTR, "");
1070
- }
1071
- if (override.fontFamily !== void 0) {
1072
- saveInline(el, "font-family");
1073
- el.style.setProperty("font-family", `'${override.fontFamily}'`, "important");
1074
- el.setAttribute(NODE_WROTE_ATTR, "");
1075
- }
1076
- }
1077
- }
1121
+ function applyBrandToDom(kit) {
1122
+ const root = document.documentElement;
1123
+ if (!kit) {
1124
+ for (const name of [...BRAND_VAR_NAMES, ...LEGACY_BRAND_VAR_NAMES, ...BUTTON_VAR_NAMES]) {
1125
+ root.style.removeProperty(name);
1078
1126
  }
1127
+ for (const name of [...FONT_VARS.heading, ...FONT_VARS.body]) root.style.removeProperty(name);
1128
+ document.getElementById(BRAND_FONT_LINK_ID)?.remove();
1129
+ document.getElementById(CUSTOM_FONT_STYLE_ID)?.remove();
1130
+ syncButtonRules(null);
1131
+ return;
1079
1132
  }
1080
- for (const [key, override] of Object.entries(store.nodes)) {
1081
- const nodes = document.querySelectorAll(`[data-ohw-key="${CSS.escape(key)}"]`);
1082
- for (const el of Array.from(nodes)) {
1083
- if (override.color !== void 0) {
1084
- saveInline(el, "color");
1085
- el.style.setProperty("color", override.color, "important");
1086
- el.setAttribute(NODE_WROTE_ATTR, "");
1087
- }
1088
- if (override.fontFamily !== void 0) {
1089
- saveInline(el, "font-family");
1090
- el.style.setProperty("font-family", `'${override.fontFamily}'`, "important");
1091
- el.setAttribute(NODE_WROTE_ATTR, "");
1092
- }
1093
- if (override.fontSize !== void 0) {
1094
- saveInline(el, "font-size");
1095
- el.style.setProperty("font-size", `${override.fontSize}px`, "important");
1096
- el.setAttribute(NODE_WROTE_ATTR, "");
1097
- }
1098
- if (override.align !== void 0) {
1099
- const subject = alignSubjectOf(el);
1100
- saveInline(subject, "text-align");
1101
- saveInline(subject, "justify-content");
1102
- subject.style.setProperty("text-align", override.align, "important");
1103
- subject.style.setProperty(
1104
- "justify-content",
1105
- ALIGN_JUSTIFY[override.align] ?? "flex-start",
1106
- "important"
1107
- );
1108
- subject.setAttribute(NODE_WROTE_ATTR, "");
1109
- }
1133
+ const applyPalette = kit.touched ? kit.touched.palette === true : true;
1134
+ const applyFonts = kit.touched ? kit.touched.fonts === true : true;
1135
+ if (kit.palette) {
1136
+ if (applyPalette) {
1137
+ for (const [name, value] of Object.entries(brandColorVars(kit.palette))) root.style.setProperty(name, value);
1138
+ } else {
1139
+ for (const name of [...BRAND_VAR_NAMES, ...LEGACY_BRAND_VAR_NAMES]) root.style.removeProperty(name);
1110
1140
  }
1111
1141
  }
1112
- for (const [sectionId, override] of Object.entries(store.sections)) {
1113
- if (override.headingColor === void 0) continue;
1114
- const escaped = CSS.escape(sectionId);
1115
- const byInstance = document.querySelectorAll(`[data-ohw-instance="${escaped}"]`);
1116
- const sections = byInstance.length ? byInstance : document.querySelectorAll(`[data-ohw-section="${escaped}"]`);
1117
- for (const marker of Array.from(sections)) {
1118
- const headings = marker.querySelectorAll(
1119
- ":is(h1, h2, h3, h4, h5, h6)[data-ohw-editable], :is(h1, h2, h3, h4, h5, h6) > [data-ohw-editable]"
1142
+ if (kit.buttons) {
1143
+ for (const name of BUTTON_VAR_NAMES) root.style.removeProperty(name);
1144
+ for (const [name, value] of Object.entries(buttonStyleVars({ ...kit, buttons: kit.buttons }))) root.style.setProperty(name, value);
1145
+ }
1146
+ syncButtonRules(kit.buttons ? kit : null);
1147
+ if (kit.customFonts) {
1148
+ loadCustomFonts(kit.customFonts);
1149
+ }
1150
+ if (kit.fonts) {
1151
+ if (applyFonts) {
1152
+ const heading = quoteFamily(kit.fonts.heading);
1153
+ const body = quoteFamily(kit.fonts.body);
1154
+ for (const name of FONT_VARS.heading) root.style.setProperty(name, heading);
1155
+ for (const name of FONT_VARS.body) root.style.setProperty(name, body);
1156
+ const customFamilies = new Set((kit.customFonts ?? []).map((f) => f.family));
1157
+ const googleFamilies = [familyOf(kit.fonts.heading), familyOf(kit.fonts.body)].filter(
1158
+ (f) => !customFamilies.has(f)
1120
1159
  );
1121
- for (const el of Array.from(headings)) {
1122
- saveInline(el, "color");
1123
- el.style.setProperty("color", override.headingColor, "important");
1124
- el.setAttribute(NODE_WROTE_ATTR, "");
1125
- }
1160
+ loadBrandFonts(googleFamilies);
1161
+ } else {
1162
+ for (const name of [...FONT_VARS.heading, ...FONT_VARS.body]) root.style.removeProperty(name);
1163
+ document.getElementById(BRAND_FONT_LINK_ID)?.remove();
1126
1164
  }
1127
1165
  }
1128
1166
  }
1129
1167
 
1168
+ // src/lib/custom-code.ts
1169
+ var ALLOWED_HEAD_TAGS = /* @__PURE__ */ new Set(["style", "meta", "link", "script", "noscript", "base"]);
1170
+ var CUSTOM_CODE_AFTER_HEAD_KEY = "__ohw_custom_code_after_head";
1171
+ var CUSTOM_CODE_BEFORE_HEAD_KEY = "__ohw_custom_code_before_head";
1172
+ function clearExisting(slot) {
1173
+ document.querySelectorAll(`[data-ohw-custom-code="${slot}"]`).forEach((el) => el.remove());
1174
+ }
1175
+ function buildNodes(raw, slot) {
1176
+ if (!raw || !raw.trim()) return [];
1177
+ const parsed = new DOMParser().parseFromString(raw, "text/html");
1178
+ const source = [...parsed.head.childNodes, ...parsed.body.childNodes];
1179
+ const nodes = [];
1180
+ for (const node of source) {
1181
+ if (node.nodeType !== Node.ELEMENT_NODE) continue;
1182
+ const el = node;
1183
+ const tag = el.tagName.toLowerCase();
1184
+ if (!ALLOWED_HEAD_TAGS.has(tag)) continue;
1185
+ const built = document.createElement(tag);
1186
+ for (const attr of Array.from(el.attributes)) built.setAttribute(attr.name, attr.value);
1187
+ if (tag === "script" || tag === "style" || tag === "noscript") built.textContent = el.textContent;
1188
+ built.dataset.ohwCustomCode = slot;
1189
+ nodes.push(built);
1190
+ }
1191
+ return nodes;
1192
+ }
1193
+ function applyCustomCodeToHead(content) {
1194
+ clearExisting("after");
1195
+ clearExisting("before");
1196
+ const afterNodes = buildNodes(content[CUSTOM_CODE_AFTER_HEAD_KEY], "after");
1197
+ if (afterNodes.length > 0) document.head.prepend(...afterNodes);
1198
+ const beforeNodes = buildNodes(content[CUSTOM_CODE_BEFORE_HEAD_KEY], "before");
1199
+ if (beforeNodes.length > 0) document.head.append(...beforeNodes);
1200
+ }
1201
+
1130
1202
  // src/ui/ai-tree/aiSectionsManager.tsx
1131
1203
  var import_react_dom = require("react-dom");
1132
1204
  var import_client = require("react-dom/client");
@@ -3751,7 +3823,9 @@ function AiTreeRenderer({
3751
3823
  ...band ? { buttonLabel: band.buttonLabel } : {}
3752
3824
  };
3753
3825
  const settings = tree.settings ?? {};
3754
- const pad = AI_TREE_TOKENS.sectionPadding[settings.spacing ?? "balanced"];
3826
+ const pad = AI_TREE_TOKENS.sectionPaddingByScale[settings.verticalPadding ?? legacyVerticalPaddingAlias(settings.spacing) ?? "m"];
3827
+ const padCss = pad === 0 ? "0px" : `clamp(${Math.max(16, Math.round(pad * 0.5))}px, ${(pad / 9).toFixed(2)}vw, ${pad}px)`;
3828
+ const minHeightCss = settings.minHeight === "full" ? "var(--ohw-canvas-h, calc(100vh - 80px))" : settings.minHeight === "half" ? "calc(var(--ohw-canvas-h, 100vh) / 2)" : void 0;
3755
3829
  const isOverlay = tree.scaffold === "overlay";
3756
3830
  const overlayAlpha = { low: 0.25, medium: 0.45, high: 0.65 }[settings.overlayIntensity ?? "medium"];
3757
3831
  const backgroundUrl = isOverlay && settings.backgroundMedia ? ctx.resolveMedia(settings.backgroundMedia) : null;
@@ -3791,7 +3865,9 @@ function AiTreeRenderer({
3791
3865
  "data-ai-responsive": "",
3792
3866
  style: {
3793
3867
  position: "relative",
3794
- padding: `${pad}px 0`,
3868
+ padding: `${padCss} 0`,
3869
+ minHeight: minHeightCss,
3870
+ boxSizing: "border-box",
3795
3871
  background: toneBackground ?? (isOverlay && !backgroundUrl ? "#DBEAFE" : void 0),
3796
3872
  backgroundImage: backgroundUrl ? `url(${backgroundUrl})` : void 0,
3797
3873
  backgroundSize: "cover",
@@ -3915,7 +3991,7 @@ function AiFooterRenderer({
3915
3991
  style: {
3916
3992
  background: bandBg,
3917
3993
  color: bandText,
3918
- padding: `${AI_TREE_TOKENS.sectionPadding.balanced}px 0 ${AI_TREE_TOKENS.spacing8}px`
3994
+ padding: `${AI_TREE_TOKENS.sectionPaddingByScale.m}px 0 ${AI_TREE_TOKENS.spacing8}px`
3919
3995
  },
3920
3996
  children: [
3921
3997
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: AI_FOOTER_MOBILE_CSS }),
@@ -27841,19 +27917,20 @@ function OhhwellsBridge() {
27841
27917
  let previousSections;
27842
27918
  const store = parseStyleStore(value);
27843
27919
  if (store) {
27844
- const folded = foldAlignIntoTrees(parseAiSectionsState(aiSectionsRef.current), store);
27845
- if (folded.changed) {
27846
- const nextSections = serializeAiSectionsState(folded.state);
27920
+ const foldedAlign = foldAlignIntoTrees(parseAiSectionsState(aiSectionsRef.current), store);
27921
+ const foldedHeight = foldSectionHeightIntoTrees(foldedAlign.state, foldedAlign.store);
27922
+ if (foldedAlign.changed || foldedHeight.changed) {
27923
+ const nextSections = serializeAiSectionsState(foldedHeight.state);
27847
27924
  if (nextSections !== aiSectionsRef.current) {
27848
27925
  previousSections = aiSectionsRef.current;
27849
27926
  aiSectionsRef.current = nextSections;
27850
- applyAiSectionsToDom(folded.state);
27927
+ applyAiSectionsToDom(foldedHeight.state);
27851
27928
  postToParentRef.current({
27852
27929
  type: "ow:change",
27853
27930
  nodes: [{ key: AI_SECTIONS_KEY, text: nextSections }]
27854
27931
  });
27855
27932
  }
27856
- value = JSON.stringify(folded.store);
27933
+ value = JSON.stringify(foldedHeight.store);
27857
27934
  }
27858
27935
  }
27859
27936
  stylesRef.current = value;
@@ -28478,7 +28555,15 @@ function OhhwellsBridge() {
28478
28555
  width: box.width,
28479
28556
  height: box.height
28480
28557
  } : null;
28481
- postToParentRef.current({ type: "ow:section-nodes", sectionId, found: !!sectionEl, nodes, rect });
28558
+ const settings = resolveSectionSettingsContext(sectionId, aiSectionsRef.current, stylesRef.current);
28559
+ postToParentRef.current({
28560
+ type: "ow:section-nodes",
28561
+ sectionId,
28562
+ found: !!sectionEl,
28563
+ nodes,
28564
+ rect,
28565
+ ...settings ? { settings } : {}
28566
+ });
28482
28567
  };
28483
28568
  let selectionChangeRaf = null;
28484
28569
  const runSelectionChange = () => {
@@ -29145,7 +29230,7 @@ function OhhwellsBridge() {
29145
29230
  postToParent2({
29146
29231
  type: "ow:ready",
29147
29232
  version: "1",
29148
- bridgeVersion: "0.1.126",
29233
+ bridgeVersion: "0.1.127",
29149
29234
  path: pathname,
29150
29235
  nodes: collectEditableNodes(editContentRef.current),
29151
29236
  sections