feat(studio): redesign left panel — Code tab, Renders header, Lint bottom (#141)

## Summary

Major layout redesign matching the Paper mockup. The Code editor moves to the left panel, Renders moves to the header, and interactions are simplified.

## Layout changes

| Before | After |
|---|---|
| Left: Compositions \| Assets | Left: **Code** \| Compositions \| Assets |
| Right: Code \| Renders tabs | Right: Renders-only (hidden by default) |
| Header: Projects ← / Lint / panel toggles | Header: title / sidebar toggle / **Renders button** |
| Lint: header button | Lint: **pinned to bottom** of left panel |

## Interaction changes

- **Renders panel** opens via a dedicated header button (teal when active), hidden by default
- **ExpandOnHover removed** — replaced with inline autoplay on thumbnails:
  - Compositions: hover shows a tiny iframe (1920px scaled to 80px, 300ms debounce)
  - Assets: hover shows `<video autoPlay muted loop>` in the thumbnail cell
- **Deleted** `ExpandOnHover.tsx` (194 lines) and `ExpandedVideoPreview.tsx` (35 lines)
- **Left panel max width**: 50% viewport, auto-expands to 50% when opening a file in Code tab

## Visual polish

- Equal-width tabs (Code \| Compositions \| Assets)
- Sidebar toggle button highlighted when panel is open
- Phosphor duotone file-type icons (`FileHtml`, `FileCss`, `FileJs`, `FileTs`, etc.)
- "FILES" header removed from file tree
- Redundant "RENDERS (N)" title removed from renders panel

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
Miguel Ángel
2026-03-31 03:57:32 +02:00
committed by GitHub
parent 05a7b03841
commit 7b18c0352e
8 changed files with 263 additions and 704 deletions
@@ -1,13 +1,5 @@
import { memo, useState, useCallback } from "react";
import {
FileCode,
Image,
Film,
Music,
File,
ChevronDown,
ChevronRight,
} from "../../icons/SystemIcons";
import { Film, Music, Image, ChevronDown, ChevronRight } from "../../icons/SystemIcons";
interface FileTreeProps {
files: string[];
@@ -15,34 +7,65 @@ interface FileTreeProps {
onSelectFile: (path: string) => void;
}
const FILE_ICONS: Record<string, { icon: typeof File; color: string }> = {
html: { icon: FileCode, color: "#3B82F6" },
css: { icon: FileCode, color: "#A855F7" },
js: { icon: FileCode, color: "#F59E0B" },
ts: { icon: FileCode, color: "#3B82F6" },
json: { icon: File, color: "#22C55E" },
md: { icon: File, color: "#737373" },
png: { icon: Image, color: "#22C55E" },
jpg: { icon: Image, color: "#22C55E" },
jpeg: { icon: Image, color: "#22C55E" },
webp: { icon: Image, color: "#22C55E" },
gif: { icon: Image, color: "#22C55E" },
svg: { icon: Image, color: "#F97316" },
mp4: { icon: Film, color: "#A855F7" },
webm: { icon: Film, color: "#A855F7" },
mov: { icon: Film, color: "#A855F7" },
mp3: { icon: Music, color: "#F59E0B" },
wav: { icon: Music, color: "#F59E0B" },
ogg: { icon: Music, color: "#F59E0B" },
m4a: { icon: Music, color: "#F59E0B" },
woff: { icon: File, color: "#525252" },
woff2: { icon: File, color: "#525252" },
ttf: { icon: File, color: "#525252" },
};
/** VS Codestyle language badge: colored rounded rect with a 23 letter label. */
function Badge({ label, bg, text = "#fff" }: { label: string; bg: string; text?: string }) {
return (
<span
className="flex-shrink-0 inline-flex items-center justify-center rounded"
style={{
width: 16,
height: 16,
background: bg,
color: text,
fontSize: 7,
fontWeight: 700,
fontFamily: "monospace",
letterSpacing: "-0.02em",
lineHeight: 1,
}}
>
{label}
</span>
);
}
function getFileIcon(path: string) {
/** Render a file-type icon for a given file path. */
function FileIcon({ path }: { path: string }) {
const ext = path.split(".").pop()?.toLowerCase() ?? "";
return FILE_ICONS[ext] ?? { icon: File, color: "#737373" };
// Language badges
if (ext === "html") return <Badge label="HTML" bg="#E44D26" />;
if (ext === "js" || ext === "mjs" || ext === "cjs")
return <Badge label="JS" bg="#F0DB4F" text="#323330" />;
if (ext === "ts" || ext === "mts") return <Badge label="TS" bg="#3178C6" />;
if (ext === "css") return <Badge label="CSS" bg="#264DE4" />;
if (ext === "json") return <Badge label="{}" bg="#1E7F34" />;
if (ext === "md" || ext === "mdx") return <Badge label="MD" bg="#555" />;
if (ext === "svg") return <Badge label="SVG" bg="#FF9900" />;
if (ext === "wav" || ext === "mp3" || ext === "ogg" || ext === "m4a")
return <Music size={13} style={{ color: "#3CE6AC" }} className="flex-shrink-0" />;
if (ext === "mp4" || ext === "webm" || ext === "mov")
return <Film size={13} style={{ color: "#A855F7" }} className="flex-shrink-0" />;
if (ext === "png" || ext === "jpg" || ext === "jpeg" || ext === "webp" || ext === "gif")
return <Image size={13} style={{ color: "#22C55E" }} className="flex-shrink-0" />;
if (ext === "woff" || ext === "woff2" || ext === "ttf" || ext === "otf")
return <Badge label="Aa" bg="#525252" />;
if (ext === "txt") return <Badge label="TXT" bg="#4B5563" />;
// Generic document
return (
<svg
width="13"
height="13"
viewBox="0 0 24 24"
fill="none"
stroke="#6B7280"
strokeWidth="1.5"
strokeLinecap="round"
className="flex-shrink-0"
>
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" />
<polyline points="14 2 14 8 20 8" />
</svg>
);
}
interface TreeNode {
@@ -160,7 +183,6 @@ function TreeFile({
activeFile: string | null;
onSelectFile: (path: string) => void;
}) {
const { icon: Icon, color } = getFileIcon(node.name);
const isActive = node.fullPath === activeFile;
return (
@@ -173,7 +195,7 @@ function TreeFile({
}`}
style={{ paddingLeft: `${8 + depth * 12 + 14}px` }}
>
<Icon size={12} style={{ color }} className="flex-shrink-0" />
<FileIcon path={node.name} />
<span className="truncate">{node.name}</span>
</button>
);
@@ -194,9 +216,6 @@ export const FileTree = memo(function FileTree({ files, activeFile, onSelectFile
return (
<div className="flex flex-col h-full min-h-0">
<div className="px-2.5 py-1.5 border-b border-neutral-800 flex-shrink-0">
<span className="text-2xs font-medium text-neutral-500 uppercase tracking-caps">Files</span>
</div>
<div className="flex-1 overflow-y-auto py-1">
{children.map((child) =>
child.isFile && child.children.size === 0 ? (