@ohhwells/bridge 0.1.127 → 0.1.128

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