import { useEffect, useMemo, useState } from 'react'; import { useT } from '../i18n'; import type { Dict } from '../i18n/types'; import { projectFileUrl } from '../providers/registry'; import type { ProjectFile, ProjectFileKind } from '../types'; import { Icon } from './Icon'; type TranslateFn = (key: keyof Dict, vars?: Record) => string; interface Props { projectId: string; files: ProjectFile[]; onRefreshFiles: () => Promise | void; onOpenFile: (name: string) => void; onDeleteFile: (name: string) => void; onUpload: () => void; onPaste: () => void; onNewSketch: () => void; } type Section = 'pages' | 'scripts' | 'images' | 'sketches' | 'other'; const SECTION_LABEL_KEY: Record = { pages: 'designFiles.sectionPages', scripts: 'designFiles.sectionScripts', images: 'designFiles.sectionImages', sketches: 'designFiles.sectionSketches', other: 'designFiles.sectionOther', }; const SECTION_ORDER: Section[] = ['pages', 'sketches', 'scripts', 'images', 'other']; /** * Full-panel browser for a project's `.od/projects//` folder. Mirrors * Claude Design's "Design Files" surface: grouped sections, hover-revealed * row menu, drop-files footer, and (when a row is selected) a right-side * preview pane. Triggered as a sticky first tab in FileWorkspace. */ export function DesignFilesPanel({ projectId, files, onRefreshFiles, onOpenFile, onDeleteFile, onUpload, onPaste, onNewSketch, }: Props) { const t = useT(); const [refreshing, setRefreshing] = useState(false); const [hover, setHover] = useState(null); const [menuPos, setMenuPos] = useState<{ name: string; top: number; left: number } | null>(null); const [preview, setPreview] = useState(null); const grouped = useMemo(() => { const groups: Record = { pages: [], sketches: [], scripts: [], images: [], other: [], }; const sorted = [...files].sort((a, b) => b.mtime - a.mtime); for (const f of sorted) { groups[sectionFor(f)].push(f); } return groups; }, [files]); const previewFile = useMemo( () => files.find((f) => f.name === preview) ?? null, [preview, files], ); // Close the row menu on outside click / escape. useEffect(() => { if (!menuPos) return; const close = () => setMenuPos(null); const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') close(); }; window.addEventListener('mousedown', close); window.addEventListener('keydown', onKey); return () => { window.removeEventListener('mousedown', close); window.removeEventListener('keydown', onKey); }; }, [menuPos]); async function handleRefresh() { setRefreshing(true); try { await onRefreshFiles(); } finally { setRefreshing(false); } } return (
{t('designFiles.crumbs')}
{files.length === 0 ? (
{t('designFiles.empty')}
) : ( SECTION_ORDER.filter((s) => grouped[s].length > 0).map((section) => (
{t(SECTION_LABEL_KEY[section])}
{grouped[section].map((f) => { const active = preview === f.name; const isHovered = hover === f.name; return ( ); })}
)) )}
{t('designFiles.dropTitle')} {t('designFiles.dropDesc')}
{preview && previewFile ? ( onOpenFile(previewFile.name)} onClose={() => setPreview(null)} /> ) : null} {menuPos ? (
e.stopPropagation()} >
) : null}
); } function DfPreview({ projectId, file, onOpen, onClose, }: { projectId: string; file: ProjectFile; onOpen: () => void; onClose: () => void; }) { const t = useT(); const url = projectFileUrl(projectId, file.name); return (