mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
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:
@@ -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>
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user