diff --git a/src/components/workspace/FileTree.tsx b/src/components/workspace/FileTree.tsx index 7ff6fa6..a0df039 100644 --- a/src/components/workspace/FileTree.tsx +++ b/src/components/workspace/FileTree.tsx @@ -1,5 +1,5 @@ import { useState, useCallback, useEffect } from 'react' -import { ChevronRight, ChevronDown, Folder, FolderOpen, FileText } from 'lucide-react' +import { ChevronRight, ChevronDown, Folder, FolderOpen, FileText, Star } from 'lucide-react' import { motion, AnimatePresence } from 'framer-motion' import type { DirectoryEntry } from '~/lib/workspace-fs' @@ -18,6 +18,8 @@ interface FileTreeProps { selectedPath: string | null onSelect: (path: string, type: 'file' | 'directory') => void onLoadDirectory?: (path: string) => Promise + onStar?: (path: string) => void + starredPaths?: Set level?: number } @@ -26,10 +28,13 @@ interface FileTreeItemProps { selectedPath: string | null onSelect: (path: string, type: 'file' | 'directory') => void onLoadDirectory?: (path: string) => Promise + onStar?: (path: string) => void + starredPaths?: Set level: number } -function FileTreeItem({ entry, selectedPath, onSelect, onLoadDirectory, level }: FileTreeItemProps) { +function FileTreeItem({ entry, selectedPath, onSelect, onLoadDirectory, onStar, starredPaths, level }: FileTreeItemProps) { + const isStarred = starredPaths?.has(entry.path) ?? false const [expanded, setExpanded] = useState(false) const [children, setChildren] = useState([]) const [loading, setLoading] = useState(false) @@ -90,7 +95,7 @@ function FileTreeItem({ entry, selectedPath, onSelect, onLoadDirectory, level }: )} + + {/* Star button for files */} + {!isDirectory && onStar && ( + + )} {/* Children */} @@ -172,6 +195,8 @@ function FileTreeItem({ entry, selectedPath, onSelect, onLoadDirectory, level }: selectedPath={selectedPath} onSelect={onSelect} onLoadDirectory={onLoadDirectory} + onStar={onStar} + starredPaths={starredPaths} level={level + 1} /> )) @@ -187,7 +212,7 @@ function FileTreeItem({ entry, selectedPath, onSelect, onLoadDirectory, level }: ) } -export function FileTree({ entries, selectedPath, onSelect, onLoadDirectory, level = 0 }: FileTreeProps) { +export function FileTree({ entries, selectedPath, onSelect, onLoadDirectory, onStar, starredPaths, level = 0 }: FileTreeProps) { return (
{entries.map((entry) => ( @@ -197,6 +222,8 @@ export function FileTree({ entries, selectedPath, onSelect, onLoadDirectory, lev selectedPath={selectedPath} onSelect={onSelect} onLoadDirectory={onLoadDirectory} + onStar={onStar} + starredPaths={starredPaths} level={level} /> ))} diff --git a/src/routes/workspace/index.tsx b/src/routes/workspace/index.tsx index 41606a6..b1b5490 100644 --- a/src/routes/workspace/index.tsx +++ b/src/routes/workspace/index.tsx @@ -8,6 +8,8 @@ import { AlertCircle, PanelLeft, PanelLeftClose, + Star, + FileText, } from 'lucide-react' import { trpc } from '~/integrations/trpc/client' import { FileTree, MarkdownViewer } from '~/components/workspace' @@ -82,10 +84,13 @@ function WorkspacePage() { // Sidebar collapse state const [sidebarCollapsed, setSidebarCollapsed] = useState(false) + // Starred files state + const [starredPaths, setStarredPaths] = useState>(new Set()) + // Root entries for FileTree const rootEntries = workspacePath && pathValid ? (pathCache.get(workspacePath) || []) : [] - // Load saved path or default on mount + // Load saved path and starred files on mount useEffect(() => { const savedPath = localStorage.getItem('crabcrawl:workspacePath') if (savedPath) { @@ -95,6 +100,17 @@ function WorkspacePage() { } else { loadDefaultPath() } + + // Load starred files + const savedStarred = localStorage.getItem('crabcrawl:starredFiles') + if (savedStarred) { + try { + const parsed = JSON.parse(savedStarred) + setStarredPaths(new Set(parsed)) + } catch { + // ignore invalid JSON + } + } }, []) // Load entries when workspace path changes and is valid @@ -312,6 +328,21 @@ function WorkspacePage() { } } + // Handle starring/unstarring files + const handleStar = useCallback((filePath: string) => { + setStarredPaths((prev) => { + const next = new Set(prev) + if (next.has(filePath)) { + next.delete(filePath) + } else { + next.add(filePath) + } + // Persist to localStorage + localStorage.setItem('crabcrawl:starredFiles', JSON.stringify([...next])) + return next + }) + }, []) + return ( @@ -436,6 +467,92 @@ function WorkspacePage() {
+ {/* Starred files section */} + {starredPaths.size > 0 && ( + <> + {sidebarCollapsed ? ( + // Collapsed: stacked file icons +
+ {[...starredPaths].slice(0, 5).map((filePath) => { + const fileName = filePath.split('/').pop() || filePath + const isSelected = selectedPath === filePath + return ( + + ) + })} + {starredPaths.size > 5 && ( + +{starredPaths.size - 5} + )} +
+ ) : ( + // Expanded: starred files list +
+
+ + + Starred + +
+
+ {[...starredPaths].map((filePath) => { + const fileName = filePath.split('/').pop() || filePath + const ext = fileName.includes('.') ? '.' + fileName.split('.').pop() : '' + const isSelected = selectedPath === filePath + return ( +
handleSelect(filePath, 'file')} + > + + + + {fileName} + +
+ ) + })} +
+
+ )} + + )} + {/* File tree */} {!sidebarCollapsed && (
@@ -445,6 +562,8 @@ function WorkspacePage() { selectedPath={selectedPath} onSelect={handleSelect} onLoadDirectory={handleLoadDirectory} + onStar={handleStar} + starredPaths={starredPaths} /> ) : (