import { useState, useMemo, useEffect, useCallback, useRef, memo } from 'react'; import { useNavigate, useLocation } from '@tanstack/react-router'; import { cn } from '@/lib/utils'; import { useAppStore, formatShortcut, type ThemeMode } from '@/store/app-store'; import { FolderOpen, Plus, Settings, FileText, LayoutGrid, Bot, Folder, X, PanelLeft, PanelLeftClose, ChevronDown, Redo2, Check, BookOpen, GripVertical, RotateCcw, Trash2, Undo2, UserCircle, MoreVertical, Palette, Monitor, Search, Bug, Activity, Recycle, Sparkles, Loader2, Terminal, Rocket, Zap, CheckCircle2, ArrowRight, Moon, Sun, } from 'lucide-react'; import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuLabel, } from '@/components/ui/dropdown-menu'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { useKeyboardShortcuts, useKeyboardShortcutsConfig, KeyboardShortcut, } from '@/hooks/use-keyboard-shortcuts'; import { getElectronAPI, Project, TrashedProject, RunningAgent } from '@/lib/electron'; import { initializeProject, hasAppSpec, hasAutomakerDir } from '@/lib/project-init'; import { toast } from 'sonner'; import { themeOptions } from '@/config/theme-options'; import type { SpecRegenerationEvent } from '@/types/electron'; import { DeleteProjectDialog } from '@/components/views/settings-view/components/delete-project-dialog'; import { NewProjectModal } from '@/components/dialogs/new-project-modal'; import { CreateSpecDialog } from '@/components/views/spec-view/dialogs'; import type { FeatureCount } from '@/components/views/spec-view/types'; import { DndContext, closestCenter } from '@dnd-kit/core'; import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'; import { getHttpApiClient } from '@/lib/http-api-client'; import type { StarterTemplate } from '@/lib/templates'; // Local imports from subfolder import type { NavSection, NavItem } from './sidebar/types'; import { SortableProjectItem, ThemeMenuItem, BugReportButton } from './sidebar/components'; import { PROJECT_DARK_THEMES, PROJECT_LIGHT_THEMES, SIDEBAR_FEATURE_FLAGS, } from './sidebar/constants'; import { useThemePreview, useSidebarAutoCollapse, useDragAndDrop } from './sidebar/hooks'; export function Sidebar() { const navigate = useNavigate(); const location = useLocation(); const { projects, trashedProjects, currentProject, sidebarOpen, projectHistory, upsertAndSetCurrentProject, setCurrentProject, toggleSidebar, restoreTrashedProject, deleteTrashedProject, emptyTrash, reorderProjects, cyclePrevProject, cycleNextProject, clearProjectHistory, setProjectTheme, setTheme, setPreviewTheme, theme: globalTheme, moveProjectToTrash, specCreatingForProject, setSpecCreatingForProject, } = useAppStore(); // Environment variable flags for hiding sidebar items const { hideTerminal, hideWiki, hideRunningAgents, hideContext, hideSpecEditor, hideAiProfiles } = SIDEBAR_FEATURE_FLAGS; // Get customizable keyboard shortcuts const shortcuts = useKeyboardShortcutsConfig(); // State for project picker dropdown const [isProjectPickerOpen, setIsProjectPickerOpen] = useState(false); const [projectSearchQuery, setProjectSearchQuery] = useState(''); const [selectedProjectIndex, setSelectedProjectIndex] = useState(0); const [showTrashDialog, setShowTrashDialog] = useState(false); const [activeTrashId, setActiveTrashId] = useState(null); const [isEmptyingTrash, setIsEmptyingTrash] = useState(false); // State for delete project confirmation dialog const [showDeleteProjectDialog, setShowDeleteProjectDialog] = useState(false); // State for running agents count const [runningAgentsCount, setRunningAgentsCount] = useState(0); // State for new project modal const [showNewProjectModal, setShowNewProjectModal] = useState(false); const [isCreatingProject, setIsCreatingProject] = useState(false); // State for new project onboarding dialog const [showOnboardingDialog, setShowOnboardingDialog] = useState(false); const [newProjectName, setNewProjectName] = useState(''); const [newProjectPath, setNewProjectPath] = useState(''); // State for new project setup dialog const [showSetupDialog, setShowSetupDialog] = useState(false); const [setupProjectPath, setSetupProjectPath] = useState(''); const [projectOverview, setProjectOverview] = useState(''); const [generateFeatures, setGenerateFeatures] = useState(true); const [analyzeProject, setAnalyzeProject] = useState(true); const [featureCount, setFeatureCount] = useState(50); const [showSpecIndicator, setShowSpecIndicator] = useState(true); // Debounced preview theme handlers const { handlePreviewEnter, handlePreviewLeave } = useThemePreview({ setPreviewTheme }); // Derive isCreatingSpec from store state const isCreatingSpec = specCreatingForProject !== null; const creatingSpecProjectPath = specCreatingForProject; // Ref for project search input const projectSearchInputRef = useRef(null); // Auto-collapse sidebar on small screens useSidebarAutoCollapse({ sidebarOpen, toggleSidebar }); // Filtered projects based on search query const filteredProjects = useMemo(() => { if (!projectSearchQuery.trim()) { return projects; } const query = projectSearchQuery.toLowerCase(); return projects.filter((project) => project.name.toLowerCase().includes(query)); }, [projects, projectSearchQuery]); // Reset selection when filtered results change useEffect(() => { setSelectedProjectIndex(0); }, [filteredProjects.length, projectSearchQuery]); // Reset search query when dropdown closes useEffect(() => { if (!isProjectPickerOpen) { setProjectSearchQuery(''); setSelectedProjectIndex(0); } }, [isProjectPickerOpen]); // Focus the search input when dropdown opens useEffect(() => { if (isProjectPickerOpen) { // Small delay to ensure the dropdown is rendered setTimeout(() => { projectSearchInputRef.current?.focus(); }, 0); } }, [isProjectPickerOpen]); // Drag-and-drop for project reordering const { sensors, handleDragEnd } = useDragAndDrop({ projects, reorderProjects }); // Subscribe to spec regeneration events useEffect(() => { const api = getElectronAPI(); if (!api.specRegeneration) return; const unsubscribe = api.specRegeneration.onEvent((event: SpecRegenerationEvent) => { console.log( '[Sidebar] Spec regeneration event:', event.type, 'for project:', event.projectPath ); // Only handle events for the project we're currently setting up if (event.projectPath !== creatingSpecProjectPath && event.projectPath !== setupProjectPath) { console.log('[Sidebar] Ignoring event - not for project being set up'); return; } if (event.type === 'spec_regeneration_complete') { setSpecCreatingForProject(null); setShowSetupDialog(false); setProjectOverview(''); setSetupProjectPath(''); // Clear onboarding state if we came from onboarding setNewProjectName(''); setNewProjectPath(''); toast.success('App specification created', { description: 'Your project is now set up and ready to go!', }); } else if (event.type === 'spec_regeneration_error') { setSpecCreatingForProject(null); toast.error('Failed to create specification', { description: event.error, }); } }); return () => { unsubscribe(); }; }, [creatingSpecProjectPath, setupProjectPath, setSpecCreatingForProject]); // Fetch running agents count function - used for initial load and event-driven updates const fetchRunningAgentsCount = useCallback(async () => { try { const api = getElectronAPI(); if (api.runningAgents) { const result = await api.runningAgents.getAll(); if (result.success && result.runningAgents) { setRunningAgentsCount(result.runningAgents.length); } } } catch (error) { console.error('[Sidebar] Error fetching running agents count:', error); } }, []); // Subscribe to auto-mode events to update running agents count in real-time useEffect(() => { const api = getElectronAPI(); if (!api.autoMode) { // If autoMode is not available, still fetch initial count fetchRunningAgentsCount(); return; } // Initial fetch on mount fetchRunningAgentsCount(); const unsubscribe = api.autoMode.onEvent((event) => { // When a feature starts, completes, or errors, refresh the count if ( event.type === 'auto_mode_feature_complete' || event.type === 'auto_mode_error' || event.type === 'auto_mode_feature_start' ) { fetchRunningAgentsCount(); } }); return () => { unsubscribe(); }; }, [fetchRunningAgentsCount]); // Handle creating initial spec for new project const handleCreateInitialSpec = useCallback(async () => { if (!setupProjectPath || !projectOverview.trim()) return; // Set store state immediately so the loader shows up right away setSpecCreatingForProject(setupProjectPath); setShowSpecIndicator(true); setShowSetupDialog(false); try { const api = getElectronAPI(); if (!api.specRegeneration) { toast.error('Spec regeneration not available'); setSpecCreatingForProject(null); return; } const result = await api.specRegeneration.create( setupProjectPath, projectOverview.trim(), generateFeatures, analyzeProject, generateFeatures ? featureCount : undefined // only pass maxFeatures if generating features ); if (!result.success) { console.error('[Sidebar] Failed to start spec creation:', result.error); setSpecCreatingForProject(null); toast.error('Failed to create specification', { description: result.error, }); } else { // Show processing toast to inform user toast.info('Generating app specification...', { description: "This may take a minute. You'll be notified when complete.", }); } // If successful, we'll wait for the events to update the state } catch (error) { console.error('[Sidebar] Failed to create spec:', error); setSpecCreatingForProject(null); toast.error('Failed to create specification', { description: error instanceof Error ? error.message : 'Unknown error', }); } }, [ setupProjectPath, projectOverview, generateFeatures, analyzeProject, featureCount, setSpecCreatingForProject, ]); // Handle skipping setup const handleSkipSetup = useCallback(() => { setShowSetupDialog(false); setProjectOverview(''); setSetupProjectPath(''); // Clear onboarding state if we came from onboarding if (newProjectPath) { setNewProjectName(''); setNewProjectPath(''); } toast.info('Setup skipped', { description: 'You can set up your app_spec.txt later from the Spec view.', }); }, [newProjectPath]); // Handle onboarding dialog - generate spec const handleOnboardingGenerateSpec = useCallback(() => { setShowOnboardingDialog(false); // Navigate to the setup dialog flow setSetupProjectPath(newProjectPath); setProjectOverview(''); setShowSetupDialog(true); }, [newProjectPath]); // Handle onboarding dialog - skip const handleOnboardingSkip = useCallback(() => { setShowOnboardingDialog(false); setNewProjectName(''); setNewProjectPath(''); toast.info('You can generate your app_spec.txt anytime from the Spec view', { description: 'Your project is ready to use!', }); }, []); /** * Create a blank project with just .automaker directory structure */ const handleCreateBlankProject = useCallback( async (projectName: string, parentDir: string) => { setIsCreatingProject(true); try { const api = getElectronAPI(); const projectPath = `${parentDir}/${projectName}`; // Create project directory const mkdirResult = await api.mkdir(projectPath); if (!mkdirResult.success) { toast.error('Failed to create project directory', { description: mkdirResult.error || 'Unknown error occurred', }); return; } // Initialize .automaker directory with all necessary files const initResult = await initializeProject(projectPath); if (!initResult.success) { toast.error('Failed to initialize project', { description: initResult.error || 'Unknown error occurred', }); return; } // Update the app_spec.txt with the project name // Note: Must follow XML format as defined in apps/server/src/lib/app-spec-format.ts await api.writeFile( `${projectPath}/.automaker/app_spec.txt`, ` ${projectName} Describe your project here. This file will be analyzed by an AI agent to understand your project structure and tech stack. ` ); const trashedProject = trashedProjects.find((p) => p.path === projectPath); const effectiveTheme = (trashedProject?.theme as ThemeMode | undefined) || (currentProject?.theme as ThemeMode | undefined) || globalTheme; const project = upsertAndSetCurrentProject(projectPath, projectName, effectiveTheme); setShowNewProjectModal(false); // Show onboarding dialog for new project setNewProjectName(projectName); setNewProjectPath(projectPath); setShowOnboardingDialog(true); toast.success('Project created', { description: `Created ${projectName} with .automaker directory`, }); } catch (error) { console.error('[Sidebar] Failed to create project:', error); toast.error('Failed to create project', { description: error instanceof Error ? error.message : 'Unknown error', }); } finally { setIsCreatingProject(false); } }, [trashedProjects, currentProject, globalTheme, upsertAndSetCurrentProject] ); /** * Create a project from a GitHub starter template */ const handleCreateFromTemplate = useCallback( async (template: StarterTemplate, projectName: string, parentDir: string) => { setIsCreatingProject(true); try { const httpClient = getHttpApiClient(); const api = getElectronAPI(); // Clone the template repository const cloneResult = await httpClient.templates.clone( template.repoUrl, projectName, parentDir ); if (!cloneResult.success || !cloneResult.projectPath) { toast.error('Failed to clone template', { description: cloneResult.error || 'Unknown error occurred', }); return; } const projectPath = cloneResult.projectPath; // Initialize .automaker directory with all necessary files const initResult = await initializeProject(projectPath); if (!initResult.success) { toast.error('Failed to initialize project', { description: initResult.error || 'Unknown error occurred', }); return; } // Update the app_spec.txt with template-specific info // Note: Must follow XML format as defined in apps/server/src/lib/app-spec-format.ts await api.writeFile( `${projectPath}/.automaker/app_spec.txt`, ` ${projectName} This project was created from the "${template.name}" starter template. ${template.description} ${template.techStack.map((tech) => `${tech}`).join('\n ')} ${template.features.map((feature) => `${feature}`).join('\n ')} ` ); const trashedProject = trashedProjects.find((p) => p.path === projectPath); const effectiveTheme = (trashedProject?.theme as ThemeMode | undefined) || (currentProject?.theme as ThemeMode | undefined) || globalTheme; const project = upsertAndSetCurrentProject(projectPath, projectName, effectiveTheme); setShowNewProjectModal(false); // Show onboarding dialog for new project setNewProjectName(projectName); setNewProjectPath(projectPath); setShowOnboardingDialog(true); toast.success('Project created from template', { description: `Created ${projectName} from ${template.name}`, }); } catch (error) { console.error('[Sidebar] Failed to create project from template:', error); toast.error('Failed to create project', { description: error instanceof Error ? error.message : 'Unknown error', }); } finally { setIsCreatingProject(false); } }, [trashedProjects, currentProject, globalTheme, upsertAndSetCurrentProject] ); /** * Create a project from a custom GitHub URL */ const handleCreateFromCustomUrl = useCallback( async (repoUrl: string, projectName: string, parentDir: string) => { setIsCreatingProject(true); try { const httpClient = getHttpApiClient(); const api = getElectronAPI(); // Clone the repository const cloneResult = await httpClient.templates.clone(repoUrl, projectName, parentDir); if (!cloneResult.success || !cloneResult.projectPath) { toast.error('Failed to clone repository', { description: cloneResult.error || 'Unknown error occurred', }); return; } const projectPath = cloneResult.projectPath; // Initialize .automaker directory with all necessary files const initResult = await initializeProject(projectPath); if (!initResult.success) { toast.error('Failed to initialize project', { description: initResult.error || 'Unknown error occurred', }); return; } // Update the app_spec.txt with basic info // Note: Must follow XML format as defined in apps/server/src/lib/app-spec-format.ts await api.writeFile( `${projectPath}/.automaker/app_spec.txt`, ` ${projectName} This project was cloned from ${repoUrl}. The AI agent will analyze the project structure. ` ); const trashedProject = trashedProjects.find((p) => p.path === projectPath); const effectiveTheme = (trashedProject?.theme as ThemeMode | undefined) || (currentProject?.theme as ThemeMode | undefined) || globalTheme; const project = upsertAndSetCurrentProject(projectPath, projectName, effectiveTheme); setShowNewProjectModal(false); // Show onboarding dialog for new project setNewProjectName(projectName); setNewProjectPath(projectPath); setShowOnboardingDialog(true); toast.success('Project created from repository', { description: `Created ${projectName} from ${repoUrl}`, }); } catch (error) { console.error('[Sidebar] Failed to create project from URL:', error); toast.error('Failed to create project', { description: error instanceof Error ? error.message : 'Unknown error', }); } finally { setIsCreatingProject(false); } }, [trashedProjects, currentProject, globalTheme, upsertAndSetCurrentProject] ); // Handle bug report button click const handleBugReportClick = useCallback(() => { const api = getElectronAPI(); api.openExternalLink('https://github.com/AutoMaker-Org/automaker/issues'); }, []); /** * Opens the system folder selection dialog and initializes the selected project. * Used by both the 'O' keyboard shortcut and the folder icon button. */ const handleOpenFolder = useCallback(async () => { const api = getElectronAPI(); const result = await api.openDirectory(); if (!result.canceled && result.filePaths[0]) { const path = result.filePaths[0]; // Extract folder name from path (works on both Windows and Mac/Linux) const name = path.split(/[/\\]/).filter(Boolean).pop() || 'Untitled Project'; try { // Check if this is a brand new project (no .automaker directory) const hadAutomakerDir = await hasAutomakerDir(path); // Initialize the .automaker directory structure const initResult = await initializeProject(path); if (!initResult.success) { toast.error('Failed to initialize project', { description: initResult.error || 'Unknown error occurred', }); return; } // Upsert project and set as current (handles both create and update cases) // Theme preservation is handled by the store action const trashedProject = trashedProjects.find((p) => p.path === path); const effectiveTheme = (trashedProject?.theme as ThemeMode | undefined) || (currentProject?.theme as ThemeMode | undefined) || globalTheme; const project = upsertAndSetCurrentProject(path, name, effectiveTheme); // Check if app_spec.txt exists const specExists = await hasAppSpec(path); if (!hadAutomakerDir && !specExists) { // This is a brand new project - show setup dialog setSetupProjectPath(path); setShowSetupDialog(true); toast.success('Project opened', { description: `Opened ${name}. Let's set up your app specification!`, }); } else if (initResult.createdFiles && initResult.createdFiles.length > 0) { toast.success(initResult.isNewProject ? 'Project initialized' : 'Project updated', { description: `Set up ${initResult.createdFiles.length} file(s) in .automaker`, }); } else { toast.success('Project opened', { description: `Opened ${name}`, }); } } catch (error) { console.error('[Sidebar] Failed to open project:', error); toast.error('Failed to open project', { description: error instanceof Error ? error.message : 'Unknown error', }); } } }, [trashedProjects, upsertAndSetCurrentProject, currentProject, globalTheme]); const handleRestoreProject = useCallback( (projectId: string) => { restoreTrashedProject(projectId); toast.success('Project restored', { description: 'Added back to your project list.', }); setShowTrashDialog(false); }, [restoreTrashedProject] ); const handleDeleteProjectFromDisk = useCallback( async (trashedProject: TrashedProject) => { const confirmed = window.confirm( `Delete "${trashedProject.name}" from disk?\nThis sends the folder to your system Trash.` ); if (!confirmed) return; setActiveTrashId(trashedProject.id); try { const api = getElectronAPI(); if (!api.trashItem) { throw new Error('System Trash is not available in this build.'); } const result = await api.trashItem(trashedProject.path); if (!result.success) { throw new Error(result.error || 'Failed to delete project folder'); } deleteTrashedProject(trashedProject.id); toast.success('Project folder sent to system Trash', { description: trashedProject.path, }); } catch (error) { console.error('[Sidebar] Failed to delete project from disk:', error); toast.error('Failed to delete project folder', { description: error instanceof Error ? error.message : 'Unknown error', }); } finally { setActiveTrashId(null); } }, [deleteTrashedProject] ); const handleEmptyTrash = useCallback(() => { if (trashedProjects.length === 0) { setShowTrashDialog(false); return; } const confirmed = window.confirm( 'Clear all projects from recycle bin? This does not delete folders from disk.' ); if (!confirmed) return; setIsEmptyingTrash(true); try { emptyTrash(); toast.success('Recycle bin cleared'); setShowTrashDialog(false); } finally { setIsEmptyingTrash(false); } }, [emptyTrash, trashedProjects.length]); const navSections: NavSection[] = useMemo(() => { const allToolsItems: NavItem[] = [ { id: 'spec', label: 'Spec Editor', icon: FileText, shortcut: shortcuts.spec, }, { id: 'context', label: 'Context', icon: BookOpen, shortcut: shortcuts.context, }, { id: 'profiles', label: 'AI Profiles', icon: UserCircle, shortcut: shortcuts.profiles, }, ]; // Filter out hidden items const visibleToolsItems = allToolsItems.filter((item) => { if (item.id === 'spec' && hideSpecEditor) { return false; } if (item.id === 'context' && hideContext) { return false; } if (item.id === 'profiles' && hideAiProfiles) { return false; } return true; }); // Build project items - Terminal is conditionally included const projectItems: NavItem[] = [ { id: 'board', label: 'Kanban Board', icon: LayoutGrid, shortcut: shortcuts.board, }, { id: 'agent', label: 'Agent Runner', icon: Bot, shortcut: shortcuts.agent, }, ]; // Add Terminal to Project section if not hidden if (!hideTerminal) { projectItems.push({ id: 'terminal', label: 'Terminal', icon: Terminal, shortcut: shortcuts.terminal, }); } return [ { label: 'Project', items: projectItems, }, { label: 'Tools', items: visibleToolsItems, }, ]; }, [shortcuts, hideSpecEditor, hideContext, hideTerminal, hideAiProfiles]); // Handle selecting the currently highlighted project const selectHighlightedProject = useCallback(() => { if (filteredProjects.length > 0 && selectedProjectIndex < filteredProjects.length) { setCurrentProject(filteredProjects[selectedProjectIndex]); setIsProjectPickerOpen(false); } }, [filteredProjects, selectedProjectIndex, setCurrentProject]); // Handle keyboard events when project picker is open useEffect(() => { if (!isProjectPickerOpen) return; const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') { setIsProjectPickerOpen(false); } else if (event.key === 'Enter') { event.preventDefault(); selectHighlightedProject(); } else if (event.key === 'ArrowDown') { event.preventDefault(); setSelectedProjectIndex((prev) => (prev < filteredProjects.length - 1 ? prev + 1 : prev)); } else if (event.key === 'ArrowUp') { event.preventDefault(); setSelectedProjectIndex((prev) => (prev > 0 ? prev - 1 : prev)); } else if (event.key.toLowerCase() === 'p' && !event.metaKey && !event.ctrlKey) { // Toggle off when P is pressed (not with modifiers) while dropdown is open // Only if not typing in the search input if (document.activeElement !== projectSearchInputRef.current) { event.preventDefault(); setIsProjectPickerOpen(false); } } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [isProjectPickerOpen, selectHighlightedProject, filteredProjects.length]); // Build keyboard shortcuts for navigation const navigationShortcuts: KeyboardShortcut[] = useMemo(() => { const shortcutsList: KeyboardShortcut[] = []; // Sidebar toggle shortcut - always available shortcutsList.push({ key: shortcuts.toggleSidebar, action: () => toggleSidebar(), description: 'Toggle sidebar', }); // Open project shortcut - opens the folder selection dialog directly shortcutsList.push({ key: shortcuts.openProject, action: () => handleOpenFolder(), description: 'Open folder selection dialog', }); // Project picker shortcut - only when we have projects if (projects.length > 0) { shortcutsList.push({ key: shortcuts.projectPicker, action: () => setIsProjectPickerOpen((prev) => !prev), description: 'Toggle project picker', }); } // Project cycling shortcuts - only when we have project history if (projectHistory.length > 1) { shortcutsList.push({ key: shortcuts.cyclePrevProject, action: () => cyclePrevProject(), description: 'Cycle to previous project (MRU)', }); shortcutsList.push({ key: shortcuts.cycleNextProject, action: () => cycleNextProject(), description: 'Cycle to next project (LRU)', }); } // Only enable nav shortcuts if there's a current project if (currentProject) { navSections.forEach((section) => { section.items.forEach((item) => { if (item.shortcut) { shortcutsList.push({ key: item.shortcut, action: () => navigate({ to: `/${item.id}` as const }), description: `Navigate to ${item.label}`, }); } }); }); // Add settings shortcut shortcutsList.push({ key: shortcuts.settings, action: () => navigate({ to: '/settings' }), description: 'Navigate to Settings', }); } return shortcutsList; }, [ shortcuts, currentProject, navigate, toggleSidebar, projects.length, handleOpenFolder, projectHistory.length, cyclePrevProject, cycleNextProject, navSections, ]); // Register keyboard shortcuts useKeyboardShortcuts(navigationShortcuts); const isActiveRoute = (id: string) => { // Map view IDs to route paths const routePath = id === 'welcome' ? '/' : `/${id}`; return location.pathname === routePath; }; return ( ); }