mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +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,9 +1,10 @@
|
||||
import { memo, useState, useRef } from "react";
|
||||
import { memo, useState, useRef, useEffect } from "react";
|
||||
import { RenderQueueItem } from "./RenderQueueItem";
|
||||
import type { RenderJob } from "./useRenderQueue";
|
||||
|
||||
interface RenderQueueProps {
|
||||
jobs: RenderJob[];
|
||||
projectId: string;
|
||||
onDelete: (jobId: string) => void;
|
||||
onClearCompleted: () => void;
|
||||
onStartRender: (format: "mp4" | "webm") => void;
|
||||
@@ -33,7 +34,7 @@ function FormatExportButton({
|
||||
<button
|
||||
onClick={() => onStartRender(format)}
|
||||
disabled={isRendering}
|
||||
className="flex items-center gap-1 px-2 py-0.5 text-[10px] font-semibold rounded-r bg-[#3CE6AC] text-[#09090B] hover:brightness-110 transition-colors disabled:opacity-50"
|
||||
className="flex items-center gap-1 px-2 py-0.5 text-[10px] font-semibold rounded-r bg-studio-accent text-[#09090B] hover:brightness-110 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{isRendering ? "Rendering..." : "Export"}
|
||||
</button>
|
||||
@@ -43,21 +44,21 @@ function FormatExportButton({
|
||||
|
||||
export const RenderQueue = memo(function RenderQueue({
|
||||
jobs,
|
||||
projectId,
|
||||
onDelete,
|
||||
onClearCompleted,
|
||||
onStartRender,
|
||||
isRendering,
|
||||
}: RenderQueueProps) {
|
||||
const listRef = useRef<HTMLDivElement>(null);
|
||||
const prevCount = useRef(jobs.length);
|
||||
|
||||
// Auto-scroll to bottom when new jobs are added (adjust during render)
|
||||
if (jobs.length > prevCount.current && listRef.current) {
|
||||
queueMicrotask(() => {
|
||||
listRef.current?.scrollTo({ top: listRef.current.scrollHeight, behavior: "smooth" });
|
||||
});
|
||||
}
|
||||
prevCount.current = jobs.length;
|
||||
// Auto-scroll to bottom when new jobs are added.
|
||||
// Runs in an effect to avoid side effects during the render phase.
|
||||
useEffect(() => {
|
||||
if (listRef.current) {
|
||||
listRef.current.scrollTo({ top: listRef.current.scrollHeight, behavior: "smooth" });
|
||||
}
|
||||
}, [jobs.length]);
|
||||
|
||||
const completedCount = jobs.filter((j) => j.status !== "rendering").length;
|
||||
|
||||
@@ -111,7 +112,12 @@ export const RenderQueue = memo(function RenderQueue({
|
||||
</div>
|
||||
) : (
|
||||
jobs.map((job) => (
|
||||
<RenderQueueItem key={job.id} job={job} onDelete={() => onDelete(job.id)} />
|
||||
<RenderQueueItem
|
||||
key={job.id}
|
||||
job={job}
|
||||
projectId={projectId}
|
||||
onDelete={() => onDelete(job.id)}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { RenderJob } from "./useRenderQueue";
|
||||
|
||||
interface RenderQueueItemProps {
|
||||
job: RenderJob;
|
||||
projectId: string;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
@@ -24,26 +25,30 @@ function formatTimeAgo(timestamp: number): string {
|
||||
|
||||
export const RenderQueueItem = memo(function RenderQueueItem({
|
||||
job,
|
||||
projectId,
|
||||
onDelete,
|
||||
}: RenderQueueItemProps) {
|
||||
const [hovered, setHovered] = useState(false);
|
||||
|
||||
// Direct file URL — serves from disk, survives server restarts
|
||||
const fileSrc = `/api/projects/${projectId}/renders/file/${job.filename}`;
|
||||
|
||||
const handleOpen = useCallback(() => {
|
||||
window.open(`/api/render/${job.id}/view`, "_blank");
|
||||
}, [job.id]);
|
||||
window.open(fileSrc, "_blank");
|
||||
}, [fileSrc]);
|
||||
|
||||
const handleDownload = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
const a = document.createElement("a");
|
||||
a.href = `/api/render/${job.id}/download`;
|
||||
a.href = fileSrc;
|
||||
a.download = job.filename;
|
||||
a.click();
|
||||
},
|
||||
[job.id, job.filename],
|
||||
[fileSrc, job.filename],
|
||||
);
|
||||
|
||||
const viewSrc = `/api/render/${job.id}/view`;
|
||||
const viewSrc = fileSrc;
|
||||
const isComplete = job.status === "complete";
|
||||
|
||||
return (
|
||||
@@ -85,7 +90,7 @@ export const RenderQueueItem = memo(function RenderQueueItem({
|
||||
)}
|
||||
{job.status === "rendering" && (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<div className="w-2 h-2 rounded-full bg-[#3CE6AC] animate-pulse" />
|
||||
<div className="w-2 h-2 rounded-full bg-studio-accent animate-pulse" />
|
||||
</div>
|
||||
)}
|
||||
{job.status === "failed" && (
|
||||
@@ -117,11 +122,11 @@ export const RenderQueueItem = memo(function RenderQueueItem({
|
||||
<div className="mt-1">
|
||||
<div className="flex items-center justify-between mb-0.5">
|
||||
<span className="text-[9px] text-neutral-500">{job.stage || "Rendering"}</span>
|
||||
<span className="text-[9px] font-mono text-[#3CE6AC]">{job.progress}%</span>
|
||||
<span className="text-[9px] font-mono text-studio-accent">{job.progress}%</span>
|
||||
</div>
|
||||
<div className="w-full h-1 bg-neutral-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-[#3CE6AC] rounded-full transition-all duration-300"
|
||||
className="h-full bg-studio-accent rounded-full transition-all duration-300"
|
||||
style={{ width: `${job.progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user