feat(studio): add left sidebar with Compositions and Assets tabs (#94)

## Summary
- Add collapsible left sidebar with Compositions and Assets tabs
- Compositions tab lists all sub-compositions with thumbnail previews and navigation
- Clicking a composition opens it in the code editor AND navigates the preview to show that composition
- Assets tab categorizes project files by type (images, fonts, media)
- Race condition guard on composition fetch with functional state update
- Thumbnail error fallback shows name initial instead of empty box

## Test plan
- [x] Sidebar opens/closes with smooth transition
- [x] Compositions tab lists files from project API
- [x] Clicking a composition changes the preview iframe to that composition
- [x] Assets tab categorizes files by type

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
Miguel Ángel
2026-03-28 20:55:15 +01:00
committed by GitHub
parent 0167e69374
commit 9ae239d177
4 changed files with 439 additions and 2 deletions
@@ -0,0 +1,99 @@
import { memo, useState, useCallback, useEffect } from "react";
import { CompositionsTab } from "./CompositionsTab";
import { AssetsTab } from "./AssetsTab";
type SidebarTab = "compositions" | "assets";
const STORAGE_KEY = "hf-studio-sidebar-tab";
function getPersistedTab(): SidebarTab {
const stored = localStorage.getItem(STORAGE_KEY);
return stored === "assets" ? "assets" : "compositions";
}
interface LeftSidebarProps {
projectId: string;
compositions: string[];
assets: string[];
activeComposition: string | null;
onSelectComposition: (comp: string) => void;
onImportFiles?: (files: FileList) => void;
}
export const LeftSidebar = memo(function LeftSidebar({
projectId,
compositions,
assets,
activeComposition,
onSelectComposition,
onImportFiles,
}: LeftSidebarProps) {
const [tab, setTab] = useState<SidebarTab>(getPersistedTab);
const selectTab = useCallback((t: SidebarTab) => {
setTab(t);
localStorage.setItem(STORAGE_KEY, t);
}, []);
// Keyboard shortcuts: Cmd+1 for Compositions, Cmd+2 for Assets
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (!e.metaKey && !e.ctrlKey) return;
if (e.key === "1") {
e.preventDefault();
selectTab("compositions");
}
if (e.key === "2") {
e.preventDefault();
selectTab("assets");
}
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, [selectTab]);
return (
<div
className="flex flex-col h-full bg-neutral-950 border-r border-neutral-800/50"
style={{ width: 240 }}
>
{/* Tabs */}
<div className="flex border-b border-neutral-800/50 flex-shrink-0">
<button
type="button"
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-500 hover:text-neutral-400"
}`}
>
Compositions
</button>
<button
type="button"
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-500 hover:text-neutral-400"
}`}
>
Assets
</button>
</div>
{/* Tab content */}
{tab === "compositions" ? (
<CompositionsTab
projectId={projectId}
compositions={compositions}
activeComposition={activeComposition}
onSelect={onSelectComposition}
/>
) : (
<AssetsTab projectId={projectId} assets={assets} onImport={onImportFiles} />
)}
</div>
);
});