mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-04 07:19:52 +00:00
refactor(studio): code quality — 22 findings, dead code removal, App.tsx split (#144)
## Summary Full code quality review of the studio package, fixing 22 of 25 findings. Removes dead code, extracts modules from App.tsx, fixes accessibility and performance issues. ## Critical fixes (3) - **`aria-valuenow`** on seek bar now updates imperatively via `liveTime.subscribe` — screen readers previously always reported position 0 - **Speed menu** closes on outside click (was permanently stuck open) - **RenderQueue auto-scroll** moved from render phase to `useEffect` (was violating React render purity via `queueMicrotask` during render) ## Dead code removed (-331 lines) | File | Lines | Why dead | |---|---|---| | `PreviewPanel.tsx` | 180 | Replaced by NLELayout + NLEPreview | | `useCodeEditor.ts` | 80 | Exported but never imported | | `formatTick` alias | 2 | Deprecated, unused | | `onClipChange` prop | 5 | Declared, never used | | `trackH` prop | 5 | Declared, never used | | `editRange*` + updaters in store | 60 | Never read or written | ## App.tsx extraction | Extracted to | Lines | What | |---|---|---| | `components/LintModal.tsx` | 130 | Lint results modal + LintFinding type | | `components/MediaPreview.tsx` | 75 | Image/video/audio/font file previewer | | `utils/mediaTypes.ts` | 15 | Shared regex constants (App.tsx and AssetsTab.tsx had diverged copies) | ## Performance fixes - `useMemo` for `compositions`/`assets` derivation from `fileTree` - `useMemo` for `buildTree(files)` in FileTree - Debounced `handleContentChange` PUT (600ms — was firing on every keystroke) - CompositionsTab iframe hover debounced (300ms — was mounting immediately) - `VideoFrameThumbnail` re-extracts frame when `src` prop changes ## Not addressed (3 — low priority) - #6: SystemIcons consolidation (large refactor across many files) - #16-17: Overlay dismiss pattern standardization - #18: Inline SVG → Phosphor replacement (gradual, per-PR) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useRef, useState, useCallback, memo } from "react";
|
||||
import { useRef, useState, useCallback, useEffect, memo } from "react";
|
||||
import { useMountEffect } from "../../hooks/useMountEffect";
|
||||
import { formatTime } from "../lib/time";
|
||||
import { usePlayerStore, liveTime } from "../store/playerStore";
|
||||
@@ -30,6 +30,8 @@ export const PlayerControls = memo(function PlayerControls({
|
||||
const progressThumbRef = useRef<HTMLDivElement>(null);
|
||||
const timeDisplayRef = useRef<HTMLSpanElement>(null);
|
||||
const seekBarRef = useRef<HTMLDivElement>(null);
|
||||
const sliderRef = useRef<HTMLDivElement>(null);
|
||||
const speedMenuContainerRef = useRef<HTMLDivElement>(null);
|
||||
const isDraggingRef = useRef(false);
|
||||
const currentTimeRef = useRef(0);
|
||||
|
||||
@@ -43,6 +45,7 @@ export const PlayerControls = memo(function PlayerControls({
|
||||
if (progressFillRef.current) progressFillRef.current.style.width = `${pct}%`;
|
||||
if (progressThumbRef.current) progressThumbRef.current.style.left = `${pct}%`;
|
||||
if (timeDisplayRef.current) timeDisplayRef.current.textContent = formatTime(t);
|
||||
if (sliderRef.current) sliderRef.current.setAttribute("aria-valuenow", String(Math.round(t)));
|
||||
};
|
||||
const unsub = liveTime.subscribe(updateProgress);
|
||||
updateProgress(usePlayerStore.getState().currentTime);
|
||||
@@ -64,6 +67,22 @@ export const PlayerControls = memo(function PlayerControls({
|
||||
};
|
||||
});
|
||||
|
||||
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]);
|
||||
|
||||
const seekFromClientX = useCallback(
|
||||
(clientX: number) => {
|
||||
const bar = seekBarRef.current;
|
||||
@@ -153,7 +172,10 @@ export const PlayerControls = memo(function PlayerControls({
|
||||
|
||||
{/* Seek bar — teal progress fill */}
|
||||
<div
|
||||
ref={seekBarRef}
|
||||
ref={(el) => {
|
||||
(seekBarRef as React.MutableRefObject<HTMLDivElement | null>).current = el;
|
||||
(sliderRef as React.MutableRefObject<HTMLDivElement | null>).current = el;
|
||||
}}
|
||||
role="slider"
|
||||
tabIndex={0}
|
||||
aria-label="Seek"
|
||||
@@ -188,7 +210,7 @@ export const PlayerControls = memo(function PlayerControls({
|
||||
</div>
|
||||
|
||||
{/* Speed control */}
|
||||
<div className="relative flex-shrink-0">
|
||||
<div ref={speedMenuContainerRef} className="relative flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowSpeedMenu((v) => !v)}
|
||||
@@ -235,7 +257,7 @@ export const PlayerControls = memo(function PlayerControls({
|
||||
onClick={onToggleTimeline}
|
||||
className={`w-7 h-7 flex items-center justify-center rounded-md border transition-colors ${
|
||||
timelineVisible
|
||||
? "text-[#3CE6AC] bg-[#3CE6AC]/10 border-[#3CE6AC]/30"
|
||||
? "text-studio-accent bg-studio-accent/10 border-studio-accent/30"
|
||||
: "border-neutral-700 text-neutral-500 hover:text-neutral-300 hover:bg-neutral-800"
|
||||
}`}
|
||||
title={timelineVisible ? "Hide timeline" : "Show timeline"}
|
||||
|
||||
Reference in New Issue
Block a user