@hyperframes/studio 0.6.26 → 0.6.27

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.html CHANGED
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
6
6
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
7
7
  <title>HyperFrames Studio</title>
8
- <script type="module" crossorigin src="/assets/index-DZWPbGBw.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-DYjmgXgg.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/assets/index-DVpLGNHi.css">
10
10
  </head>
11
11
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/studio",
3
- "version": "0.6.26",
3
+ "version": "0.6.27",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -30,8 +30,8 @@
30
30
  "@codemirror/theme-one-dark": "^6.1.2",
31
31
  "@codemirror/view": "6.40.0",
32
32
  "@phosphor-icons/react": "^2.1.10",
33
- "@hyperframes/core": "0.6.26",
34
- "@hyperframes/player": "0.6.26"
33
+ "@hyperframes/core": "0.6.27",
34
+ "@hyperframes/player": "0.6.27"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/react": "19",
@@ -45,7 +45,7 @@
45
45
  "vite": "^6.4.2",
46
46
  "vitest": "^3.2.4",
47
47
  "zustand": "^5.0.0",
48
- "@hyperframes/producer": "0.6.26"
48
+ "@hyperframes/producer": "0.6.27"
49
49
  },
50
50
  "peerDependencies": {
51
51
  "react": "19",
@@ -321,6 +321,29 @@ describe("resolveVisualDomEditSelectionTarget", () => {
321
321
  expect(visualTarget).toBe(headline);
322
322
  expect(explicitSelection?.id).toBe("container");
323
323
  });
324
+
325
+ it("prefers the visually-on-top sibling over a deeper element in a separate visual layer", () => {
326
+ const document = createDocument(`
327
+ <div id="comp-root">
328
+ <div id="sub-comp" class="sub-comp">
329
+ <img id="sf-chrome" class="sf-chrome" style="width:100%;height:100%" />
330
+ </div>
331
+ <video id="pip-studio" class="pip-studio" style="position:absolute;z-index:15" />
332
+ </div>
333
+ `);
334
+ const pipStudio = document.getElementById("pip-studio") as HTMLElement;
335
+ const sfChrome = document.getElementById("sf-chrome") as HTMLElement;
336
+ const subComp = document.getElementById("sub-comp") as HTMLElement;
337
+ setElementRect(pipStudio, { left: 50, top: 50, width: 320, height: 320 });
338
+ setElementRect(sfChrome, { left: 0, top: 0, width: 1920, height: 1080 });
339
+ setElementRect(subComp, { left: 0, top: 0, width: 1920, height: 1080 });
340
+
341
+ expect(
342
+ resolveVisualDomEditSelectionTarget([pipStudio, subComp, sfChrome], {
343
+ activeCompositionPath: "index.html",
344
+ }),
345
+ ).toBe(pipStudio);
346
+ });
324
347
  });
325
348
 
