@ohhwells/bridge 0.1.158 → 0.1.160

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.d.cts CHANGED
@@ -348,6 +348,51 @@ interface AiFooterRendererProps {
348
348
  */
349
349
  declare function AiFooterRenderer({ tree, brand, resolveMedia, editKeyPrefix, logoUrl, resolvePageHref, legal, surface, }: AiFooterRendererProps): react_jsx_runtime.JSX.Element | null;
350
350
 
351
+ /**
352
+ * Relative headline sizing: "make the section heading bigger" as a MULTIPLIER on whatever the
353
+ * template already renders, not an absolute px token.
354
+ *
355
+ * Why this exists. The original `headlineScale` prop is an absolute 4-token px scale (md 24 / lg 32
356
+ * / xl 40 / display 56) painted by a stylesheet rule. Measuring two real templates at 1440px found
357
+ * 12 of 13 section headings rendering ABOVE the top of that scale (rebound: 148, 64, 72, 84;
358
+ * haven-realty: 86, 58, 48, 64, 58 x5), so every token the model could pick, `display` included,
359
+ * made the heading SMALLER. That is the bug users hit: "make it bigger" visibly shrank the heading.
360
+ *
361
+ * Why a multiplier rather than more tokens. The same headings render 30-56px at 390px wide, i.e.
362
+ * the templates have a working responsive type scale. Any absolute px override freezes one width's
363
+ * value at every width: a `96px` token chosen to beat a 1440px heading would still be 96px on a
364
+ * phone where the template wanted 36px. A multiplier rides the template's own scale instead, so it
365
+ * is correct at every breakpoint by construction.
366
+ *
367
+ * Why this is runtime JS and not CSS. CSS cannot multiply an element's OWN font-size: `em` and `%`
368
+ * both resolve against the PARENT's size, so `font-size: calc(1em * 1.2)` on an <h1> throws away
369
+ * the template's h1 sizing rather than scaling it. The only way to scale what the element actually
370
+ * renders is to read it and write it back, which is what this module does, re-measuring on resize
371
+ * so a breakpoint change re-reads the template's new base.
372
+ */
373
+ /** Multiplier per token. `base` is the template's own size, which is also the reset target. */
374
+ declare const HEADLINE_SIZE_MULTIPLIERS: Record<string, number>;
375
+
376
+ /**
377
+ * Per-section and per-node style overrides. The backend's update_style verb (and, later, toolbar
378
+ * UI) produces a delta {target, changed}; the editor merges it onto one reserved node,
379
+ * `__ohw_styles`, and hands the whole store to the bridge. The bridge applies it to the DOM:
380
+ *
381
+ * - Section props are written as `data-ohw-style-*` attributes on the `[data-ohw-section]` element
382
+ * plus one injected stylesheet of attribute-scoped rules. The rules are best-effort by design —
383
+ * they nudge presentation (background band, vertical distribution, headline scale, image fit,
384
+ * spacing) and a template whose structure doesn't tolerate one simply keeps its current look.
385
+ * - Node props (text colour/size, button colours) are written inline on every element carrying the
386
+ * node's `data-ohw-key`, which covers native template nodes and generated-section slots alike.
387
+ *
388
+ * Application is idempotent: each pass clears what it wrote before (tracked via the attributes and
389
+ * a wrote-marker) and re-applies from the store, so removing an override restores the original look
390
+ * and re-rendered generated sections just need a re-apply.
391
+ *
392
+ * The store shape, the node key, and the enums are a contract with the backend
393
+ * (`app/helpers/style-update.ts`) and the editor (which persists/undoes the node).
394
+ */
395
+
351
396
  declare const SECTION_BACKGROUNDS: readonly ["default", "surface", "accent", "accent-soft"];
352
397
  declare const TEXT_DISTRIBUTIONS: readonly ["top", "center", "space-between"];
353
398
  declare const HEADLINE_SCALES: readonly ["md", "lg", "xl", "display"];
@@ -369,8 +414,20 @@ interface SectionStyleOverride {
369
414
  fontFamily?: string;
370
415
  /** Section-wide heading colour — painted onto the section's h1-h6 editables, over textColor. */
371
416
  headingColor?: string;
417
+ /** Section-wide ICON colour (OHH-875) — the glyphs in the section's icon slots, which are
418
+ * neither headings nor body text, so neither textColor nor headingColor could ever reach them.
419
+ * Painted as `color` on each `[data-ohw-editable="icon"]`, which a glyph drawn with
420
+ * stroke/fill="currentColor" (lucide and every set like it) follows. Best-effort, like every
421
+ * other section prop: a glyph with a hardcoded colour of its own keeps it. */
422
+ iconColor?: string;
372
423
  textDistribution?: (typeof TEXT_DISTRIBUTIONS)[number];
424
+ /** @deprecated Absolute px headline tokens. Still rendered for stores saved before the switch,
425
+ * but nothing writes it any more: it could only ever name a size BELOW what most templates
426
+ * render, so "bigger" shrank the heading. New edits use `headlineSize`. */
373
427
  headlineScale?: (typeof HEADLINE_SCALES)[number];
428
+ /** Headline size as a multiplier on the template's own size (`base` = untouched). Relative on
429
+ * purpose, and applied by measuring rather than by a CSS rule — see lib/headline-size.ts. */
430
+ headlineSize?: keyof typeof HEADLINE_SIZE_MULTIPLIERS;
374
431
  imageAspect?: string;
375
432
  /** @deprecated OHH-789 folds this into verticalPadding — a store saved before the fold may
376
433
  * still carry it; legacyVerticalPaddingAlias resolves it at apply time. */
@@ -666,7 +723,7 @@ declare function filterAvailablePages(pages: LinkPage[], existingTargets: string
666
723
 
667
724
  declare const toggleVariants: (props?: ({
668
725
  variant?: "default" | "outline" | null | undefined;
669
- size?: "default" | "lg" | "sm" | null | undefined;
726
+ size?: "default" | "sm" | "lg" | null | undefined;
670
727
  } & class_variance_authority_types.ClassProp) | undefined) => string;
671
728
  declare function Toggle({ className, variant, size, ...props }: React.ComponentProps<typeof Toggle$1.Root> & VariantProps<typeof toggleVariants>): react_jsx_runtime.JSX.Element;
672
729
 
package/dist/index.d.ts CHANGED
@@ -348,6 +348,51 @@ interface AiFooterRendererProps {
348
348
  */
349
349
  declare function AiFooterRenderer({ tree, brand, resolveMedia, editKeyPrefix, logoUrl, resolvePageHref, legal, surface, }: AiFooterRendererProps): react_jsx_runtime.JSX.Element | null;
350
350
 
351
+ /**
352
+ * Relative headline sizing: "make the section heading bigger" as a MULTIPLIER on whatever the
353
+ * template already renders, not an absolute px token.
354
+ *
355
+ * Why this exists. The original `headlineScale` prop is an absolute 4-token px scale (md 24 / lg 32
356
+ * / xl 40 / display 56) painted by a stylesheet rule. Measuring two real templates at 1440px found
357
+ * 12 of 13 section headings rendering ABOVE the top of that scale (rebound: 148, 64, 72, 84;
358
+ * haven-realty: 86, 58, 48, 64, 58 x5), so every token the model could pick, `display` included,
359
+ * made the heading SMALLER. That is the bug users hit: "make it bigger" visibly shrank the heading.
360
+ *
361
+ * Why a multiplier rather than more tokens. The same headings render 30-56px at 390px wide, i.e.
362
+ * the templates have a working responsive type scale. Any absolute px override freezes one width's
363
+ * value at every width: a `96px` token chosen to beat a 1440px heading would still be 96px on a
364
+ * phone where the template wanted 36px. A multiplier rides the template's own scale instead, so it
365
+ * is correct at every breakpoint by construction.
366
+ *
367
+ * Why this is runtime JS and not CSS. CSS cannot multiply an element's OWN font-size: `em` and `%`
368
+ * both resolve against the PARENT's size, so `font-size: calc(1em * 1.2)` on an <h1> throws away
369
+ * the template's h1 sizing rather than scaling it. The only way to scale what the element actually
370
+ * renders is to read it and write it back, which is what this module does, re-measuring on resize
371
+ * so a breakpoint change re-reads the template's new base.
372
+ */
373
+ /** Multiplier per token. `base` is the template's own size, which is also the reset target. */
374
+ declare const HEADLINE_SIZE_MULTIPLIERS: Record<string, number>;
375
+
376
+ /**
377
+ * Per-section and per-node style overrides. The backend's update_style verb (and, later, toolbar
378
+ * UI) produces a delta {target, changed}; the editor merges it onto one reserved node,
379
+ * `__ohw_styles`, and hands the whole store to the bridge. The bridge applies it to the DOM:
380
+ *
381
+ * - Section props are written as `data-ohw-style-*` attributes on the `[data-ohw-section]` element
382
+ * plus one injected stylesheet of attribute-scoped rules. The rules are best-effort by design —
383
+ * they nudge presentation (background band, vertical distribution, headline scale, image fit,
384
+ * spacing) and a template whose structure doesn't tolerate one simply keeps its current look.
385
+ * - Node props (text colour/size, button colours) are written inline on every element carrying the
386
+ * node's `data-ohw-key`, which covers native template nodes and generated-section slots alike.
387
+ *
388
+ * Application is idempotent: each pass clears what it wrote before (tracked via the attributes and
389
+ * a wrote-marker) and re-applies from the store, so removing an override restores the original look
390
+ * and re-rendered generated sections just need a re-apply.
391
+ *
392
+ * The store shape, the node key, and the enums are a contract with the backend
393
+ * (`app/helpers/style-update.ts`) and the editor (which persists/undoes the node).
394
+ */
395
+
351
396
  declare const SECTION_BACKGROUNDS: readonly ["default", "surface", "accent", "accent-soft"];
352
397
  declare const TEXT_DISTRIBUTIONS: readonly ["top", "center", "space-between"];
353
398
  declare const HEADLINE_SCALES: readonly ["md", "lg", "xl", "display"];
@@ -369,8 +414,20 @@ interface SectionStyleOverride {
369
414
  fontFamily?: string;
370
415
  /** Section-wide heading colour — painted onto the section's h1-h6 editables, over textColor. */
371
416
  headingColor?: string;
417
+ /** Section-wide ICON colour (OHH-875) — the glyphs in the section's icon slots, which are
418
+ * neither headings nor body text, so neither textColor nor headingColor could ever reach them.
419
+ * Painted as `color` on each `[data-ohw-editable="icon"]`, which a glyph drawn with
420
+ * stroke/fill="currentColor" (lucide and every set like it) follows. Best-effort, like every
421
+ * other section prop: a glyph with a hardcoded colour of its own keeps it. */
422
+ iconColor?: string;
372
423
  textDistribution?: (typeof TEXT_DISTRIBUTIONS)[number];
424
+ /** @deprecated Absolute px headline tokens. Still rendered for stores saved before the switch,
425
+ * but nothing writes it any more: it could only ever name a size BELOW what most templates
426
+ * render, so "bigger" shrank the heading. New edits use `headlineSize`. */
373
427
  headlineScale?: (typeof HEADLINE_SCALES)[number];
428
+ /** Headline size as a multiplier on the template's own size (`base` = untouched). Relative on
429
+ * purpose, and applied by measuring rather than by a CSS rule — see lib/headline-size.ts. */
430
+ headlineSize?: keyof typeof HEADLINE_SIZE_MULTIPLIERS;
374
431
  imageAspect?: string;
375
432
  /** @deprecated OHH-789 folds this into verticalPadding — a store saved before the fold may
376
433
  * still carry it; legacyVerticalPaddingAlias resolves it at apply time. */
@@ -666,7 +723,7 @@ declare function filterAvailablePages(pages: LinkPage[], existingTargets: string
666
723
 
667
724
  declare const toggleVariants: (props?: ({
668
725
  variant?: "default" | "outline" | null | undefined;
669
- size?: "default" | "lg" | "sm" | null | undefined;
726
+ size?: "default" | "sm" | "lg" | null | undefined;
670
727
  } & class_variance_authority_types.ClassProp) | undefined) => string;
671
728
  declare function Toggle({ className, variant, size, ...props }: React.ComponentProps<typeof Toggle$1.Root> & VariantProps<typeof toggleVariants>): react_jsx_runtime.JSX.Element;
672
729
 
package/dist/index.js CHANGED
@@ -106,6 +106,56 @@ function isRenderableTree(value) {
106
106
  return AI_TREE_SCHEMA_VERSIONS.includes(tree.schemaVersion) && tree.type === "section" && Array.isArray(tree.rows);
107
107
  }
108
108
 
109
+ // src/lib/headline-size.ts
110
+ var HEADLINE_SIZE_MULTIPLIERS = {
111
+ xs: 0.7,
112
+ sm: 0.85,
113
+ base: 1,
114
+ lg: 1.2,
115
+ xl: 1.45,
116
+ "2xl": 1.75
117
+ };
118
+ var HEADLINE_SIZE_ATTR = "data-ohw-style-headlinesize";
119
+ var PREV_ATTR = "data-ohw-style-prev-fsmul";
120
+ var NODE_FONT_SIZE_ATTR = "data-ohw-style-prev-font-size";
121
+ function headingsIn(section) {
122
+ return Array.from(section.querySelectorAll("h1, h2, h3"));
123
+ }
124
+ function clearHeadlineSizes(root = document) {
125
+ for (const el of Array.from(root.querySelectorAll(`[${PREV_ATTR}]`))) {
126
+ const prev = el.getAttribute(PREV_ATTR) ?? "";
127
+ if (prev) el.style.setProperty("font-size", prev);
128
+ else el.style.removeProperty("font-size");
129
+ el.removeAttribute(PREV_ATTR);
130
+ }
131
+ }
132
+ function applyHeadlineSizes(root = document) {
133
+ clearHeadlineSizes(root);
134
+ for (const section of Array.from(
135
+ root.querySelectorAll(`[${HEADLINE_SIZE_ATTR}]`)
136
+ )) {
137
+ const multiplier = HEADLINE_SIZE_MULTIPLIERS[section.getAttribute(HEADLINE_SIZE_ATTR) ?? ""];
138
+ if (!multiplier || multiplier === 1) continue;
139
+ for (const heading of headingsIn(section)) {
140
+ if (heading.hasAttribute(NODE_FONT_SIZE_ATTR)) continue;
141
+ const base = Number.parseFloat(window.getComputedStyle(heading).fontSize);
142
+ if (!Number.isFinite(base) || base <= 0) continue;
143
+ heading.setAttribute(PREV_ATTR, heading.style.getPropertyValue("font-size"));
144
+ heading.style.setProperty("font-size", `${Math.round(base * multiplier * 100) / 100}px`, "important");
145
+ }
146
+ }
147
+ }
148
+ var resizeInstalled = false;
149
+ function installHeadlineSizeResize() {
150
+ if (resizeInstalled || typeof window === "undefined") return;
151
+ resizeInstalled = true;
152
+ let timer;
153
+ window.addEventListener("resize", () => {
154
+ if (timer) clearTimeout(timer);
155
+ timer = setTimeout(() => applyHeadlineSizes(), 120);
156
+ });
157
+ }
158
+
109
159
  // src/lib/sections.ts
110
160
  var LINK_PICKER_EXCLUDED_IDS = /* @__PURE__ */ new Set(["navbar", "footer"]);
111
161
  function isChromeSection(el) {
@@ -5897,6 +5947,7 @@ var SECTION_ATTRS = {
5897
5947
  sectionBackground: "data-ohw-style-bg",
5898
5948
  textDistribution: "data-ohw-style-distribution",
5899
5949
  headlineScale: "data-ohw-style-headline",
5950
+ headlineSize: HEADLINE_SIZE_ATTR,
5900
5951
  imageAspect: "data-ohw-style-aspect",
5901
5952
  spacing: "data-ohw-style-spacing",
5902
5953
  verticalPadding: "data-ohw-style-vpadding",
@@ -5965,6 +6016,15 @@ function clearSectionAttrs(root) {
5965
6016
  }
5966
6017
  }
5967
6018
  var ITEM_WROTE_ATTR = "data-ohw-style-item";
6019
+ function paintIcon(slot, color) {
6020
+ for (const el of [slot, ...Array.from(slot.querySelectorAll("svg, svg *"))]) {
6021
+ const styled = el;
6022
+ if (!styled.style) continue;
6023
+ saveInline(styled, "color");
6024
+ styled.style.setProperty("color", color, "important");
6025
+ styled.setAttribute(NODE_WROTE_ATTR, "");
6026
+ }
6027
+ }
5968
6028
  function clearNodeProps(root) {
5969
6029
  for (const el of Array.from(root.querySelectorAll(`[${NODE_WROTE_ATTR}]`))) {
5970
6030
  const h = el;
@@ -6002,6 +6062,7 @@ function applyStylesToDom(store) {
6002
6062
  ensureStyleSheet();
6003
6063
  clearSectionAttrs(document);
6004
6064
  clearNodeProps(document);
6065
+ clearHeadlineSizes(document);
6005
6066
  loadStyleFonts(
6006
6067
  store ? [
6007
6068
  ...Object.values(store.nodes).flatMap((n) => n.fontFamily ? [n.fontFamily] : []),
@@ -6075,9 +6136,13 @@ function applyStylesToDom(store) {
6075
6136
  const nodes = document.querySelectorAll(`[data-ohw-key="${CSS.escape(key)}"]`);
6076
6137
  for (const el of Array.from(nodes)) {
6077
6138
  if (override.color !== void 0) {
6078
- saveInline(el, "color");
6079
- el.style.setProperty("color", override.color, "important");
6080
- el.setAttribute(NODE_WROTE_ATTR, "");
6139
+ if (el.dataset.ohwEditable === "icon") {
6140
+ paintIcon(el, override.color);
6141
+ } else {
6142
+ saveInline(el, "color");
6143
+ el.style.setProperty("color", override.color, "important");
6144
+ el.setAttribute(NODE_WROTE_ATTR, "");
6145
+ }
6081
6146
  }
6082
6147
  if (override.fontFamily !== void 0) {
6083
6148
  saveInline(el, "font-family");
@@ -6134,6 +6199,18 @@ function applyStylesToDom(store) {
6134
6199
  }
6135
6200
  }
6136
6201
  }
6202
+ for (const [sectionId, override] of Object.entries(store.sections)) {
6203
+ if (override.iconColor === void 0) continue;
6204
+ const escaped = CSS.escape(sectionId);
6205
+ const byInstance = document.querySelectorAll(`[data-ohw-instance="${escaped}"]`);
6206
+ const sections = byInstance.length ? byInstance : document.querySelectorAll(`[data-ohw-section="${escaped}"]`);
6207
+ for (const marker of Array.from(sections)) {
6208
+ const icons = marker.querySelectorAll('[data-ohw-editable="icon"]');
6209
+ for (const el of Array.from(icons)) paintIcon(el, override.iconColor);
6210
+ }
6211
+ }
6212
+ applyHeadlineSizes(document);
6213
+ installHeadlineSizeResize();
6137
6214
  }
6138
6215
 
6139
6216
  // src/lib/ai-sections-store.ts
@@ -6173,17 +6250,21 @@ function resolveSectionTree(sectionId, aiSectionsRaw) {
6173
6250
  return parseAiSectionsState(aiSectionsRaw).sections.find((entry) => entry.id === sectionId)?.tree;
6174
6251
  }
6175
6252
  function resolveSectionSettingsContext(sectionId, aiSectionsRaw, stylesRaw) {
6253
+ const styleOverride = parseStyleStore(stylesRaw)?.sections[sectionId];
6254
+ const headlineSize = styleOverride?.headlineSize;
6176
6255
  const genSettings = parseAiSectionsState(aiSectionsRaw).sections.find(
6177
6256
  (entry) => entry.id === sectionId
6178
6257
  )?.tree.settings;
6179
- if (genSettings) return genSettings;
6180
- const styleOverride = parseStyleStore(stylesRaw)?.sections[sectionId];
6181
- if (!styleOverride?.verticalPadding && !styleOverride?.minHeight && !styleOverride?.spacing) {
6258
+ if (genSettings) {
6259
+ return { ...genSettings, ...headlineSize ? { headlineSize } : {} };
6260
+ }
6261
+ if (!styleOverride?.verticalPadding && !styleOverride?.minHeight && !styleOverride?.spacing && !headlineSize) {
6182
6262
  return void 0;
6183
6263
  }
6184
6264
  return {
6185
- verticalPadding: styleOverride.verticalPadding ?? legacyVerticalPaddingAlias(styleOverride.spacing),
6186
- minHeight: styleOverride.minHeight
6265
+ verticalPadding: styleOverride?.verticalPadding ?? legacyVerticalPaddingAlias(styleOverride?.spacing),
6266
+ minHeight: styleOverride?.minHeight,
6267
+ ...headlineSize ? { headlineSize } : {}
6187
6268
  };
6188
6269
  }
6189
6270
  function applyTreeToState(state, payload) {
@@ -7188,6 +7269,23 @@ function applyCustomCodeToHead(content) {
7188
7269
  if (beforeNodes.length > 0) document.head.append(...beforeNodes);
7189
7270
  }
7190
7271
 
7272
+ // src/lib/node-font-size.ts
7273
+ var TEXT_NODE_TYPES = /* @__PURE__ */ new Set(["text", "text/rich", "plain", "link"]);
7274
+ function escapeKey(key) {
7275
+ return typeof CSS !== "undefined" && typeof CSS.escape === "function" ? CSS.escape(key) : key.replace(/["\\]/g, "\\$&");
7276
+ }
7277
+ function measureNodeFontSize(key, type, root) {
7278
+ if (!TEXT_NODE_TYPES.has(type)) return void 0;
7279
+ const el = root.querySelector(`[data-ohw-editable][data-ohw-key="${escapeKey(key)}"]`);
7280
+ if (!el) return void 0;
7281
+ const size = Number.parseFloat(window.getComputedStyle(el).fontSize);
7282
+ return Number.isFinite(size) && size > 0 ? Math.round(size) : void 0;
7283
+ }
7284
+ function withNodeFontSize(node, root) {
7285
+ const fontSize = measureNodeFontSize(node.key, node.type, root);
7286
+ return fontSize === void 0 ? node : { ...node, fontSize };
7287
+ }
7288
+
7191
7289
  // src/lib/selectNavContainer.ts
7192
7290
  function isOnScreen(el) {
7193
7291
  const rect = el.getBoundingClientRect();
@@ -17127,7 +17225,7 @@ function apiBase() {
17127
17225
  function bridgeVersion() {
17128
17226
  try {
17129
17227
  if (true) {
17130
- return "0.1.158";
17228
+ return "0.1.160";
17131
17229
  }
17132
17230
  } catch {
17133
17231
  }
@@ -17141,7 +17239,7 @@ function stampBridgeVersionOnDocument() {
17141
17239
  return;
17142
17240
  }
17143
17241
  try {
17144
- const v = true ? "0.1.158" : "";
17242
+ const v = true ? "0.1.160" : "";
17145
17243
  if (v) {
17146
17244
  document.documentElement.setAttribute("data-ohw-bridge-version", v);
17147
17245
  window.__OHW_BRIDGE_VERSION__ = v;
@@ -18050,6 +18148,89 @@ function CarouselOverlay({
18050
18148
  // src/ui/ai-section/AiSectionOverlay.tsx
18051
18149
  import { useCallback as useCallback2, useEffect as useEffect5, useRef as useRef4, useState as useState7 } from "react";
18052
18150
  import { Check, X } from "lucide-react";
18151
+
18152
+ // src/lib/ai-change-highlight.ts
18153
+ var STYLE_ID = "ohw-ai-changed-style";
18154
+ var MARK_ATTR = "data-ohw-ai-changed";
18155
+ var HIGHLIGHT_MS = 2200;
18156
+ var HIGHLIGHT_CSS = `
18157
+ @keyframes ohw-ai-changed-pulse {
18158
+ 0% { outline-color: rgba(0, 120, 229, 0.95); box-shadow: 0 0 0 5px rgba(0, 120, 229, 0.20); }
18159
+ 72% { outline-color: rgba(0, 120, 229, 0.95); box-shadow: 0 0 0 5px rgba(0, 120, 229, 0.12); }
18160
+ 100% { outline-color: rgba(0, 120, 229, 0); box-shadow: 0 0 0 5px rgba(0, 120, 229, 0); }
18161
+ }
18162
+ [${MARK_ATTR}] {
18163
+ outline: 2px solid rgba(0, 120, 229, 0.95) !important;
18164
+ outline-offset: 3px !important;
18165
+ animation: ohw-ai-changed-pulse ${HIGHLIGHT_MS}ms ease-out forwards !important;
18166
+ }
18167
+ @media (prefers-reduced-motion: reduce) {
18168
+ [${MARK_ATTR}] { animation: none !important; }
18169
+ }
18170
+ `;
18171
+ function ensureStyleSheet2() {
18172
+ if (document.getElementById(STYLE_ID)) return;
18173
+ const el = document.createElement("style");
18174
+ el.id = STYLE_ID;
18175
+ el.textContent = HIGHLIGHT_CSS;
18176
+ document.head.appendChild(el);
18177
+ }
18178
+ function targetsFor(keys, item) {
18179
+ const out = /* @__PURE__ */ new Set();
18180
+ for (const key of keys) {
18181
+ if (!key) continue;
18182
+ for (const el of Array.from(
18183
+ document.querySelectorAll(`[data-ohw-key="${CSS.escape(key)}"]`)
18184
+ )) {
18185
+ out.add(el);
18186
+ }
18187
+ }
18188
+ if (item) {
18189
+ const section = document.querySelector(`[data-ohw-instance="${CSS.escape(item.sectionId)}"]`) ?? document.querySelector(`[data-ohw-section="${CSS.escape(item.sectionId)}"]`);
18190
+ const stamp = `${item.group}:${item.index}`;
18191
+ const el = section?.querySelector(`[data-ohw-item="${CSS.escape(stamp)}"]`);
18192
+ if (el) out.add(el);
18193
+ }
18194
+ return [...out];
18195
+ }
18196
+ var clearTimer = null;
18197
+ function clearChangeHighlight() {
18198
+ if (clearTimer !== null) {
18199
+ clearTimeout(clearTimer);
18200
+ clearTimer = null;
18201
+ }
18202
+ for (const el of Array.from(document.querySelectorAll(`[${MARK_ATTR}]`))) {
18203
+ el.removeAttribute(MARK_ATTR);
18204
+ }
18205
+ }
18206
+ function highlightChangedElements(keys, item) {
18207
+ clearChangeHighlight();
18208
+ const targets = targetsFor(keys, item);
18209
+ if (targets.length === 0) return 0;
18210
+ ensureStyleSheet2();
18211
+ for (const el of targets) el.setAttribute(MARK_ATTR, "");
18212
+ const rect = targets[0].getBoundingClientRect();
18213
+ if (rect.bottom < 0 || rect.top > window.innerHeight) {
18214
+ targets[0].scrollIntoView({ behavior: "smooth", block: "center" });
18215
+ }
18216
+ clearTimer = setTimeout(clearChangeHighlight, HIGHLIGHT_MS);
18217
+ return targets.length;
18218
+ }
18219
+ function installAiChangeHighlight() {
18220
+ const onMessage = (e) => {
18221
+ const data = e.data;
18222
+ if (data?.type !== "ow:ai-highlight") return;
18223
+ const keys = Array.isArray(data.keys) ? data.keys.filter((k) => typeof k === "string") : [];
18224
+ const raw = data.item;
18225
+ const item = raw && typeof raw.sectionId === "string" && typeof raw.group === "string" && typeof raw.index === "number" ? { sectionId: raw.sectionId, group: raw.group, index: raw.index } : void 0;
18226
+ const found = highlightChangedElements(keys, item);
18227
+ window.parent?.postMessage({ type: "ow:ai-highlight-done", found }, "*");
18228
+ };
18229
+ window.addEventListener("message", onMessage);
18230
+ return () => window.removeEventListener("message", onMessage);
18231
+ }
18232
+
18233
+ // src/ui/ai-section/AiSectionOverlay.tsx
18053
18234
  import { Fragment as Fragment6, jsx as jsx19, jsxs as jsxs10 } from "react/jsx-runtime";
18054
18235
  var findSectionElement = findByInstanceId;
18055
18236
  function readRect(instanceId) {
@@ -18301,6 +18482,7 @@ function AiSectionOverlay({
18301
18482
  window.addEventListener("message", onMessage);
18302
18483
  return () => window.removeEventListener("message", onMessage);
18303
18484
  }, [postToParent2]);
18485
+ useEffect5(() => installAiChangeHighlight(), []);
18304
18486
  const [hoveredId, setHoveredId] = useState7(null);
18305
18487
  useEffect5(() => {
18306
18488
  const onMove = (e) => {
@@ -31220,13 +31402,15 @@ function OhhwellsBridge() {
31220
31402
  if (e.data?.type !== "ow:collect-section") return;
31221
31403
  const sectionId = e.data.sectionId;
31222
31404
  if (typeof sectionId !== "string" || !sectionId) return;
31223
- const foundEl = document.querySelector(
31224
- `[data-ohw-section="${CSS.escape(sectionId)}"]`
31405
+ const escapedSectionId = CSS.escape(sectionId);
31406
+ const instanceEl = document.querySelector(
31407
+ `[data-ohw-instance="${escapedSectionId}"]`
31225
31408
  );
31409
+ const foundEl = instanceEl ? sectionElementOf(instanceEl) : document.querySelector(`[data-ohw-section="${escapedSectionId}"]`);
31226
31410
  const sectionEl = foundEl && !foundEl.hasAttribute("data-ohw-ai-template-hidden") && !foundEl.hasAttribute("data-ohw-ai-removed") && !foundEl.hasAttribute("data-ohw-ai-replaced-by") ? foundEl : null;
31227
31411
  const nodes = sectionEl ? collectEditableNodes(void 0, sectionEl).filter(
31228
31412
  (n) => n.key && !n.key.startsWith("__ohw_") && !["meta", "sections", "video-setting"].includes(n.type)
31229
- ).map((n) => ({ ...n, text: n.text ?? "", type: normalizeAiNodeType(n.type) })) : [];
31413
+ ).map((n) => ({ ...n, text: n.text ?? "", type: normalizeAiNodeType(n.type) })).map((n) => withNodeFontSize(n, sectionEl)) : [];
31230
31414
  const items = sectionEl ? collectItemGroups(sectionEl) : [];
31231
31415
  const box = sectionEl?.getBoundingClientRect();
31232
31416
  const rect = box ? {
@@ -31235,7 +31419,11 @@ function OhhwellsBridge() {
31235
31419
  width: box.width,
31236
31420
  height: box.height
31237
31421
  } : null;
31238
- const settings = resolveSectionSettingsContext(sectionId, aiSectionsRef.current, stylesRef.current);
31422
+ const settings = resolveSectionSettingsContext(
31423
+ sectionId,
31424
+ aiSectionsRef.current,
31425
+ stylesRef.current
31426
+ );
31239
31427
  const tree = resolveSectionTree(sectionId, aiSectionsRef.current);
31240
31428
  postToParentRef.current({
31241
31429
  type: "ow:section-nodes",
@@ -31966,7 +32154,7 @@ function OhhwellsBridge() {
31966
32154
  postToParent2({
31967
32155
  type: "ow:ready",
31968
32156
  version: "1",
31969
- bridgeVersion: "0.1.158",
32157
+ bridgeVersion: "0.1.160",
31970
32158
  path: pathname,
31971
32159
  nodes: collectEditableNodes(editContentRef.current),
31972
32160
  sections