refactor(studio): split oversized files and raise line limit to 600

Split PlayerControls.tsx into focused sub-components (SeekBar,
WorkAreaOverlay, MuteButton, LoopButton, FullscreenButton,
ShortcutsPanel, SpeedMenu) and extracted seek bar drag/progress
tracking into useSeekBarDrag hook.

Split manualEditsDom.ts patch-builder functions into
manualEditsDomPatches.ts with data-driven helpers to reduce
duplication and complexity.

Extracted per-type reapply helpers from reapplyPositionEditsAfterSeek
and factored out identity-matrix check from
stripGsapTranslateFromTransform.

Raised file-size limit from 500 to 600 lines, removed
.filesize-allowlist.
This commit is contained in:
Miguel Ángel
2026-05-22 22:57:12 -04:00
parent 06e4db8e7b
commit ea4d920589
10 changed files with 1167 additions and 1104 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,277 @@
import { useState, useCallback, useRef, useEffect, memo } from "react";
import { formatTime, frameToSeconds } from "../lib/time";
import { Tooltip } from "../../components/ui";
const SHORTCUT_SECTIONS = [
{
title: "Playback",
hints: [
{ key: "Space", label: "Play / Pause" },
{ key: "J", label: "Play backward" },
{ key: "K", label: "Stop" },
{ key: "L", label: "Play forward" },
{ key: "M", label: "Toggle mute" },
{ key: "⇧L", label: "Toggle loop" },
{ key: "←/→", label: "Step 1 frame" },
{ key: "⇧←/⇧→", label: "Step 10 frames" },
{ key: "F", label: "Toggle fullscreen" },
],
},
{
title: "Work area",
hints: [
{ key: "I", label: "Set in-point" },
{ key: "⇧I", label: "Clear in-point" },
{ key: "O", label: "Set out-point" },
{ key: "⇧O", label: "Clear out-point" },
{ key: "A", label: "Jump to in-point" },
{ key: "E", label: "Jump to out-point" },
],
},
] as const;
interface ShortcutsPanelProps {
disabled: boolean;
duration: number;
inPoint: number | null;
outPoint: number | null;
setInPoint: (v: number | null) => void;
setOutPoint: (v: number | null) => void;
onSeek: (time: number) => void;
}
export const ShortcutsPanel = memo(function ShortcutsPanel({
disabled,
duration,
inPoint,
outPoint,
setInPoint,
setOutPoint,
onSeek,
}: ShortcutsPanelProps) {
const [showShortcuts, setShowShortcuts] = useState(false);
const [jumpFrame, setJumpFrame] = useState("");
const shortcutsPanelRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!showShortcuts) return;
const handleMouseDown = (e: MouseEvent) => {
if (shortcutsPanelRef.current && !shortcutsPanelRef.current.contains(e.target as Node)) {
setShowShortcuts(false);
}
};
document.addEventListener("mousedown", handleMouseDown);
return () => {
document.removeEventListener("mousedown", handleMouseDown);
};
}, [showShortcuts]);
const commitJumpFrame = useCallback(() => {
if (disabled) return;
const frame = Number.parseInt(jumpFrame, 10);
if (!Number.isFinite(frame) || duration <= 0) return;
onSeek(Math.min(duration, frameToSeconds(Math.max(0, frame))));
}, [disabled, duration, jumpFrame, onSeek]);
const handleJumpSubmit = useCallback(
(e: React.FormEvent) => {
e.preventDefault();
commitJumpFrame();
},
[commitJumpFrame],
);
const handleJumpKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key !== "Enter") return;
e.preventDefault();
commitJumpFrame();
},
[commitJumpFrame],
);
return (
<div ref={shortcutsPanelRef} className="relative flex-shrink-0">
<Tooltip label="Shortcuts and tools">
<button
type="button"
onClick={() => setShowShortcuts((v) => !v)}
className={`w-6 h-6 flex items-center justify-center rounded border transition-colors ${
showShortcuts
? "border-neutral-600 text-neutral-200 bg-neutral-800"
: "border-neutral-800 text-neutral-600 hover:text-neutral-300 hover:border-neutral-600"
}`}
aria-label="Shortcuts and tools"
aria-expanded={showShortcuts}
>
<svg
width="11"
height="11"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.75"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<rect x="2" y="4" width="20" height="16" rx="2" />
<path d="M6 8h.01M10 8h.01M14 8h.01M18 8h.01M6 12h.01M10 12h.01M14 12h.01M18 12h.01M8 16h8" />
</svg>
</button>
</Tooltip>
{showShortcuts && (
<div
className="absolute bottom-full right-0 mb-2 z-50 rounded-lg shadow-xl min-w-[220px] overflow-y-auto"
style={{
background: "#161618",
border: "1px solid rgba(255,255,255,0.08)",
maxHeight: "min(280px, calc(100vh - 80px))",
}}
>
<div className="px-3 pt-3 pb-2.5">
<p className="text-[9px] font-medium text-neutral-500 uppercase tracking-wider mb-1.5">
Jump to frame
</p>
<form onSubmit={handleJumpSubmit} className="flex items-center gap-1.5">
<input
value={jumpFrame}
onChange={(e) => setJumpFrame(e.target.value)}
disabled={disabled}
inputMode="numeric"
pattern="[0-9]*"
aria-label="Jump to frame"
placeholder="frame number"
className="h-6 flex-1 rounded border border-neutral-700 bg-neutral-900 px-2 text-[10px] font-mono tabular-nums text-neutral-200 outline-none transition-colors placeholder:text-neutral-600 focus:border-studio-accent/60"
onKeyDown={handleJumpKeyDown}
onBlur={commitJumpFrame}
/>
<Tooltip label="Jump to frame">
<button
type="submit"
disabled={disabled}
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"
>
Go
</button>
</Tooltip>
</form>
</div>
<div style={{ borderTop: "1px solid rgba(255,255,255,0.06)" }} />
<div className="px-3 pt-2.5 pb-2">
<p className="text-[9px] font-medium text-neutral-500 uppercase tracking-wider mb-1.5">
Work area
</p>
<div className="flex flex-col gap-1">
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<span
className="font-mono text-[10px] rounded border border-neutral-700 px-1.5 py-0.5 text-neutral-300 min-w-[20px] text-center"
style={{ background: "rgba(255,255,255,0.05)" }}
>
I
</span>
<span className="text-[10px] text-neutral-400">In-point</span>
</div>
<div className="flex items-center gap-1.5">
{inPoint !== null ? (
<>
<span className="font-mono text-[10px] text-neutral-300">
{formatTime(inPoint)}
</span>
<Tooltip label="Clear in-point">
<button
type="button"
onClick={() => setInPoint(null)}
className="w-4 h-4 flex items-center justify-center rounded text-neutral-500 hover:text-neutral-200 transition-colors"
aria-label="Clear in-point"
>
<svg
width="8"
height="8"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
>
<path d="M18 6L6 18M6 6l12 12" />
</svg>
</button>
</Tooltip>
</>
) : (
<span className="text-[10px] text-neutral-600"></span>
)}
</div>
</div>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<span
className="font-mono text-[10px] rounded border border-neutral-700 px-1.5 py-0.5 text-neutral-300 min-w-[20px] text-center"
style={{ background: "rgba(255,255,255,0.05)" }}
>
O
</span>
<span className="text-[10px] text-neutral-400">Out-point</span>
</div>
<div className="flex items-center gap-1.5">
{outPoint !== null ? (
<>
<span className="font-mono text-[10px] text-neutral-300">
{formatTime(outPoint)}
</span>
<Tooltip label="Clear out-point">
<button
type="button"
onClick={() => setOutPoint(null)}
className="w-4 h-4 flex items-center justify-center rounded text-neutral-500 hover:text-neutral-200 transition-colors"
aria-label="Clear out-point"
>
<svg
width="8"
height="8"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
>
<path d="M18 6L6 18M6 6l12 12" />
</svg>
</button>
</Tooltip>
</>
) : (
<span className="text-[10px] text-neutral-600"></span>
)}
</div>
</div>
</div>
</div>
<div style={{ borderTop: "1px solid rgba(255,255,255,0.06)" }} />
<div className="px-3 pt-2.5 pb-3 flex flex-col gap-3">
{SHORTCUT_SECTIONS.map((section) => (
<div key={section.title}>
<p className="text-[9px] font-medium text-neutral-500 uppercase tracking-wider mb-1.5">
{section.title}
</p>
<div className="flex flex-col gap-1">
{section.hints.map((hint) => (
<div key={hint.key} className="flex items-center gap-3">
<span
className="font-mono text-[10px] rounded border border-neutral-700 px-1.5 py-0.5 text-neutral-300 min-w-[36px] text-center"
style={{ background: "rgba(255,255,255,0.05)" }}
>
{hint.key}
</span>
<span className="text-[10px] text-neutral-400">{hint.label}</span>
</div>
))}
</div>
</div>
))}
</div>
</div>
)}
</div>
);
});
@@ -0,0 +1,83 @@
import { useState, useRef, useEffect, memo } from "react";
import { trackStudioEvent } from "../../utils/studioTelemetry";
import { Tooltip } from "../../components/ui";
const SPEED_OPTIONS = [0.25, 0.5, 1, 1.5, 2] as const;
interface SpeedMenuProps {
playbackRate: number;
setPlaybackRate: (rate: number) => void;
disabled: boolean;
}
export const SpeedMenu = memo(function SpeedMenu({
playbackRate,
setPlaybackRate,
disabled,
}: SpeedMenuProps) {
const [showSpeedMenu, setShowSpeedMenu] = useState(false);
const speedMenuContainerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!showSpeedMenu) return;
const handleMouseDown = (e: MouseEvent) => {
if (
speedMenuContainerRef.current &&
!speedMenuContainerRef.current.contains(e.target as Node)
) {
setShowSpeedMenu(false);
}
};
document.addEventListener("mousedown", handleMouseDown);
return () => {
document.removeEventListener("mousedown", handleMouseDown);
};
}, [showSpeedMenu]);
return (
<div ref={speedMenuContainerRef} className="relative flex-shrink-0">
<Tooltip label="Playback speed">
<button
type="button"
onClick={() => setShowSpeedMenu((v) => !v)}
disabled={disabled}
className="w-10 px-2 py-1 rounded-md text-[10px] font-mono tabular-nums transition-colors"
style={{ color: "#71717A", background: "rgba(255,255,255,0.04)" }}
>
{playbackRate === 1 ? "1x" : `${playbackRate}x`}
</button>
</Tooltip>
{showSpeedMenu && (
<div
className="absolute bottom-full right-0 mb-1.5 rounded-lg shadow-xl z-50 min-w-[56px] overflow-hidden"
style={{ background: "#161618", border: "1px solid rgba(255,255,255,0.08)" }}
>
{SPEED_OPTIONS.map((rate) => (
<button
key={rate}
onClick={() => {
trackStudioEvent("playback", { action: "speed_change", rate });
setPlaybackRate(rate);
setShowSpeedMenu(false);
}}
className="block w-full px-3 py-1.5 text-[11px] text-left font-mono tabular-nums transition-colors"
style={{
color: rate === playbackRate ? "#FAFAFA" : "#71717A",
background: rate === playbackRate ? "rgba(255,255,255,0.06)" : "transparent",
}}
onMouseEnter={(e) => {
if (rate !== playbackRate)
e.currentTarget.style.background = "rgba(255,255,255,0.04)";
}}
onMouseLeave={(e) => {
if (rate !== playbackRate) e.currentTarget.style.background = "transparent";
}}
>
{rate}x
</button>
))}
</div>
)}
</div>
);
});
@@ -0,0 +1,168 @@
import { useCallback } from "react";
import { useMountEffect } from "../../hooks/useMountEffect";
import { formatFrameTime, formatTime } from "../lib/time";
import { usePlayerStore, liveTime } from "../store/playerStore";
const SEEK_EDGE_SNAP_PX = 8;
export function resolveSeekPercent(clientX: number, rectLeft: number, rectWidth: number): number {
if (!Number.isFinite(rectWidth) || rectWidth <= 0) return 0;
const rawPercent = (clientX - rectLeft) / rectWidth;
const clamped = Math.max(0, Math.min(1, rawPercent));
const snapThreshold = Math.min(0.5, SEEK_EDGE_SNAP_PX / rectWidth);
if (clamped <= snapThreshold) return 0;
if (clamped >= 1 - snapThreshold) return 1;
return clamped;
}
interface SeekBarRefs {
seekBarRef: React.RefObject<HTMLDivElement | null>;
progressFillRef: React.RefObject<HTMLDivElement | null>;
progressThumbRef: React.RefObject<HTMLDivElement | null>;
sliderRef: React.RefObject<HTMLDivElement | null>;
timeDisplayRef: React.RefObject<HTMLSpanElement | null>;
isDraggingRef: React.MutableRefObject<boolean>;
durationRef: React.MutableRefObject<number>;
currentTimeRef: React.MutableRefObject<number>;
timeDisplayModeRef: React.MutableRefObject<"time" | "frame">;
}
function updateProgressUI(
fillRef: React.RefObject<HTMLDivElement | null>,
thumbRef: React.RefObject<HTMLDivElement | null>,
pct: number,
): void {
if (fillRef.current) fillRef.current.style.width = `${pct}%`;
if (thumbRef.current) thumbRef.current.style.left = `${pct}%`;
}
export function useSeekBarDrag(
refs: SeekBarRefs,
onSeek: (time: number) => void,
disabled: boolean,
duration: number,
) {
const seekFromClientX = useCallback(
(clientX: number) => {
if (disabled) return;
const bar = refs.seekBarRef.current;
if (!bar || duration <= 0) return;
const rect = bar.getBoundingClientRect();
const percent = resolveSeekPercent(clientX, rect.left, rect.width);
updateProgressUI(refs.progressFillRef, refs.progressThumbRef, percent * 100);
onSeek(percent * duration);
},
[disabled, duration, onSeek, refs],
);
const handlePointerDown = useCallback(
(e: React.PointerEvent<HTMLDivElement>) => {
if (e.button !== 0) return;
e.preventDefault();
e.currentTarget.focus();
refs.isDraggingRef.current = true;
const target = e.currentTarget;
const pointerId = e.pointerId;
try {
target.setPointerCapture(pointerId);
} catch {
/* fallback to window listeners */
}
seekFromClientX(e.clientX);
let seekRafId = 0;
let pendingClientX = e.clientX;
const onMove = (ev: PointerEvent) => {
if (ev.pointerId !== pointerId || !refs.isDraggingRef.current) return;
pendingClientX = ev.clientX;
const bar = refs.seekBarRef.current;
const dur = refs.durationRef.current;
if (bar && dur > 0) {
const rect = bar.getBoundingClientRect();
const pct = resolveSeekPercent(ev.clientX, rect.left, rect.width) * 100;
updateProgressUI(refs.progressFillRef, refs.progressThumbRef, pct);
}
if (!seekRafId) {
seekRafId = requestAnimationFrame(() => {
seekRafId = 0;
if (refs.isDraggingRef.current) seekFromClientX(pendingClientX);
});
}
};
const cleanup = () => {
refs.isDraggingRef.current = false;
if (seekRafId) {
cancelAnimationFrame(seekRafId);
seekRafId = 0;
}
seekFromClientX(pendingClientX);
try {
target.releasePointerCapture(pointerId);
} catch {
/* already released */
}
target.removeEventListener("pointermove", onMove);
target.removeEventListener("pointerup", onUp);
target.removeEventListener("pointercancel", onUp);
window.removeEventListener("pointerup", onUp);
window.removeEventListener("pointercancel", onUp);
document.removeEventListener("visibilitychange", onVisibilityChange);
window.removeEventListener("blur", cleanup);
};
const onUp = (ev: PointerEvent) => {
if (ev.pointerId !== pointerId) return;
cleanup();
};
const onVisibilityChange = () => {
if (document.visibilityState === "hidden") cleanup();
};
target.addEventListener("pointermove", onMove);
target.addEventListener("pointerup", onUp);
target.addEventListener("pointercancel", onUp);
window.addEventListener("pointerup", onUp);
window.addEventListener("pointercancel", onUp);
document.addEventListener("visibilitychange", onVisibilityChange);
window.addEventListener("blur", cleanup);
},
[seekFromClientX, refs],
);
useMountEffect(() => {
const updateProgress = (t: number) => {
refs.currentTimeRef.current = t;
const dur = refs.durationRef.current;
const pct = dur > 0 ? Math.min(100, (t / dur) * 100) : 0;
updateProgressUI(refs.progressFillRef, refs.progressThumbRef, pct);
if (refs.timeDisplayRef.current) {
refs.timeDisplayRef.current.textContent =
refs.timeDisplayModeRef.current === "frame" ? formatFrameTime(t, dur) : formatTime(t);
}
if (refs.sliderRef.current)
refs.sliderRef.current.setAttribute("aria-valuenow", String(Math.round(t)));
};
const unsub = liveTime.subscribe(updateProgress);
updateProgress(usePlayerStore.getState().currentTime);
const interval = setInterval(() => {
const t = usePlayerStore.getState().currentTime;
const dur = usePlayerStore.getState().duration;
if (dur > 0 && t > 0) {
updateProgressUI(
refs.progressFillRef,
refs.progressThumbRef,
Math.min(100, (t / dur) * 100),
);
}
}, 500);
return () => {
unsub();
clearInterval(interval);
};
});
return { handlePointerDown };
}