@hyperframes/studio 0.8.85 → 0.8.87
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/hyperframes-player-CtNVImMi.js +473 -0
- package/dist/assets/{index-Bus1BKtk.js → index-B_MF86RE.js} +1 -1
- package/dist/assets/{index-CZxhjQpw.js → index-DJ-Ol7YS.js} +1 -1
- package/dist/assets/{index-ClLdmgua.js → index-DZKcDAtf.js} +203 -203
- package/dist/assets/{index-mzZCha_N.css → index-Dx79V1Se.css} +1 -1
- package/dist/{chunk-7VDQIGOU.js → chunk-7PLTC6SV.js} +8 -2
- package/dist/chunk-7PLTC6SV.js.map +1 -0
- package/dist/{chunk-3IOCVWB6.js → chunk-PQFEGZWM.js} +3 -1
- package/dist/chunk-PQFEGZWM.js.map +1 -0
- package/dist/{domEditingLayers-ZOOMN7CQ.js → domEditingLayers-3BAP6NNO.js} +2 -2
- package/dist/index.d.ts +207 -161
- package/dist/index.html +2 -2
- package/dist/index.js +2084 -1705
- package/dist/index.js.map +1 -1
- package/dist/player.js +1 -1
- package/dist/styles.css +1 -1
- package/package.json +8 -8
- package/src/components/TimelineToolbar.test.tsx +53 -2
- package/src/components/TimelineToolbar.tsx +119 -101
- package/src/components/editor/DomEditOverlay.tsx +1 -2
- package/src/components/editor/DomEditOverlayHover.test.tsx +5 -0
- package/src/components/editor/DomEditSelectionChrome.test.tsx +0 -3
- package/src/components/editor/DomEditSelectionChrome.tsx +3 -13
- package/src/components/editor/LayersPanel.test.ts +14 -0
- package/src/components/editor/LayersPanel.tsx +2 -2
- package/src/components/editor/TopologyLens.tsx +2 -2
- package/src/components/editor/commitPreflightHandles.test.tsx +466 -0
- package/src/components/editor/domEditOverlayGestures.ts +0 -1
- package/src/components/editor/domEditOverlayStartGesture.ts +10 -1
- package/src/components/editor/domEditing.test.ts +29 -0
- package/src/components/editor/domEditingDom.ts +1 -0
- package/src/components/editor/domEditingTypes.ts +1 -0
- package/src/components/editor/marqueeCommit.test.tsx +84 -0
- package/src/components/editor/marqueeCommit.ts +13 -1
- package/src/components/editor/useColorGradingController.test.ts +24 -0
- package/src/components/editor/useColorGradingController.ts +3 -2
- package/src/components/editor/useDomEditOverlayGestures.ts +20 -5
- package/src/components/nle/useCompositionStack.test.tsx +179 -0
- package/src/components/nle/useCompositionStack.ts +26 -20
- package/src/hooks/domStyleCommit.ts +129 -0
- package/src/hooks/gsapEditOutcome.ts +2 -2
- package/src/hooks/gsapElementMatch.ts +38 -0
- package/src/hooks/gsapResizeIntercept.ts +10 -31
- package/src/hooks/gsapResizePreflight.ts +56 -0
- package/src/hooks/gsapRuntimeBridge.ts +66 -26
- package/src/hooks/keyframeCacheAstLoad.ts +4 -1
- package/src/hooks/useCommitPreflightCapabilities.ts +147 -0
- package/src/hooks/useDomEditCommits.ts +17 -182
- package/src/hooks/useDomEditPersist.ts +225 -0
- package/src/hooks/useDomEditPreviewSync.test.ts +58 -0
- package/src/hooks/useDomEditPreviewSync.ts +2 -4
- package/src/hooks/useDomEditSession.test.tsx +51 -1
- package/src/hooks/useDomEditSession.ts +11 -2
- package/src/hooks/useDomEditTextCommits.ts +15 -99
- package/src/hooks/useDomStyleCommit.test.tsx +155 -0
- package/src/hooks/useDomStyleCommit.ts +74 -0
- package/src/hooks/useGsapAnimationFetchFallback.test.ts +12 -0
- package/src/hooks/useGsapAnimationFetchFallback.ts +8 -2
- package/src/hooks/useGsapAwareEditing.test.tsx +4 -0
- package/src/hooks/useGsapAwareEditing.ts +1 -1
- package/src/hooks/useGsapTweenCache.ts +2 -48
- package/src/index.ts +10 -1
- package/src/player/components/Timeline.audioEffects.test.tsx +80 -0
- package/src/player/components/Timeline.bandEscape.test.tsx +75 -0
- package/src/player/components/Timeline.playheadRemount.test.tsx +41 -0
- package/src/player/components/Timeline.test.ts +32 -19
- package/src/player/components/TimelineGroupHeader.tsx +21 -10
- package/src/player/components/TimelineGroupLaneLabels.tsx +7 -2
- package/src/player/components/TimelineGroupRow.test.tsx +24 -33
- package/src/player/components/TimelineGroupRow.tsx +4 -1
- package/src/player/components/TimelineLanes.tsx +5 -5
- package/src/player/components/TimelineTrackHeader.test.tsx +27 -0
- package/src/player/components/TimelineTrackHeader.tsx +6 -3
- package/src/player/components/TimelineTypes.ts +1 -0
- package/src/player/components/timelineLaneProps.ts +1 -0
- package/src/player/components/timelineLayout.ts +16 -23
- package/src/player/components/timelineMountTestEnv.ts +29 -0
- package/src/player/components/useTimelineGeometry.test.tsx +105 -0
- package/src/player/components/useTimelineGeometry.ts +18 -10
- package/src/player/components/useTimelineLabelColumn.ts +18 -0
- package/src/player/components/useTimelineLaneRowIndexes.ts +6 -1
- package/src/player/components/useTimelinePlayhead.ts +1 -1
- package/src/player/components/useTimelineProviderState.tsx +6 -12
- package/src/player/components/useTimelineRangeSelection.ts +13 -12
- package/src/player/hooks/useShadowPreviewReload.ts +2 -0
- package/src/player/hooks/useTimelinePlayer.shadowReload.test.ts +191 -0
- package/src/player/hooks/useTimelinePlayer.ts +29 -7
- package/src/player/lib/runtimeProtocol.test.ts +1 -0
- package/src/player/sceneSwap.test.ts +95 -0
- package/src/player/sceneSwap.ts +53 -0
- package/src/player/store/liveTime.ts +6 -1
- package/dist/assets/hyperframes-player-BK18SggX.js +0 -464
- package/dist/chunk-3IOCVWB6.js.map +0 -1
- package/dist/chunk-7VDQIGOU.js.map +0 -1
- /package/dist/{domEditingLayers-ZOOMN7CQ.js.map → domEditingLayers-3BAP6NNO.js.map} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.87",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -60,12 +60,12 @@
|
|
|
60
60
|
"dockview-react": "8.3.1",
|
|
61
61
|
"gsap": "^3.13.0",
|
|
62
62
|
"mediabunny": "^1.45.3",
|
|
63
|
-
"@hyperframes/core": "0.8.
|
|
64
|
-
"@hyperframes/
|
|
65
|
-
"@hyperframes/
|
|
66
|
-
"@hyperframes/shader-transitions": "0.8.
|
|
67
|
-
"@hyperframes/studio-server": "0.8.
|
|
68
|
-
"@hyperframes/sdk": "0.8.
|
|
63
|
+
"@hyperframes/core": "0.8.87",
|
|
64
|
+
"@hyperframes/parsers": "0.8.87",
|
|
65
|
+
"@hyperframes/player": "0.8.87",
|
|
66
|
+
"@hyperframes/shader-transitions": "0.8.87",
|
|
67
|
+
"@hyperframes/studio-server": "0.8.87",
|
|
68
|
+
"@hyperframes/sdk": "0.8.87"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@tailwindcss/vite": "^4.3.3",
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"vite": "^6.4.2",
|
|
82
82
|
"vitest": "^4.1.11",
|
|
83
83
|
"zustand": "^5.0.0",
|
|
84
|
-
"@hyperframes/producer": "0.8.
|
|
84
|
+
"@hyperframes/producer": "0.8.87"
|
|
85
85
|
},
|
|
86
86
|
"peerDependencies": {
|
|
87
87
|
"react": "19",
|
|
@@ -24,17 +24,22 @@ vi.mock("../contexts/StudioContext", () => ({
|
|
|
24
24
|
|
|
25
25
|
afterEach(() => {
|
|
26
26
|
document.body.innerHTML = "";
|
|
27
|
-
usePlayerStore.setState({
|
|
27
|
+
usePlayerStore.setState({
|
|
28
|
+
autoKeyframeEnabled: true,
|
|
29
|
+
thumbnailMode: "adaptive",
|
|
30
|
+
zoomMode: "fit",
|
|
31
|
+
});
|
|
28
32
|
});
|
|
29
33
|
|
|
30
34
|
function renderToolbar(
|
|
31
35
|
domEditSession?: React.ComponentProps<typeof TimelineToolbar>["domEditSession"],
|
|
36
|
+
props: Partial<React.ComponentProps<typeof TimelineToolbar>> = {},
|
|
32
37
|
) {
|
|
33
38
|
const host = document.createElement("div");
|
|
34
39
|
document.body.append(host);
|
|
35
40
|
const root = createRoot(host);
|
|
36
41
|
act(() => {
|
|
37
|
-
root.render(<TimelineToolbar domEditSession={domEditSession} />);
|
|
42
|
+
root.render(<TimelineToolbar domEditSession={domEditSession} {...props} />);
|
|
38
43
|
});
|
|
39
44
|
return { host, root };
|
|
40
45
|
}
|
|
@@ -175,6 +180,52 @@ describe("TimelineToolbar — keyframes on audio tracks", () => {
|
|
|
175
180
|
expect(button?.disabled).toBe(false);
|
|
176
181
|
act(() => root.unmount());
|
|
177
182
|
});
|
|
183
|
+
|
|
184
|
+
const keyframeControls = (host: HTMLElement) => [
|
|
185
|
+
host.querySelector('button[aria-label="Add keyframe at playhead"]'),
|
|
186
|
+
host.querySelector('button[aria-label="Auto-record manual edits as keyframes"]'),
|
|
187
|
+
];
|
|
188
|
+
/** True when the keyframe shortcut claimed K, so playback never saw it. */
|
|
189
|
+
const pressK = () => {
|
|
190
|
+
const event = new KeyboardEvent("keydown", { key: "k", bubbles: true, cancelable: true });
|
|
191
|
+
act(() => {
|
|
192
|
+
window.dispatchEvent(event);
|
|
193
|
+
});
|
|
194
|
+
return event.defaultPrevented;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
it("shows Add keyframe and auto-record, and K adds a keyframe, by default", () => {
|
|
198
|
+
const { host, root } = renderToolbar(sessionFor("div"));
|
|
199
|
+
expect(keyframeControls(host).every(Boolean)).toBe(true);
|
|
200
|
+
expect(pressK()).toBe(true);
|
|
201
|
+
act(() => root.unmount());
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("hides both controls for a host without keyframes, turns auto-record off and leaves K alone", () => {
|
|
205
|
+
const { host, root } = renderToolbar(sessionFor("div"), { showKeyframes: false });
|
|
206
|
+
expect(keyframeControls(host)).toEqual([null, null]);
|
|
207
|
+
expect(usePlayerStore.getState().autoKeyframeEnabled).toBe(false);
|
|
208
|
+
expect(pressK()).toBe(false);
|
|
209
|
+
act(() => root.unmount());
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe("TimelineToolbar Fit", () => {
|
|
214
|
+
it("shows Fit as a named icon and says whether fit is on", () => {
|
|
215
|
+
const { host, root } = renderToolbar();
|
|
216
|
+
const fit = () => host.querySelector('button[aria-label="Fit timeline to width"]');
|
|
217
|
+
expect(fit()?.textContent).toBe("");
|
|
218
|
+
expect(fit()?.querySelector("svg")).not.toBeNull();
|
|
219
|
+
act(() => fit()?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
|
|
220
|
+
expect(fit()?.getAttribute("aria-pressed")).toBe("true");
|
|
221
|
+
act(() =>
|
|
222
|
+
host
|
|
223
|
+
.querySelector('button[aria-label="Zoom in"]')
|
|
224
|
+
?.dispatchEvent(new MouseEvent("click", { bubbles: true })),
|
|
225
|
+
);
|
|
226
|
+
expect(fit()?.getAttribute("aria-pressed")).toBe("false");
|
|
227
|
+
act(() => root.unmount());
|
|
228
|
+
});
|
|
178
229
|
});
|
|
179
230
|
|
|
180
231
|
describe("TimelineToolbar audio meters", () => {
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { useEffect, useRef } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
ArrowsOutLineHorizontal,
|
|
4
|
+
Image,
|
|
5
|
+
Magnet,
|
|
6
|
+
MagnifyingGlassMinus,
|
|
7
|
+
MagnifyingGlassPlus,
|
|
8
|
+
} from "@phosphor-icons/react";
|
|
3
9
|
import {
|
|
4
10
|
useEnableKeyframes,
|
|
5
11
|
isPlayheadWithinTween,
|
|
@@ -38,6 +44,8 @@ export interface TimelineToolbarProps {
|
|
|
38
44
|
onSplitElement?: (element: TimelineElement, splitTime: number) => void;
|
|
39
45
|
history?: TimelineHistoryButtonsProps;
|
|
40
46
|
showAddBeat?: boolean;
|
|
47
|
+
/** Hides Add keyframe and auto-record, and turns off auto-record and the K shortcut with them. */
|
|
48
|
+
showKeyframes?: boolean;
|
|
41
49
|
}
|
|
42
50
|
|
|
43
51
|
interface KeyframeToggleState {
|
|
@@ -134,6 +142,7 @@ export function TimelineToolbar({
|
|
|
134
142
|
onSplitElement,
|
|
135
143
|
history,
|
|
136
144
|
showAddBeat = true,
|
|
145
|
+
showKeyframes = true,
|
|
137
146
|
}: TimelineToolbarProps) {
|
|
138
147
|
const activeTool = usePlayerStore((s) => s.activeTool);
|
|
139
148
|
const setActiveTool = usePlayerStore((s) => s.setActiveTool);
|
|
@@ -174,10 +183,14 @@ export function TimelineToolbar({
|
|
|
174
183
|
// Wire the "Add keyframe (K)" shortcut the toolbar advertises. Active only when
|
|
175
184
|
// there's a keyframeable selection; otherwise K stays JKL-pause in playback.
|
|
176
185
|
useKeyframeKeyboard({
|
|
177
|
-
enabled: Boolean(onToggleKeyframe),
|
|
186
|
+
enabled: showKeyframes && Boolean(onToggleKeyframe),
|
|
178
187
|
onAddKeyframe: onToggleKeyframe,
|
|
179
188
|
});
|
|
180
189
|
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
if (!showKeyframes) setAutoKeyframeEnabled(false);
|
|
192
|
+
}, [showKeyframes, setAutoKeyframeEnabled]);
|
|
193
|
+
|
|
181
194
|
// "N" toggles timeline snapping (industry convention: Resolve/FCP).
|
|
182
195
|
// Skip when typing in an input/contenteditable.
|
|
183
196
|
useEffect(() => {
|
|
@@ -270,107 +283,110 @@ export function TimelineToolbar({
|
|
|
270
283
|
</button>
|
|
271
284
|
</Tooltip>
|
|
272
285
|
)}
|
|
273
|
-
{
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
? "Extend motion path to playhead (K)"
|
|
285
|
-
: keyframeState === "active"
|
|
286
|
-
? "Remove waypoint from motion path (K)"
|
|
287
|
-
: "Add waypoint to motion path (K)"
|
|
288
|
-
: keyframeState === "active"
|
|
289
|
-
? "Remove keyframe at playhead (K)"
|
|
290
|
-
: keyframeState === "inactive"
|
|
286
|
+
{showKeyframes && (
|
|
287
|
+
<>
|
|
288
|
+
{/* With no keyframeable selection the button fades to disabled instead of
|
|
289
|
+
unmounting, so the toolbar layout never shifts (CapCut-style). */}
|
|
290
|
+
<Tooltip
|
|
291
|
+
label={
|
|
292
|
+
keyframePathEndpoint
|
|
293
|
+
? "Motion path endpoints cannot be removed"
|
|
294
|
+
: !onToggleKeyframe
|
|
295
|
+
? "Select an animated element to add keyframes"
|
|
296
|
+
: keyframeIsMotionPath
|
|
291
297
|
? keyframeWillExtend
|
|
292
|
-
? "
|
|
293
|
-
:
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
? "
|
|
312
|
-
:
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
298
|
+
? "Extend motion path to playhead (K)"
|
|
299
|
+
: keyframeState === "active"
|
|
300
|
+
? "Remove waypoint from motion path (K)"
|
|
301
|
+
: "Add waypoint to motion path (K)"
|
|
302
|
+
: keyframeState === "active"
|
|
303
|
+
? "Remove keyframe at playhead (K)"
|
|
304
|
+
: keyframeState === "inactive"
|
|
305
|
+
? keyframeWillExtend
|
|
306
|
+
? "Add keyframe at playhead, extends animation (K)"
|
|
307
|
+
: "Add keyframe at playhead (K)"
|
|
308
|
+
: "Add keyframe (K)"
|
|
309
|
+
}
|
|
310
|
+
>
|
|
311
|
+
<button
|
|
312
|
+
type="button"
|
|
313
|
+
disabled={!onToggleKeyframe}
|
|
314
|
+
onClick={onToggleKeyframe}
|
|
315
|
+
aria-label={
|
|
316
|
+
keyframePathEndpoint
|
|
317
|
+
? "Motion path endpoint"
|
|
318
|
+
: keyframeIsMotionPath
|
|
319
|
+
? keyframeState === "active"
|
|
320
|
+
? "Remove motion path waypoint"
|
|
321
|
+
: keyframeWillExtend
|
|
322
|
+
? "Extend motion path to playhead"
|
|
323
|
+
: "Add motion path waypoint"
|
|
324
|
+
: keyframeState === "active"
|
|
325
|
+
? "Remove keyframe at playhead"
|
|
326
|
+
: "Add keyframe at playhead"
|
|
327
|
+
}
|
|
328
|
+
className={
|
|
329
|
+
!onToggleKeyframe
|
|
330
|
+
? flatDisabled
|
|
331
|
+
: `${flatBtn} active:scale-[0.98] hover:bg-white/6 ${
|
|
332
|
+
keyframeState === "active"
|
|
333
|
+
? "text-studio-accent"
|
|
334
|
+
: keyframeState === "inactive"
|
|
335
|
+
? "text-neutral-400 hover:text-studio-accent"
|
|
336
|
+
: "text-neutral-600 hover:text-neutral-400"
|
|
337
|
+
}`
|
|
338
|
+
}
|
|
339
|
+
>
|
|
340
|
+
<svg width="16" height="16" viewBox="0 0 10 10" fill="currentColor">
|
|
341
|
+
{keyframeState === "active" ? (
|
|
342
|
+
<path d="M5 0.5L9.5 5L5 9.5L0.5 5Z" />
|
|
343
|
+
) : (
|
|
344
|
+
<path
|
|
345
|
+
d="M5 1.2L8.8 5L5 8.8L1.2 5Z"
|
|
346
|
+
fill="none"
|
|
347
|
+
stroke="currentColor"
|
|
348
|
+
strokeWidth="1.2"
|
|
349
|
+
/>
|
|
350
|
+
)}
|
|
351
|
+
</svg>
|
|
352
|
+
</button>
|
|
353
|
+
</Tooltip>
|
|
354
|
+
<Tooltip
|
|
355
|
+
label={
|
|
356
|
+
autoKeyframeEnabled
|
|
357
|
+
? "Auto-record manual edits as keyframes (click to turn off)"
|
|
358
|
+
: "Manual edits will not be recorded as keyframes (click to turn on)"
|
|
359
|
+
}
|
|
360
|
+
>
|
|
361
|
+
<button
|
|
362
|
+
type="button"
|
|
363
|
+
onClick={() => setAutoKeyframeEnabled(!autoKeyframeEnabled)}
|
|
364
|
+
aria-label="Auto-record manual edits as keyframes"
|
|
365
|
+
aria-pressed={autoKeyframeEnabled}
|
|
366
|
+
className={`${flatBtn} active:scale-[0.98] hover:bg-white/6 ${
|
|
367
|
+
autoKeyframeEnabled
|
|
368
|
+
? "text-red-400 hover:text-red-300"
|
|
369
|
+
: "text-neutral-600 hover:text-neutral-400"
|
|
370
|
+
}`}
|
|
371
|
+
>
|
|
372
|
+
<svg width="16" height="16" viewBox="0 0 10 10" fill="none">
|
|
373
|
+
{/* Same diamond outline as the Add-keyframe icon, with a
|
|
360
374
|
record-style dot inside: filled = auto-recording,
|
|
361
375
|
hollow = manual edits won't be keyframed. */}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
376
|
+
<path d="M5 0.7L9.3 5L5 9.3L0.7 5Z" stroke="currentColor" strokeWidth="1" />
|
|
377
|
+
<circle
|
|
378
|
+
cx="5"
|
|
379
|
+
cy="5"
|
|
380
|
+
r="1.8"
|
|
381
|
+
fill={autoKeyframeEnabled ? "currentColor" : "none"}
|
|
382
|
+
stroke="currentColor"
|
|
383
|
+
strokeWidth="1"
|
|
384
|
+
/>
|
|
385
|
+
</svg>
|
|
386
|
+
</button>
|
|
387
|
+
</Tooltip>
|
|
388
|
+
</>
|
|
389
|
+
)}
|
|
374
390
|
{onSplitElement &&
|
|
375
391
|
(() => {
|
|
376
392
|
// Render the button unconditionally (disabled when unusable):
|
|
@@ -495,13 +511,15 @@ export function TimelineToolbar({
|
|
|
495
511
|
<button
|
|
496
512
|
type="button"
|
|
497
513
|
onClick={() => setZoomMode("fit")}
|
|
514
|
+
aria-label="Fit timeline to width"
|
|
515
|
+
aria-pressed={zoomMode === "fit"}
|
|
498
516
|
className={`h-7 px-2 rounded-md text-[11px] font-medium transition-colors ${
|
|
499
517
|
zoomMode === "fit"
|
|
500
518
|
? "bg-studio-accent/10 text-studio-accent"
|
|
501
519
|
: "text-neutral-400 hover:bg-white/6 hover:text-neutral-200"
|
|
502
520
|
}`}
|
|
503
521
|
>
|
|
504
|
-
|
|
522
|
+
<ArrowsOutLineHorizontal size={16} aria-hidden="true" />
|
|
505
523
|
</button>
|
|
506
524
|
</Tooltip>
|
|
507
525
|
<Tooltip label="Zoom out">
|
|
@@ -47,7 +47,7 @@ export {
|
|
|
47
47
|
} from "./domEditOverlayGestures";
|
|
48
48
|
export type { DomEditGroupPathOffsetCommit } from "./domEditOverlayGestures";
|
|
49
49
|
|
|
50
|
-
interface DomEditOverlayProps {
|
|
50
|
+
export interface DomEditOverlayProps {
|
|
51
51
|
iframeRef: RefObject<HTMLIFrameElement | null>;
|
|
52
52
|
activeCompositionPath: string | null;
|
|
53
53
|
selection: DomEditSelection | null;
|
|
@@ -518,7 +518,6 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
518
518
|
boxClipPath={boxClipPath}
|
|
519
519
|
selectionKey={selectionKey}
|
|
520
520
|
groupSelectionCount={groupSelections.length}
|
|
521
|
-
blockedMoveRef={blockedMoveRef}
|
|
522
521
|
gestures={gestures}
|
|
523
522
|
onStyleCommit={onStyleCommitRef.current}
|
|
524
523
|
onBoxMouseDown={suppressBoxMouseDown}
|
|
@@ -6,6 +6,7 @@ import { expect, it, vi } from "vitest";
|
|
|
6
6
|
import type { DomEditSelection } from "./domEditing";
|
|
7
7
|
import "./domEditOverlayTestMocks";
|
|
8
8
|
import { DomEditOverlay } from "./DomEditOverlay";
|
|
9
|
+
import { DomEditOverlay as PackageDomEditOverlay } from "../../index";
|
|
9
10
|
|
|
10
11
|
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
11
12
|
|
|
@@ -74,3 +75,7 @@ it("rotates the hover box with the element", () => {
|
|
|
74
75
|
it("leaves the hover box untransformed at angle zero", () => {
|
|
75
76
|
expect(renderHover(0)).toBe("");
|
|
76
77
|
});
|
|
78
|
+
|
|
79
|
+
it("is exported from the package entry, so a host can mount it with its own commits", () => {
|
|
80
|
+
expect(PackageDomEditOverlay).toBe(DomEditOverlay);
|
|
81
|
+
});
|
|
@@ -53,7 +53,6 @@ describe("DomEditSelectionChrome crop composition", () => {
|
|
|
53
53
|
boxClipPath={undefined}
|
|
54
54
|
selectionKey="headline"
|
|
55
55
|
groupSelectionCount={0}
|
|
56
|
-
blockedMoveRef={createRef()}
|
|
57
56
|
gestures={{ startGesture: vi.fn() } as never}
|
|
58
57
|
onStyleCommit={vi.fn()}
|
|
59
58
|
onBoxMouseDown={vi.fn()}
|
|
@@ -109,7 +108,6 @@ describe("DomEditSelectionChrome crop composition", () => {
|
|
|
109
108
|
boxClipPath={undefined}
|
|
110
109
|
selectionKey="clip"
|
|
111
110
|
groupSelectionCount={0}
|
|
112
|
-
blockedMoveRef={createRef()}
|
|
113
111
|
gestures={{ startGesture: vi.fn() } as never}
|
|
114
112
|
onStyleCommit={vi.fn()}
|
|
115
113
|
onBoxMouseDown={vi.fn()}
|
|
@@ -170,7 +168,6 @@ describe("DomEditSelectionChrome while editing text", () => {
|
|
|
170
168
|
boxClipPath={undefined}
|
|
171
169
|
selectionKey="copy"
|
|
172
170
|
groupSelectionCount={0}
|
|
173
|
-
blockedMoveRef={createRef()}
|
|
174
171
|
gestures={{ startGesture: vi.fn() } as never}
|
|
175
172
|
onStyleCommit={vi.fn()}
|
|
176
173
|
onBoxMouseDown={vi.fn()}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { RefObject } from "react";
|
|
2
2
|
import { type DomEditSelection } from "./domEditing";
|
|
3
3
|
import type { GroupOverlayItem, OverlayRect } from "./domEditOverlayGeometry";
|
|
4
|
-
import type {
|
|
4
|
+
import type { ResizeHandle } from "./domEditOverlayGestures";
|
|
5
5
|
import type { createDomEditOverlayGestureHandlers } from "./useDomEditOverlayGestures";
|
|
6
6
|
import { DomEditCropHandles } from "./DomEditCropHandles";
|
|
7
7
|
import { DomEditRotateHandle } from "./DomEditRotateHandle";
|
|
@@ -103,7 +103,7 @@ export function DomEditGroupChrome({
|
|
|
103
103
|
cursor: canManipulate && groupCanMove ? "move" : "default",
|
|
104
104
|
}}
|
|
105
105
|
onPointerDown={(e) => {
|
|
106
|
-
if (!canManipulate ||
|
|
106
|
+
if (!canManipulate || e.shiftKey) return;
|
|
107
107
|
gestures.startGroupDrag(e);
|
|
108
108
|
}}
|
|
109
109
|
onMouseDown={onBoxMouseDown}
|
|
@@ -123,7 +123,6 @@ interface DomEditSelectionChromeProps {
|
|
|
123
123
|
boxClipPath: string | undefined;
|
|
124
124
|
selectionKey: string;
|
|
125
125
|
groupSelectionCount: number;
|
|
126
|
-
blockedMoveRef: RefObject<BlockedMoveState | null>;
|
|
127
126
|
gestures: GestureHandlers;
|
|
128
127
|
onStyleCommit?: (property: string, value: string) => Promise<unknown> | void;
|
|
129
128
|
onBoxMouseDown: (e: React.MouseEvent) => void;
|
|
@@ -152,7 +151,6 @@ export function DomEditSelectionChrome({
|
|
|
152
151
|
boxClipPath,
|
|
153
152
|
selectionKey,
|
|
154
153
|
groupSelectionCount,
|
|
155
|
-
blockedMoveRef,
|
|
156
154
|
gestures,
|
|
157
155
|
onStyleCommit,
|
|
158
156
|
onBoxMouseDown,
|
|
@@ -218,15 +216,7 @@ export function DomEditSelectionChrome({
|
|
|
218
216
|
gestures.startGesture("drag", e);
|
|
219
217
|
return;
|
|
220
218
|
}
|
|
221
|
-
|
|
222
|
-
e.stopPropagation();
|
|
223
|
-
e.currentTarget.setPointerCapture(e.pointerId);
|
|
224
|
-
blockedMoveRef.current = {
|
|
225
|
-
pointerId: e.pointerId,
|
|
226
|
-
startX: e.clientX,
|
|
227
|
-
startY: e.clientY,
|
|
228
|
-
notified: false,
|
|
229
|
-
};
|
|
219
|
+
gestures.startBlockedMove(e, selection);
|
|
230
220
|
}}
|
|
231
221
|
onMouseDown={onBoxMouseDown}
|
|
232
222
|
onClick={onBoxClick}
|
|
@@ -9,6 +9,7 @@ import type { DomEditLayerItem } from "./domEditingTypes";
|
|
|
9
9
|
import { createRafThrottle, LayersPanel, sortLayersByZIndex } from "./LayersPanel";
|
|
10
10
|
import { isLayerDraggable } from "./useLayerDrag";
|
|
11
11
|
import { liveTime } from "../../player";
|
|
12
|
+
import { sceneSwapFor } from "../../player/sceneSwap";
|
|
12
13
|
|
|
13
14
|
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
14
15
|
|
|
@@ -292,4 +293,17 @@ describe("LayersPanel preview promotion", () => {
|
|
|
292
293
|
});
|
|
293
294
|
expect(collectedRootIds()).toEqual(["b"]);
|
|
294
295
|
});
|
|
296
|
+
it("recollects layers when edited scenes are swapped into the live document", async () => {
|
|
297
|
+
const a = makeIframe("a");
|
|
298
|
+
Object.defineProperty(a, "contentWindow", { value: { __hfSwapScenes: async () => {} } });
|
|
299
|
+
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response("<html></html>"));
|
|
300
|
+
mocks.previewIframeRef.current = a;
|
|
301
|
+
usePreviewIframeStore.setState({ iframe: a });
|
|
302
|
+
act(() => root.render(createElement(LayersPanel)));
|
|
303
|
+
mocks.collect.mockClear();
|
|
304
|
+
|
|
305
|
+
await act(() => sceneSwapFor(a)!("/preview", () => true));
|
|
306
|
+
expect(collectedRootIds()).toEqual(["a"]);
|
|
307
|
+
fetchSpy.mockRestore();
|
|
308
|
+
});
|
|
295
309
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useLivePreviewIframe } from "../../player/store/previewIframeStore";
|
|
2
|
+
import { onPreviewContentReplaced } from "../../player/sceneSwap";
|
|
2
3
|
import { memo, useState, useCallback, useEffect, useRef } from "react";
|
|
3
4
|
import {
|
|
4
5
|
collectDomEditLayerItems,
|
|
@@ -164,8 +165,7 @@ export const LayersPanel = memo(function LayersPanel() {
|
|
|
164
165
|
prevDocVersionRef.current += 1;
|
|
165
166
|
collectLayers();
|
|
166
167
|
};
|
|
167
|
-
iframe
|
|
168
|
-
return () => iframe.removeEventListener("load", handleLoad);
|
|
168
|
+
return onPreviewContentReplaced(iframe, handleLoad);
|
|
169
169
|
}, [previewIframeRef, livePreviewIframe, collectLayers]);
|
|
170
170
|
|
|
171
171
|
useEffect(() => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useLivePreviewIframe } from "../../player/store/previewIframeStore";
|
|
2
|
+
import { onPreviewContentReplaced } from "../../player/sceneSwap";
|
|
2
3
|
import {
|
|
3
4
|
useEffect,
|
|
4
5
|
useLayoutEffect,
|
|
@@ -119,8 +120,7 @@ export function TopologyLens({ iframeRef, activeCompositionPath }: TopologyLensP
|
|
|
119
120
|
const iframe = iframeRef.current;
|
|
120
121
|
if (!iframe || !callId) return;
|
|
121
122
|
const dismiss = () => studioEditLifecycle.dismiss(callId);
|
|
122
|
-
iframe
|
|
123
|
-
return () => iframe.removeEventListener("load", dismiss);
|
|
123
|
+
return onPreviewContentReplaced(iframe, dismiss);
|
|
124
124
|
}, [callId, iframeRef, livePreviewIframe]);
|
|
125
125
|
|
|
126
126
|
useEffect(() => {
|