@hyperframes/studio 0.8.51 → 0.8.52
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-Cn_PpHqP.js → hyperframes-player-I5YRzB02.js} +1 -1
- package/dist/assets/{index-Clrb11lj.js → index-BTAfgesM.js} +1 -1
- package/dist/assets/index-CxAu_Okg.css +1 -0
- package/dist/assets/index-DocJlW2v.js +376 -0
- package/dist/assets/{index-CPS3Cvj8.js → index-Du5x7KUR.js} +1 -1
- package/dist/index.d.ts +401 -7
- package/dist/index.html +2 -2
- package/dist/index.js +6545 -5869
- package/dist/index.js.map +1 -1
- package/package.json +9 -7
- package/src/App.tsx +3 -2
- package/src/captions/components/CaptionOverlay.sender.test.tsx +52 -0
- package/src/captions/components/CaptionOverlay.tsx +8 -4
- package/src/components/EditorShell.tsx +4 -0
- package/src/components/RightPanelTabs.test.tsx +151 -0
- package/src/components/RightPanelTabs.tsx +59 -0
- package/src/components/StudioHeader.dom.test.tsx +181 -0
- package/src/components/StudioHeader.tsx +109 -149
- package/src/components/StudioRightPanel.tsx +47 -35
- package/src/components/TimelineHistoryButtons.dom.test.tsx +117 -0
- package/src/components/TimelineHistoryButtons.tsx +54 -0
- package/src/components/TimelineToolbar.test.tsx +8 -0
- package/src/components/TimelineToolbar.tsx +3 -7
- package/src/components/dock/Dock.test.tsx +84 -16
- package/src/components/dock/Dock.tsx +19 -2
- package/src/components/dock/dock.css +5 -0
- package/src/components/dock/dockAccessibility.test.ts +166 -0
- package/src/components/dock/dockAccessibility.ts +98 -0
- package/src/components/dock/dockLayout.test.ts +134 -0
- package/src/components/dock/dockLayout.ts +53 -6
- package/src/components/dock/dockTestHarness.ts +11 -0
- package/src/components/editor/LayersPanel.test.ts +97 -1
- package/src/components/editor/LayersPanel.tsx +4 -2
- package/src/components/editor/PreviewGuides.tsx +1 -1
- package/src/components/editor/TopologyLens.test.tsx +28 -0
- package/src/components/editor/TopologyLens.tsx +3 -1
- package/src/components/editor/useColorGradingController.test.ts +38 -0
- package/src/components/editor/useColorGradingController.ts +9 -1
- package/src/components/editor/useColorGradingPreviews.test.ts +76 -0
- package/src/components/editor/useColorGradingPreviews.ts +11 -1
- package/src/components/editor/usePreviewCompositionRect.test.tsx +95 -0
- package/src/components/editor/usePreviewCompositionRect.ts +21 -0
- package/src/components/nle/NLEPreview.tsx +1 -1
- package/src/components/nle/PreviewPane.tsx +1 -1
- package/src/components/pointerTargetSize.test.tsx +8 -0
- package/src/components/renders/RenderQueue.tsx +1 -1
- package/src/components/timelineToolbarStyles.ts +5 -0
- package/src/components/ui/Button.test.tsx +222 -30
- package/src/components/ui/Button.tsx +88 -125
- package/src/components/ui/IconButton.tsx +38 -0
- package/src/components/ui/Input.tsx +115 -0
- package/src/components/ui/Menu.test.tsx +374 -0
- package/src/components/ui/Menu.tsx +213 -0
- package/src/components/ui/NumberField.tsx +122 -0
- package/src/components/ui/Popover.tsx +74 -0
- package/src/components/ui/Select.tsx +100 -0
- package/src/components/ui/Slider.tsx +153 -0
- package/src/components/ui/Tabs.test.tsx +98 -0
- package/src/components/ui/Tabs.tsx +71 -0
- package/src/components/ui/Toggle.tsx +63 -0
- package/src/components/ui/Tooltip.test.tsx +110 -0
- package/src/components/ui/Tooltip.tsx +38 -112
- package/src/components/ui/cn.test.ts +30 -0
- package/src/components/ui/cn.ts +25 -0
- package/src/components/ui/index.ts +29 -2
- package/src/components/ui/mountHost.testHelpers.ts +24 -0
- package/src/components/ui/valueControls.test.tsx +524 -0
- package/src/hooks/useAppHotkeys.ts +20 -79
- package/src/hooks/useCaptionDetection.sender.test.tsx +46 -0
- package/src/hooks/useCaptionDetection.ts +3 -1
- package/src/hooks/useEditHistoryActions.test.tsx +90 -0
- package/src/hooks/useEditHistoryActions.ts +106 -0
- package/src/hooks/useServerConnection.hashProject.test.tsx +94 -0
- package/src/hooks/useServerConnection.ts +43 -10
- package/src/index.ts +51 -0
- package/src/player/components/AutomationSelectionMenu.tsx +1 -1
- package/src/player/components/TimelineCanvas.tsx +4 -4
- package/src/player/components/TimelineClip.tsx +0 -1
- package/src/player/lib/playbackShortcuts.ts +2 -0
- package/src/player/store/previewIframeStore.notify.test.tsx +75 -0
- package/src/player/store/previewIframeStore.test.tsx +29 -0
- package/src/player/store/previewIframeStore.ts +25 -0
- package/src/renderQueueExports.test.tsx +58 -0
- package/src/styles/classCandidates.test.ts +105 -0
- package/src/styles/classCandidates.ts +281 -0
- package/src/styles/hex-baseline.json +84 -0
- package/src/styles/hexRatchet.test.ts +201 -0
- package/src/styles/styleSources.test.ts +20 -0
- package/src/styles/styleSources.ts +47 -0
- package/src/styles/theme.css +30 -0
- package/src/styles/theme.test.ts +2 -15
- package/src/styles/tokenGate.test.ts +109 -0
- package/src/utils/sourcePatcher.ts +1 -1
- package/src/utils/studioHelpers.test.ts +18 -0
- package/src/utils/studioHelpers.ts +11 -1
- package/src/utils/typingTarget.ts +4 -0
- package/dist/assets/index-C05SNrvY.js +0 -360
- package/dist/assets/index-DUR9qzQM.css +0 -1
- package/src/components/PanelTabButton.tsx +0 -31
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import type { MouseEvent } from "react";
|
|
2
|
-
import {
|
|
3
|
-
import { getHistoryShortcutLabel } from "../utils/studioHelpers";
|
|
2
|
+
import { Camera } from "../icons/SystemIcons";
|
|
4
3
|
import { useStudioShellContext } from "../contexts/StudioContext";
|
|
5
4
|
import { usePanelLayoutContext } from "../contexts/PanelLayoutContext";
|
|
6
5
|
import { trackStudioEvent } from "../utils/studioTelemetry";
|
|
7
|
-
import { Tooltip } from "./ui";
|
|
6
|
+
import { Button, buttonBase, buttonSizes, buttonVariants, cn, Tooltip } from "./ui";
|
|
8
7
|
|
|
9
8
|
export interface StudioHeaderProps {
|
|
10
9
|
captureFrameHref: string;
|
|
@@ -40,8 +39,8 @@ function HyperframesLogo() {
|
|
|
40
39
|
y2="37.482"
|
|
41
40
|
gradientUnits="userSpaceOnUse"
|
|
42
41
|
>
|
|
43
|
-
<stop stopColor="
|
|
44
|
-
<stop offset="1" stopColor="
|
|
42
|
+
<stop stopColor="var(--color-brand-gradient-from)" />
|
|
43
|
+
<stop offset="1" stopColor="var(--color-brand-gradient-to)" />
|
|
45
44
|
</linearGradient>
|
|
46
45
|
<linearGradient
|
|
47
46
|
id="hf-g1"
|
|
@@ -51,8 +50,8 @@ function HyperframesLogo() {
|
|
|
51
50
|
y2="6.303"
|
|
52
51
|
gradientUnits="userSpaceOnUse"
|
|
53
52
|
>
|
|
54
|
-
<stop stopColor="
|
|
55
|
-
<stop offset="1" stopColor="
|
|
53
|
+
<stop stopColor="var(--color-brand-gradient-from)" />
|
|
54
|
+
<stop offset="1" stopColor="var(--color-brand-gradient-to)" />
|
|
56
55
|
</linearGradient>
|
|
57
56
|
</defs>
|
|
58
57
|
{/* heygen label */}
|
|
@@ -164,7 +163,7 @@ export function StudioHeader({
|
|
|
164
163
|
inspectorPanelActive,
|
|
165
164
|
onExport,
|
|
166
165
|
}: StudioHeaderProps) {
|
|
167
|
-
const { projectId,
|
|
166
|
+
const { projectId, renderQueue } = useStudioShellContext();
|
|
168
167
|
// effectiveRightCollapsed, not the raw intent: in the auto-railed state the
|
|
169
168
|
// intent is still "open" while the panel is hidden, so branching on intent
|
|
170
169
|
// made this button write rightCollapsed=true — and that value is synced into
|
|
@@ -174,153 +173,114 @@ export function StudioHeader({
|
|
|
174
173
|
const ffmpegMissing = renderQueue.ffmpegMissing;
|
|
175
174
|
|
|
176
175
|
return (
|
|
177
|
-
<div className="flex items-center justify-between h-10 px-3 bg-
|
|
176
|
+
<div className="flex items-center justify-between h-10 px-3 bg-surface border-b border-border-strong shrink-0">
|
|
178
177
|
{/* Left: logo + project name */}
|
|
179
178
|
<div className="flex items-center gap-3">
|
|
180
179
|
<HyperframesLogo />
|
|
181
|
-
<span className="text-
|
|
180
|
+
<span className="text-text-5 select-none" aria-hidden="true">
|
|
182
181
|
|
|
|
183
182
|
</span>
|
|
184
|
-
<span className="text-
|
|
183
|
+
<span className="text-step-11 font-medium text-text-1">{projectId}</span>
|
|
185
184
|
</div>
|
|
186
185
|
{/* Right: toolbar buttons */}
|
|
187
|
-
<div className="flex items-center gap-
|
|
188
|
-
<
|
|
189
|
-
label={
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
:
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
<svg
|
|
262
|
-
className="animate-spin motion-reduce:animate-none h-3.5 w-3.5"
|
|
263
|
-
viewBox="0 0 24 24"
|
|
264
|
-
fill="none"
|
|
265
|
-
aria-hidden="true"
|
|
266
|
-
>
|
|
267
|
-
<circle
|
|
268
|
-
className="opacity-25"
|
|
269
|
-
cx="12"
|
|
270
|
-
cy="12"
|
|
271
|
-
r="10"
|
|
186
|
+
<div className="flex items-center gap-3">
|
|
187
|
+
<div className="flex h-ctl items-center divide-x divide-border-strong overflow-hidden rounded-md border border-border-strong bg-bg-2">
|
|
188
|
+
<Tooltip label={capturing ? "Capturing frame…" : "Capture current frame"} side="bottom">
|
|
189
|
+
{/* A real download link, so it wears Button's recipe rather than being
|
|
190
|
+
one: `download` on an <a> is what saves the frame, and no <button>
|
|
191
|
+
can do that. `enabled:` never matches a link, so the ghost
|
|
192
|
+
variant's hover look is repeated unprefixed here. */}
|
|
193
|
+
<a
|
|
194
|
+
href={captureFrameHref}
|
|
195
|
+
download={captureFrameFilename}
|
|
196
|
+
onClick={(e) => {
|
|
197
|
+
if (capturing) {
|
|
198
|
+
e.preventDefault();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
trackStudioEvent("toolbar_action", { action: "capture_frame" });
|
|
202
|
+
handleCaptureFrameClick(e);
|
|
203
|
+
}}
|
|
204
|
+
onFocus={refreshCaptureFrameTime}
|
|
205
|
+
onPointerDown={refreshCaptureFrameTime}
|
|
206
|
+
aria-disabled={capturing || undefined}
|
|
207
|
+
className={cn(
|
|
208
|
+
buttonBase,
|
|
209
|
+
buttonVariants.ghost,
|
|
210
|
+
buttonSizes.md,
|
|
211
|
+
"h-full rounded-none max-[1000px]:px-2",
|
|
212
|
+
capturing
|
|
213
|
+
? "text-text-4 cursor-default"
|
|
214
|
+
: "hover:bg-hover hover:text-text-0 active:scale-[0.98]",
|
|
215
|
+
)}
|
|
216
|
+
aria-label={capturing ? "Capturing frame" : "Capture current frame"}
|
|
217
|
+
>
|
|
218
|
+
{capturing ? (
|
|
219
|
+
<svg
|
|
220
|
+
className="animate-spin motion-reduce:animate-none h-3.5 w-3.5"
|
|
221
|
+
viewBox="0 0 24 24"
|
|
222
|
+
fill="none"
|
|
223
|
+
aria-hidden="true"
|
|
224
|
+
>
|
|
225
|
+
<circle
|
|
226
|
+
className="opacity-25"
|
|
227
|
+
cx="12"
|
|
228
|
+
cy="12"
|
|
229
|
+
r="10"
|
|
230
|
+
stroke="currentColor"
|
|
231
|
+
strokeWidth="4"
|
|
232
|
+
/>
|
|
233
|
+
<path
|
|
234
|
+
className="opacity-75"
|
|
235
|
+
fill="currentColor"
|
|
236
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
|
237
|
+
/>
|
|
238
|
+
</svg>
|
|
239
|
+
) : (
|
|
240
|
+
<Camera size={14} />
|
|
241
|
+
)}
|
|
242
|
+
<span className="max-[1000px]:hidden">{capturing ? "Capturing…" : "Capture"}</span>
|
|
243
|
+
</a>
|
|
244
|
+
</Tooltip>
|
|
245
|
+
<Tooltip label="Inspector" side="bottom">
|
|
246
|
+
<Button
|
|
247
|
+
variant="ghost"
|
|
248
|
+
aria-label="Inspector"
|
|
249
|
+
aria-pressed={inspectorButtonActive}
|
|
250
|
+
className={cn(
|
|
251
|
+
"h-full rounded-none",
|
|
252
|
+
inspectorButtonActive && "bg-hover text-accent enabled:hover:text-accent",
|
|
253
|
+
)}
|
|
254
|
+
icon={
|
|
255
|
+
<svg
|
|
256
|
+
width="12"
|
|
257
|
+
height="12"
|
|
258
|
+
viewBox="0 0 24 24"
|
|
259
|
+
fill="none"
|
|
272
260
|
stroke="currentColor"
|
|
273
|
-
strokeWidth="
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
|
279
|
-
/>
|
|
280
|
-
</svg>
|
|
281
|
-
) : (
|
|
282
|
-
<Camera size={14} />
|
|
283
|
-
)}
|
|
284
|
-
<span>{capturing ? "Capturing…" : "Capture"}</span>
|
|
285
|
-
</a>
|
|
286
|
-
</Tooltip>
|
|
287
|
-
<Tooltip label="Inspector" side="bottom">
|
|
288
|
-
<button
|
|
289
|
-
type="button"
|
|
290
|
-
onClick={() => {
|
|
291
|
-
if (shouldOpenInspector(effectiveRightCollapsed, inspectorPanelActive)) {
|
|
292
|
-
trackStudioEvent("panel_toggle", { panel: "inspector", collapsed: false });
|
|
293
|
-
setRightPanelTab("design");
|
|
294
|
-
setRightCollapsed(false);
|
|
295
|
-
return;
|
|
261
|
+
strokeWidth="2"
|
|
262
|
+
>
|
|
263
|
+
<circle cx="12" cy="12" r="10" />
|
|
264
|
+
<polygon points="10 8 16 12 10 16" fill="currentColor" stroke="none" />
|
|
265
|
+
</svg>
|
|
296
266
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
>
|
|
310
|
-
<svg
|
|
311
|
-
width="12"
|
|
312
|
-
height="12"
|
|
313
|
-
viewBox="0 0 24 24"
|
|
314
|
-
fill="none"
|
|
315
|
-
stroke="currentColor"
|
|
316
|
-
strokeWidth="2"
|
|
267
|
+
onClick={() => {
|
|
268
|
+
if (shouldOpenInspector(effectiveRightCollapsed, inspectorPanelActive)) {
|
|
269
|
+
trackStudioEvent("panel_toggle", { panel: "inspector", collapsed: false });
|
|
270
|
+
setRightPanelTab("design");
|
|
271
|
+
setRightCollapsed(false);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
trackStudioEvent("panel_toggle", { panel: "inspector", collapsed: true });
|
|
275
|
+
// Keep the current selection when collapsing the Inspector — closing
|
|
276
|
+
// the panel shouldn't deselect the element.
|
|
277
|
+
setRightCollapsed(true);
|
|
278
|
+
}}
|
|
317
279
|
>
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
</button>
|
|
323
|
-
</Tooltip>
|
|
280
|
+
Inspector
|
|
281
|
+
</Button>
|
|
282
|
+
</Tooltip>
|
|
283
|
+
</div>
|
|
324
284
|
<Tooltip
|
|
325
285
|
label={
|
|
326
286
|
ffmpegMissing
|
|
@@ -331,8 +291,9 @@ export function StudioHeader({
|
|
|
331
291
|
}
|
|
332
292
|
side="bottom"
|
|
333
293
|
>
|
|
334
|
-
<
|
|
335
|
-
|
|
294
|
+
<Button
|
|
295
|
+
variant="primary"
|
|
296
|
+
data-testid="header-export"
|
|
336
297
|
disabled={isRendering}
|
|
337
298
|
onClick={() => {
|
|
338
299
|
if (isRendering) return;
|
|
@@ -346,10 +307,9 @@ export function StudioHeader({
|
|
|
346
307
|
if (ffmpegMissing) return;
|
|
347
308
|
onExport?.();
|
|
348
309
|
}}
|
|
349
|
-
className="h-7 flex items-center gap-1.5 px-3 rounded-md text-[11px] font-semibold bg-studio-accent text-[#09090B] enabled:hover:brightness-110 transition-[filter,transform] enabled:active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed"
|
|
350
310
|
>
|
|
351
311
|
{isRendering ? "Rendering…" : "Export"}
|
|
352
|
-
</
|
|
312
|
+
</Button>
|
|
353
313
|
</Tooltip>
|
|
354
314
|
</div>
|
|
355
315
|
</div>
|
|
@@ -9,7 +9,7 @@ import { BlockParamsPanel } from "./editor/BlockParamsPanel";
|
|
|
9
9
|
import { RenderQueuePanel } from "./renders/RenderQueuePanel";
|
|
10
10
|
import { SlideshowPanel } from "./panels/SlideshowPanel";
|
|
11
11
|
import { VariablesPanel } from "./panels/VariablesPanel";
|
|
12
|
-
import {
|
|
12
|
+
import { RightPanelTabs, type RightPanelTabDescriptor } from "./RightPanelTabs";
|
|
13
13
|
import type { RenderJob } from "./renders/useRenderQueue";
|
|
14
14
|
import { STUDIO_FLAT_INSPECTOR_ENABLED } from "./editor/manualEditingAvailability";
|
|
15
15
|
import { useSlideshowPersist } from "../hooks/useSlideshowPersist";
|
|
@@ -339,6 +339,51 @@ export function StudioRightPanel({
|
|
|
339
339
|
|
|
340
340
|
const renderQueuePanel = <RenderQueuePanel />;
|
|
341
341
|
|
|
342
|
+
// Slideshow appears only for a slideshow composition, so the strip is built
|
|
343
|
+
// rather than written out: a tab that is not in this list is not reachable by
|
|
344
|
+
// an arrow key either.
|
|
345
|
+
const inspectorTabs: RightPanelTabDescriptor[] = [
|
|
346
|
+
{
|
|
347
|
+
id: "design",
|
|
348
|
+
label: "Design",
|
|
349
|
+
tooltip: "Element styles and properties",
|
|
350
|
+
active: designPaneOpen,
|
|
351
|
+
onSelect: () => handleInspectorPaneButtonClick("design"),
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
id: "layers",
|
|
355
|
+
label: "Layers",
|
|
356
|
+
tooltip: "Composition layer stack",
|
|
357
|
+
active: layersPaneOpen,
|
|
358
|
+
onSelect: () => handleInspectorPaneButtonClick("layers"),
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
id: "renders",
|
|
362
|
+
label: renderJobs.length > 0 ? `Renders (${renderJobs.length})` : "Renders",
|
|
363
|
+
tooltip: "Render queue and exports",
|
|
364
|
+
active: rightPanelTab === "renders",
|
|
365
|
+
onSelect: () => setRightPanelTab("renders"),
|
|
366
|
+
},
|
|
367
|
+
...(isSlideshowComposition
|
|
368
|
+
? [
|
|
369
|
+
{
|
|
370
|
+
id: "slideshow",
|
|
371
|
+
label: "Slideshow",
|
|
372
|
+
tooltip: "Slideshow branching editor",
|
|
373
|
+
active: rightPanelTab === "slideshow",
|
|
374
|
+
onSelect: () => setRightPanelTab("slideshow"),
|
|
375
|
+
},
|
|
376
|
+
]
|
|
377
|
+
: []),
|
|
378
|
+
{
|
|
379
|
+
id: "variables",
|
|
380
|
+
label: "Variables",
|
|
381
|
+
tooltip: "Template variables — declare, preview with values",
|
|
382
|
+
active: rightPanelTab === "variables",
|
|
383
|
+
onSelect: () => setRightPanelTab("variables"),
|
|
384
|
+
},
|
|
385
|
+
];
|
|
386
|
+
|
|
342
387
|
return (
|
|
343
388
|
<>
|
|
344
389
|
{/* Vertical resize divider: 3px visible seam, 13px hit zone via the inner div. */}
|
|
@@ -376,40 +421,7 @@ export function StudioRightPanel({
|
|
|
376
421
|
<CaptionPropertyPanel iframeRef={previewIframeRef} />
|
|
377
422
|
) : (
|
|
378
423
|
<>
|
|
379
|
-
<
|
|
380
|
-
<PanelTabButton
|
|
381
|
-
label="Design"
|
|
382
|
-
tooltip="Element styles and properties"
|
|
383
|
-
active={designPaneOpen}
|
|
384
|
-
onClick={() => handleInspectorPaneButtonClick("design")}
|
|
385
|
-
/>
|
|
386
|
-
<PanelTabButton
|
|
387
|
-
label="Layers"
|
|
388
|
-
tooltip="Composition layer stack"
|
|
389
|
-
active={layersPaneOpen}
|
|
390
|
-
onClick={() => handleInspectorPaneButtonClick("layers")}
|
|
391
|
-
/>
|
|
392
|
-
<PanelTabButton
|
|
393
|
-
label={renderJobs.length > 0 ? `Renders (${renderJobs.length})` : "Renders"}
|
|
394
|
-
tooltip="Render queue and exports"
|
|
395
|
-
active={rightPanelTab === "renders"}
|
|
396
|
-
onClick={() => setRightPanelTab("renders")}
|
|
397
|
-
/>
|
|
398
|
-
{isSlideshowComposition && (
|
|
399
|
-
<PanelTabButton
|
|
400
|
-
label="Slideshow"
|
|
401
|
-
tooltip="Slideshow branching editor"
|
|
402
|
-
active={rightPanelTab === "slideshow"}
|
|
403
|
-
onClick={() => setRightPanelTab("slideshow")}
|
|
404
|
-
/>
|
|
405
|
-
)}
|
|
406
|
-
<PanelTabButton
|
|
407
|
-
label="Variables"
|
|
408
|
-
tooltip="Template variables — declare, preview with values"
|
|
409
|
-
active={rightPanelTab === "variables"}
|
|
410
|
-
onClick={() => setRightPanelTab("variables")}
|
|
411
|
-
/>
|
|
412
|
-
</div>
|
|
424
|
+
<RightPanelTabs tabs={inspectorTabs} activateOnFocus={STUDIO_FLAT_INSPECTOR_ENABLED} />
|
|
413
425
|
<div className="min-h-0 min-w-0 flex-1 overflow-hidden">
|
|
414
426
|
{rightPanelTab === "block-params" && activeBlockParams ? (
|
|
415
427
|
<BlockParamsPanel
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { act } from "react";
|
|
4
|
+
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
|
5
|
+
import { cleanupMounted, mountHost } from "./ui/mountHost.testHelpers";
|
|
6
|
+
import { isTypingTarget } from "../utils/typingTarget";
|
|
7
|
+
import { shouldIgnorePlaybackShortcutTarget } from "../player/lib/playbackShortcuts";
|
|
8
|
+
|
|
9
|
+
const editHistory = {
|
|
10
|
+
canUndo: false,
|
|
11
|
+
canRedo: false,
|
|
12
|
+
undoLabel: undefined as string | undefined,
|
|
13
|
+
redoLabel: undefined as string | undefined,
|
|
14
|
+
};
|
|
15
|
+
const handleUndo = vi.fn();
|
|
16
|
+
const handleRedo = vi.fn();
|
|
17
|
+
const trackStudioEvent = vi.fn();
|
|
18
|
+
|
|
19
|
+
vi.mock("../contexts/StudioContext", () => ({
|
|
20
|
+
useStudioShellContext: () => ({ editHistory, handleUndo, handleRedo }),
|
|
21
|
+
}));
|
|
22
|
+
vi.mock("../utils/studioTelemetry", () => ({ trackStudioEvent }));
|
|
23
|
+
|
|
24
|
+
const { TimelineHistoryButtons } = await import("./TimelineHistoryButtons");
|
|
25
|
+
const { TimelineToolbar } = await import("./TimelineToolbar");
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
Object.assign(editHistory, {
|
|
29
|
+
canUndo: false,
|
|
30
|
+
canRedo: false,
|
|
31
|
+
undoLabel: undefined,
|
|
32
|
+
redoLabel: undefined,
|
|
33
|
+
});
|
|
34
|
+
vi.clearAllMocks();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
afterEach(cleanupMounted);
|
|
38
|
+
|
|
39
|
+
const mount = mountHost;
|
|
40
|
+
|
|
41
|
+
function button(host: HTMLElement, label: string): HTMLButtonElement {
|
|
42
|
+
const el = host.querySelector<HTMLButtonElement>(`button[aria-label="${label}"]`);
|
|
43
|
+
if (!el) throw new Error(`not rendered: ${label}`);
|
|
44
|
+
return el;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function click(el: HTMLElement): void {
|
|
48
|
+
act(() => {
|
|
49
|
+
el.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
it("calls handleUndo once and tracks the event when Undo is enabled", () => {
|
|
54
|
+
editHistory.canUndo = true;
|
|
55
|
+
const host = mount(<TimelineHistoryButtons />);
|
|
56
|
+
|
|
57
|
+
click(button(host, "Undo"));
|
|
58
|
+
|
|
59
|
+
expect(handleUndo).toHaveBeenCalledTimes(1);
|
|
60
|
+
expect(handleRedo).not.toHaveBeenCalled();
|
|
61
|
+
expect(trackStudioEvent).toHaveBeenCalledWith("toolbar_action", { action: "undo" });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("calls handleRedo once and tracks the event when Redo is enabled", () => {
|
|
65
|
+
editHistory.canRedo = true;
|
|
66
|
+
const host = mount(<TimelineHistoryButtons />);
|
|
67
|
+
|
|
68
|
+
click(button(host, "Redo"));
|
|
69
|
+
|
|
70
|
+
expect(handleRedo).toHaveBeenCalledTimes(1);
|
|
71
|
+
expect(trackStudioEvent).toHaveBeenCalledWith("toolbar_action", { action: "redo" });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("disables both buttons and ignores clicks on an empty history", () => {
|
|
75
|
+
const host = mount(<TimelineHistoryButtons />);
|
|
76
|
+
|
|
77
|
+
for (const label of ["Undo", "Redo"]) {
|
|
78
|
+
const el = button(host, label);
|
|
79
|
+
expect(el.disabled).toBe(true);
|
|
80
|
+
expect(el.className).toContain("cursor-not-allowed");
|
|
81
|
+
click(el);
|
|
82
|
+
}
|
|
83
|
+
expect(handleUndo).not.toHaveBeenCalled();
|
|
84
|
+
expect(handleRedo).not.toHaveBeenCalled();
|
|
85
|
+
expect(trackStudioEvent).not.toHaveBeenCalled();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("leaves enabled buttons without the disabled attribute", () => {
|
|
89
|
+
editHistory.canUndo = true;
|
|
90
|
+
const el = button(mount(<TimelineHistoryButtons />), "Undo");
|
|
91
|
+
|
|
92
|
+
expect(el.disabled).toBe(false);
|
|
93
|
+
expect(el.className).not.toContain("cursor-not-allowed");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("orders the toolbar Undo, Redo, Select, Razor", () => {
|
|
97
|
+
const host = mount(<TimelineToolbar />);
|
|
98
|
+
const labels = Array.from(host.querySelectorAll("button"))
|
|
99
|
+
.map((b) => b.getAttribute("aria-label"))
|
|
100
|
+
.filter((l) => l !== null)
|
|
101
|
+
.slice(0, 4);
|
|
102
|
+
|
|
103
|
+
expect(labels).toEqual(["Undo", "Redo", "Selection tool", "Razor tool"]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("classifies Undo and Redo for the hotkey filters at their new location (KTD13)", () => {
|
|
107
|
+
// The header's own version of this check dropped Undo/Redo when they moved
|
|
108
|
+
// here; a <button> is never a typing target and is always claimed by the
|
|
109
|
+
// playback filter, same as every other toolbar tool.
|
|
110
|
+
const host = mount(<TimelineHistoryButtons />);
|
|
111
|
+
|
|
112
|
+
for (const label of ["Undo", "Redo"]) {
|
|
113
|
+
const el = button(host, label);
|
|
114
|
+
expect(isTypingTarget(el), label).toBe(false);
|
|
115
|
+
expect(shouldIgnorePlaybackShortcutTarget(el), label).toBe(true);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { useStudioShellContext } from "../contexts/StudioContext";
|
|
2
|
+
import { RotateCcw, RotateCw } from "../icons/SystemIcons";
|
|
3
|
+
import { trackStudioEvent } from "../utils/studioTelemetry";
|
|
4
|
+
import { historyTooltipLabel } from "../utils/studioHelpers";
|
|
5
|
+
import { flatDisabled, flatIdle } from "./timelineToolbarStyles";
|
|
6
|
+
import { Tooltip } from "./ui";
|
|
7
|
+
|
|
8
|
+
interface HistoryButtonProps {
|
|
9
|
+
action: "undo" | "redo";
|
|
10
|
+
enabled: boolean;
|
|
11
|
+
label: string | undefined;
|
|
12
|
+
onClick: () => Promise<void> | void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function HistoryButton({ action, enabled, label, onClick }: HistoryButtonProps) {
|
|
16
|
+
const Icon = action === "undo" ? RotateCcw : RotateCw;
|
|
17
|
+
return (
|
|
18
|
+
<Tooltip label={historyTooltipLabel(action, label)}>
|
|
19
|
+
<button
|
|
20
|
+
type="button"
|
|
21
|
+
aria-label={action === "undo" ? "Undo" : "Redo"}
|
|
22
|
+
disabled={!enabled}
|
|
23
|
+
className={enabled ? flatIdle : flatDisabled}
|
|
24
|
+
onClick={() => {
|
|
25
|
+
trackStudioEvent("toolbar_action", { action });
|
|
26
|
+
void onClick();
|
|
27
|
+
}}
|
|
28
|
+
>
|
|
29
|
+
<Icon size={16} />
|
|
30
|
+
</button>
|
|
31
|
+
</Tooltip>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Undo and Redo, on the one edit-history path the shell already owns. */
|
|
36
|
+
export function TimelineHistoryButtons() {
|
|
37
|
+
const { editHistory, handleUndo, handleRedo } = useStudioShellContext();
|
|
38
|
+
return (
|
|
39
|
+
<>
|
|
40
|
+
<HistoryButton
|
|
41
|
+
action="undo"
|
|
42
|
+
enabled={editHistory.canUndo}
|
|
43
|
+
label={editHistory.undoLabel}
|
|
44
|
+
onClick={handleUndo}
|
|
45
|
+
/>
|
|
46
|
+
<HistoryButton
|
|
47
|
+
action="redo"
|
|
48
|
+
enabled={editHistory.canRedo}
|
|
49
|
+
label={editHistory.redoLabel}
|
|
50
|
+
onClick={handleRedo}
|
|
51
|
+
/>
|
|
52
|
+
</>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -8,6 +8,14 @@ import { usePlayerStore } from "../player/store/playerStore";
|
|
|
8
8
|
import { makeSelection } from "../hooks/domSelectionTestHarness";
|
|
9
9
|
import { TimelineToolbar } from "./TimelineToolbar";
|
|
10
10
|
|
|
11
|
+
vi.mock("../contexts/StudioContext", () => ({
|
|
12
|
+
useStudioShellContext: () => ({
|
|
13
|
+
editHistory: { canUndo: false, canRedo: false },
|
|
14
|
+
handleUndo: vi.fn(),
|
|
15
|
+
handleRedo: vi.fn(),
|
|
16
|
+
}),
|
|
17
|
+
}));
|
|
18
|
+
|
|
11
19
|
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
12
20
|
|
|
13
21
|
afterEach(() => {
|
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
import { useTimelineZoom } from "../player/components/useTimelineZoom";
|
|
23
23
|
import { usePlayerStore, type TimelineElement } from "../player";
|
|
24
24
|
import { Tooltip } from "./ui";
|
|
25
|
+
import { flatActive, flatBtn, flatDisabled, flatIdle } from "./timelineToolbarStyles";
|
|
26
|
+
import { TimelineHistoryButtons } from "./TimelineHistoryButtons";
|
|
25
27
|
import { Scissors } from "../icons/SystemIcons";
|
|
26
28
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
27
29
|
import type { DomEditSelection } from "./editor/domEditingTypes";
|
|
@@ -186,19 +188,13 @@ export function TimelineToolbar({ domEditSession, onSplitElement }: TimelineTool
|
|
|
186
188
|
return () => window.removeEventListener("keydown", onKeyDown);
|
|
187
189
|
}, []);
|
|
188
190
|
|
|
189
|
-
// CapCut-flat icon buttons: no per-button border/box chrome — a transparent
|
|
190
|
-
// 28px hit area with a subtle rounded hover wash, consistent 16px glyphs.
|
|
191
|
-
const flatBtn = "flex h-7 w-7 items-center justify-center rounded-md transition-colors";
|
|
192
|
-
const flatIdle = `${flatBtn} text-neutral-400 hover:bg-white/6 hover:text-neutral-200 active:scale-[0.98]`;
|
|
193
|
-
const flatActive = `${flatBtn} bg-white/8 text-neutral-100 active:scale-[0.98]`;
|
|
194
|
-
const flatDisabled = `${flatBtn} text-neutral-700 cursor-not-allowed`;
|
|
195
|
-
|
|
196
191
|
return (
|
|
197
192
|
// The "TIMELINE" label is dropped for CapCut-like density — the pane's
|
|
198
193
|
// position (tracks right below) makes it self-evident.
|
|
199
194
|
<div className="border-b border-neutral-800/60">
|
|
200
195
|
<div className="flex items-center justify-between px-2 py-0.5">
|
|
201
196
|
<div className="flex items-center gap-0.5">
|
|
197
|
+
<TimelineHistoryButtons />
|
|
202
198
|
<Tooltip label="Selection tool (V)">
|
|
203
199
|
<button
|
|
204
200
|
type="button"
|