@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/assets/{index-DZWPbGBw.js → index-DYjmgXgg.js} +33 -34
- package/dist/index.html +1 -1
- package/package.json +4 -4
- package/src/components/editor/domEditing.test.ts +23 -0
- package/src/components/editor/domEditingElement.ts +18 -31
- package/src/hooks/useDomEditSession.ts +0 -5
- package/src/hooks/usePreviewInteraction.ts +2 -40
- package/src/utils/studioPreviewHelpers.ts +3 -69
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-
|
|
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.
|
|
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.
|
|
34
|
-
"@hyperframes/player": "0.6.
|
|
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.
|
|
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
|
-
|
|
176
|
-
let pointerStackIndex = 0;
|
|
156
|
+
const candidates: HTMLElement[] = [];
|
|
177
157
|
|
|
178
158
|
for (const entry of elementsFromPoint) {
|
|
179
|
-
if (!isHtmlElement(entry))
|
|
180
|
-
|
|
181
|
-
|
|
159
|
+
if (!isHtmlElement(entry)) continue;
|
|
160
|
+
if (hasRenderedBox(entry) && getDomLayerPatchTarget(entry, options.activeCompositionPath)) {
|
|
161
|
+
candidates.push(entry);
|
|
182
162
|
}
|
|
163
|
+
}
|
|
183
164
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
9
|
+
interface PreviewLocalPointer {
|
|
11
10
|
x: number;
|
|
12
11
|
y: number;
|
|
13
12
|
viewport: DomEditViewport;
|
|
14
13
|
}
|
|
15
14
|
|
|
16
|
-
|
|
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;
|