/** * DashboardView - Main dashboard with "Productivity Luxury" aesthetic. * Inspired by Linear, Vercel, and Raycast design patterns. * Features: * - Subtle spotlight gradient * - Centralized command search with inline project filtering * - Border-first project cards with minimal backgrounds */ import React, { useEffect, useMemo, useState } from 'react'; import { api } from '@renderer/api'; import { useStore } from '@renderer/store'; import { createLogger } from '@shared/utils/logger'; import { useShallow } from 'zustand/react/shallow'; const logger = createLogger('Component:DashboardView'); import { formatDistanceToNow } from 'date-fns'; import { Command, FolderGit2, FolderOpen, GitBranch, Search, Settings } from 'lucide-react'; import type { RepositoryGroup } from '@renderer/types/data'; // ============================================================================= // Command Search Input // ============================================================================= interface CommandSearchProps { value: string; onChange: (value: string) => void; } const CommandSearch = ({ value, onChange }: Readonly): React.JSX.Element => { const [isFocused, setIsFocused] = useState(false); const { openCommandPalette, selectedProjectId } = useStore( useShallow((s) => ({ openCommandPalette: s.openCommandPalette, selectedProjectId: s.selectedProjectId, })) ); // Handle Cmd+K to open full command palette useEffect(() => { const handleKeyDown = (e: KeyboardEvent): void => { if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); openCommandPalette(); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [openCommandPalette]); return (
{/* Search container with glow effect on focus */}
onChange(e.target.value)} placeholder="Search projects..." className="flex-1 bg-transparent text-sm text-text outline-none placeholder:text-text-muted" onFocus={() => setIsFocused(true)} onBlur={() => setIsFocused(false)} /> {/* Keyboard shortcut badge - opens full command palette */}
); }; // ============================================================================= // Repository Card // ============================================================================= interface RepositoryCardProps { repo: RepositoryGroup; onClick: () => void; isHighlighted?: boolean; } /** * Truncate path to show ~/relative/path format */ function formatProjectPath(path: string): string { const p = path.replace(/\\/g, '/'); if (p.startsWith('/Users/') || p.startsWith('/home/')) { const parts = p.split('/').filter(Boolean); if (parts.length >= 2) { const rest = parts.slice(2).join('/'); return rest ? `~/${rest}` : '~'; } } if (isWindowsUserPath(path)) { const parts = p.split('/').filter(Boolean); if (parts.length >= 3) { const rest = parts.slice(3).join('/'); return rest ? `~/${rest}` : '~'; } } return p; } function isWindowsUserPath(input: string): boolean { if (input.length < 10) { return false; } const drive = input.charCodeAt(0); const hasDriveLetter = ((drive >= 65 && drive <= 90) || (drive >= 97 && drive <= 122)) && input[1] === ':'; return hasDriveLetter && input.startsWith('\\Users\\', 2); } const RepositoryCard = ({ repo, onClick, isHighlighted, }: Readonly): React.JSX.Element => { const lastActivity = repo.mostRecentSession ? formatDistanceToNow(new Date(repo.mostRecentSession), { addSuffix: true }) : 'No recent activity'; const worktreeCount = repo.worktrees.length; const hasMultipleWorktrees = worktreeCount > 1; // Get the path from the first worktree const projectPath = repo.worktrees[0]?.path || ''; const formattedPath = formatProjectPath(projectPath); return ( ); }; // ============================================================================= // Ghost Card (New Project) // ============================================================================= const NewProjectCard = (): React.JSX.Element => { const { repositoryGroups, selectRepository } = useStore( useShallow((s) => ({ repositoryGroups: s.repositoryGroups, selectRepository: s.selectRepository, })) ); const handleClick = async (): Promise => { try { const selectedPaths = await api.config.selectFolders(); if (!selectedPaths || selectedPaths.length === 0) { return; // User cancelled } const selectedPath = selectedPaths[0]; // Match selected path against known repository worktrees for (const repo of repositoryGroups) { for (const worktree of repo.worktrees) { if (worktree.path === selectedPath) { selectRepository(repo.id); return; } } } // No match found - open the folder in file manager as fallback const result = await api.openPath(selectedPath); if (!result.success) { logger.error('Failed to open folder:', result.error); } } catch (error) { logger.error('Error selecting folder:', error); } }; return ( ); }; // ============================================================================= // Projects Grid // ============================================================================= interface ProjectsGridProps { searchQuery: string; maxProjects?: number; } const ProjectsGrid = ({ searchQuery, maxProjects = 12, }: Readonly): React.JSX.Element => { const { repositoryGroups, repositoryGroupsLoading, fetchRepositoryGroups, selectRepository } = useStore( useShallow((s) => ({ repositoryGroups: s.repositoryGroups, repositoryGroupsLoading: s.repositoryGroupsLoading, fetchRepositoryGroups: s.fetchRepositoryGroups, selectRepository: s.selectRepository, })) ); useEffect(() => { if (repositoryGroups.length === 0) { void fetchRepositoryGroups(); } }, [repositoryGroups.length, fetchRepositoryGroups]); // Filter projects based on search query const filteredRepos = useMemo(() => { if (!searchQuery.trim()) { return repositoryGroups.slice(0, maxProjects); } const query = searchQuery.toLowerCase().trim(); return repositoryGroups .filter((repo) => { // Match by name if (repo.name.toLowerCase().includes(query)) return true; // Match by path const path = repo.worktrees[0]?.path || ''; if (path.toLowerCase().includes(query)) return true; return false; }) .slice(0, maxProjects); }, [repositoryGroups, searchQuery, maxProjects]); if (repositoryGroupsLoading) { // Organic widths per card — no repeating stamp const titleWidths = [60, 66, 50, 55, 75, 45, 40, 65]; const pathWidths = [80, 75, 85, 66, 70, 80, 60, 72]; return (
{Array.from({ length: 8 }).map((_, i) => (
{/* Icon placeholder */}
{/* Title placeholder */}
{/* Path placeholder */}
{/* Meta row placeholder */}
))}
); } if (filteredRepos.length === 0 && searchQuery.trim()) { return (

No projects found

No matches for "{searchQuery}"

); } if (repositoryGroups.length === 0) { return (

No projects found

~/.claude/projects/

); } return (
{filteredRepos.map((repo) => ( selectRepository(repo.id)} isHighlighted={!!searchQuery.trim()} /> ))} {!searchQuery.trim() && }
); }; // ============================================================================= // Dashboard View // ============================================================================= export const DashboardView = (): React.JSX.Element => { const [searchQuery, setSearchQuery] = useState(''); const openSettingsTab = useStore((s) => s.openSettingsTab); return (
{/* Spotlight gradient background */} ); };