fix(ci): scope LOC check to studio, split useTimelinePlayer + hyperframes-player under 500 LOC (#750)

* refactor: split useTimelinePlayer.ts and hyperframes-player.ts into focused modules (<500 LOC each)

* fix(ci): scope 500 LOC check to packages/studio, add allowlist for grandfathered files

* feat(cli): Linux ARM64 support — auto-install Chromium on DGX Spark / GB10 / Jetson

Chrome Headless Shell has no Linux ARM64 binary. On arm64 Linux:
- Detects the platform automatically
- Tries to auto-install system Chromium via apt-get (works on Ubuntu/Debian ARM)
- Falls back to clear manual instructions with exact commands
- 'hyperframes browser ensure' guides through the setup interactively
- After setup, all render commands work without any flags

* fix(ci): disable Windows Defender real-time monitoring to prevent EPERM builds

Path exclusions are insufficient — Defender re-scans new files created
during bun install before the exclusion takes effect. Disable real-time
monitoring for the entire job duration instead (standard CI practice).

* refactor(studio): split all files >500 LOC + extract useToast, delete allowlist

All 11 large files split into focused modules under 500 LOC.
App.tsx extracted toast logic into useToast hook (493 LOC now).
.filesize-allowlist deleted — no longer needed.

* fix: remove unused imports from split files, extract useToast from App.tsx

App.tsx: 504 → 493 lines (toast logic extracted to useToast hook)
timelineDOM.ts: remove unused imports from re-export pattern
MotionPanel.tsx: remove unused clampStudioCustomEasePoints import
studioMotionOps.ts: remove unused StudioGsapMotionDirection import

* fix(ci): use Set-MpPreference to fully disable Windows Defender (both jobs)

* fix(producer): use node --experimental-strip-types instead of tsx for build:fonts

Eliminates the tsx binary dependency that Windows Defender locks during
bun install, causing EPERM errors. Node 22.6+ strips TypeScript types
natively with no external binary.

* chore: remove .filesize-allowlist — App.tsx is now 493 lines (<500)

* fix(ci): disable Windows Defender before checkout to prevent all EPERM races

* fix(producer): skip build:fonts if fontData.generated.ts already exists

The generated file is tracked in git, so CI doesn't need to regenerate
it. This avoids @fontsource/inter node_modules access on Windows which
triggers EPERM from Defender scanning during bun install.
This commit is contained in:
Miguel Ángel
2026-05-13 01:48:12 +02:00
committed by GitHub
parent 03475d54c6
commit 91bdffffe6
74 changed files with 11760 additions and 9759 deletions
@@ -0,0 +1,135 @@
import { useRef, useState, useCallback } from "react";
import { buildClipRangeSelection, type TimelineRangeSelection } from "./timelineEditing";
import type { TimelineElement } from "../store/playerStore";
import { liveTime } from "../store/playerStore";
import { GUTTER } from "./timelineLayout";
interface UseTimelineRangeSelectionInput {
scrollRef: React.RefObject<HTMLDivElement | null>;
ppsRef: React.RefObject<number>;
effectiveDuration: number;
pps: number;
onSeek?: (time: number) => void;
seekFromX: (clientX: number) => void;
autoScrollDuringDrag: (clientX: number) => void;
dragScrollRaf: React.RefObject<number>;
isDragging: React.RefObject<boolean>;
setShowPopover: (v: boolean) => void;
}
export function useTimelineRangeSelection({
scrollRef,
ppsRef: _ppsRef,
effectiveDuration: _effectiveDuration,
pps,
onSeek: _onSeek,
seekFromX,
autoScrollDuringDrag,
dragScrollRaf,
isDragging,
setShowPopover,
}: UseTimelineRangeSelectionInput) {
const isRangeSelecting = useRef(false);
const rangeAnchorTime = useRef(0);
const [rangeSelection, setRangeSelection] = useState<TimelineRangeSelection | null>(null);
const shiftClickClipRef = useRef<{
element: TimelineElement;
anchorX: number;
anchorY: number;
} | null>(null);
const handlePointerDown = useCallback(
(e: React.PointerEvent) => {
if (e.button !== 0) return;
if (e.shiftKey) {
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
isRangeSelecting.current = true;
setShowPopover(false);
const rect = scrollRef.current?.getBoundingClientRect();
if (rect) {
const x = e.clientX - rect.left + (scrollRef.current?.scrollLeft ?? 0) - GUTTER;
const time = Math.max(0, x / pps);
rangeAnchorTime.current = time;
setRangeSelection({ start: time, end: time, anchorX: e.clientX, anchorY: e.clientY });
}
return;
}
shiftClickClipRef.current = null;
if ((e.target as HTMLElement).closest("[data-clip]")) return;
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
isDragging.current = true;
setRangeSelection(null);
setShowPopover(false);
seekFromX(e.clientX);
},
[seekFromX, pps, scrollRef, isDragging, setShowPopover],
);
const handlePointerMove = useCallback(
(e: React.PointerEvent) => {
if (isRangeSelecting.current) {
const rect = scrollRef.current?.getBoundingClientRect();
if (rect) {
const x = e.clientX - rect.left + (scrollRef.current?.scrollLeft ?? 0) - GUTTER;
setRangeSelection((prev) =>
prev
? { ...prev, end: Math.max(0, x / pps), anchorX: e.clientX, anchorY: e.clientY }
: null,
);
}
return;
}
if (!isDragging.current) return;
seekFromX(e.clientX);
autoScrollDuringDrag(e.clientX);
},
[seekFromX, autoScrollDuringDrag, pps, scrollRef, isDragging],
);
const handlePointerUp = useCallback(() => {
if (isRangeSelecting.current) {
isRangeSelecting.current = false;
const pendingShiftClick = shiftClickClipRef.current;
shiftClickClipRef.current = null;
setRangeSelection((prev) => {
if (prev && pendingShiftClick && Math.abs(prev.end - prev.start) <= 0.2) {
setShowPopover(true);
return buildClipRangeSelection(pendingShiftClick.element, pendingShiftClick);
}
if (prev && Math.abs(prev.end - prev.start) > 0.2) {
setShowPopover(true);
return prev;
}
return null;
});
return;
}
isDragging.current = false;
cancelAnimationFrame(dragScrollRaf.current);
}, [isDragging, dragScrollRaf, setShowPopover]);
return {
rangeSelection,
setRangeSelection,
shiftClickClipRef,
handlePointerDown,
handlePointerMove,
handlePointerUp,
};
}
/* ── Seek + scroll utilities (used in Timeline only) ──────────────── */
export function seekTimeFromScrollX(
scrollEl: HTMLDivElement,
clientX: number,
effectiveDuration: number,
pps: number,
onSeek?: (time: number) => void,
): void {
const rect = scrollEl.getBoundingClientRect();
const x = clientX - rect.left + scrollEl.scrollLeft - GUTTER;
if (x < 0) return;
const time = Math.max(0, Math.min(effectiveDuration, x / pps));
liveTime.notify(time);
onSeek?.(time);
}