326
349
  describe("isLargeRasterDomEditSelection", () => {
@@ -12,11 +12,9 @@ import type {
12
12
  import {
13
13
  buildStableSelector,
14
14
  escapeCssString,
15
- getElementDepth,
16
15
  getSelectorIndex,
17
16
  getSourceFileForElement,
18
17
  isHtmlElement,
19
- isTextBearingTag,
20
18
  normalizeTimelineCompositionSource,
21
19
  querySelectorAllSafely,
22
20
  } from "./domEditingDom";
@@ -68,23 +66,6 @@ function hasRenderedBox(el: HTMLElement): boolean {
68
66
 
69
67
  // ─── Visual scoring ──────────────────────────────────────────────────────────
70
68
 
71
- function isEditableTextLeafForScoring(el: HTMLElement): boolean {
72
- return isTextBearingTag(el.tagName.toLowerCase()) && el.children.length === 0;
73
- }
74
-
75
- function getVisualElementScore(el: HTMLElement, pointerStackIndex: number): number {
76
- const tagName = el.tagName.toLowerCase();
77
- const rect = el.getBoundingClientRect();
78
- const area = Math.max(1, rect.width * rect.height);
79
- const smallerElementBonus = Math.max(0, 1_000_000 - Math.min(area, 1_000_000)) / 1_000;
80
- const visualLeafBonus =
81
- isEditableTextLeafForScoring(el) || ["img", "video", "canvas", "svg"].includes(tagName)
82
- ? 2_000
83
- : 0;
84
-
85
- return getElementDepth(el) * 10_000 + visualLeafBonus + smallerElementBonus - pointerStackIndex;
86
- }
87
-
88
69
  // ─── Layer patch target ──────────────────────────────────────────────────────
89
70
 
90
71
  const DOM_LAYER_IGNORED_TAGS = new Set([
@@ -172,25 +153,31 @@ export function resolveVisualDomEditSelectionTarget(
172
153
  elementsFromPoint: Iterable<Element | null | undefined>,
173
154
  options: Pick<DomEditContextOptions, "activeCompositionPath">,
174
155
  ): HTMLElement | null {
175
- let best: { element: HTMLElement; score: number } | null = null;
176
- let pointerStackIndex = 0;
156
+ const candidates: HTMLElement[] = [];
177
157
 
178
158
  for (const entry of elementsFromPoint) {
179
- if (!isHtmlElement(entry)) {
180
- pointerStackIndex += 1;
181
- continue;
159
+ if (!isHtmlElement(entry)) continue;
160
+ if (hasRenderedBox(entry) && getDomLayerPatchTarget(entry, options.activeCompositionPath)) {
161
+ candidates.push(entry);
182
162
  }
163
+ }
183
164
 
184
- if (hasRenderedBox(entry) && getDomLayerPatchTarget(entry, options.activeCompositionPath)) {
185
- const score = getVisualElementScore(entry, pointerStackIndex);
186
- if (!best || score > best.score) {
187
- best = { element: entry, score };
188
- }
165
+ if (candidates.length === 0) return null;
166
+
167
+ // candidates are in visual stacking order (topmost first, from elementsFromPoint).
168
+ // Start with the topmost and only replace with a descendant that is more
169
+ // specific within the same visual subtree. Never jump to an unrelated
170
+ // element that happens to be painted behind the current pick.
171
+ let best = candidates[0];
172
+
173
+ for (let i = 1; i < candidates.length; i++) {
174
+ const candidate = candidates[i];
175
+ if (best.contains(candidate)) {
176
+ best = candidate;
189
177
  }
190
- pointerStackIndex += 1;
191
178
  }
192
179
 
193
- return best?.element ?? null;
180
+ return best;
194
181
  }
195
182
 
196
183
  // ─── Raster detection ────────────────────────────────────────────────────────
@@ -151,7 +151,6 @@ export function useDomEditSession({
151
151
  setAgentModalOpen,
152
152
  setAgentPromptSelectionContext,
153
153
  setAgentModalAnchorPoint,
154
- preloadAgentPromptSnippet,
155
154
  handleAskAgent,
156
155
  handleAgentModalSubmit,
157
156
  } = useAskAgentModal({
@@ -181,10 +180,6 @@ export function useDomEditSession({
181
180
  applyDomSelection,
182
181
  resolveDomSelectionFromPreviewPoint,
183
182
  updateDomEditHoverSelection,
184
- preloadAgentPromptSnippet,
185
- setAgentPromptSelectionContext,
186
- setAgentModalAnchorPoint,
187
- setAgentModalOpen,
188
183
  onClickToSource,
189
184
  });
190
185
 
@@ -1,16 +1,8 @@
1
1
  import { useCallback } from "react";
2
2
  import { liveTime, usePlayerStore } from "../player";
3
- import {
4
- getPreviewLocalPointer,
5
- buildRasterClickSelectionContext,
6
- pauseStudioPreviewPlayback,
7
- } from "../utils/studioPreviewHelpers";
3
+ import { pauseStudioPreviewPlayback } from "../utils/studioPreviewHelpers";
8
4
  import { STUDIO_PREVIEW_SELECTION_ENABLED } from "../components/editor/manualEditingAvailability";
9
- import {
10
- isLargeRasterDomEditSelection,
11
- type DomEditSelection,
12
- } from "../components/editor/domEditing";
13
- import type { AgentModalAnchorPoint } from "../utils/studioHelpers";
5
+ import { type DomEditSelection } from "../components/editor/domEditing";
14
6
 
15
7
  // ── Types ──
16
8
 
@@ -32,12 +24,6 @@ export interface UsePreviewInteractionParams {
32
24
  ) => DomEditSelection | null;
33
25
  updateDomEditHoverSelection: (selection: DomEditSelection | null) => void;
34
26
 
35
- // From useAskAgentModal
36
- preloadAgentPromptSnippet: (selection: DomEditSelection) => Promise<void>;
37
- setAgentPromptSelectionContext: (context: string | undefined) => void;
38
- setAgentModalAnchorPoint: (point: AgentModalAnchorPoint | null) => void;
39
- setAgentModalOpen: (open: boolean) => void;
40
-
41
27
  onClickToSource?: (selection: DomEditSelection) => void;
42
28
  }
43
29
 
@@ -51,10 +37,6 @@ export function usePreviewInteraction({
51
37
  applyDomSelection,
52
38
  resolveDomSelectionFromPreviewPoint,
53
39
  updateDomEditHoverSelection,
54
- preloadAgentPromptSnippet,
55
- setAgentPromptSelectionContext,
56
- setAgentModalAnchorPoint,
57
- setAgentModalOpen,
58
40
  onClickToSource,
59
41
  }: UsePreviewInteractionParams) {
60
42
  const handlePreviewCanvasMouseDown = useCallback(
@@ -69,37 +51,17 @@ export function usePreviewInteraction({
69
51
  }
70
52
  e.preventDefault();
71
53
  e.stopPropagation();
72
- const localPointer = previewIframeRef.current
73
- ? getPreviewLocalPointer(previewIframeRef.current, e.clientX, e.clientY)
74
- : null;
75
54
  applyDomSelection(nextSelection, { additive: e.shiftKey });
76
55
  if (!e.shiftKey && e.altKey && onClickToSource) {
77
56
  onClickToSource(nextSelection);
78
57
  }
79
- if (
80
- !e.shiftKey &&
81
- localPointer &&
82
- isLargeRasterDomEditSelection(nextSelection, localPointer.viewport)
83
- ) {
84
- setAgentPromptSelectionContext(
85
- buildRasterClickSelectionContext(nextSelection, localPointer),
86
- );
87
- setAgentModalAnchorPoint({ x: e.clientX, y: e.clientY });
88
- void preloadAgentPromptSnippet(nextSelection);
89
- setAgentModalOpen(true);
90
- }
91
58
  },
92
59
  [
93
60
  applyDomSelection,
94
61
  captionEditMode,
95
62
  compositionLoading,
96
63
  onClickToSource,
97
- preloadAgentPromptSnippet,
98
64
  resolveDomSelectionFromPreviewPoint,
99
- previewIframeRef,
100
- setAgentModalAnchorPoint,
101
- setAgentModalOpen,
102
- setAgentPromptSelectionContext,
103
65
  ],
104
66
  );
105
67
 
@@ -1,24 +1,18 @@
1
- import type { DomEditViewport, DomEditSelection } from "../components/editor/domEditing";
1
+ import type { DomEditViewport } from "../components/editor/domEditing";
2
2
  import { resolveVisualDomEditSelectionTarget } from "../components/editor/domEditing";
3
3
  import {
4
4
  getDomLayerPatchTarget,
5
5
  isElementComputedVisible,
6
6
  } from "../components/editor/domEditingElement";
7
- import { usePlayerStore, liveTime } from "../player";
8
7
  import { getEventTargetElement } from "./studioHelpers";
9
8
 
10
- export interface PreviewLocalPointer {
9
+ interface PreviewLocalPointer {
11
10
  x: number;
12
11
  y: number;
13
12
  viewport: DomEditViewport;
14
13
  }
15
14
 
16
- export interface PreviewPlayerCompat {
17
- getTime: () => number;
18
- renderSeek: (timeSeconds: number) => void;
19
- }
20
-
21
- export function resolvePreviewLocalPointer(
15
+ function resolvePreviewLocalPointer(
22
16
  iframe: HTMLIFrameElement,
23
17
  doc: Document,
24
18
  win: Window,
@@ -42,24 +36,6 @@ export function resolvePreviewLocalPointer(
42
36
  };
43
37
  }
44
38
 
45
- export function getPreviewLocalPointer(
46
- iframe: HTMLIFrameElement,
47
- clientX: number,
48
- clientY: number,
49
- ): PreviewLocalPointer | null {
50
- let doc: Document | null = null;
51
- let win: Window | null = null;
52
- try {
53
- doc = iframe.contentDocument;
54
- win = iframe.contentWindow;
55
- } catch {
56
- return null;
57
- }
58
- if (!doc || !win) return null;
59
-
60
- return resolvePreviewLocalPointer(iframe, doc, win, clientX, clientY);
61
- }
62
-
63
39
  const POINTER_EVENTS_OVERRIDE_ID = "__hf_studio_pointer_events_override__";
64
40
 
65
41
  function forcePointerEventsAuto(doc: Document): HTMLStyleElement | null {
@@ -122,21 +98,6 @@ export function getPreviewTargetFromPointer(
122
98
  }
123
99
  }
124
100
 
125
- export function buildRasterClickSelectionContext(
126
- selection: DomEditSelection,
127
- localPointer: PreviewLocalPointer,
128
- ): string {
129
- return [
130
- "The user clicked a large raster/background element in the Studio preview.",
131
- `Preview click: x=${Math.round(localPointer.x)}px, y=${Math.round(localPointer.y)}px in a ${Math.round(
132
- localPointer.viewport.width,
133
- )}x${Math.round(localPointer.viewport.height)} composition.`,
134
- `Selected target: <${selection.tagName}> ${selection.selector ?? selection.id ?? selection.label}.`,
135
- "Visible copy or artwork at that point may be baked into the selected image/background rather than a selectable DOM text layer.",
136
- "If the request mentions text seen at the click location, inspect or replace the image asset, or recreate that visible copy as editable DOM.",
137
- ].join("\n");
138
- }
139
-
140
101
  function objectLike(value: unknown): object | null {
141
102
  return value && (typeof value === "object" || typeof value === "function") ? value : null;
142
103
  }
@@ -162,33 +123,6 @@ function readPlaybackTime(target: object | null, key: string): number | null {
162
123
  }
163
124
  }
164
125
 
165
- export function getPreviewPlayer(win: Window | null | undefined): PreviewPlayerCompat | null {
166
- const player = objectLike(win ? Reflect.get(win, "__player") : null);
167
- if (!player) return null;
168
- const getTime = Reflect.get(player, "getTime");
169
- const renderSeek = Reflect.get(player, "renderSeek");
170
- if (typeof getTime !== "function" || typeof renderSeek !== "function") return null;
171
- return {
172
- getTime: () => {
173
- const value = getTime.call(player);
174
- return typeof value === "number" && Number.isFinite(value) ? value : 0;
175
- },
176
- renderSeek: (timeSeconds: number) => {
177
- renderSeek.call(player, timeSeconds);
178
- },
179
- };
180
- }
181
-
182
- export function seekStudioPreview(iframe: HTMLIFrameElement | null, timeSeconds: number): boolean {
183
- const player = getPreviewPlayer(iframe?.contentWindow);
184
- if (!player) return false;
185
- const nextTime = Math.max(0, timeSeconds);
186
- player.renderSeek(nextTime);
187
- usePlayerStore.getState().setCurrentTime(nextTime);
188
- liveTime.notify(nextTime);
189
- return true;
190
- }
191
-
192
126
  export function pauseStudioPreviewPlayback(iframe: HTMLIFrameElement | null): number | null {
193
127
  const win = iframe?.contentWindow;
194
128
  if (!win) return null;