@hyperframes/studio 0.6.32 → 0.6.33
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-CSG9kRJg.js +138 -0
- package/dist/assets/index-SKRp8mGz.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/components/StudioRightPanel.tsx +46 -37
- package/src/components/TimelineToolbar.tsx +62 -55
- package/src/components/nle/NLEPreview.tsx +15 -0
- package/src/components/sidebar/BlocksTab.tsx +304 -29
- package/src/components/sidebar/LeftSidebar.tsx +47 -38
- package/src/components/ui/Tooltip.tsx +63 -0
- package/src/components/ui/index.ts +1 -0
- package/src/hooks/useBlockCatalog.ts +5 -1
- package/src/player/components/PlayerControls.tsx +253 -234
- package/src/player/lib/playbackAdapter.test.ts +3 -3
- package/src/player/lib/playbackAdapter.ts +3 -1
- package/src/utils/timelineAssetDrop.test.ts +2 -1
- package/dist/assets/index-C-pv1DOD.js +0 -120
- package/dist/assets/index-Cd3DF1je.css +0 -1
|
@@ -4,6 +4,7 @@ import { formatFrameTime, frameToSeconds, stepFrameTime, formatTime } from "../l
|
|
|
4
4
|
import { shouldMutePreviewAudio } from "../lib/timelineIframeHelpers";
|
|
5
5
|
import { usePlayerStore, liveTime } from "../store/playerStore";
|
|
6
6
|
import { trackStudioEvent } from "../../utils/studioTelemetry";
|
|
7
|
+
import { Tooltip } from "../../components/ui";
|
|
7
8
|
|
|
8
9
|
const SPEED_OPTIONS = [0.25, 0.5, 1, 1.5, 2] as const;
|
|
9
10
|
const SEEK_EDGE_SNAP_PX = 8;
|
|
@@ -340,46 +341,51 @@ export const PlayerControls = memo(function PlayerControls({
|
|
|
340
341
|
}}
|
|
341
342
|
>
|
|
342
343
|
{/* Play/Pause button */}
|
|
343
|
-
<
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
<
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
<
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
344
|
+
<Tooltip label={isPlaying ? "Pause" : "Play"}>
|
|
345
|
+
<button
|
|
346
|
+
type="button"
|
|
347
|
+
aria-label={isPlaying ? "Pause" : "Play"}
|
|
348
|
+
onClick={() => {
|
|
349
|
+
trackStudioEvent("playback", { action: isPlaying ? "pause" : "play" });
|
|
350
|
+
onTogglePlay();
|
|
351
|
+
}}
|
|
352
|
+
disabled={controlsDisabled}
|
|
353
|
+
className="flex-shrink-0 w-8 h-8 flex items-center justify-center rounded-lg disabled:opacity-30 disabled:pointer-events-none transition-colors"
|
|
354
|
+
style={{ background: "rgba(255,255,255,0.06)" }}
|
|
355
|
+
>
|
|
356
|
+
{isPlaying ? (
|
|
357
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="#FAFAFA" aria-hidden="true">
|
|
358
|
+
<rect x="6" y="4" width="4" height="16" rx="1" />
|
|
359
|
+
<rect x="14" y="4" width="4" height="16" rx="1" />
|
|
360
|
+
</svg>
|
|
361
|
+
) : (
|
|
362
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="#FAFAFA" aria-hidden="true">
|
|
363
|
+
<polygon points="6,3 20,12 6,21" />
|
|
364
|
+
</svg>
|
|
365
|
+
)}
|
|
366
|
+
</button>
|
|
367
|
+
</Tooltip>
|
|
365
368
|
|
|
366
369
|
{/* Time display — click to toggle time/frame mode */}
|
|
367
|
-
<
|
|
368
|
-
|
|
369
|
-
onClick={() => setTimeDisplayMode((m) => (m === "time" ? "frame" : "time"))}
|
|
370
|
-
disabled={disabled}
|
|
371
|
-
title={timeDisplayMode === "time" ? "Switch to frame display" : "Switch to time display"}
|
|
372
|
-
className="font-mono text-[11px] tabular-nums flex-shrink-0 w-[118px] text-left transition-colors disabled:pointer-events-none hover:opacity-80"
|
|
373
|
-
style={{ color: "#A1A1AA", cursor: "pointer" }}
|
|
370
|
+
<Tooltip
|
|
371
|
+
label={timeDisplayMode === "time" ? "Switch to frame display" : "Switch to time display"}
|
|
374
372
|
>
|
|
375
|
-
<
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
373
|
+
<button
|
|
374
|
+
type="button"
|
|
375
|
+
onClick={() => setTimeDisplayMode((m) => (m === "time" ? "frame" : "time"))}
|
|
376
|
+
disabled={disabled}
|
|
377
|
+
className="font-mono text-[11px] tabular-nums flex-shrink-0 w-[118px] text-left transition-colors disabled:pointer-events-none hover:opacity-80"
|
|
378
|
+
style={{ color: "#A1A1AA", cursor: "pointer" }}
|
|
379
|
+
>
|
|
380
|
+
<span ref={timeDisplayRef}>{formatTime(0)}</span>
|
|
381
|
+
{timeDisplayMode === "time" ? (
|
|
382
|
+
<>
|
|
383
|
+
<span style={{ color: "#3F3F46", margin: "0 2px" }}>/</span>
|
|
384
|
+
<span style={{ color: "#52525B" }}>{formatTime(duration)}</span>
|
|
385
|
+
</>
|
|
386
|
+
) : null}
|
|
387
|
+
</button>
|
|
388
|
+
</Tooltip>
|
|
383
389
|
|
|
384
390
|
{/* Seek bar — teal progress fill */}
|
|
385
391
|
<div
|
|
@@ -469,70 +475,73 @@ export const PlayerControls = memo(function PlayerControls({
|
|
|
469
475
|
</div>
|
|
470
476
|
|
|
471
477
|
{/* Mute toggle */}
|
|
472
|
-
<
|
|
473
|
-
type="button"
|
|
474
|
-
onClick={() => {
|
|
475
|
-
if (!audioAutoMuted) {
|
|
476
|
-
trackStudioEvent("playback", { action: "mute_toggle", muted: !audioMuted });
|
|
477
|
-
setAudioMuted(!audioMuted);
|
|
478
|
-
}
|
|
479
|
-
}}
|
|
480
|
-
disabled={controlsDisabled || audioAutoMuted}
|
|
481
|
-
title={muteButtonLabel}
|
|
482
|
-
aria-label={muteButtonLabel}
|
|
483
|
-
aria-pressed={effectiveAudioMuted}
|
|
484
|
-
className={`h-7 w-7 flex-shrink-0 flex items-center justify-center rounded-md border transition-colors disabled:pointer-events-none ${
|
|
485
|
-
effectiveAudioMuted
|
|
486
|
-
? "text-studio-accent bg-studio-accent/10 border-studio-accent/30"
|
|
487
|
-
: "border-neutral-700 text-neutral-400 hover:border-neutral-500 hover:bg-neutral-800"
|
|
488
|
-
} ${audioAutoMuted ? "opacity-70" : ""}`}
|
|
489
|
-
>
|
|
490
|
-
{effectiveAudioMuted ? (
|
|
491
|
-
<svg
|
|
492
|
-
width="13"
|
|
493
|
-
height="13"
|
|
494
|
-
viewBox="0 0 24 24"
|
|
495
|
-
fill="none"
|
|
496
|
-
stroke="currentColor"
|
|
497
|
-
strokeWidth="2"
|
|
498
|
-
strokeLinecap="round"
|
|
499
|
-
strokeLinejoin="round"
|
|
500
|
-
aria-hidden="true"
|
|
501
|
-
>
|
|
502
|
-
<path d="M11 5 6 9H3v6h3l5 4V5Z" />
|
|
503
|
-
<path d="m19 9-6 6" />
|
|
504
|
-
<path d="m13 9 6 6" />
|
|
505
|
-
</svg>
|
|
506
|
-
) : (
|
|
507
|
-
<svg
|
|
508
|
-
width="13"
|
|
509
|
-
height="13"
|
|
510
|
-
viewBox="0 0 24 24"
|
|
511
|
-
fill="none"
|
|
512
|
-
stroke="currentColor"
|
|
513
|
-
strokeWidth="2"
|
|
514
|
-
strokeLinecap="round"
|
|
515
|
-
strokeLinejoin="round"
|
|
516
|
-
aria-hidden="true"
|
|
517
|
-
>
|
|
518
|
-
<path d="M11 5 6 9H3v6h3l5 4V5Z" />
|
|
519
|
-
<path d="M15.5 8.5a5 5 0 0 1 0 7" />
|
|
520
|
-
<path d="M18.5 5.5a9 9 0 0 1 0 13" />
|
|
521
|
-
</svg>
|
|
522
|
-
)}
|
|
523
|
-
</button>
|
|
524
|
-
|
|
525
|
-
{/* Speed control */}
|
|
526
|
-
<div ref={speedMenuContainerRef} className="relative flex-shrink-0">
|
|
478
|
+
<Tooltip label={muteButtonLabel}>
|
|
527
479
|
<button
|
|
528
480
|
type="button"
|
|
529
|
-
onClick={() =>
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
481
|
+
onClick={() => {
|
|
482
|
+
if (!audioAutoMuted) {
|
|
483
|
+
trackStudioEvent("playback", { action: "mute_toggle", muted: !audioMuted });
|
|
484
|
+
setAudioMuted(!audioMuted);
|
|
485
|
+
}
|
|
486
|
+
}}
|
|
487
|
+
disabled={controlsDisabled || audioAutoMuted}
|
|
488
|
+
aria-label={muteButtonLabel}
|
|
489
|
+
aria-pressed={effectiveAudioMuted}
|
|
490
|
+
className={`h-7 w-7 flex-shrink-0 flex items-center justify-center rounded-md border transition-colors disabled:pointer-events-none ${
|
|
491
|
+
effectiveAudioMuted
|
|
492
|
+
? "text-studio-accent bg-studio-accent/10 border-studio-accent/30"
|
|
493
|
+
: "border-neutral-700 text-neutral-400 hover:border-neutral-500 hover:bg-neutral-800"
|
|
494
|
+
} ${audioAutoMuted ? "opacity-70" : ""}`}
|
|
533
495
|
>
|
|
534
|
-
{
|
|
496
|
+
{effectiveAudioMuted ? (
|
|
497
|
+
<svg
|
|
498
|
+
width="13"
|
|
499
|
+
height="13"
|
|
500
|
+
viewBox="0 0 24 24"
|
|
501
|
+
fill="none"
|
|
502
|
+
stroke="currentColor"
|
|
503
|
+
strokeWidth="2"
|
|
504
|
+
strokeLinecap="round"
|
|
505
|
+
strokeLinejoin="round"
|
|
506
|
+
aria-hidden="true"
|
|
507
|
+
>
|
|
508
|
+
<path d="M11 5 6 9H3v6h3l5 4V5Z" />
|
|
509
|
+
<path d="m19 9-6 6" />
|
|
510
|
+
<path d="m13 9 6 6" />
|
|
511
|
+
</svg>
|
|
512
|
+
) : (
|
|
513
|
+
<svg
|
|
514
|
+
width="13"
|
|
515
|
+
height="13"
|
|
516
|
+
viewBox="0 0 24 24"
|
|
517
|
+
fill="none"
|
|
518
|
+
stroke="currentColor"
|
|
519
|
+
strokeWidth="2"
|
|
520
|
+
strokeLinecap="round"
|
|
521
|
+
strokeLinejoin="round"
|
|
522
|
+
aria-hidden="true"
|
|
523
|
+
>
|
|
524
|
+
<path d="M11 5 6 9H3v6h3l5 4V5Z" />
|
|
525
|
+
<path d="M15.5 8.5a5 5 0 0 1 0 7" />
|
|
526
|
+
<path d="M18.5 5.5a9 9 0 0 1 0 13" />
|
|
527
|
+
</svg>
|
|
528
|
+
)}
|
|
535
529
|
</button>
|
|
530
|
+
</Tooltip>
|
|
531
|
+
|
|
532
|
+
{/* Speed control */}
|
|
533
|
+
<div ref={speedMenuContainerRef} className="relative flex-shrink-0">
|
|
534
|
+
<Tooltip label="Playback speed">
|
|
535
|
+
<button
|
|
536
|
+
type="button"
|
|
537
|
+
onClick={() => setShowSpeedMenu((v) => !v)}
|
|
538
|
+
disabled={disabled}
|
|
539
|
+
className="w-10 px-2 py-1 rounded-md text-[10px] font-mono tabular-nums transition-colors"
|
|
540
|
+
style={{ color: "#71717A", background: "rgba(255,255,255,0.04)" }}
|
|
541
|
+
>
|
|
542
|
+
{playbackRate === 1 ? "1x" : `${playbackRate}x`}
|
|
543
|
+
</button>
|
|
544
|
+
</Tooltip>
|
|
536
545
|
{showSpeedMenu && (
|
|
537
546
|
<div
|
|
538
547
|
className="absolute bottom-full right-0 mb-1.5 rounded-lg shadow-xl z-50 min-w-[56px] overflow-hidden"
|
|
@@ -566,122 +575,126 @@ export const PlayerControls = memo(function PlayerControls({
|
|
|
566
575
|
)}
|
|
567
576
|
</div>
|
|
568
577
|
|
|
569
|
-
<
|
|
570
|
-
type="button"
|
|
571
|
-
onClick={() => {
|
|
572
|
-
trackStudioEvent("playback", { action: "loop_toggle", enabled: !loopEnabled });
|
|
573
|
-
setLoopEnabled(!loopEnabled);
|
|
574
|
-
}}
|
|
575
|
-
disabled={disabled}
|
|
576
|
-
className={`h-7 w-7 flex items-center justify-center rounded-md border transition-colors ${
|
|
577
|
-
loopEnabled
|
|
578
|
-
? "text-studio-accent bg-studio-accent/10 border-studio-accent/30"
|
|
579
|
-
: "border-neutral-700 text-neutral-400 hover:border-neutral-500 hover:bg-neutral-800"
|
|
580
|
-
}`}
|
|
581
|
-
title="Loop playback"
|
|
582
|
-
aria-label={loopEnabled ? "Disable loop playback" : "Enable loop playback"}
|
|
583
|
-
aria-pressed={loopEnabled}
|
|
584
|
-
>
|
|
585
|
-
<svg
|
|
586
|
-
width="13"
|
|
587
|
-
height="13"
|
|
588
|
-
viewBox="0 0 24 24"
|
|
589
|
-
fill="none"
|
|
590
|
-
stroke="currentColor"
|
|
591
|
-
strokeWidth="2"
|
|
592
|
-
strokeLinecap="round"
|
|
593
|
-
strokeLinejoin="round"
|
|
594
|
-
aria-hidden="true"
|
|
595
|
-
>
|
|
596
|
-
<path d="M17 2l4 4-4 4" />
|
|
597
|
-
<path d="M3 11V9a4 4 0 0 1 4-4h14" />
|
|
598
|
-
<path d="M7 22l-4-4 4-4" />
|
|
599
|
-
<path d="M21 13v2a4 4 0 0 1-4 4H3" />
|
|
600
|
-
</svg>
|
|
601
|
-
</button>
|
|
602
|
-
|
|
603
|
-
{/* Fullscreen toggle */}
|
|
604
|
-
{onToggleFullscreen && (
|
|
578
|
+
<Tooltip label="Loop playback">
|
|
605
579
|
<button
|
|
606
580
|
type="button"
|
|
607
581
|
onClick={() => {
|
|
608
|
-
trackStudioEvent("playback", { action: "
|
|
609
|
-
|
|
582
|
+
trackStudioEvent("playback", { action: "loop_toggle", enabled: !loopEnabled });
|
|
583
|
+
setLoopEnabled(!loopEnabled);
|
|
610
584
|
}}
|
|
611
|
-
|
|
612
|
-
|
|
585
|
+
disabled={disabled}
|
|
586
|
+
className={`h-7 w-7 flex items-center justify-center rounded-md border transition-colors ${
|
|
587
|
+
loopEnabled
|
|
613
588
|
? "text-studio-accent bg-studio-accent/10 border-studio-accent/30"
|
|
614
589
|
: "border-neutral-700 text-neutral-400 hover:border-neutral-500 hover:bg-neutral-800"
|
|
615
590
|
}`}
|
|
616
|
-
|
|
617
|
-
aria-
|
|
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
|
-
|
|
657
|
-
{/* Keyboard shortcuts + frame jump + work area — click to open panel */}
|
|
658
|
-
<div ref={shortcutsPanelRef} className="relative flex-shrink-0">
|
|
659
|
-
<button
|
|
660
|
-
type="button"
|
|
661
|
-
onClick={() => setShowShortcuts((v) => !v)}
|
|
662
|
-
className={`w-6 h-6 flex items-center justify-center rounded border transition-colors ${
|
|
663
|
-
showShortcuts
|
|
664
|
-
? "border-neutral-600 text-neutral-200 bg-neutral-800"
|
|
665
|
-
: "border-neutral-800 text-neutral-600 hover:text-neutral-300 hover:border-neutral-600"
|
|
666
|
-
}`}
|
|
667
|
-
aria-label="Shortcuts and tools"
|
|
668
|
-
aria-expanded={showShortcuts}
|
|
591
|
+
aria-label={loopEnabled ? "Disable loop playback" : "Enable loop playback"}
|
|
592
|
+
aria-pressed={loopEnabled}
|
|
669
593
|
>
|
|
670
594
|
<svg
|
|
671
|
-
width="
|
|
672
|
-
height="
|
|
595
|
+
width="13"
|
|
596
|
+
height="13"
|
|
673
597
|
viewBox="0 0 24 24"
|
|
674
598
|
fill="none"
|
|
675
599
|
stroke="currentColor"
|
|
676
|
-
strokeWidth="
|
|
600
|
+
strokeWidth="2"
|
|
677
601
|
strokeLinecap="round"
|
|
678
602
|
strokeLinejoin="round"
|
|
679
603
|
aria-hidden="true"
|
|
680
604
|
>
|
|
681
|
-
<
|
|
682
|
-
<path d="
|
|
605
|
+
<path d="M17 2l4 4-4 4" />
|
|
606
|
+
<path d="M3 11V9a4 4 0 0 1 4-4h14" />
|
|
607
|
+
<path d="M7 22l-4-4 4-4" />
|
|
608
|
+
<path d="M21 13v2a4 4 0 0 1-4 4H3" />
|
|
683
609
|
</svg>
|
|
684
610
|
</button>
|
|
611
|
+
</Tooltip>
|
|
612
|
+
|
|
613
|
+
{/* Fullscreen toggle */}
|
|
614
|
+
{onToggleFullscreen && (
|
|
615
|
+
<Tooltip label={isFullscreen ? "Exit fullscreen (F)" : "Enter fullscreen (F)"}>
|
|
616
|
+
<button
|
|
617
|
+
type="button"
|
|
618
|
+
onClick={() => {
|
|
619
|
+
trackStudioEvent("playback", { action: "fullscreen_toggle", active: !isFullscreen });
|
|
620
|
+
onToggleFullscreen();
|
|
621
|
+
}}
|
|
622
|
+
className={`h-7 w-7 flex-shrink-0 flex items-center justify-center rounded-md border transition-colors ${
|
|
623
|
+
isFullscreen
|
|
624
|
+
? "text-studio-accent bg-studio-accent/10 border-studio-accent/30"
|
|
625
|
+
: "border-neutral-700 text-neutral-400 hover:border-neutral-500 hover:bg-neutral-800"
|
|
626
|
+
}`}
|
|
627
|
+
aria-label={isFullscreen ? "Exit fullscreen" : "Enter fullscreen"}
|
|
628
|
+
>
|
|
629
|
+
{isFullscreen ? (
|
|
630
|
+
<svg
|
|
631
|
+
width="13"
|
|
632
|
+
height="13"
|
|
633
|
+
viewBox="0 0 24 24"
|
|
634
|
+
fill="none"
|
|
635
|
+
stroke="currentColor"
|
|
636
|
+
strokeWidth="2"
|
|
637
|
+
strokeLinecap="round"
|
|
638
|
+
strokeLinejoin="round"
|
|
639
|
+
aria-hidden="true"
|
|
640
|
+
>
|
|
641
|
+
<path d="M8 3v3a2 2 0 0 1-2 2H3" />
|
|
642
|
+
<path d="M21 8h-3a2 2 0 0 1-2-2V3" />
|
|
643
|
+
<path d="M3 16h3a2 2 0 0 1 2 2v3" />
|
|
644
|
+
<path d="M16 21v-3a2 2 0 0 1 2-2h3" />
|
|
645
|
+
</svg>
|
|
646
|
+
) : (
|
|
647
|
+
<svg
|
|
648
|
+
width="13"
|
|
649
|
+
height="13"
|
|
650
|
+
viewBox="0 0 24 24"
|
|
651
|
+
fill="none"
|
|
652
|
+
stroke="currentColor"
|
|
653
|
+
strokeWidth="2"
|
|
654
|
+
strokeLinecap="round"
|
|
655
|
+
strokeLinejoin="round"
|
|
656
|
+
aria-hidden="true"
|
|
657
|
+
>
|
|
658
|
+
<path d="M8 3H5a2 2 0 0 0-2 2v3" />
|
|
659
|
+
<path d="M21 8V5a2 2 0 0 0-2-2h-3" />
|
|
660
|
+
<path d="M3 16v3a2 2 0 0 0 2 2h3" />
|
|
661
|
+
<path d="M16 21h3a2 2 0 0 0 2-2v-3" />
|
|
662
|
+
</svg>
|
|
663
|
+
)}
|
|
664
|
+
</button>
|
|
665
|
+
</Tooltip>
|
|
666
|
+
)}
|
|
667
|
+
|
|
668
|
+
{/* Keyboard shortcuts + frame jump + work area — click to open panel */}
|
|
669
|
+
<div ref={shortcutsPanelRef} className="relative flex-shrink-0">
|
|
670
|
+
<Tooltip label="Shortcuts and tools">
|
|
671
|
+
<button
|
|
672
|
+
type="button"
|
|
673
|
+
onClick={() => setShowShortcuts((v) => !v)}
|
|
674
|
+
className={`w-6 h-6 flex items-center justify-center rounded border transition-colors ${
|
|
675
|
+
showShortcuts
|
|
676
|
+
? "border-neutral-600 text-neutral-200 bg-neutral-800"
|
|
677
|
+
: "border-neutral-800 text-neutral-600 hover:text-neutral-300 hover:border-neutral-600"
|
|
678
|
+
}`}
|
|
679
|
+
aria-label="Shortcuts and tools"
|
|
680
|
+
aria-expanded={showShortcuts}
|
|
681
|
+
>
|
|
682
|
+
<svg
|
|
683
|
+
width="11"
|
|
684
|
+
height="11"
|
|
685
|
+
viewBox="0 0 24 24"
|
|
686
|
+
fill="none"
|
|
687
|
+
stroke="currentColor"
|
|
688
|
+
strokeWidth="1.75"
|
|
689
|
+
strokeLinecap="round"
|
|
690
|
+
strokeLinejoin="round"
|
|
691
|
+
aria-hidden="true"
|
|
692
|
+
>
|
|
693
|
+
<rect x="2" y="4" width="20" height="16" rx="2" />
|
|
694
|
+
<path d="M6 8h.01M10 8h.01M14 8h.01M18 8h.01M6 12h.01M10 12h.01M14 12h.01M18 12h.01M8 16h8" />
|
|
695
|
+
</svg>
|
|
696
|
+
</button>
|
|
697
|
+
</Tooltip>
|
|
685
698
|
{showShortcuts && (
|
|
686
699
|
<div
|
|
687
700
|
className="absolute bottom-full right-0 mb-2 z-50 rounded-lg shadow-xl min-w-[220px] overflow-y-auto"
|
|
@@ -709,13 +722,15 @@ export const PlayerControls = memo(function PlayerControls({
|
|
|
709
722
|
onKeyDown={handleJumpKeyDown}
|
|
710
723
|
onBlur={commitJumpFrame}
|
|
711
724
|
/>
|
|
712
|
-
<
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
725
|
+
<Tooltip label="Jump to frame">
|
|
726
|
+
<button
|
|
727
|
+
type="submit"
|
|
728
|
+
disabled={disabled}
|
|
729
|
+
className="h-6 px-2 rounded border border-neutral-700 text-[10px] text-neutral-300 transition-colors hover:border-neutral-500 hover:bg-neutral-800 disabled:opacity-40"
|
|
730
|
+
>
|
|
731
|
+
Go
|
|
732
|
+
</button>
|
|
733
|
+
</Tooltip>
|
|
719
734
|
</form>
|
|
720
735
|
</div>
|
|
721
736
|
<div style={{ borderTop: "1px solid rgba(255,255,255,0.06)" }} />
|
|
@@ -741,23 +756,25 @@ export const PlayerControls = memo(function PlayerControls({
|
|
|
741
756
|
<span className="font-mono text-[10px] text-neutral-300">
|
|
742
757
|
{formatTime(inPoint)}
|
|
743
758
|
</span>
|
|
744
|
-
<
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
<svg
|
|
751
|
-
width="8"
|
|
752
|
-
height="8"
|
|
753
|
-
viewBox="0 0 24 24"
|
|
754
|
-
fill="none"
|
|
755
|
-
stroke="currentColor"
|
|
756
|
-
strokeWidth="2.5"
|
|
759
|
+
<Tooltip label="Clear in-point">
|
|
760
|
+
<button
|
|
761
|
+
type="button"
|
|
762
|
+
onClick={() => setInPoint(null)}
|
|
763
|
+
className="w-4 h-4 flex items-center justify-center rounded text-neutral-500 hover:text-neutral-200 transition-colors"
|
|
764
|
+
aria-label="Clear in-point"
|
|
757
765
|
>
|
|
758
|
-
<
|
|
759
|
-
|
|
760
|
-
|
|
766
|
+
<svg
|
|
767
|
+
width="8"
|
|
768
|
+
height="8"
|
|
769
|
+
viewBox="0 0 24 24"
|
|
770
|
+
fill="none"
|
|
771
|
+
stroke="currentColor"
|
|
772
|
+
strokeWidth="2.5"
|
|
773
|
+
>
|
|
774
|
+
<path d="M18 6L6 18M6 6l12 12" />
|
|
775
|
+
</svg>
|
|
776
|
+
</button>
|
|
777
|
+
</Tooltip>
|
|
761
778
|
</>
|
|
762
779
|
) : (
|
|
763
780
|
<span className="text-[10px] text-neutral-600">—</span>
|
|
@@ -780,23 +797,25 @@ export const PlayerControls = memo(function PlayerControls({
|
|
|
780
797
|
<span className="font-mono text-[10px] text-neutral-300">
|
|
781
798
|
{formatTime(outPoint)}
|
|
782
799
|
</span>
|
|
783
|
-
<
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
<svg
|
|
790
|
-
width="8"
|
|
791
|
-
height="8"
|
|
792
|
-
viewBox="0 0 24 24"
|
|
793
|
-
fill="none"
|
|
794
|
-
stroke="currentColor"
|
|
795
|
-
strokeWidth="2.5"
|
|
800
|
+
<Tooltip label="Clear out-point">
|
|
801
|
+
<button
|
|
802
|
+
type="button"
|
|
803
|
+
onClick={() => setOutPoint(null)}
|
|
804
|
+
className="w-4 h-4 flex items-center justify-center rounded text-neutral-500 hover:text-neutral-200 transition-colors"
|
|
805
|
+
aria-label="Clear out-point"
|
|
796
806
|
>
|
|
797
|
-
<
|
|
798
|
-
|
|
799
|
-
|
|
807
|
+
<svg
|
|
808
|
+
width="8"
|
|
809
|
+
height="8"
|
|
810
|
+
viewBox="0 0 24 24"
|
|
811
|
+
fill="none"
|
|
812
|
+
stroke="currentColor"
|
|
813
|
+
strokeWidth="2.5"
|
|
814
|
+
>
|
|
815
|
+
<path d="M18 6L6 18M6 6l12 12" />
|
|
816
|
+
</svg>
|
|
817
|
+
</button>
|
|
818
|
+
</Tooltip>
|
|
800
819
|
</>
|
|
801
820
|
) : (
|
|
802
821
|
<span className="text-[10px] text-neutral-600">—</span>
|
|
@@ -18,13 +18,13 @@ describe("wrapTimeline seek keepPlaying option (#834)", () => {
|
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
it("default seek pauses the GSAP timeline before seeking", () => {
|
|
21
|
+
it("default seek pauses the GSAP timeline before and after seeking", () => {
|
|
22
22
|
const tl = mockTimeline();
|
|
23
23
|
const adapter = wrapTimeline(tl);
|
|
24
24
|
|
|
25
25
|
adapter.seek(5);
|
|
26
26
|
|
|
27
|
-
expect(tl.pause).toHaveBeenCalledTimes(
|
|
27
|
+
expect(tl.pause).toHaveBeenCalledTimes(2);
|
|
28
28
|
expect(tl.seek).toHaveBeenCalledWith(5);
|
|
29
29
|
});
|
|
30
30
|
|
|
@@ -44,7 +44,7 @@ describe("wrapTimeline seek keepPlaying option (#834)", () => {
|
|
|
44
44
|
|
|
45
45
|
adapter.seek(5, { keepPlaying: false });
|
|
46
46
|
|
|
47
|
-
expect(tl.pause).toHaveBeenCalledTimes(
|
|
47
|
+
expect(tl.pause).toHaveBeenCalledTimes(2);
|
|
48
48
|
expect(tl.seek).toHaveBeenCalledWith(5);
|
|
49
49
|
});
|
|
50
50
|
});
|
|
@@ -135,8 +135,10 @@ export function wrapTimeline(tl: TimelineLike): PlaybackAdapter {
|
|
|
135
135
|
play: () => tl.play(),
|
|
136
136
|
pause: () => tl.pause(),
|
|
137
137
|
seek: (t, options) => {
|
|
138
|
-
|
|
138
|
+
const shouldPause = !options?.keepPlaying;
|
|
139
|
+
if (shouldPause) tl.pause();
|
|
139
140
|
tl.seek(t);
|
|
141
|
+
if (shouldPause) tl.pause();
|
|
140
142
|
},
|
|
141
143
|
getTime: () => tl.time(),
|
|
142
144
|
getDuration: () => tl.duration(),
|
|
@@ -155,6 +155,7 @@ describe("insertTimelineAssetIntoSource", () => {
|
|
|
155
155
|
'<img id="photo_asset" data-start="0" data-duration="3" />',
|
|
156
156
|
);
|
|
157
157
|
|
|
158
|
-
expect(html).toContain('
|
|
158
|
+
expect(html).toContain('data-composition-id="main">');
|
|
159
|
+
expect(html).toContain('<img id="photo_asset"');
|
|
159
160
|
});
|
|
160
161
|
});
|