@hyperframes/studio 0.7.81 → 0.7.83
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-B32VouCm.js → hyperframes-player-DSRuJMfh.js} +1 -1
- package/dist/assets/{index-DCTAKfpa.js → index-BrWVfpCQ.js} +1 -1
- package/dist/assets/{index-B5u2_pSh.js → index-DRtvHA1J.js} +150 -150
- package/dist/assets/{index-HeUw0EKI.js → index-YZR6OfQ5.js} +1 -1
- package/dist/index.html +1 -1
- package/dist/index.js +3057 -3101
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +2 -5
- package/src/components/StudioHeader.tsx +3 -18
- package/src/components/StudioRightPanel.tsx +14 -22
- package/src/components/TimelineToolbar.tsx +120 -132
- package/src/components/editor/EaseCurveSection.tsx +9 -90
- package/src/components/editor/EaseModeControls.tsx +110 -0
- package/src/components/editor/PropertyPanel.test.tsx +2 -3
- package/src/components/editor/PropertyPanel.tsx +7 -12
- package/src/components/editor/PropertyPanelFlat.tsx +0 -2
- package/src/components/editor/floatingPanel.test.ts +20 -1
- package/src/components/editor/floatingPanel.ts +15 -0
- package/src/components/editor/manualEditingAvailability.test.ts +13 -40
- package/src/components/editor/manualEditingAvailability.ts +0 -41
- package/src/components/editor/propertyPanel3dTransform.tsx +1 -2
- package/src/components/editor/propertyPanelFlatLayoutSection.tsx +1 -2
- package/src/components/nle/PreviewOverlays.tsx +8 -20
- package/src/components/renders/RenderQueue.test.tsx +62 -0
- package/src/components/renders/RenderQueue.tsx +60 -38
- package/src/components/renders/useRenderQueue.ts +5 -11
- package/src/components/sidebar/LeftSidebar.tsx +15 -22
- package/src/components/ui/Tooltip.tsx +25 -7
- package/src/hooks/useAppHotkeys.ts +1 -2
- package/src/hooks/useDomEditPreviewSync.ts +1 -2
- package/src/hooks/useDomEditWiring.ts +2 -3
- package/src/hooks/useDomSelection.ts +0 -27
- package/src/hooks/usePreviewInteraction.ts +2 -3
- package/src/hooks/useStudioContextValue.ts +3 -7
- package/src/player/components/Timeline.tsx +1 -2
- package/src/player/components/TimelineLanes.tsx +33 -35
- package/src/player/components/useAutoExpandKeyframedClips.ts +0 -2
- package/src/player/components/useTimelineTrackLayout.ts +1 -5
- package/src/utils/studioUrlState.test.ts +5 -8
- package/src/utils/studioUrlState.ts +1 -10
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import { memo, useState, useRef, useEffect, useId } from "react";
|
|
1
|
+
import { memo, useState, useRef, useEffect, useLayoutEffect, useId } from "react";
|
|
2
|
+
import { createPortal } from "react-dom";
|
|
3
|
+
import { CANVAS_DIMENSIONS } from "@hyperframes/parsers";
|
|
2
4
|
import { RenderQueueItem } from "./RenderQueueItem";
|
|
3
5
|
import { Button } from "../ui/Button";
|
|
6
|
+
import { resolveFloatingPanelPosition, type FloatingPosition } from "../editor/floatingPanel";
|
|
4
7
|
import type { RenderJob, ResolutionPreset } from "./useRenderQueue";
|
|
5
8
|
import { getPersistedRenderSettings, persistRenderSettings } from "./renderSettings";
|
|
6
9
|
import { trackStudioEvent } from "../../utils/studioTelemetry";
|
|
@@ -53,17 +56,6 @@ const SCALE_LABEL: Record<RenderScale, string> = {
|
|
|
53
56
|
"4k": "4K",
|
|
54
57
|
};
|
|
55
58
|
|
|
56
|
-
// Mirrors `CANVAS_DIMENSIONS` in @hyperframes/core. Studio can't import from
|
|
57
|
-
// the core barrel (it transitively pulls in node:fs) and the values are stable.
|
|
58
|
-
const CANVAS_DIMENSIONS: Record<ResolutionPreset, CompositionDimensions> = {
|
|
59
|
-
landscape: { width: 1920, height: 1080 },
|
|
60
|
-
portrait: { width: 1080, height: 1920 },
|
|
61
|
-
"landscape-4k": { width: 3840, height: 2160 },
|
|
62
|
-
"portrait-4k": { width: 2160, height: 3840 },
|
|
63
|
-
square: { width: 1080, height: 1080 },
|
|
64
|
-
"square-4k": { width: 2160, height: 2160 },
|
|
65
|
-
};
|
|
66
|
-
|
|
67
59
|
type CompAspect = "landscape" | "portrait" | "square";
|
|
68
60
|
|
|
69
61
|
function compAspect(dims: CompositionDimensions | null | undefined): CompAspect {
|
|
@@ -142,12 +134,20 @@ const FORMAT_INFO: Record<"mp4" | "webm" | "mov", { label: string; desc: string
|
|
|
142
134
|
},
|
|
143
135
|
};
|
|
144
136
|
|
|
137
|
+
// Estimated, like COLOR_PICKER_SIZE in propertyPanelColor: only the flip
|
|
138
|
+
// decision uses the height, and the clamp keeps the panel on screen either way.
|
|
139
|
+
const FORMAT_PANEL_SIZE = { width: 208, height: 150 };
|
|
140
|
+
|
|
145
141
|
// Rich format guidance in a keyboard-reachable disclosure: the trigger is a
|
|
146
142
|
// real button (focusable, labelled), the panel is tied to it via
|
|
147
143
|
// aria-describedby, and Escape dismisses (WCAG 1.4.13). Content is too rich
|
|
148
144
|
// for the one-line ui/Tooltip primitive, so this stays a local popover.
|
|
145
|
+
// It renders in a portal because the right panel is overflow-hidden: an
|
|
146
|
+
// in-flow absolute panel gets clipped at the panel edge.
|
|
149
147
|
function FormatInfoTooltip({ format }: { format: "mp4" | "webm" | "mov" }) {
|
|
150
148
|
const [open, setOpen] = useState(false);
|
|
149
|
+
const [position, setPosition] = useState<FloatingPosition | null>(null);
|
|
150
|
+
const triggerRef = useRef<HTMLDivElement>(null);
|
|
151
151
|
const timeoutRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
|
152
152
|
const panelId = useId();
|
|
153
153
|
|
|
@@ -161,6 +161,22 @@ function FormatInfoTooltip({ format }: { format: "mp4" | "webm" | "mov" }) {
|
|
|
161
161
|
|
|
162
162
|
useEffect(() => () => clearTimeout(timeoutRef.current), []);
|
|
163
163
|
|
|
164
|
+
// Positioned once on open, so it does not follow panel scroll. The popover
|
|
165
|
+
// is hover-lived; add a scroll listener only if that ever shows up.
|
|
166
|
+
useLayoutEffect(() => {
|
|
167
|
+
if (!open) return;
|
|
168
|
+
const el = triggerRef.current;
|
|
169
|
+
if (!el) return;
|
|
170
|
+
setPosition(
|
|
171
|
+
resolveFloatingPanelPosition(
|
|
172
|
+
el.getBoundingClientRect(),
|
|
173
|
+
{ width: window.innerWidth, height: window.innerHeight },
|
|
174
|
+
FORMAT_PANEL_SIZE,
|
|
175
|
+
{ offset: 6 },
|
|
176
|
+
),
|
|
177
|
+
);
|
|
178
|
+
}, [open]);
|
|
179
|
+
|
|
164
180
|
useEffect(() => {
|
|
165
181
|
if (!open) return;
|
|
166
182
|
const onKeyDown = (e: KeyboardEvent) => {
|
|
@@ -173,7 +189,7 @@ function FormatInfoTooltip({ format }: { format: "mp4" | "webm" | "mov" }) {
|
|
|
173
189
|
const info = FORMAT_INFO[format];
|
|
174
190
|
|
|
175
191
|
return (
|
|
176
|
-
<div className="relative" onPointerEnter={show} onPointerLeave={hide}>
|
|
192
|
+
<div ref={triggerRef} className="relative" onPointerEnter={show} onPointerLeave={hide}>
|
|
177
193
|
<button
|
|
178
194
|
type="button"
|
|
179
195
|
aria-label="About video formats"
|
|
@@ -200,27 +216,32 @@ function FormatInfoTooltip({ format }: { format: "mp4" | "webm" | "mov" }) {
|
|
|
200
216
|
<line x1="12" y1="17" x2="12.01" y2="17" />
|
|
201
217
|
</svg>
|
|
202
218
|
</button>
|
|
203
|
-
{open &&
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
219
|
+
{open &&
|
|
220
|
+
createPortal(
|
|
221
|
+
<div
|
|
222
|
+
id={panelId}
|
|
223
|
+
role="tooltip"
|
|
224
|
+
onPointerEnter={show}
|
|
225
|
+
onPointerLeave={hide}
|
|
226
|
+
className="fixed w-52 p-2 rounded bg-panel-input border border-neutral-700 shadow-lg z-[200]"
|
|
227
|
+
style={{ left: position?.left ?? -9999, top: position?.top ?? -9999 }}
|
|
228
|
+
>
|
|
229
|
+
<p className="text-[10px] font-semibold text-panel-text-1 mb-0.5">{info.label}</p>
|
|
230
|
+
<p className="text-[9px] text-panel-text-3 leading-tight">{info.desc}</p>
|
|
231
|
+
<div className="mt-1.5 pt-1.5 border-t border-neutral-800">
|
|
232
|
+
{(["mp4", "mov", "webm"] as const)
|
|
233
|
+
.filter((f) => f !== format)
|
|
234
|
+
.map((f) => (
|
|
235
|
+
<p key={f} className="text-[9px] text-panel-text-4 leading-relaxed">
|
|
236
|
+
<span className="text-panel-text-3 font-medium">{FORMAT_INFO[f].label}</span>
|
|
237
|
+
{" — "}
|
|
238
|
+
{FORMAT_INFO[f].desc}
|
|
239
|
+
</p>
|
|
240
|
+
))}
|
|
241
|
+
</div>
|
|
242
|
+
</div>,
|
|
243
|
+
document.body,
|
|
244
|
+
)}
|
|
224
245
|
</div>
|
|
225
246
|
);
|
|
226
247
|
}
|
|
@@ -254,7 +275,7 @@ function FormatExportButton({
|
|
|
254
275
|
const persisted = getPersistedRenderSettings();
|
|
255
276
|
const [format, setFormat] = useState<"mp4" | "webm" | "mov">(persisted.format);
|
|
256
277
|
const [quality, setQuality] = useState<"draft" | "standard" | "high">(persisted.quality);
|
|
257
|
-
const [resolution, setResolution] = useState<
|
|
278
|
+
const [resolution, setResolution] = useState<RenderScale>("auto");
|
|
258
279
|
const [fps, setFps] = useState<24 | 30 | 60>(persisted.fps);
|
|
259
280
|
|
|
260
281
|
// MOV (ProRes) is a fixed-quality codec — quality selector has no effect.
|
|
@@ -290,7 +311,7 @@ function FormatExportButton({
|
|
|
290
311
|
<span className="text-[10px] text-panel-text-4">Resolution</span>
|
|
291
312
|
<select
|
|
292
313
|
value={resolution}
|
|
293
|
-
onChange={(e) => setResolution(e.target.value as
|
|
314
|
+
onChange={(e) => setResolution(e.target.value as RenderScale)}
|
|
294
315
|
disabled={isRendering}
|
|
295
316
|
className={selectCls}
|
|
296
317
|
>
|
|
@@ -352,8 +373,9 @@ function FormatExportButton({
|
|
|
352
373
|
// loading already disables the button; this guard also stops a
|
|
353
374
|
// double-click in the same frame from enqueueing two renders.
|
|
354
375
|
if (isRendering) return;
|
|
355
|
-
|
|
356
|
-
|
|
376
|
+
const outputResolution = resolveResolution(resolution, compositionDimensions);
|
|
377
|
+
trackStudioEvent("render_start", { format, quality, resolution: outputResolution, fps });
|
|
378
|
+
void onStartRender(format, quality, outputResolution, fps);
|
|
357
379
|
}}
|
|
358
380
|
className="w-full text-[11px] font-semibold"
|
|
359
381
|
>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback, useRef, useMemo } from "react";
|
|
2
|
+
import type { CanvasResolution } from "@hyperframes/parsers";
|
|
2
3
|
import { trackStudioRenderStart } from "../../telemetry/events";
|
|
3
4
|
import { getAnonymousId } from "../../telemetry/config";
|
|
4
5
|
import { generateId } from "../../utils/generateId";
|
|
@@ -14,17 +15,10 @@ export interface RenderJob {
|
|
|
14
15
|
durationMs?: number;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
export type ResolutionPreset =
|
|
22
|
-
| "landscape"
|
|
23
|
-
| "portrait"
|
|
24
|
-
| "landscape-4k"
|
|
25
|
-
| "portrait-4k"
|
|
26
|
-
| "square"
|
|
27
|
-
| "square-4k";
|
|
18
|
+
// The CLI consumes this same source through @hyperframes/core's re-export.
|
|
19
|
+
// Importing from the browser-safe parsers package avoids the core barrel's
|
|
20
|
+
// Node-only transitive modules without duplicating the preset union in Studio.
|
|
21
|
+
export type ResolutionPreset = CanvasResolution;
|
|
28
22
|
|
|
29
23
|
export interface StartRenderOptions {
|
|
30
24
|
fps?: number;
|
|
@@ -12,7 +12,6 @@ import { AssetsTab } from "./AssetsTab";
|
|
|
12
12
|
import { trackStudioEvent } from "../../utils/studioTelemetry";
|
|
13
13
|
import { BlocksTab, type BlockPreviewInfo } from "./BlocksTab";
|
|
14
14
|
import { FileTree } from "../editor/FileTree";
|
|
15
|
-
import { STUDIO_BLOCKS_PANEL_ENABLED } from "../editor/manualEditingAvailability";
|
|
16
15
|
import { Tooltip } from "../ui";
|
|
17
16
|
|
|
18
17
|
export type SidebarTab = "compositions" | "assets" | "code" | "blocks";
|
|
@@ -127,11 +126,7 @@ export const LeftSidebar = memo(
|
|
|
127
126
|
<div className="flex items-center gap-2">
|
|
128
127
|
<div
|
|
129
128
|
className="grid min-w-0 flex-1 gap-0.5 rounded-[18px] bg-neutral-900 p-1 shadow-[inset_0_1px_0_rgba(255,255,255,0.03)]"
|
|
130
|
-
style={{
|
|
131
|
-
gridTemplateColumns: STUDIO_BLOCKS_PANEL_ENABLED
|
|
132
|
-
? "1fr 1fr 1fr 1fr"
|
|
133
|
-
: "1fr 1fr 1fr",
|
|
134
|
-
}}
|
|
129
|
+
style={{ gridTemplateColumns: "1fr 1fr 1fr 1fr" }}
|
|
135
130
|
>
|
|
136
131
|
<Tooltip label="Source code editor" side="bottom">
|
|
137
132
|
<button
|
|
@@ -172,21 +167,19 @@ export const LeftSidebar = memo(
|
|
|
172
167
|
Assets
|
|
173
168
|
</button>
|
|
174
169
|
</Tooltip>
|
|
175
|
-
|
|
176
|
-
<
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
</Tooltip>
|
|
189
|
-
)}
|
|
170
|
+
<Tooltip label="Browse blocks and components" side="bottom">
|
|
171
|
+
<button
|
|
172
|
+
type="button"
|
|
173
|
+
onClick={() => selectTab("blocks")}
|
|
174
|
+
className={`rounded-[14px] px-1.5 py-2 text-[10px] font-semibold truncate transition-all ${
|
|
175
|
+
tab === "blocks"
|
|
176
|
+
? "bg-neutral-800 text-white"
|
|
177
|
+
: "text-neutral-500 hover:text-neutral-200"
|
|
178
|
+
}`}
|
|
179
|
+
>
|
|
180
|
+
Catalog
|
|
181
|
+
</button>
|
|
182
|
+
</Tooltip>
|
|
190
183
|
</div>
|
|
191
184
|
{onToggleCollapse && (
|
|
192
185
|
<button
|
|
@@ -267,7 +260,7 @@ export const LeftSidebar = memo(
|
|
|
267
260
|
</div>
|
|
268
261
|
)}
|
|
269
262
|
|
|
270
|
-
{
|
|
263
|
+
{tab === "blocks" && (
|
|
271
264
|
<BlocksTab onAddBlock={onAddBlock} onPreviewBlock={onPreviewBlock} />
|
|
272
265
|
)}
|
|
273
266
|
|
|
@@ -1,5 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
useState,
|
|
3
|
+
useRef,
|
|
4
|
+
useCallback,
|
|
5
|
+
useEffect,
|
|
6
|
+
useLayoutEffect,
|
|
7
|
+
useId,
|
|
8
|
+
type ReactNode,
|
|
9
|
+
} from "react";
|
|
2
10
|
import { createPortal } from "react-dom";
|
|
11
|
+
import { clampCentredLeft } from "../editor/floatingPanel";
|
|
3
12
|
|
|
4
13
|
interface TooltipProps {
|
|
5
14
|
label: string;
|
|
@@ -19,6 +28,8 @@ export function Tooltip({ label, children, delay = 400, side = "top" }: TooltipP
|
|
|
19
28
|
const [resolvedSide, setResolvedSide] = useState<"top" | "bottom">(side);
|
|
20
29
|
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
21
30
|
const triggerRef = useRef<HTMLSpanElement>(null);
|
|
31
|
+
const bubbleRef = useRef<HTMLDivElement>(null);
|
|
32
|
+
const [bubbleWidth, setBubbleWidth] = useState(0);
|
|
22
33
|
// WCAG 4.1.2: programmatically associate the bubble with its trigger.
|
|
23
34
|
const tooltipId = useId();
|
|
24
35
|
|
|
@@ -40,13 +51,11 @@ export function Tooltip({ label, children, delay = 400, side = "top" }: TooltipP
|
|
|
40
51
|
) {
|
|
41
52
|
nextSide = "top";
|
|
42
53
|
}
|
|
43
|
-
const x = Math.min(
|
|
44
|
-
Math.max(rect.left + rect.width / 2, VIEWPORT_MARGIN),
|
|
45
|
-
window.innerWidth - VIEWPORT_MARGIN,
|
|
46
|
-
);
|
|
47
54
|
setResolvedSide(nextSide);
|
|
48
55
|
setPos({
|
|
49
|
-
|
|
56
|
+
// Raw trigger centre; clamped to the viewport at render, once the
|
|
57
|
+
// bubble's own width is known (see clampedX).
|
|
58
|
+
x: rect.left + rect.width / 2,
|
|
50
59
|
y: nextSide === "top" ? rect.top - 6 : rect.bottom + 6,
|
|
51
60
|
});
|
|
52
61
|
setVisible(true);
|
|
@@ -61,6 +70,12 @@ export function Tooltip({ label, children, delay = 400, side = "top" }: TooltipP
|
|
|
61
70
|
setVisible(false);
|
|
62
71
|
}, []);
|
|
63
72
|
|
|
73
|
+
// Measure before paint so a wide bubble near a viewport edge is clamped in
|
|
74
|
+
// the same commit it appears in (no visible jump).
|
|
75
|
+
useLayoutEffect(() => {
|
|
76
|
+
setBubbleWidth(visible ? (bubbleRef.current?.offsetWidth ?? 0) : 0);
|
|
77
|
+
}, [visible, label]);
|
|
78
|
+
|
|
64
79
|
// WCAG 1.4.13: tooltip content must be dismissible with Escape.
|
|
65
80
|
useEffect(() => {
|
|
66
81
|
if (!visible) return;
|
|
@@ -71,6 +86,8 @@ export function Tooltip({ label, children, delay = 400, side = "top" }: TooltipP
|
|
|
71
86
|
return () => document.removeEventListener("keydown", onKeyDown);
|
|
72
87
|
}, [visible, hide]);
|
|
73
88
|
|
|
89
|
+
const clampedX = clampCentredLeft(pos.x, bubbleWidth, window.innerWidth, VIEWPORT_MARGIN);
|
|
90
|
+
|
|
74
91
|
return (
|
|
75
92
|
<>
|
|
76
93
|
<span
|
|
@@ -89,12 +106,13 @@ export function Tooltip({ label, children, delay = 400, side = "top" }: TooltipP
|
|
|
89
106
|
<div
|
|
90
107
|
className="fixed z-[200] pointer-events-none"
|
|
91
108
|
style={{
|
|
92
|
-
left:
|
|
109
|
+
left: clampedX,
|
|
93
110
|
top: pos.y,
|
|
94
111
|
transform: resolvedSide === "top" ? "translate(-50%, -100%)" : "translate(-50%, 0)",
|
|
95
112
|
}}
|
|
96
113
|
>
|
|
97
114
|
<div
|
|
115
|
+
ref={bubbleRef}
|
|
98
116
|
role="tooltip"
|
|
99
117
|
id={tooltipId}
|
|
100
118
|
className="px-2 py-1 rounded-md bg-neutral-800 border border-neutral-700/50 text-[10px] font-medium text-neutral-200 whitespace-nowrap shadow-lg"
|
|
@@ -7,7 +7,6 @@ import { STUDIO_MOTION_PATH } from "../components/editor/studioMotion";
|
|
|
7
7
|
import { isEditableTarget } from "../utils/timelineDiscovery";
|
|
8
8
|
import { shouldIgnoreHistoryShortcut } from "../utils/studioHelpers";
|
|
9
9
|
import { canSplitElement } from "../utils/timelineElementSplit";
|
|
10
|
-
import { STUDIO_RAZOR_TOOL_ENABLED } from "../components/editor/manualEditingAvailability";
|
|
11
10
|
import { trackStudioEvent } from "../utils/studioTelemetry";
|
|
12
11
|
import { serializeStudioFileMutations } from "../utils/studioFileMutationCoordinator";
|
|
13
12
|
|
|
@@ -258,7 +257,7 @@ function dispatchPlainKey(event: KeyboardEvent, key: string, cb: HotkeyCallbacks
|
|
|
258
257
|
}
|
|
259
258
|
}
|
|
260
259
|
|
|
261
|
-
if (
|
|
260
|
+
if (key === "b" && !event.shiftKey && !event.altKey) {
|
|
262
261
|
event.preventDefault();
|
|
263
262
|
const { activeTool, setActiveTool } = usePlayerStore.getState();
|
|
264
263
|
setActiveTool(activeTool === "razor" ? "select" : "razor");
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
* Extracted from useDomEditSession to keep file sizes under the 600-line limit.
|
|
5
5
|
*/
|
|
6
6
|
import { useEffect, useRef } from "react";
|
|
7
|
-
import { STUDIO_INSPECTOR_PANELS_ENABLED } from "../components/editor/manualEditingAvailability";
|
|
8
7
|
import { findElementForSelection, type DomEditSelection } from "../components/editor/domEditing";
|
|
9
8
|
import { reapplyPositionEditsAfterSeek } from "../components/editor/manualEdits";
|
|
10
9
|
import type { SidebarTab } from "../components/sidebar/LeftSidebar";
|
|
@@ -53,7 +52,7 @@ export function useDomEditPreviewSync({
|
|
|
53
52
|
|
|
54
53
|
// fallow-ignore-next-line complexity
|
|
55
54
|
const syncSelectionFromDocument = async () => {
|
|
56
|
-
if (
|
|
55
|
+
if (captionEditMode) return;
|
|
57
56
|
const currentSelection = domEditSelectionRef.current;
|
|
58
57
|
if (!currentSelection) return;
|
|
59
58
|
let doc: Document | null = null;
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { useCallback, useEffect, useRef } from "react";
|
|
11
11
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
12
|
-
import { STUDIO_GSAP_PANEL_ENABLED } from "../components/editor/manualEditingAvailability";
|
|
13
12
|
import { usePlayerStore } from "../player";
|
|
14
13
|
import { useDomEditPreviewSync } from "./useDomEditPreviewSync";
|
|
15
14
|
import { useGsapAnimationsForElement, usePopulateKeyframeCacheForFile } from "./useGsapTweenCache";
|
|
@@ -195,7 +194,7 @@ export function useDomEditWiring({
|
|
|
195
194
|
const gsapSourceFile = domEditSelection?.sourceFile || activeCompPath || "index.html";
|
|
196
195
|
|
|
197
196
|
usePopulateKeyframeCacheForFile(
|
|
198
|
-
|
|
197
|
+
projectId ?? null,
|
|
199
198
|
gsapSourceFile,
|
|
200
199
|
gsapCacheVersion,
|
|
201
200
|
previewIframeRef,
|
|
@@ -206,7 +205,7 @@ export function useDomEditWiring({
|
|
|
206
205
|
multipleTimelines: gsapMultipleTimelines,
|
|
207
206
|
unsupportedTimelinePattern: gsapUnsupportedTimelinePattern,
|
|
208
207
|
} = useGsapAnimationsForElement(
|
|
209
|
-
|
|
208
|
+
projectId ?? null,
|
|
210
209
|
gsapSourceFile,
|
|
211
210
|
domEditSelection
|
|
212
211
|
? { id: domEditSelection.id ?? null, selector: domEditSelection.selector ?? null }
|
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
replaceDomEditGroupSelection,
|
|
17
17
|
seedDomEditGroupWithSelection,
|
|
18
18
|
} from "../utils/domEditHelpers";
|
|
19
|
-
import { STUDIO_INSPECTOR_PANELS_ENABLED } from "../components/editor/manualEditingAvailability";
|
|
20
19
|
import {
|
|
21
20
|
findElementForSelection,
|
|
22
21
|
findElementForTimelineElement,
|
|
@@ -164,14 +163,6 @@ export function useDomSelection({
|
|
|
164
163
|
setSelectedTimelineElementId(null);
|
|
165
164
|
return;
|
|
166
165
|
}
|
|
167
|
-
if (!STUDIO_INSPECTOR_PANELS_ENABLED) {
|
|
168
|
-
domEditSelectionRef.current = null;
|
|
169
|
-
domEditGroupSelectionsRef.current = [];
|
|
170
|
-
setDomEditSelection(null);
|
|
171
|
-
setDomEditGroupSelections([]);
|
|
172
|
-
setSelectedTimelineElementId(null);
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
166
|
|
|
176
167
|
const isAdditiveSelection = Boolean(options?.additive);
|
|
177
168
|
const currentSelection = domEditSelectionRef.current;
|
|
@@ -370,7 +361,6 @@ export function useDomSelection({
|
|
|
370
361
|
|
|
371
362
|
const handleTimelineElementSelect = useCallback(
|
|
372
363
|
async (element: TimelineElement | null) => {
|
|
373
|
-
if (!STUDIO_INSPECTOR_PANELS_ENABLED) return;
|
|
374
364
|
const seq = ++timelineSelectSeqRef.current;
|
|
375
365
|
if (!element) {
|
|
376
366
|
applyDomSelection(null, { revealPanel: false });
|
|
@@ -513,14 +503,6 @@ export function useDomSelection({
|
|
|
513
503
|
const applyMarqueeSelection = useCallback(
|
|
514
504
|
// fallow-ignore-next-line complexity
|
|
515
505
|
(selections: DomEditSelection[], additive: boolean) => {
|
|
516
|
-
// Honor the inspector-panels kill switch like applyDomSelection does.
|
|
517
|
-
if (!STUDIO_INSPECTOR_PANELS_ENABLED) {
|
|
518
|
-
domEditSelectionRef.current = null;
|
|
519
|
-
domEditGroupSelectionsRef.current = [];
|
|
520
|
-
setDomEditSelection(null);
|
|
521
|
-
setDomEditGroupSelections([]);
|
|
522
|
-
return;
|
|
523
|
-
}
|
|
524
506
|
if (selections.length === 0) {
|
|
525
507
|
if (!additive) applyDomSelection(null, { revealPanel: false });
|
|
526
508
|
return;
|
|
@@ -557,15 +539,6 @@ export function useDomSelection({
|
|
|
557
539
|
[applyDomSelection, timelineElements, setSelectedTimelineElementId],
|
|
558
540
|
);
|
|
559
541
|
|
|
560
|
-
// Disabled inspector effect
|
|
561
|
-
// eslint-disable-next-line no-restricted-syntax
|
|
562
|
-
useEffect(() => {
|
|
563
|
-
if (STUDIO_INSPECTOR_PANELS_ENABLED) return;
|
|
564
|
-
updateDomEditHoverSelection(null);
|
|
565
|
-
applyDomSelection(null, { revealPanel: false });
|
|
566
|
-
if (rightPanelTab !== "renders") setRightPanelTab("renders");
|
|
567
|
-
}, [applyDomSelection, rightPanelTab, updateDomEditHoverSelection, setRightPanelTab]);
|
|
568
|
-
|
|
569
542
|
return {
|
|
570
543
|
// State
|
|
571
544
|
domEditSelection,
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { useCallback, useRef } from "react";
|
|
2
2
|
import { liveTime, usePlayerStore } from "../player";
|
|
3
3
|
import { pauseStudioPreviewPlayback } from "../utils/studioPreviewHelpers";
|
|
4
|
-
import { STUDIO_PREVIEW_SELECTION_ENABLED } from "../components/editor/manualEditingAvailability";
|
|
5
4
|
import { type DomEditSelection } from "../components/editor/domEditing";
|
|
6
5
|
import type { ApplyDomSelectionOptions, ResolveDomSelectionOptions } from "./useDomSelection";
|
|
7
6
|
import { trackStudioEvent } from "../utils/studioTelemetry";
|
|
@@ -87,7 +86,7 @@ export function usePreviewInteraction({
|
|
|
87
86
|
const handlePreviewCanvasMouseDown = useCallback(
|
|
88
87
|
// fallow-ignore-next-line complexity
|
|
89
88
|
async (e: React.MouseEvent<HTMLDivElement>, options?: PreviewMouseDownOptions) => {
|
|
90
|
-
if (
|
|
89
|
+
if (captionEditMode || compositionLoading) return;
|
|
91
90
|
|
|
92
91
|
// Manual double-click detection (see DOUBLE_CLICK_MS): the first click
|
|
93
92
|
// re-renders the overlay so `e.detail` never reaches 2 on the canvas.
|
|
@@ -236,7 +235,7 @@ export function usePreviewInteraction({
|
|
|
236
235
|
const handlePreviewCanvasPointerMove = useCallback(
|
|
237
236
|
// fallow-ignore-next-line complexity
|
|
238
237
|
async (e: React.PointerEvent<HTMLDivElement>, options?: { preferClipAncestor?: boolean }) => {
|
|
239
|
-
if (
|
|
238
|
+
if (captionEditMode || compositionLoading) {
|
|
240
239
|
updateDomEditHoverSelection(null);
|
|
241
240
|
return null;
|
|
242
241
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { useCallback, useMemo, useRef, useState, type DragEvent } from "react";
|
|
2
|
-
import { STUDIO_INSPECTOR_PANELS_ENABLED } from "../components/editor/manualEditingAvailability";
|
|
3
2
|
import type { DomEditSelection } from "../components/editor/domEditing";
|
|
4
3
|
import type { StudioContextValue } from "../contexts/StudioContext";
|
|
5
4
|
import type { RightInspectorPanes } from "../utils/studioHelpers";
|
|
@@ -85,17 +84,14 @@ export function useInspectorState(
|
|
|
85
84
|
// fallow-ignore-next-line complexity
|
|
86
85
|
return useMemo(() => {
|
|
87
86
|
const inspectorTabActive = rightPanelTab === "design" || rightPanelTab === "layers";
|
|
88
|
-
const layersPanelActive =
|
|
89
|
-
|
|
90
|
-
const designPanelActive =
|
|
91
|
-
STUDIO_INSPECTOR_PANELS_ENABLED && inspectorTabActive && rightInspectorPanes.design;
|
|
87
|
+
const layersPanelActive = inspectorTabActive && rightInspectorPanes.layers;
|
|
88
|
+
const designPanelActive = inspectorTabActive && rightInspectorPanes.design;
|
|
92
89
|
const inspectorPanelActive = layersPanelActive || designPanelActive;
|
|
93
90
|
return {
|
|
94
91
|
layersPanelActive,
|
|
95
92
|
designPanelActive,
|
|
96
93
|
inspectorPanelActive,
|
|
97
|
-
inspectorButtonActive:
|
|
98
|
-
STUDIO_INSPECTOR_PANELS_ENABLED && !rightCollapsed && inspectorPanelActive,
|
|
94
|
+
inspectorButtonActive: !rightCollapsed && inspectorPanelActive,
|
|
99
95
|
// Deliberately wider than shouldShowSelectedDomBounds: the on-canvas path
|
|
100
96
|
// handles ARE the arc-drag affordance, so gating them on an open Inspector
|
|
101
97
|
// would make keyframe path editing reachable only from a side panel.
|
|
@@ -32,7 +32,6 @@ import {
|
|
|
32
32
|
useTimelineTrackLayout,
|
|
33
33
|
} from "./useTimelineTrackLayout";
|
|
34
34
|
import { useTimelineKeyframeHandlers } from "./useTimelineKeyframeHandlers";
|
|
35
|
-
import { STUDIO_KEYFRAMES_ENABLED } from "../../components/editor/manualEditingAvailability";
|
|
36
35
|
import { useTrackGapMenu } from "./useTrackGapMenu";
|
|
37
36
|
import { useTimelineGapHighlights } from "./useTimelineGapHighlights";
|
|
38
37
|
import { useStudioPlaybackContextOptional } from "../../contexts/StudioContext";
|
|
@@ -126,7 +125,7 @@ export const Timeline = memo(function Timeline({
|
|
|
126
125
|
),
|
|
127
126
|
[gsapAnimations],
|
|
128
127
|
);
|
|
129
|
-
const labelMode =
|
|
128
|
+
const labelMode = hasKeyframedClips;
|
|
130
129
|
// Without the label column the pre-t=0 breathing room is still TRACKS_LEFT_PAD
|
|
131
130
|
// (dropping it would jam clip 0 against the gutter on every non-keyframed
|
|
132
131
|
// composition); in label mode the 232px label column already provides it.
|
|
@@ -17,7 +17,6 @@ import {
|
|
|
17
17
|
} from "./timelineMultiDragPreview";
|
|
18
18
|
import type { TimelineLaneBaseProps } from "./timelineLaneProps";
|
|
19
19
|
import type { TimelineEditCallbacks } from "./timelineCallbacks";
|
|
20
|
-
import { STUDIO_KEYFRAMES_ENABLED } from "../../components/editor/manualEditingAvailability";
|
|
21
20
|
import { trackStudioKeyframeLaneExpand } from "../../telemetry/events";
|
|
22
21
|
import { SPLIT_BOUNDARY_EPSILON_S } from "../../utils/timelineElementSplit";
|
|
23
22
|
import { isAudioTimelineElement, isMusicTrack } from "../../utils/timelineInspector";
|
|
@@ -134,9 +133,12 @@ export function TimelineLanes({
|
|
|
134
133
|
// The one keyframed element this track shows lanes for (selected, else
|
|
135
134
|
// most lanes). A track can hold several elements; scoping to one keeps
|
|
136
135
|
// their keyframes from cramming into a single row.
|
|
137
|
-
const keyframeClip =
|
|
138
|
-
|
|
139
|
-
|
|
136
|
+
const keyframeClip = resolveTrackKeyframeClip(
|
|
137
|
+
els,
|
|
138
|
+
laneCounts,
|
|
139
|
+
selectedElementId,
|
|
140
|
+
selectedElementIds,
|
|
141
|
+
);
|
|
140
142
|
const keyframeClipKey = keyframeClip?.key ?? keyframeClip?.id;
|
|
141
143
|
const keyframeClipExpanded =
|
|
142
144
|
keyframeClipKey != null && expandedClipIds.has(keyframeClipKey);
|
|
@@ -249,8 +251,7 @@ export function TimelineLanes({
|
|
|
249
251
|
// Only the track's active keyframe clip shows expanded lanes;
|
|
250
252
|
// other clips (incl. siblings on a shared track) show compact
|
|
251
253
|
// diamonds on their own bar instead.
|
|
252
|
-
const isTrackKeyframeClip =
|
|
253
|
-
STUDIO_KEYFRAMES_ENABLED && elementKey === keyframeClipKey;
|
|
254
|
+
const isTrackKeyframeClip = elementKey === keyframeClipKey;
|
|
254
255
|
const showsLanes = isTrackKeyframeClip && keyframeClipExpanded;
|
|
255
256
|
const capabilities = getTimelineEditCapabilities(el);
|
|
256
257
|
const isSelected =
|
|
@@ -428,35 +429,32 @@ export function TimelineLanes({
|
|
|
428
429
|
renderClipContent,
|
|
429
430
|
renderClipOverlay,
|
|
430
431
|
)}
|
|
431
|
-
{
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
suppressClickRef={suppressClickRef}
|
|
458
|
-
/>
|
|
459
|
-
)}
|
|
432
|
+
{!showsLanes && keyframeCache?.get(elementKey) && (
|
|
433
|
+
<TimelineClipDiamonds
|
|
434
|
+
keyframesData={keyframeCache.get(elementKey)!}
|
|
435
|
+
clipWidthPx={Math.max(previewElement.duration * pps, 4)}
|
|
436
|
+
clipHeightPx={rowHeight - 2 * CLIP_Y}
|
|
437
|
+
beatsActive={beatStripOnTrack}
|
|
438
|
+
accentColor={clipStyle.accent}
|
|
439
|
+
isSelected={isSelected}
|
|
440
|
+
currentPercentage={
|
|
441
|
+
previewElement.duration > 0
|
|
442
|
+
? ((currentTime - previewElement.start) / previewElement.duration) *
|
|
443
|
+
100
|
|
444
|
+
: 0
|
|
445
|
+
}
|
|
446
|
+
elementId={elementKey}
|
|
447
|
+
selectedKeyframes={selectedKeyframes}
|
|
448
|
+
onClickKeyframe={(_elId, target) =>
|
|
449
|
+
onClickKeyframe?.(previewElement, target)
|
|
450
|
+
}
|
|
451
|
+
onShiftClickKeyframe={onShiftClickKeyframe}
|
|
452
|
+
onContextMenuKeyframe={onContextMenuKeyframe}
|
|
453
|
+
onMoveKeyframe={onMoveKeyframe}
|
|
454
|
+
onSelectSegment={onSelectSegment}
|
|
455
|
+
suppressClickRef={suppressClickRef}
|
|
456
|
+
/>
|
|
457
|
+
)}
|
|
460
458
|
</TimelineClip>
|
|
461
459
|
);
|
|
462
460
|
// Mounted for the track's keyframe clip in BOTH disclosure
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { useEffect, useRef } from "react";
|
|
2
2
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
3
3
|
import { usePlayerStore } from "../store/playerStore";
|
|
4
|
-
import { STUDIO_KEYFRAMES_ENABLED } from "../../components/editor/manualEditingAvailability";
|
|
5
4
|
import { useStudioShellContextOptional } from "../../contexts/StudioContext";
|
|
6
5
|
import { animationContributesLane } from "./TimelinePropertyLanes";
|
|
7
6
|
|
|
@@ -34,7 +33,6 @@ export function useAutoExpandKeyframedClips(gsapAnimations: Map<string, GsapAnim
|
|
|
34
33
|
const projectId = useStudioShellContextOptional()?.projectId ?? null;
|
|
35
34
|
const seen = useRef({ projectId, source: gsapAnimations, clips: new Set<string>() });
|
|
36
35
|
useEffect(() => {
|
|
37
|
-
if (!STUDIO_KEYFRAMES_ENABLED) return;
|
|
38
36
|
if (seen.current.projectId !== projectId) {
|
|
39
37
|
const sourceChanged = seen.current.source !== gsapAnimations;
|
|
40
38
|
seen.current = { projectId, source: gsapAnimations, clips: new Set() };
|