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:
Miguel Ángel
2026-03-31 04:21:12 +02:00
committed by GitHub
parent 1bc83c62a5
commit dac304ed9f
26 changed files with 475 additions and 753 deletions
@@ -1,5 +1,6 @@
import { memo, useState, useCallback, useRef } from "react";
import { VideoFrameThumbnail } from "../ui/VideoFrameThumbnail";
import { MEDIA_EXT, IMAGE_EXT, VIDEO_EXT, AUDIO_EXT } from "../../utils/mediaTypes";
interface AssetsTabProps {
projectId: string;
@@ -7,11 +8,6 @@ interface AssetsTabProps {
onImport?: (files: FileList) => void;
}
const MEDIA_EXT = /\.(mp4|webm|mov|mp3|wav|ogg|m4a|jpg|jpeg|png|gif|webp|svg)$/i;
const IMAGE_EXT = /\.(jpg|jpeg|png|gif|webp|svg)$/i;
const VIDEO_EXT = /\.(mp4|webm|mov)$/i;
const AUDIO_EXT = /\.(mp3|wav|ogg|m4a)$/i;
/** Inline thumbnail content — rendered inside the container div in AssetCard. */
function AssetThumbnail({
serveUrl,
@@ -104,7 +100,7 @@ function AssetCard({
onPointerLeave={() => setHovered(false)}
className={`w-full text-left px-2 py-1.5 flex items-center gap-2.5 transition-colors cursor-pointer ${
isCopied
? "bg-[#3CE6AC]/10 border-l-2 border-[#3CE6AC]"
? "bg-studio-accent/10 border-l-2 border-studio-accent"
: "border-l-2 border-transparent hover:bg-neutral-800/50"
}`}
>
@@ -131,7 +127,7 @@ function AssetCard({
<div className="min-w-0 flex-1">
<span className="text-[11px] font-medium text-neutral-300 truncate block">{name}</span>
{isCopied ? (
<span className="text-[9px] text-[#3CE6AC]">Copied!</span>
<span className="text-[9px] text-studio-accent">Copied!</span>
) : (
<span className="text-[9px] text-neutral-600 truncate block">{asset}</span>
)}
@@ -168,7 +164,7 @@ export const AssetsTab = memo(function AssetsTab({ projectId, assets, onImport }
return (
<div
className={`flex-1 flex flex-col min-h-0 transition-colors ${dragOver ? "bg-blue-950/20" : ""}`}
className={`flex-1 flex flex-col min-h-0 transition-colors ${dragOver ? "bg-studio-accent/[0.05]" : ""}`}
onDragOver={(e) => {
e.preventDefault();
setDragOver(true);
@@ -1,4 +1,4 @@
import { memo, useState } from "react";
import { memo, useRef, useState } from "react";
interface CompositionsTabProps {
projectId: string;
@@ -19,6 +19,17 @@ function CompCard({
onSelect: () => void;
}) {
const [hovered, setHovered] = useState(false);
const hoverTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const handleEnter = () => {
hoverTimer.current = setTimeout(() => setHovered(true), 300);
};
const handleLeave = () => {
if (hoverTimer.current) {
clearTimeout(hoverTimer.current);
hoverTimer.current = null;
}
setHovered(false);
};
const name = comp.replace(/^compositions\//, "").replace(/\.html$/, "");
const thumbnailUrl = `/api/projects/${projectId}/thumbnail/${comp}?t=2`;
const previewUrl = `/api/projects/${projectId}/preview/comp/${comp}`;
@@ -26,11 +37,11 @@ function CompCard({
return (
<div
onClick={onSelect}
onPointerEnter={() => setHovered(true)}
onPointerLeave={() => setHovered(false)}
onPointerEnter={handleEnter}
onPointerLeave={handleLeave}
className={`w-full text-left px-2 py-1.5 flex items-center gap-2.5 transition-colors cursor-pointer ${
isActive
? "bg-[#3CE6AC]/10 border-l-2 border-[#3CE6AC]"
? "bg-studio-accent/10 border-l-2 border-studio-accent"
: "border-l-2 border-transparent hover:bg-neutral-800/50"
}`}
>
@@ -82,7 +82,7 @@ export const LeftSidebar = memo(function LeftSidebar({
onClick={() => selectTab("code")}
className={`flex-1 py-2 text-[11px] font-medium transition-colors ${
tab === "code"
? "text-neutral-200 border-b-2 border-[#3CE6AC]"
? "text-neutral-200 border-b-2 border-studio-accent"
: "text-neutral-500 hover:text-neutral-400"
}`}
>
@@ -93,7 +93,7 @@ export const LeftSidebar = memo(function LeftSidebar({
onClick={() => selectTab("compositions")}
className={`flex-1 py-2 text-[11px] font-medium transition-colors ${
tab === "compositions"
? "text-neutral-200 border-b-2 border-blue-500"
? "text-neutral-200 border-b-2 border-studio-accent"
: "text-neutral-500 hover:text-neutral-400"
}`}
>
@@ -104,7 +104,7 @@ export const LeftSidebar = memo(function LeftSidebar({
onClick={() => selectTab("assets")}
className={`flex-1 py-2 text-[11px] font-medium transition-colors ${
tab === "assets"
? "text-neutral-200 border-b-2 border-blue-500"
? "text-neutral-200 border-b-2 border-studio-accent"
: "text-neutral-500 hover:text-neutral-400"
}`}
>