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