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,88 +0,0 @@
import { useState, useCallback } from "react";
export interface OpenFile {
path: string;
content: string;
savedContent: string;
isDirty: boolean;
}
export interface UseCodeEditorReturn {
openFiles: OpenFile[];
activeFilePath: string | null;
activeFile: OpenFile | null;
openFile: (path: string, content: string) => void;
closeFile: (path: string) => void;
setActiveFile: (path: string) => void;
updateContent: (content: string) => void;
markSaved: (path: string) => void;
/** External update — updates saved content, shows reload indicator */
externalUpdate: (path: string, content: string) => void;
}
export function useCodeEditor(): UseCodeEditorReturn {
const [openFiles, setOpenFiles] = useState<OpenFile[]>([]);
const [activeFilePath, setActiveFilePath] = useState<string | null>(null);
const activeFile = openFiles.find((f) => f.path === activeFilePath) ?? null;
const openFile = useCallback((path: string, content: string) => {
setOpenFiles((prev) => {
const existing = prev.find((f) => f.path === path);
if (existing) return prev;
return [...prev, { path, content, savedContent: content, isDirty: false }];
});
setActiveFilePath(path);
}, []);
const closeFile = useCallback(
(path: string) => {
setOpenFiles((prev) => prev.filter((f) => f.path !== path));
setActiveFilePath((prev) => {
if (prev === path) {
const remaining = openFiles.filter((f) => f.path !== path);
return remaining.length > 0 ? remaining[remaining.length - 1].path : null;
}
return prev;
});
},
[openFiles],
);
const updateContent = useCallback(
(content: string) => {
setOpenFiles((prev) =>
prev.map((f) =>
f.path === activeFilePath ? { ...f, content, isDirty: content !== f.savedContent } : f,
),
);
},
[activeFilePath],
);
const markSaved = useCallback((path: string) => {
setOpenFiles((prev) =>
prev.map((f) => (f.path === path ? { ...f, savedContent: f.content, isDirty: false } : f)),
);
}, []);
const externalUpdate = useCallback((path: string, content: string) => {
setOpenFiles((prev) =>
prev.map((f) =>
f.path === path ? { ...f, savedContent: content, content, isDirty: false } : f,
),
);
}, []);
return {
openFiles,
activeFilePath,
activeFile,
openFile,
closeFile,
setActiveFile: setActiveFilePath,
updateContent,
markSaved,
externalUpdate,
};
}