diff --git a/.filesize-allowlist b/.filesize-allowlist
index 94f488521..b0cfc09e1 100644
--- a/.filesize-allowlist
+++ b/.filesize-allowlist
@@ -9,3 +9,4 @@ packages/studio/src/utils/sourcePatcher.test.ts
packages/studio/src/App.tsx
packages/studio/src/player/components/Timeline.tsx
packages/studio/src/player/components/timelineEditing.test.ts
+packages/studio/src/components/editor/domEditing.test.ts
diff --git a/packages/core/src/studio-api/helpers/manualEditsRenderScript.test.ts b/packages/core/src/studio-api/helpers/manualEditsRenderScript.test.ts
index 3e41a15f2..f052a6d2c 100644
--- a/packages/core/src/studio-api/helpers/manualEditsRenderScript.test.ts
+++ b/packages/core/src/studio-api/helpers/manualEditsRenderScript.test.ts
@@ -1,6 +1,9 @@
import { describe, expect, it } from "vitest";
import { Window } from "happy-dom";
-import { createStudioManualEditsRenderBodyScript } from "./manualEditsRenderScript";
+import {
+ createStudioManualEditsRenderBodyScript,
+ createStudioPositionSeekReapplyScript,
+} from "./manualEditsRenderScript";
function runScript(
window: Window,
@@ -380,3 +383,182 @@ describe("createStudioManualEditsRenderBodyScript", () => {
expect(card.style.getPropertyValue("translate")).toContain("--hf-studio-offset-x");
});
});
+
+describe("createStudioPositionSeekReapplyScript", () => {
+ function runPositionScript(
+ window: Window,
+ timers: {
+ setInterval?: typeof globalThis.setInterval;
+ clearInterval?: typeof globalThis.clearInterval;
+ } = {},
+ ): void {
+ Object.assign(window, { SyntaxError });
+ const script = createStudioPositionSeekReapplyScript();
+ const execute = new Function(
+ "window",
+ "document",
+ "HTMLElement",
+ "DOMMatrix",
+ "setInterval",
+ "clearInterval",
+ script,
+ );
+ execute(
+ window,
+ window.document,
+ window.HTMLElement,
+ globalThis.DOMMatrix,
+ timers.setInterval ??
+ (((callback: TimerHandler) => {
+ void callback;
+ return 0 as never;
+ }) as typeof globalThis.setInterval),
+ timers.clearInterval ?? globalThis.clearInterval,
+ );
+ }
+
+ it("reapplies box-size after seek", () => {
+ const window = new Window();
+ window.document.body.innerHTML = `
+
+
+ `;
+ const card = window.document.getElementById("card") as unknown as HTMLElement;
+
+ const originalSeek = () => {
+ card.style.removeProperty("width");
+ card.style.removeProperty("height");
+ };
+ (window as unknown as { __hf: Record }).__hf = { seek: originalSeek };
+
+ runPositionScript(window);
+ const wrappedSeek = (window as unknown as { __hf: { seek: (t: number) => void } }).__hf.seek;
+ wrappedSeek(1);
+
+ expect(card.style.getPropertyValue("width")).toBe("200px");
+ expect(card.style.getPropertyValue("height")).toBe("100px");
+ });
+
+ it("strips GSAP translate from transform after reapplying path offset", () => {
+ const window = new Window();
+ window.document.body.innerHTML = `
+
+
+ `;
+ const card = window.document.getElementById("card") as unknown as HTMLElement;
+
+ const originalSeek = () => {
+ card.style.setProperty("transform", "matrix(1, 0, 0, 1, 120, 60)");
+ };
+ (window as unknown as { __hf: Record }).__hf = { seek: originalSeek };
+
+ runPositionScript(window);
+ const wrappedSeek = (window as unknown as { __hf: { seek: (t: number) => void } }).__hf.seek;
+ wrappedSeek(1);
+
+ expect(card.style.getPropertyValue("translate")).toContain("--hf-studio-offset-x");
+ const transform = card.style.getPropertyValue("transform");
+ if (transform && transform !== "none") {
+ const m = new DOMMatrix(transform);
+ expect(m.m41).toBe(0);
+ expect(m.m42).toBe(0);
+ }
+ });
+
+ it("preserves non-translate components when stripping GSAP transform", () => {
+ const window = new Window();
+ window.document.body.innerHTML = `
+
+
+ `;
+ const card = window.document.getElementById("card") as unknown as HTMLElement;
+
+ const originalSeek = () => {
+ card.style.setProperty("transform", "matrix(0.5, 0, 0, 0.5, 80, 40)");
+ };
+ (window as unknown as { __hf: Record }).__hf = { seek: originalSeek };
+
+ runPositionScript(window);
+ const wrappedSeek = (window as unknown as { __hf: { seek: (t: number) => void } }).__hf.seek;
+ wrappedSeek(1);
+
+ const transform = card.style.getPropertyValue("transform");
+ expect(transform).toBeTruthy();
+ expect(transform).not.toContain("80");
+ expect(transform).not.toContain("40");
+ });
+
+ it("removes transform entirely when it becomes identity after stripping translate", () => {
+ const window = new Window();
+ window.document.body.innerHTML = `
+
+
+ `;
+ const card = window.document.getElementById("card") as unknown as HTMLElement;
+
+ const originalSeek = () => {
+ card.style.setProperty("transform", "matrix(1, 0, 0, 1, 50, 25)");
+ };
+ (window as unknown as { __hf: Record }).__hf = { seek: originalSeek };
+
+ runPositionScript(window);
+ const wrappedSeek = (window as unknown as { __hf: { seek: (t: number) => void } }).__hf.seek;
+ wrappedSeek(1);
+
+ const transform = card.style.getPropertyValue("transform");
+ expect(!transform || transform === "none" || transform === "").toBe(true);
+ });
+
+ it("no-ops when transform is 'none'", () => {
+ const window = new Window();
+ window.document.body.innerHTML = `
+
+
+ `;
+ const card = window.document.getElementById("card") as unknown as HTMLElement;
+
+ (window as unknown as { __hf: Record }).__hf = { seek: () => {} };
+ runPositionScript(window);
+
+ expect(card.style.getPropertyValue("transform")).toBe("none");
+ });
+
+ it("strips GSAP translate for rotation-only elements", () => {
+ const window = new Window();
+ window.document.body.innerHTML = `
+
+
+ `;
+ const card = window.document.getElementById("card") as unknown as HTMLElement;
+
+ const originalSeek = () => {
+ card.style.setProperty("transform", "matrix(1, 0, 0, 1, 100, 50)");
+ };
+ (window as unknown as { __hf: Record }).__hf = { seek: originalSeek };
+
+ runPositionScript(window);
+ const wrappedSeek = (window as unknown as { __hf: { seek: (t: number) => void } }).__hf.seek;
+ wrappedSeek(1);
+
+ expect(card.style.getPropertyValue("rotate")).toContain("--hf-studio-rotation");
+ const transform = card.style.getPropertyValue("transform");
+ expect(!transform || transform === "none" || transform === "").toBe(true);
+ });
+});
diff --git a/packages/core/src/studio-api/helpers/manualEditsRenderScript.ts b/packages/core/src/studio-api/helpers/manualEditsRenderScript.ts
index 976f49df0..f5b57f9f1 100644
--- a/packages/core/src/studio-api/helpers/manualEditsRenderScript.ts
+++ b/packages/core/src/studio-api/helpers/manualEditsRenderScript.ts
@@ -25,8 +25,11 @@ export function createStudioPositionSeekReapplyScript(): string {
function studioPositionSeekReapplyRuntime(): void {
const OFFSET_X_PROP = "--hf-studio-offset-x";
const OFFSET_Y_PROP = "--hf-studio-offset-y";
+ const WIDTH_PROP = "--hf-studio-width";
+ const HEIGHT_PROP = "--hf-studio-height";
const ROTATION_PROP = "--hf-studio-rotation";
const PATH_OFFSET_ATTR = "data-hf-studio-path-offset";
+ const BOX_SIZE_ATTR = "data-hf-studio-box-size";
const ROTATION_ATTR = "data-hf-studio-rotation";
const ORIGINAL_TRANSLATE_ATTR = "data-hf-studio-original-translate";
const ORIGINAL_ROTATE_ATTR = "data-hf-studio-original-rotate";
@@ -36,6 +39,7 @@ function studioPositionSeekReapplyRuntime(): void {
if (
!document.querySelector("[" + PATH_OFFSET_ATTR + '="true"]') &&
+ !document.querySelector("[" + BOX_SIZE_ATTR + '="true"]') &&
!document.querySelector("[" + ROTATION_ATTR + '="true"]') &&
!document.querySelector("[" + MOTION_ATTR + "]")
)
@@ -191,6 +195,27 @@ function studioPositionSeekReapplyRuntime(): void {
(tl.totalTime as (t: number, s: boolean) => void)(lastSeekTime, false);
};
+ const stripGsapTranslateFromTransform = (el: HTMLElement): void => {
+ const transform = el.style.getPropertyValue("transform");
+ if (!transform || transform === "none") return;
+ const win = el.ownerDocument.defaultView as (Window & typeof globalThis) | null;
+ const MatrixCtor = (win as unknown as { DOMMatrix?: typeof DOMMatrix })?.DOMMatrix;
+ if (!MatrixCtor) return;
+ try {
+ const m = new MatrixCtor(transform);
+ if (m.m41 === 0 && m.m42 === 0) return;
+ m.m41 = 0;
+ m.m42 = 0;
+ if (m.is2D && m.a === 1 && m.b === 0 && m.c === 0 && m.d === 1) {
+ el.style.removeProperty("transform");
+ } else {
+ el.style.setProperty("transform", m.toString());
+ }
+ } catch {
+ /* non-parseable transform — leave as-is */
+ }
+ };
+
const reapplyAll = (): void => {
const offsetEls = document.querySelectorAll("[" + PATH_OFFSET_ATTR + '="true"]');
for (let i = 0; i < offsetEls.length; i++) {
@@ -207,8 +232,18 @@ function studioPositionSeekReapplyRuntime(): void {
"var(" + OFFSET_Y_PROP + ", 0px)",
),
);
+ stripGsapTranslateFromTransform(el);
}
}
+ const boxSizeEls = document.querySelectorAll("[" + BOX_SIZE_ATTR + '="true"]');
+ for (let i = 0; i < boxSizeEls.length; i++) {
+ const el = boxSizeEls[i] as HTMLElement;
+ if (!(el instanceof HTMLElement)) continue;
+ const w = el.style.getPropertyValue(WIDTH_PROP);
+ const h = el.style.getPropertyValue(HEIGHT_PROP);
+ if (w) el.style.setProperty("width", w);
+ if (h) el.style.setProperty("height", h);
+ }
const rotEls = document.querySelectorAll("[" + ROTATION_ATTR + '="true"]');
for (let i = 0; i < rotEls.length; i++) {
const el = rotEls[i] as HTMLElement;
@@ -216,6 +251,7 @@ function studioPositionSeekReapplyRuntime(): void {
const rot = el.style.getPropertyValue(ROTATION_PROP);
if (rot) {
el.style.setProperty("rotate", composeRotation(el, "var(" + ROTATION_PROP + ", 0deg)"));
+ stripGsapTranslateFromTransform(el);
}
}
reapplyMotionTimeline();
diff --git a/packages/producer/src/services/htmlCompiler.ts b/packages/producer/src/services/htmlCompiler.ts
index 4b9362404..5894a3e58 100644
--- a/packages/producer/src/services/htmlCompiler.ts
+++ b/packages/producer/src/services/htmlCompiler.ts
@@ -904,6 +904,7 @@ export async function compileForRender(
// positions survive frame-by-frame rendering without a JSON sidecar.
const HF_POSITION_ATTRS = [
'data-hf-studio-path-offset="true"',
+ 'data-hf-studio-box-size="true"',
'data-hf-studio-rotation="true"',
'data-hf-studio-motion="',
];
diff --git a/packages/studio/src/components/editor/domEditing.test.ts b/packages/studio/src/components/editor/domEditing.test.ts
index a15d31083..6368e0fde 100644
--- a/packages/studio/src/components/editor/domEditing.test.ts
+++ b/packages/studio/src/components/editor/domEditing.test.ts
@@ -321,6 +321,29 @@ describe("resolveVisualDomEditSelectionTarget", () => {
expect(visualTarget).toBe(headline);
expect(explicitSelection?.id).toBe("container");
});
+
+ it("prefers the visually-on-top sibling over a deeper element in a separate visual layer", () => {
+ const document = createDocument(`
+
+
+
![]()
+
+
+
+ `);
+ const pipStudio = document.getElementById("pip-studio") as HTMLElement;
+ const sfChrome = document.getElementById("sf-chrome") as HTMLElement;
+ const subComp = document.getElementById("sub-comp") as HTMLElement;
+ setElementRect(pipStudio, { left: 50, top: 50, width: 320, height: 320 });
+ setElementRect(sfChrome, { left: 0, top: 0, width: 1920, height: 1080 });
+ setElementRect(subComp, { left: 0, top: 0, width: 1920, height: 1080 });
+
+ expect(
+ resolveVisualDomEditSelectionTarget([pipStudio, subComp, sfChrome], {
+ activeCompositionPath: "index.html",
+ }),
+ ).toBe(pipStudio);
+ });
});
describe("isLargeRasterDomEditSelection", () => {
diff --git a/packages/studio/src/components/editor/domEditingElement.ts b/packages/studio/src/components/editor/domEditingElement.ts
index 4a339c859..7d986234a 100644
--- a/packages/studio/src/components/editor/domEditingElement.ts
+++ b/packages/studio/src/components/editor/domEditingElement.ts
@@ -12,11 +12,9 @@ import type {
import {
buildStableSelector,
escapeCssString,
- getElementDepth,
getSelectorIndex,
getSourceFileForElement,
isHtmlElement,
- isTextBearingTag,
normalizeTimelineCompositionSource,
querySelectorAllSafely,
} from "./domEditingDom";
@@ -68,23 +66,6 @@ function hasRenderedBox(el: HTMLElement): boolean {
// ─── Visual scoring ──────────────────────────────────────────────────────────
-function isEditableTextLeafForScoring(el: HTMLElement): boolean {
- return isTextBearingTag(el.tagName.toLowerCase()) && el.children.length === 0;
-}
-
-function getVisualElementScore(el: HTMLElement, pointerStackIndex: number): number {
- const tagName = el.tagName.toLowerCase();
- const rect = el.getBoundingClientRect();
- const area = Math.max(1, rect.width * rect.height);
- const smallerElementBonus = Math.max(0, 1_000_000 - Math.min(area, 1_000_000)) / 1_000;
- const visualLeafBonus =
- isEditableTextLeafForScoring(el) || ["img", "video", "canvas", "svg"].includes(tagName)
- ? 2_000
- : 0;
-
- return getElementDepth(el) * 10_000 + visualLeafBonus + smallerElementBonus - pointerStackIndex;
-}
-
// ─── Layer patch target ──────────────────────────────────────────────────────
const DOM_LAYER_IGNORED_TAGS = new Set([
@@ -172,25 +153,31 @@ export function resolveVisualDomEditSelectionTarget(
elementsFromPoint: Iterable,
options: Pick,
): HTMLElement | null {
- let best: { element: HTMLElement; score: number } | null = null;
- let pointerStackIndex = 0;
+ const candidates: HTMLElement[] = [];
for (const entry of elementsFromPoint) {
- if (!isHtmlElement(entry)) {
- pointerStackIndex += 1;
- continue;
+ if (!isHtmlElement(entry)) continue;
+ if (hasRenderedBox(entry) && getDomLayerPatchTarget(entry, options.activeCompositionPath)) {
+ candidates.push(entry);
}
+ }
- if (hasRenderedBox(entry) && getDomLayerPatchTarget(entry, options.activeCompositionPath)) {
- const score = getVisualElementScore(entry, pointerStackIndex);
- if (!best || score > best.score) {
- best = { element: entry, score };
- }
+ if (candidates.length === 0) return null;
+
+ // candidates are in visual stacking order (topmost first, from elementsFromPoint).
+ // Start with the topmost and only replace with a descendant that is more
+ // specific within the same visual subtree. Never jump to an unrelated
+ // element that happens to be painted behind the current pick.
+ let best = candidates[0];
+
+ for (let i = 1; i < candidates.length; i++) {
+ const candidate = candidates[i];
+ if (best.contains(candidate)) {
+ best = candidate;
}
- pointerStackIndex += 1;
}
- return best?.element ?? null;
+ return best;
}
// ─── Raster detection ────────────────────────────────────────────────────────
diff --git a/packages/studio/src/hooks/useDomEditSession.ts b/packages/studio/src/hooks/useDomEditSession.ts
index ba0260e06..d33508440 100644
--- a/packages/studio/src/hooks/useDomEditSession.ts
+++ b/packages/studio/src/hooks/useDomEditSession.ts
@@ -151,7 +151,6 @@ export function useDomEditSession({
setAgentModalOpen,
setAgentPromptSelectionContext,
setAgentModalAnchorPoint,
- preloadAgentPromptSnippet,
handleAskAgent,
handleAgentModalSubmit,
} = useAskAgentModal({
@@ -181,10 +180,6 @@ export function useDomEditSession({
applyDomSelection,
resolveDomSelectionFromPreviewPoint,
updateDomEditHoverSelection,
- preloadAgentPromptSnippet,
- setAgentPromptSelectionContext,
- setAgentModalAnchorPoint,
- setAgentModalOpen,
onClickToSource,
});
diff --git a/packages/studio/src/hooks/usePreviewInteraction.ts b/packages/studio/src/hooks/usePreviewInteraction.ts
index 289acc1e9..0da7e6645 100644
--- a/packages/studio/src/hooks/usePreviewInteraction.ts
+++ b/packages/studio/src/hooks/usePreviewInteraction.ts
@@ -1,16 +1,8 @@
import { useCallback } from "react";
import { liveTime, usePlayerStore } from "../player";
-import {
- getPreviewLocalPointer,
- buildRasterClickSelectionContext,
- pauseStudioPreviewPlayback,
-} from "../utils/studioPreviewHelpers";
+import { pauseStudioPreviewPlayback } from "../utils/studioPreviewHelpers";
import { STUDIO_PREVIEW_SELECTION_ENABLED } from "../components/editor/manualEditingAvailability";
-import {
- isLargeRasterDomEditSelection,
- type DomEditSelection,
-} from "../components/editor/domEditing";
-import type { AgentModalAnchorPoint } from "../utils/studioHelpers";
+import { type DomEditSelection } from "../components/editor/domEditing";
// ── Types ──
@@ -32,12 +24,6 @@ export interface UsePreviewInteractionParams {
) => DomEditSelection | null;
updateDomEditHoverSelection: (selection: DomEditSelection | null) => void;
- // From useAskAgentModal
- preloadAgentPromptSnippet: (selection: DomEditSelection) => Promise;
- setAgentPromptSelectionContext: (context: string | undefined) => void;
- setAgentModalAnchorPoint: (point: AgentModalAnchorPoint | null) => void;
- setAgentModalOpen: (open: boolean) => void;
-
onClickToSource?: (selection: DomEditSelection) => void;
}
@@ -51,10 +37,6 @@ export function usePreviewInteraction({
applyDomSelection,
resolveDomSelectionFromPreviewPoint,
updateDomEditHoverSelection,
- preloadAgentPromptSnippet,
- setAgentPromptSelectionContext,
- setAgentModalAnchorPoint,
- setAgentModalOpen,
onClickToSource,
}: UsePreviewInteractionParams) {
const handlePreviewCanvasMouseDown = useCallback(
@@ -69,37 +51,17 @@ export function usePreviewInteraction({
}
e.preventDefault();
e.stopPropagation();
- const localPointer = previewIframeRef.current
- ? getPreviewLocalPointer(previewIframeRef.current, e.clientX, e.clientY)
- : null;
applyDomSelection(nextSelection, { additive: e.shiftKey });
if (!e.shiftKey && e.altKey && onClickToSource) {
onClickToSource(nextSelection);
}
- if (
- !e.shiftKey &&
- localPointer &&
- isLargeRasterDomEditSelection(nextSelection, localPointer.viewport)
- ) {
- setAgentPromptSelectionContext(
- buildRasterClickSelectionContext(nextSelection, localPointer),
- );
- setAgentModalAnchorPoint({ x: e.clientX, y: e.clientY });
- void preloadAgentPromptSnippet(nextSelection);
- setAgentModalOpen(true);
- }
},
[
applyDomSelection,
captionEditMode,
compositionLoading,
onClickToSource,
- preloadAgentPromptSnippet,
resolveDomSelectionFromPreviewPoint,
- previewIframeRef,
- setAgentModalAnchorPoint,
- setAgentModalOpen,
- setAgentPromptSelectionContext,
],
);
diff --git a/packages/studio/src/utils/studioPreviewHelpers.ts b/packages/studio/src/utils/studioPreviewHelpers.ts
index 6c9485bcd..654016539 100644
--- a/packages/studio/src/utils/studioPreviewHelpers.ts
+++ b/packages/studio/src/utils/studioPreviewHelpers.ts
@@ -1,24 +1,18 @@
-import type { DomEditViewport, DomEditSelection } from "../components/editor/domEditing";
+import type { DomEditViewport } from "../components/editor/domEditing";
import { resolveVisualDomEditSelectionTarget } from "../components/editor/domEditing";
import {
getDomLayerPatchTarget,
isElementComputedVisible,
} from "../components/editor/domEditingElement";
-import { usePlayerStore, liveTime } from "../player";
import { getEventTargetElement } from "./studioHelpers";
-export interface PreviewLocalPointer {
+interface PreviewLocalPointer {
x: number;
y: number;
viewport: DomEditViewport;
}
-export interface PreviewPlayerCompat {
- getTime: () => number;
- renderSeek: (timeSeconds: number) => void;
-}
-
-export function resolvePreviewLocalPointer(
+function resolvePreviewLocalPointer(
iframe: HTMLIFrameElement,
doc: Document,
win: Window,
@@ -42,24 +36,6 @@ export function resolvePreviewLocalPointer(
};
}
-export function getPreviewLocalPointer(
- iframe: HTMLIFrameElement,
- clientX: number,
- clientY: number,
-): PreviewLocalPointer | null {
- let doc: Document | null = null;
- let win: Window | null = null;
- try {
- doc = iframe.contentDocument;
- win = iframe.contentWindow;
- } catch {
- return null;
- }
- if (!doc || !win) return null;
-
- return resolvePreviewLocalPointer(iframe, doc, win, clientX, clientY);
-}
-
const POINTER_EVENTS_OVERRIDE_ID = "__hf_studio_pointer_events_override__";
function forcePointerEventsAuto(doc: Document): HTMLStyleElement | null {
@@ -122,21 +98,6 @@ export function getPreviewTargetFromPointer(
}
}
-export function buildRasterClickSelectionContext(
- selection: DomEditSelection,
- localPointer: PreviewLocalPointer,
-): string {
- return [
- "The user clicked a large raster/background element in the Studio preview.",
- `Preview click: x=${Math.round(localPointer.x)}px, y=${Math.round(localPointer.y)}px in a ${Math.round(
- localPointer.viewport.width,
- )}x${Math.round(localPointer.viewport.height)} composition.`,
- `Selected target: <${selection.tagName}> ${selection.selector ?? selection.id ?? selection.label}.`,
- "Visible copy or artwork at that point may be baked into the selected image/background rather than a selectable DOM text layer.",
- "If the request mentions text seen at the click location, inspect or replace the image asset, or recreate that visible copy as editable DOM.",
- ].join("\n");
-}
-
function objectLike(value: unknown): object | null {
return value && (typeof value === "object" || typeof value === "function") ? value : null;
}
@@ -162,33 +123,6 @@ function readPlaybackTime(target: object | null, key: string): number | null {
}
}
-export function getPreviewPlayer(win: Window | null | undefined): PreviewPlayerCompat | null {
- const player = objectLike(win ? Reflect.get(win, "__player") : null);
- if (!player) return null;
- const getTime = Reflect.get(player, "getTime");
- const renderSeek = Reflect.get(player, "renderSeek");
- if (typeof getTime !== "function" || typeof renderSeek !== "function") return null;
- return {
- getTime: () => {
- const value = getTime.call(player);
- return typeof value === "number" && Number.isFinite(value) ? value : 0;
- },
- renderSeek: (timeSeconds: number) => {
- renderSeek.call(player, timeSeconds);
- },
- };
-}
-
-export function seekStudioPreview(iframe: HTMLIFrameElement | null, timeSeconds: number): boolean {
- const player = getPreviewPlayer(iframe?.contentWindow);
- if (!player) return false;
- const nextTime = Math.max(0, timeSeconds);
- player.renderSeek(nextTime);
- usePlayerStore.getState().setCurrentTime(nextTime);
- liveTime.notify(nextTime);
- return true;
-}
-
export function pauseStudioPreviewPlayback(iframe: HTMLIFrameElement | null): number | null {
const win = iframe?.contentWindow;
if (!win) return null;