@hyperframes/studio 0.6.30 → 0.6.31
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-D790O3az.js → index-Do0kAMcy.js} +37 -37
- package/dist/index.html +1 -1
- package/package.json +4 -4
- package/src/components/StudioErrorBoundary.tsx +2 -1
- package/src/components/nle/NLELayout.tsx +41 -6
- package/src/hooks/useAppHotkeys.ts +18 -0
- package/src/main.tsx +20 -6
- package/src/player/components/PlayerControls.tsx +59 -0
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-Do0kAMcy.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/assets/index-BWBj8I6Q.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.31",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
"@codemirror/view": "6.40.0",
|
|
32
32
|
"@phosphor-icons/react": "^2.1.10",
|
|
33
33
|
"mediabunny": "^1.45.3",
|
|
34
|
-
"@hyperframes/
|
|
35
|
-
"@hyperframes/
|
|
34
|
+
"@hyperframes/player": "0.6.31",
|
|
35
|
+
"@hyperframes/core": "0.6.31"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/react": "19",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"vite": "^6.4.2",
|
|
47
47
|
"vitest": "^3.2.4",
|
|
48
48
|
"zustand": "^5.0.0",
|
|
49
|
-
"@hyperframes/producer": "0.6.
|
|
49
|
+
"@hyperframes/producer": "0.6.31"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
52
|
"react": "19",
|
|
@@ -21,7 +21,8 @@ export class StudioErrorBoundary extends Component<Props, State> {
|
|
|
21
21
|
trackStudioEvent("crash", {
|
|
22
22
|
error_message: error.message,
|
|
23
23
|
error_name: error.name,
|
|
24
|
-
|
|
24
|
+
stack_trace: error.stack?.slice(0, 4000) ?? null,
|
|
25
|
+
component_stack: info.componentStack?.slice(0, 2000) ?? null,
|
|
25
26
|
});
|
|
26
27
|
}
|
|
27
28
|
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
useState,
|
|
3
|
+
useCallback,
|
|
4
|
+
useRef,
|
|
5
|
+
useEffect,
|
|
6
|
+
useSyncExternalStore,
|
|
7
|
+
memo,
|
|
8
|
+
type ReactNode,
|
|
9
|
+
} from "react";
|
|
2
10
|
import { useMountEffect } from "../../hooks/useMountEffect";
|
|
3
11
|
import { useTimelinePlayer, PlayerControls, Timeline, usePlayerStore } from "../../player";
|
|
4
12
|
import type { TimelineElement } from "../../player";
|
|
@@ -71,6 +79,15 @@ const MIN_TIMELINE_H = 100;
|
|
|
71
79
|
const DEFAULT_TIMELINE_H = 220;
|
|
72
80
|
const MIN_PREVIEW_H = 120;
|
|
73
81
|
|
|
82
|
+
function subscribeFullscreen(cb: () => void) {
|
|
83
|
+
document.addEventListener("fullscreenchange", cb);
|
|
84
|
+
return () => document.removeEventListener("fullscreenchange", cb);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function getFullscreenElement() {
|
|
88
|
+
return document.fullscreenElement;
|
|
89
|
+
}
|
|
90
|
+
|
|
74
91
|
export function shouldDisableTimelineWhileCompositionLoading(compositionLoading: boolean): boolean {
|
|
75
92
|
return compositionLoading;
|
|
76
93
|
}
|
|
@@ -248,9 +265,20 @@ export const NLELayout = memo(function NLELayout({
|
|
|
248
265
|
onCompositionLoadingChangeParent?.(compositionLoading);
|
|
249
266
|
}, [compositionLoading, onCompositionLoadingChangeParent]);
|
|
250
267
|
|
|
268
|
+
const fullscreenElement = useSyncExternalStore(subscribeFullscreen, getFullscreenElement);
|
|
251
269
|
const isTimelineVisible = timelineVisible ?? true;
|
|
252
270
|
const isDragging = useRef(false);
|
|
253
271
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
272
|
+
const isFullscreen = fullscreenElement === containerRef.current && fullscreenElement != null;
|
|
273
|
+
|
|
274
|
+
const toggleFullscreen = useCallback(() => {
|
|
275
|
+
if (!containerRef.current) return;
|
|
276
|
+
if (document.fullscreenElement) {
|
|
277
|
+
void document.exitFullscreen();
|
|
278
|
+
} else {
|
|
279
|
+
void containerRef.current.requestFullscreen();
|
|
280
|
+
}
|
|
281
|
+
}, []);
|
|
254
282
|
|
|
255
283
|
const currentLevel = compositionStack[compositionStack.length - 1];
|
|
256
284
|
const directUrl = compositionStack.length > 1 ? currentLevel.previewUrl : undefined;
|
|
@@ -312,6 +340,7 @@ export const NLELayout = memo(function NLELayout({
|
|
|
312
340
|
className="flex flex-col h-full min-h-0 bg-neutral-950"
|
|
313
341
|
onKeyDown={handleKeyDown}
|
|
314
342
|
tabIndex={-1}
|
|
343
|
+
data-studio-fullscreen-target=""
|
|
315
344
|
>
|
|
316
345
|
{/* Preview + player controls */}
|
|
317
346
|
<div className="flex-1 min-h-0 flex flex-col">
|
|
@@ -326,20 +355,26 @@ export const NLELayout = memo(function NLELayout({
|
|
|
326
355
|
refreshKey={refreshKey}
|
|
327
356
|
suppressLoadingOverlay={hasLoadedOnceRef.current}
|
|
328
357
|
/>
|
|
329
|
-
{previewOverlay}
|
|
358
|
+
{!isFullscreen && previewOverlay}
|
|
330
359
|
</div>
|
|
331
360
|
<div className="bg-neutral-950 border-t border-neutral-800/50 flex-shrink-0">
|
|
332
|
-
{compositionStack.length > 1 && (
|
|
361
|
+
{!isFullscreen && compositionStack.length > 1 && (
|
|
333
362
|
<CompositionBreadcrumb
|
|
334
363
|
stack={compositionStack}
|
|
335
364
|
onNavigate={handleNavigateComposition}
|
|
336
365
|
/>
|
|
337
366
|
)}
|
|
338
|
-
<PlayerControls
|
|
367
|
+
<PlayerControls
|
|
368
|
+
onTogglePlay={togglePlay}
|
|
369
|
+
onSeek={seek}
|
|
370
|
+
disabled={timelineDisabled}
|
|
371
|
+
isFullscreen={isFullscreen}
|
|
372
|
+
onToggleFullscreen={toggleFullscreen}
|
|
373
|
+
/>
|
|
339
374
|
</div>
|
|
340
375
|
</div>
|
|
341
376
|
|
|
342
|
-
{isTimelineVisible ? (
|
|
377
|
+
{!isFullscreen && isTimelineVisible ? (
|
|
343
378
|
<>
|
|
344
379
|
{/* Resize divider */}
|
|
345
380
|
<div
|
|
@@ -396,7 +431,7 @@ export const NLELayout = memo(function NLELayout({
|
|
|
396
431
|
)}
|
|
397
432
|
</div>
|
|
398
433
|
</>
|
|
399
|
-
) : onToggleTimeline ? (
|
|
434
|
+
) : !isFullscreen && onToggleTimeline ? (
|
|
400
435
|
<div className="flex-shrink-0 border-t border-neutral-800/50 bg-neutral-950/96">
|
|
401
436
|
<div className="flex h-10 items-center justify-between px-3">
|
|
402
437
|
<div className="text-[10px] font-medium uppercase tracking-[0.16em] text-neutral-500">
|
|
@@ -248,6 +248,24 @@ export function useAppHotkeys({
|
|
|
248
248
|
}
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
+
// F — toggle fullscreen preview
|
|
252
|
+
if (
|
|
253
|
+
event.key.toLowerCase() === "f" &&
|
|
254
|
+
!event.metaKey &&
|
|
255
|
+
!event.ctrlKey &&
|
|
256
|
+
!event.altKey &&
|
|
257
|
+
!event.shiftKey &&
|
|
258
|
+
!isEditableTarget(event.target)
|
|
259
|
+
) {
|
|
260
|
+
event.preventDefault();
|
|
261
|
+
if (document.fullscreenElement) {
|
|
262
|
+
void document.exitFullscreen();
|
|
263
|
+
} else {
|
|
264
|
+
document.querySelector<HTMLElement>("[data-studio-fullscreen-target]")?.requestFullscreen();
|
|
265
|
+
}
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
251
269
|
// Delete / Backspace — remove selected element (timeline clip or preview selection)
|
|
252
270
|
if (
|
|
253
271
|
(event.key === "Delete" || event.key === "Backspace") &&
|
package/src/main.tsx
CHANGED
|
@@ -7,19 +7,33 @@ import "./styles/studio.css";
|
|
|
7
7
|
|
|
8
8
|
trackStudioEvent("session_start");
|
|
9
9
|
|
|
10
|
+
function errorProps(value: unknown): {
|
|
11
|
+
error_message: string;
|
|
12
|
+
error_name: string | null;
|
|
13
|
+
stack_trace: string | null;
|
|
14
|
+
} {
|
|
15
|
+
if (value instanceof Error) {
|
|
16
|
+
return {
|
|
17
|
+
error_message: value.message,
|
|
18
|
+
error_name: value.name,
|
|
19
|
+
stack_trace: value.stack?.slice(0, 4000) ?? null,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return { error_message: String(value), error_name: null, stack_trace: null };
|
|
23
|
+
}
|
|
24
|
+
|
|
10
25
|
window.addEventListener("error", (event) => {
|
|
11
26
|
trackStudioEvent("unhandled_error", {
|
|
27
|
+
...errorProps(event.error),
|
|
12
28
|
error_message: event.message,
|
|
13
|
-
filename: event.filename
|
|
14
|
-
lineno: event.lineno
|
|
15
|
-
colno: event.colno
|
|
29
|
+
filename: event.filename,
|
|
30
|
+
lineno: event.lineno,
|
|
31
|
+
colno: event.colno,
|
|
16
32
|
});
|
|
17
33
|
});
|
|
18
34
|
|
|
19
35
|
window.addEventListener("unhandledrejection", (event) => {
|
|
20
|
-
trackStudioEvent("unhandled_promise_rejection",
|
|
21
|
-
error_message: event.reason instanceof Error ? event.reason.message : String(event.reason),
|
|
22
|
-
});
|
|
36
|
+
trackStudioEvent("unhandled_promise_rejection", errorProps(event.reason));
|
|
23
37
|
});
|
|
24
38
|
|
|
25
39
|
createRoot(document.getElementById("root")!).render(
|
|
@@ -20,6 +20,7 @@ const SHORTCUT_SECTIONS = [
|
|
|
20
20
|
{ key: "⇧L", label: "Toggle loop" },
|
|
21
21
|
{ key: "←/→", label: "Step 1 frame" },
|
|
22
22
|
{ key: "⇧←/⇧→", label: "Step 10 frames" },
|
|
23
|
+
{ key: "F", label: "Toggle fullscreen" },
|
|
23
24
|
],
|
|
24
25
|
},
|
|
25
26
|
{
|
|
@@ -49,12 +50,16 @@ interface PlayerControlsProps {
|
|
|
49
50
|
onTogglePlay: () => void;
|
|
50
51
|
onSeek: (time: number) => void;
|
|
51
52
|
disabled?: boolean;
|
|
53
|
+
isFullscreen?: boolean;
|
|
54
|
+
onToggleFullscreen?: () => void;
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
export const PlayerControls = memo(function PlayerControls({
|
|
55
58
|
onTogglePlay,
|
|
56
59
|
onSeek,
|
|
57
60
|
disabled = false,
|
|
61
|
+
isFullscreen = false,
|
|
62
|
+
onToggleFullscreen,
|
|
58
63
|
}: PlayerControlsProps) {
|
|
59
64
|
// Subscribe to only the fields we render — each selector prevents cascading re-renders
|
|
60
65
|
const isPlaying = usePlayerStore((s) => s.isPlaying);
|
|
@@ -595,6 +600,60 @@ export const PlayerControls = memo(function PlayerControls({
|
|
|
595
600
|
</svg>
|
|
596
601
|
</button>
|
|
597
602
|
|
|
603
|
+
{/* Fullscreen toggle */}
|
|
604
|
+
{onToggleFullscreen && (
|
|
605
|
+
<button
|
|
606
|
+
type="button"
|
|
607
|
+
onClick={() => {
|
|
608
|
+
trackStudioEvent("playback", { action: "fullscreen_toggle", active: !isFullscreen });
|
|
609
|
+
onToggleFullscreen();
|
|
610
|
+
}}
|
|
611
|
+
className={`h-7 w-7 flex-shrink-0 flex items-center justify-center rounded-md border transition-colors ${
|
|
612
|
+
isFullscreen
|
|
613
|
+
? "text-studio-accent bg-studio-accent/10 border-studio-accent/30"
|
|
614
|
+
: "border-neutral-700 text-neutral-400 hover:border-neutral-500 hover:bg-neutral-800"
|
|
615
|
+
}`}
|
|
616
|
+
title={isFullscreen ? "Exit fullscreen (F)" : "Enter fullscreen (F)"}
|
|
617
|
+
aria-label={isFullscreen ? "Exit fullscreen" : "Enter fullscreen"}
|
|
618
|
+
>
|
|
619
|
+
{isFullscreen ? (
|
|
620
|
+
<svg
|
|
621
|
+
width="13"
|
|
622
|
+
height="13"
|
|
623
|
+
viewBox="0 0 24 24"
|
|
624
|
+
fill="none"
|
|
625
|
+
stroke="currentColor"
|
|
626
|
+
strokeWidth="2"
|
|
627
|
+
strokeLinecap="round"
|
|
628
|
+
strokeLinejoin="round"
|
|
629
|
+
aria-hidden="true"
|
|
630
|
+
>
|
|
631
|
+
<path d="M8 3v3a2 2 0 0 1-2 2H3" />
|
|
632
|
+
<path d="M21 8h-3a2 2 0 0 1-2-2V3" />
|
|
633
|
+
<path d="M3 16h3a2 2 0 0 1 2 2v3" />
|
|
634
|
+
<path d="M16 21v-3a2 2 0 0 1 2-2h3" />
|
|
635
|
+
</svg>
|
|
636
|
+
) : (
|
|
637
|
+
<svg
|
|
638
|
+
width="13"
|
|
639
|
+
height="13"
|
|
640
|
+
viewBox="0 0 24 24"
|
|
641
|
+
fill="none"
|
|
642
|
+
stroke="currentColor"
|
|
643
|
+
strokeWidth="2"
|
|
644
|
+
strokeLinecap="round"
|
|
645
|
+
strokeLinejoin="round"
|
|
646
|
+
aria-hidden="true"
|
|
647
|
+
>
|
|
648
|
+
<path d="M8 3H5a2 2 0 0 0-2 2v3" />
|
|
649
|
+
<path d="M21 8V5a2 2 0 0 0-2-2h-3" />
|
|
650
|
+
<path d="M3 16v3a2 2 0 0 0 2 2h3" />
|
|
651
|
+
<path d="M16 21h3a2 2 0 0 0 2-2v-3" />
|
|
652
|
+
</svg>
|
|
653
|
+
)}
|
|
654
|
+
</button>
|
|
655
|
+
)}
|
|
656
|
+
|
|
598
657
|
{/* Keyboard shortcuts + frame jump + work area — click to open panel */}
|
|
599
658
|
<div ref={shortcutsPanelRef} className="relative flex-shrink-0">
|
|
600
659
|
<button
|