@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/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-D790O3az.js"></script>
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.30",
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/core": "0.6.30",
35
- "@hyperframes/player": "0.6.30"
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.30"
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
- component_stack: info.componentStack?.slice(0, 500) ?? null,
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 { useState, useCallback, useRef, useEffect, memo, type ReactNode } from "react";
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 onTogglePlay={togglePlay} onSeek={seek} disabled={timelineDisabled} />
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 ?? null,
14
- lineno: event.lineno ?? null,
15
- colno: event.colno ?? null,
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