From 9c233b30e426f6d49253f8f2eb1f0ee4ef731214 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Mon, 30 Mar 2026 23:42:31 +0200 Subject: [PATCH 1/6] feat(studio): code tab in left panel, renders via header button, lint at bottom Move Code editor/file-tree to a new Code tab in LeftSidebar; replace right panel's Code+Renders tabs with a Renders-only panel toggled by a header Renders button; pin Lint button at the bottom of LeftSidebar; remove Projects back button from header. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- packages/studio/src/App.tsx | 178 ++++++------------ .../src/components/sidebar/LeftSidebar.tsx | 81 +++++++- 2 files changed, 132 insertions(+), 127 deletions(-) diff --git a/packages/studio/src/App.tsx b/packages/studio/src/App.tsx index cb790ece7..e0cb4b978 100644 --- a/packages/studio/src/App.tsx +++ b/packages/studio/src/App.tsx @@ -2,7 +2,6 @@ import { useState, useCallback, useRef, useEffect, type ReactNode } from "react" import { useMountEffect } from "./hooks/useMountEffect"; import { NLELayout } from "./components/nle/NLELayout"; import { SourceEditor } from "./components/editor/SourceEditor"; -import { FileTree } from "./components/editor/FileTree"; import { LeftSidebar } from "./components/sidebar/LeftSidebar"; import { RenderQueue } from "./components/renders/RenderQueue"; import { useRenderQueue } from "./components/renders/useRenderQueue"; @@ -277,7 +276,6 @@ export function StudioApp() { }, []); const [editingFile, setEditingFile] = useState(null); - const [rightTab, setRightTab] = useState<"code" | "renders">("code"); const [activeCompPath, setActiveCompPath] = useState(null); const [fileTree, setFileTree] = useState([]); const [compIdToSrc, setCompIdToSrc] = useState>(new Map()); @@ -288,9 +286,11 @@ export function StudioApp() { const [rightWidth, setRightWidth] = useState(400); const [leftCollapsed, setLeftCollapsed] = useState(false); const [rightCollapsed, setRightCollapsed] = useState(false); - const panelDragRef = useRef<{ side: "left" | "right"; startX: number; startW: number } | null>( - null, - ); + const panelDragRef = useRef<{ + side: "left" | "right"; + startX: number; + startW: number; + } | null>(null); // Derive active preview URL from composition path (for drilled-down thumbnails) const activePreviewUrl = activeCompPath @@ -540,31 +540,9 @@ export function StudioApp() {
{/* Header bar */}
- {/* Left: back button + project name */} + {/* Left: project name */}
- - / - {projectId} + {projectId}
{/* Right: toolbar buttons */}
@@ -591,35 +569,27 @@ export function StudioApp() { -
@@ -649,6 +619,24 @@ export function StudioApp() { .then((data) => setEditingFile({ path: comp, content: data.content })) .catch(() => {}); }} + fileTree={fileTree} + editingFile={editingFile} + onSelectFile={handleFileSelect} + codeChildren={ + editingFile ? ( + isMediaFile(editingFile.path) ? ( + + ) : ( + + ) + ) : undefined + } + onLint={handleLint} + linting={linting} /> )} @@ -682,80 +670,26 @@ export function StudioApp() { />
- {/* Right resize handle */} - {!rightCollapsed && ( -
handlePanelResizeStart("right", e)} - onPointerMove={handlePanelResizeMove} - onPointerUp={handlePanelResizeEnd} - /> - )} - - {/* Right panel: Code + Renders tabs (resizable, collapsible) */} + {/* Right panel: Renders-only (resizable, collapsible via header Renders button) */} {!rightCollapsed && ( -
- {/* Tab bar */} -
- - -
- - {/* Tab content */} - {rightTab === "code" ? ( -
- {/* File tree sidebar */} - {fileTree.length > 0 && ( -
- -
- )} - {/* Code editor or media preview */} -
- {editingFile ? ( - isMediaFile(editingFile.path) ? ( - - ) : ( - - ) - ) : ( -
- Select a file to edit -
- )} -
+ <> +
handlePanelResizeStart("right", e)} + onPointerMove={handlePanelResizeMove} + onPointerUp={handlePanelResizeEnd} + /> +
+
+ + Renders + {renderQueue.jobs.length > 0 ? ` (${renderQueue.jobs.length})` : ""} +
- ) : ( renderQueue.startRender(30, "standard", format)} isRendering={renderQueue.isRendering} /> - )} -
+
+ )}
diff --git a/packages/studio/src/components/sidebar/LeftSidebar.tsx b/packages/studio/src/components/sidebar/LeftSidebar.tsx index 9aedd1891..58a535511 100644 --- a/packages/studio/src/components/sidebar/LeftSidebar.tsx +++ b/packages/studio/src/components/sidebar/LeftSidebar.tsx @@ -1,15 +1,18 @@ -import { memo, useState, useCallback } from "react"; +import { memo, useState, useCallback, type ReactNode } from "react"; import { useMountEffect } from "../../hooks/useMountEffect"; import { CompositionsTab } from "./CompositionsTab"; import { AssetsTab } from "./AssetsTab"; +import { FileTree } from "../editor/FileTree"; -type SidebarTab = "compositions" | "assets"; +type SidebarTab = "compositions" | "assets" | "code"; const STORAGE_KEY = "hf-studio-sidebar-tab"; function getPersistedTab(): SidebarTab { const stored = localStorage.getItem(STORAGE_KEY); - return stored === "assets" ? "assets" : "compositions"; + if (stored === "assets") return "assets"; + if (stored === "code") return "code"; + return "compositions"; } interface LeftSidebarProps { @@ -20,6 +23,12 @@ interface LeftSidebarProps { activeComposition: string | null; onSelectComposition: (comp: string) => void; onImportFiles?: (files: FileList) => void; + fileTree?: string[]; + editingFile?: { path: string; content: string | null } | null; + onSelectFile?: (path: string) => void; + codeChildren?: ReactNode; + onLint?: () => void; + linting?: boolean; } export const LeftSidebar = memo(function LeftSidebar({ @@ -30,6 +39,12 @@ export const LeftSidebar = memo(function LeftSidebar({ activeComposition, onSelectComposition, onImportFiles, + fileTree: fileProp, + editingFile, + onSelectFile, + codeChildren, + onLint, + linting, }: LeftSidebarProps) { const [tab, setTab] = useState(getPersistedTab); @@ -84,19 +99,75 @@ export const LeftSidebar = memo(function LeftSidebar({ > Assets +
{/* Tab content */} - {tab === "compositions" ? ( + {tab === "compositions" && ( - ) : ( + )} + {tab === "assets" && ( )} + {tab === "code" && ( +
+ {(fileProp?.length ?? 0) > 0 && ( +
+ {})} + /> +
+ )} +
+ {codeChildren ?? ( +
+ Select a file to edit +
+ )} +
+
+ )} + + {/* Lint button pinned at the bottom */} + {onLint && ( +
+ +
+ )}
); }); From cbfa8f796766d4180f8084802e5ee09e517ce5b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Tue, 31 Mar 2026 00:28:46 +0200 Subject: [PATCH 2/6] fix(studio): equal-width tabs, 50vw max left panel, auto-expand on file --- packages/studio/src/App.tsx | 14 +++++++------- .../studio/src/components/sidebar/LeftSidebar.tsx | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/studio/src/App.tsx b/packages/studio/src/App.tsx index e0cb4b978..addc37115 100644 --- a/packages/studio/src/App.tsx +++ b/packages/studio/src/App.tsx @@ -431,6 +431,8 @@ export function StudioApp() { const handleFileSelect = useCallback((path: string) => { const pid = projectIdRef.current; if (!pid) return; + // Expand left panel to 50vw when opening a file in Code tab + setLeftWidth((prev) => Math.max(prev, Math.floor(window.innerWidth * 0.5))); // Skip fetching binary content for media files — just set the path for preview if (isMediaFile(path)) { setEditingFile({ path, content: null }); @@ -509,9 +511,13 @@ export function StudioApp() { const drag = panelDragRef.current; if (!drag) return; const delta = e.clientX - drag.startX; + const maxLeft = Math.floor(window.innerWidth * 0.5); const newW = Math.max( 160, - Math.min(600, drag.startW + (drag.side === "left" ? delta : -delta)), + Math.min( + drag.side === "left" ? maxLeft : 600, + drag.startW + (drag.side === "left" ? delta : -delta), + ), ); if (drag.side === "left") setLeftWidth(newW); else setRightWidth(newW); @@ -684,12 +690,6 @@ export function StudioApp() { className="flex flex-col border-l border-neutral-800 bg-neutral-900 flex-shrink-0" style={{ width: rightWidth }} > -
- - Renders - {renderQueue.jobs.length > 0 ? ` (${renderQueue.jobs.length})` : ""} - -
selectTab("code")} - className={`ml-auto px-3 py-2 text-[11px] font-medium transition-colors ${ + className={`flex-1 py-2 text-[11px] font-medium transition-colors ${ tab === "code" ? "text-neutral-200 border-b-2 border-[#3CE6AC]" : "text-neutral-500 hover:text-neutral-400" From 86952005b0de82e97071b37966de2b82e08256ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Tue, 31 Mar 2026 00:32:57 +0200 Subject: [PATCH 3/6] feat(studio): language badge icons in file tree (VS Code style) --- .../studio/src/components/editor/FileTree.tsx | 96 ++++++++++++------- 1 file changed, 59 insertions(+), 37 deletions(-) diff --git a/packages/studio/src/components/editor/FileTree.tsx b/packages/studio/src/components/editor/FileTree.tsx index b416c68c5..ea609d76e 100644 --- a/packages/studio/src/components/editor/FileTree.tsx +++ b/packages/studio/src/components/editor/FileTree.tsx @@ -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 = { - 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 Code–style language badge: colored rounded rect with a 2–3 letter label. */ +function Badge({ label, bg, text = "#fff" }: { label: string; bg: string; text?: string }) { + return ( + + {label} + + ); +} -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 ; + if (ext === "js" || ext === "mjs" || ext === "cjs") + return ; + if (ext === "ts" || ext === "mts") return ; + if (ext === "css") return ; + if (ext === "json") return ; + if (ext === "md" || ext === "mdx") return ; + if (ext === "svg") return ; + if (ext === "wav" || ext === "mp3" || ext === "ogg" || ext === "m4a") + return ; + if (ext === "mp4" || ext === "webm" || ext === "mov") + return ; + if (ext === "png" || ext === "jpg" || ext === "jpeg" || ext === "webp" || ext === "gif") + return ; + if (ext === "woff" || ext === "woff2" || ext === "ttf" || ext === "otf") + return ; + if (ext === "txt") return ; + // Generic document + return ( + + + + + ); } 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` }} > - + {node.name} ); From 4302d822f87e6c09cbae639b468cf4a5acd31889 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Tue, 31 Mar 2026 00:42:34 +0200 Subject: [PATCH 4/6] feat(studio): renders hidden by default; remove redundant title from renders panel --- packages/studio/src/App.tsx | 2 +- packages/studio/src/components/renders/RenderQueue.tsx | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/studio/src/App.tsx b/packages/studio/src/App.tsx index addc37115..c28e69dbe 100644 --- a/packages/studio/src/App.tsx +++ b/packages/studio/src/App.tsx @@ -285,7 +285,7 @@ export function StudioApp() { const [leftWidth, setLeftWidth] = useState(240); const [rightWidth, setRightWidth] = useState(400); const [leftCollapsed, setLeftCollapsed] = useState(false); - const [rightCollapsed, setRightCollapsed] = useState(false); + const [rightCollapsed, setRightCollapsed] = useState(true); const panelDragRef = useRef<{ side: "left" | "right"; startX: number; diff --git a/packages/studio/src/components/renders/RenderQueue.tsx b/packages/studio/src/components/renders/RenderQueue.tsx index 522fbf662..9a6e23a53 100644 --- a/packages/studio/src/components/renders/RenderQueue.tsx +++ b/packages/studio/src/components/renders/RenderQueue.tsx @@ -63,11 +63,8 @@ export const RenderQueue = memo(function RenderQueue({ return (
- {/* Header */} -
- - Renders ({jobs.length}) - + {/* Header — no title, already shown in header button */} +
{completedCount > 0 && (
From 60fec9f20edb073339f3c781581e46802b9ce43a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Tue, 31 Mar 2026 00:55:43 +0200 Subject: [PATCH 6/6] refactor(studio): replace ExpandOnHover with inline autoplay on thumbnails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Delete ExpandOnHover.tsx and ExpandedVideoPreview.tsx — replaced with simpler inline hover patterns: - CompositionsTab: hover shows a small iframe playing the composition in the same thumbnail cell (scaled 1920→80px) - AssetsTab: hover shows autoplay