From 433e6016c3ad25dc01cb06eb96554d591920244a Mon Sep 17 00:00:00 2001 From: Stefan de Vogelaere Date: Thu, 22 Jan 2026 18:52:30 +0100 Subject: [PATCH] refactor(ui): consolidate unified-sidebar into sidebar folder Merge the unified-sidebar implementation into the standard sidebar folder structure. The unified sidebar becomes the canonical sidebar with improved features including collapsible sections, scroll indicators, and enhanced mobile support. - Delete old sidebar.tsx - Move unified-sidebar components to sidebar/components - Rename UnifiedSidebar to Sidebar - Update all imports in __root.tsx - Remove redundant unified-sidebar folder --- apps/ui/src/components/layout/index.ts | 1 - apps/ui/src/components/layout/sidebar.tsx | 397 -------------- .../components/collapse-toggle-button.tsx | 2 +- .../sidebar/components/sidebar-footer.tsx | 377 +++++++++---- .../sidebar/components/sidebar-header.tsx | 501 +++++++++++++----- .../sidebar/components/sidebar-navigation.tsx | 419 ++++++++++----- .../layout/sidebar/hooks/use-navigation.ts | 20 + .../ui/src/components/layout/sidebar/index.ts | 1 + .../sidebar.tsx} | 51 +- .../ui/src/components/layout/sidebar/types.ts | 4 + .../unified-sidebar/components/index.ts | 2 - .../components/sidebar-footer.tsx | 372 ------------- .../components/sidebar-header.tsx | 349 ------------ .../layout/unified-sidebar/index.ts | 1 - apps/ui/src/routes/__root.tsx | 4 +- 15 files changed, 974 insertions(+), 1527 deletions(-) delete mode 100644 apps/ui/src/components/layout/sidebar.tsx create mode 100644 apps/ui/src/components/layout/sidebar/index.ts rename apps/ui/src/components/layout/{unified-sidebar/unified-sidebar.tsx => sidebar/sidebar.tsx} (92%) delete mode 100644 apps/ui/src/components/layout/unified-sidebar/components/index.ts delete mode 100644 apps/ui/src/components/layout/unified-sidebar/components/sidebar-footer.tsx delete mode 100644 apps/ui/src/components/layout/unified-sidebar/components/sidebar-header.tsx delete mode 100644 apps/ui/src/components/layout/unified-sidebar/index.ts diff --git a/apps/ui/src/components/layout/index.ts b/apps/ui/src/components/layout/index.ts index d702d78d..bfed6246 100644 --- a/apps/ui/src/components/layout/index.ts +++ b/apps/ui/src/components/layout/index.ts @@ -1,2 +1 @@ export { Sidebar } from './sidebar'; -export { UnifiedSidebar } from './unified-sidebar'; diff --git a/apps/ui/src/components/layout/sidebar.tsx b/apps/ui/src/components/layout/sidebar.tsx deleted file mode 100644 index 05ff1328..00000000 --- a/apps/ui/src/components/layout/sidebar.tsx +++ /dev/null @@ -1,397 +0,0 @@ -import { useState, useCallback } from 'react'; -import { createLogger } from '@automaker/utils/logger'; -import { useNavigate, useLocation } from '@tanstack/react-router'; - -const logger = createLogger('Sidebar'); -import { cn } from '@/lib/utils'; -import { useAppStore } from '@/store/app-store'; -import { useNotificationsStore } from '@/store/notifications-store'; -import { useKeyboardShortcuts, useKeyboardShortcutsConfig } from '@/hooks/use-keyboard-shortcuts'; -import { getElectronAPI } from '@/lib/electron'; -import { initializeProject, hasAppSpec, hasAutomakerDir } from '@/lib/project-init'; -import { toast } from 'sonner'; -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'; - -// Local imports from subfolder -import { - CollapseToggleButton, - SidebarHeader, - SidebarNavigation, - SidebarFooter, - MobileSidebarToggle, -} from './sidebar/components'; -import { useIsCompact } from '@/hooks/use-media-query'; -import { PanelLeftClose } from 'lucide-react'; -import { TrashDialog, OnboardingDialog } from './sidebar/dialogs'; -import { SIDEBAR_FEATURE_FLAGS } from './sidebar/constants'; -import { - useSidebarAutoCollapse, - useRunningAgents, - useSpecRegeneration, - useNavigation, - useProjectCreation, - useSetupDialog, - useTrashOperations, - useUnviewedValidations, -} from './sidebar/hooks'; - -export function Sidebar() { - const navigate = useNavigate(); - const location = useLocation(); - - const { - projects, - trashedProjects, - currentProject, - sidebarOpen, - mobileSidebarHidden, - projectHistory, - upsertAndSetCurrentProject, - toggleSidebar, - toggleMobileSidebarHidden, - restoreTrashedProject, - deleteTrashedProject, - emptyTrash, - cyclePrevProject, - cycleNextProject, - moveProjectToTrash, - specCreatingForProject, - setSpecCreatingForProject, - } = useAppStore(); - - const isCompact = useIsCompact(); - - // Environment variable flags for hiding sidebar items - const { hideTerminal, hideRunningAgents, hideContext, hideSpecEditor } = SIDEBAR_FEATURE_FLAGS; - - // Get customizable keyboard shortcuts - const shortcuts = useKeyboardShortcutsConfig(); - - // Get unread notifications count - const unreadNotificationsCount = useNotificationsStore((s) => s.unreadCount); - - // State for delete project confirmation dialog - const [showDeleteProjectDialog, setShowDeleteProjectDialog] = useState(false); - - // State for trash dialog - const [showTrashDialog, setShowTrashDialog] = useState(false); - - // Project creation state and handlers - const { - showNewProjectModal, - setShowNewProjectModal, - isCreatingProject, - showOnboardingDialog, - setShowOnboardingDialog, - newProjectName, - setNewProjectName, - newProjectPath, - setNewProjectPath, - handleCreateBlankProject, - handleCreateFromTemplate, - handleCreateFromCustomUrl, - } = useProjectCreation({ - upsertAndSetCurrentProject, - }); - - // Setup dialog state and handlers - const { - showSetupDialog, - setShowSetupDialog, - setupProjectPath, - setSetupProjectPath, - projectOverview, - setProjectOverview, - generateFeatures, - setGenerateFeatures, - analyzeProject, - setAnalyzeProject, - featureCount, - setFeatureCount, - handleCreateInitialSpec, - handleSkipSetup, - handleOnboardingGenerateSpec, - handleOnboardingSkip, - } = useSetupDialog({ - setSpecCreatingForProject, - newProjectPath, - setNewProjectName, - setNewProjectPath, - setShowOnboardingDialog, - }); - - // Derive isCreatingSpec from store state - const isCreatingSpec = specCreatingForProject !== null; - const creatingSpecProjectPath = specCreatingForProject; - // Check if the current project is specifically the one generating spec - const isCurrentProjectGeneratingSpec = - specCreatingForProject !== null && specCreatingForProject === currentProject?.path; - - // Auto-collapse sidebar on small screens and update Electron window minWidth - useSidebarAutoCollapse({ sidebarOpen, toggleSidebar }); - - // Running agents count - const { runningAgentsCount } = useRunningAgents(); - - // Unviewed validations count - const { count: unviewedValidationsCount } = useUnviewedValidations(currentProject); - - // Trash operations - const { - activeTrashId, - isEmptyingTrash, - handleRestoreProject, - handleDeleteProjectFromDisk, - handleEmptyTrash, - } = useTrashOperations({ - restoreTrashedProject, - deleteTrashedProject, - emptyTrash, - }); - - // Spec regeneration events - useSpecRegeneration({ - creatingSpecProjectPath, - setupProjectPath, - setSpecCreatingForProject, - setShowSetupDialog, - setProjectOverview, - setSetupProjectPath, - setNewProjectName, - setNewProjectPath, - }); - - /** - * 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 handling (trashed project recovery or undefined for global) is done by the store - upsertAndSetCurrentProject(path, name); - - // 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) { - logger.error('Failed to open project:', error); - toast.error('Failed to open project', { - description: error instanceof Error ? error.message : 'Unknown error', - }); - } - } - }, [upsertAndSetCurrentProject]); - - // Navigation sections and keyboard shortcuts (defined after handlers) - const { navSections, navigationShortcuts } = useNavigation({ - shortcuts, - hideSpecEditor, - hideContext, - hideTerminal, - currentProject, - projects, - projectHistory, - navigate, - toggleSidebar, - handleOpenFolder, - cyclePrevProject, - cycleNextProject, - unviewedValidationsCount, - unreadNotificationsCount, - isSpecGenerating: isCurrentProjectGeneratingSpec, - }); - - // Register keyboard shortcuts - useKeyboardShortcuts(navigationShortcuts); - - const isActiveRoute = (id: string) => { - // Map view IDs to route paths - const routePath = id === 'welcome' ? '/' : `/${id}`; - return location.pathname === routePath; - }; - - // Check if sidebar should be completely hidden on mobile - const shouldHideSidebar = isCompact && mobileSidebarHidden; - - return ( - <> - {/* Floating toggle to show sidebar on mobile when hidden */} - - - {/* Mobile backdrop overlay */} - {sidebarOpen && !shouldHideSidebar && ( -
- )} - - - ); -} diff --git a/apps/ui/src/components/layout/sidebar/components/collapse-toggle-button.tsx b/apps/ui/src/components/layout/sidebar/components/collapse-toggle-button.tsx index 29a71644..2a503fc5 100644 --- a/apps/ui/src/components/layout/sidebar/components/collapse-toggle-button.tsx +++ b/apps/ui/src/components/layout/sidebar/components/collapse-toggle-button.tsx @@ -25,7 +25,7 @@ export function CollapseToggleButton({ + + + Running Agents + {runningAgentsCount > 0 && ( + + {runningAgentsCount} + + )} + + + + )} + + {/* Settings */} + + + + + + + Global Settings + + {formatShortcut(shortcuts.settings, true)} + + + + + + {/* Documentation */} + {!hideWiki && ( + + + + + + + Documentation + + + + )} + + {/* Feedback */} + + + + + + + Feedback + + + +
+ + ); + } + + // Expanded state return ( -
+
{/* Running Agents Link */} {!hideRunningAgents && ( -
+
)} + {/* Settings Link */} -
+
+ + {/* Separator */} +
+ + {/* Documentation Link */} + {!hideWiki && ( +
+ +
+ )} + + {/* Feedback Link */} +
+ +
+ + {/* Version */} +
+ + v{appVersion} {versionSuffix} + +
); } diff --git a/apps/ui/src/components/layout/sidebar/components/sidebar-header.tsx b/apps/ui/src/components/layout/sidebar/components/sidebar-header.tsx index 8f3d921e..db4835dd 100644 --- a/apps/ui/src/components/layout/sidebar/components/sidebar-header.tsx +++ b/apps/ui/src/components/layout/sidebar/components/sidebar-header.tsx @@ -1,179 +1,406 @@ -import { useState } from 'react'; -import { Folder, LucideIcon, X, Menu, Check } from 'lucide-react'; +import { useState, useCallback } from 'react'; +import { useNavigate } from '@tanstack/react-router'; +import { ChevronsUpDown, Folder, Plus, FolderOpen } from 'lucide-react'; import * as LucideIcons from 'lucide-react'; +import type { LucideIcon } from 'lucide-react'; import { cn, isMac } from '@/lib/utils'; -import { getAuthenticatedImageUrl } from '@/lib/api-fetch'; import { isElectron, type Project } from '@/lib/electron'; -import { useIsCompact } from '@/hooks/use-media-query'; -import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import { getAuthenticatedImageUrl } from '@/lib/api-fetch'; import { useAppStore } from '@/store/app-store'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; interface SidebarHeaderProps { sidebarOpen: boolean; currentProject: Project | null; - onClose?: () => void; - onExpand?: () => void; + onNewProject: () => void; + onOpenFolder: () => void; + onProjectContextMenu: (project: Project, event: React.MouseEvent) => void; } export function SidebarHeader({ sidebarOpen, currentProject, - onClose, - onExpand, + onNewProject, + onOpenFolder, + onProjectContextMenu, }: SidebarHeaderProps) { - const isCompact = useIsCompact(); - const [projectListOpen, setProjectListOpen] = useState(false); + const navigate = useNavigate(); const { projects, setCurrentProject } = useAppStore(); - // Get the icon component from lucide-react - const getIconComponent = (): LucideIcon => { - if (currentProject?.icon && currentProject.icon in LucideIcons) { - return (LucideIcons as unknown as Record)[currentProject.icon]; + const [dropdownOpen, setDropdownOpen] = useState(false); + + const handleLogoClick = useCallback(() => { + navigate({ to: '/dashboard' }); + }, [navigate]); + + const handleProjectSelect = useCallback( + (project: Project) => { + setCurrentProject(project); + setDropdownOpen(false); + navigate({ to: '/board' }); + }, + [setCurrentProject, navigate] + ); + + const getIconComponent = (project: Project): LucideIcon => { + if (project?.icon && project.icon in LucideIcons) { + return (LucideIcons as unknown as Record)[project.icon]; } return Folder; }; - const IconComponent = getIconComponent(); - const hasCustomIcon = !!currentProject?.customIconPath; + const renderProjectIcon = (project: Project, size: 'sm' | 'md' = 'md') => { + const IconComponent = getIconComponent(project); + const sizeClasses = size === 'sm' ? 'w-6 h-6' : 'w-8 h-8'; + const iconSizeClasses = size === 'sm' ? 'w-4 h-4' : 'w-5 h-5'; + if (project.customIconPath) { + return ( + {project.name} + ); + } + + return ( +
+ +
+ ); + }; + + // Collapsed state - show logo only + if (!sidebarOpen) { + return ( +
+ + + + + + + Go to Dashboard + + + + + {/* Collapsed project icon with dropdown */} + {currentProject && ( + <> +
+ + + + + + + + + + {currentProject.name} + + + + +
+ Projects +
+ {projects.map((project, index) => { + const isActive = currentProject?.id === project.id; + const hotkeyLabel = index < 9 ? `${index + 1}` : index === 9 ? '0' : undefined; + + return ( + handleProjectSelect(project)} + onContextMenu={(e) => { + e.preventDefault(); + e.stopPropagation(); + setDropdownOpen(false); + onProjectContextMenu(project, e); + }} + className="flex items-center gap-3 cursor-pointer" + data-testid={`collapsed-project-item-${project.id}`} + > + {renderProjectIcon(project, 'sm')} + + {project.name} + + {hotkeyLabel && ( + ⌘{hotkeyLabel} + )} + + ); + })} + + { + setDropdownOpen(false); + onNewProject(); + }} + className="cursor-pointer" + data-testid="collapsed-new-project-dropdown-item" + > + + New Project + + { + setDropdownOpen(false); + onOpenFolder(); + }} + className="cursor-pointer" + data-testid="collapsed-open-project-dropdown-item" + > + + Open Project + +
+
+ + )} +
+ ); + } + + // Expanded state - show logo + project dropdown return (
- {/* Mobile close button - only visible on mobile when sidebar is open */} - {sidebarOpen && onClose && ( + {/* Header with logo and project dropdown */} +
+ {/* Logo */} - )} - {/* Mobile expand button - hamburger menu to expand sidebar when collapsed on mobile */} - {!sidebarOpen && isCompact && onExpand && ( - - )} - {/* Project name and icon display - entire element clickable on mobile */} - {currentProject && ( - - - - {/* Project Name - only show when sidebar is open */} - {sidebarOpen && ( -
-

- {currentProject.name} -

-
- )} - -
- -
-

Switch Project

- {projects.map((project) => { - const ProjectIcon = - project.icon && project.icon in LucideIcons - ? (LucideIcons as unknown as Record)[project.icon] - : Folder; + {/* Project Dropdown */} + {currentProject ? ( + + + + + +
+ Projects +
+ {projects.map((project, index) => { const isActive = currentProject?.id === project.id; + const hotkeyLabel = index < 9 ? `${index + 1}` : index === 9 ? '0' : undefined; return ( - + ); })} -
-
-
- )} + + { + setDropdownOpen(false); + onNewProject(); + }} + className="cursor-pointer" + data-testid="new-project-dropdown-item" + > + + New Project + + { + setDropdownOpen(false); + onOpenFolder(); + }} + className="cursor-pointer" + data-testid="open-project-dropdown-item" + > + + Open Project + + + + ) : ( +
+ + +
+ )} +
); } diff --git a/apps/ui/src/components/layout/sidebar/components/sidebar-navigation.tsx b/apps/ui/src/components/layout/sidebar/components/sidebar-navigation.tsx index c4956159..f303ad44 100644 --- a/apps/ui/src/components/layout/sidebar/components/sidebar-navigation.tsx +++ b/apps/ui/src/components/layout/sidebar/components/sidebar-navigation.tsx @@ -1,9 +1,24 @@ +import { useState, useCallback, useEffect, useRef } from 'react'; import type { NavigateOptions } from '@tanstack/react-router'; +import { ChevronDown, Wrench, Github } from 'lucide-react'; import { cn } from '@/lib/utils'; import { formatShortcut } from '@/store/app-store'; import type { NavSection } from '../types'; import type { Project } from '@/lib/electron'; import { Spinner } from '@/components/ui/spinner'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; + +// Map section labels to icons +const sectionIcons: Record> = { + Tools: Wrench, + GitHub: Github, +}; interface SidebarNavigationProps { currentProject: Project | null; @@ -11,6 +26,7 @@ interface SidebarNavigationProps { navSections: NavSection[]; isActiveRoute: (id: string) => boolean; navigate: (opts: NavigateOptions) => void; + onScrollStateChange?: (canScrollDown: boolean) => void; } export function SidebarNavigation({ @@ -19,174 +35,305 @@ export function SidebarNavigation({ navSections, isActiveRoute, navigate, + onScrollStateChange, }: SidebarNavigationProps) { + const navRef = useRef(null); + + // Track collapsed state for each collapsible section + const [collapsedSections, setCollapsedSections] = useState>({}); + + // Initialize collapsed state when sections change (e.g., GitHub section appears) + useEffect(() => { + setCollapsedSections((prev) => { + const updated = { ...prev }; + navSections.forEach((section) => { + if (section.collapsible && section.label && !(section.label in updated)) { + updated[section.label] = section.defaultCollapsed ?? false; + } + }); + return updated; + }); + }, [navSections]); + + // Check scroll state + const checkScrollState = useCallback(() => { + if (!navRef.current || !onScrollStateChange) return; + const { scrollTop, scrollHeight, clientHeight } = navRef.current; + const canScrollDown = scrollTop + clientHeight < scrollHeight - 10; + onScrollStateChange(canScrollDown); + }, [onScrollStateChange]); + + // Monitor scroll state + useEffect(() => { + checkScrollState(); + const nav = navRef.current; + if (!nav) return; + + nav.addEventListener('scroll', checkScrollState); + const resizeObserver = new ResizeObserver(checkScrollState); + resizeObserver.observe(nav); + + return () => { + nav.removeEventListener('scroll', checkScrollState); + resizeObserver.disconnect(); + }; + }, [checkScrollState, collapsedSections]); + + const toggleSection = useCallback((label: string) => { + setCollapsedSections((prev) => ({ + ...prev, + [label]: !prev[label], + })); + }, []); + + // Filter sections: always show non-project sections, only show project sections when project exists + const visibleSections = navSections.filter((section) => { + // Always show Dashboard (first section with no label) + if (!section.label && section.items.some((item) => item.id === 'dashboard')) { + return true; + } + // Show other sections only when project is selected + return !!currentProject; + }); + return ( ); } diff --git a/apps/ui/src/components/layout/sidebar/hooks/use-navigation.ts b/apps/ui/src/components/layout/sidebar/hooks/use-navigation.ts index 91b40e4a..df5d033f 100644 --- a/apps/ui/src/components/layout/sidebar/hooks/use-navigation.ts +++ b/apps/ui/src/components/layout/sidebar/hooks/use-navigation.ts @@ -13,6 +13,7 @@ import { Network, Bell, Settings, + Home, } from 'lucide-react'; import type { NavSection, NavItem } from '../types'; import type { KeyboardShortcut } from '@/hooks/use-keyboard-shortcuts'; @@ -174,13 +175,30 @@ export function useNavigation({ } const sections: NavSection[] = [ + // Dashboard - standalone at top + { + label: '', + items: [ + { + id: 'dashboard', + label: 'Dashboard', + icon: Home, + }, + ], + }, + // Project section - expanded by default { label: 'Project', items: projectItems, + collapsible: true, + defaultCollapsed: false, }, + // Tools section - collapsed by default { label: 'Tools', items: visibleToolsItems, + collapsible: true, + defaultCollapsed: true, }, ]; @@ -203,6 +221,8 @@ export function useNavigation({ shortcut: shortcuts.githubPrs, }, ], + collapsible: true, + defaultCollapsed: true, }); } diff --git a/apps/ui/src/components/layout/sidebar/index.ts b/apps/ui/src/components/layout/sidebar/index.ts new file mode 100644 index 00000000..bfed6246 --- /dev/null +++ b/apps/ui/src/components/layout/sidebar/index.ts @@ -0,0 +1 @@ +export { Sidebar } from './sidebar'; diff --git a/apps/ui/src/components/layout/unified-sidebar/unified-sidebar.tsx b/apps/ui/src/components/layout/sidebar/sidebar.tsx similarity index 92% rename from apps/ui/src/components/layout/unified-sidebar/unified-sidebar.tsx rename to apps/ui/src/components/layout/sidebar/sidebar.tsx index eb8841ac..5b63921f 100644 --- a/apps/ui/src/components/layout/unified-sidebar/unified-sidebar.tsx +++ b/apps/ui/src/components/layout/sidebar/sidebar.tsx @@ -1,7 +1,7 @@ import { useState, useCallback, useEffect } from 'react'; import { createLogger } from '@automaker/utils/logger'; import { useNavigate, useLocation } from '@tanstack/react-router'; -import { PanelLeftClose } from 'lucide-react'; +import { PanelLeftClose, ChevronDown } from 'lucide-react'; import { cn } from '@/lib/utils'; import { useAppStore } from '@/store/app-store'; import { useNotificationsStore } from '@/store/notifications-store'; @@ -12,9 +12,15 @@ import { toast } from 'sonner'; import { useIsCompact } from '@/hooks/use-media-query'; import type { Project } from '@/lib/electron'; -// Reuse existing sidebar components -import { SidebarNavigation, CollapseToggleButton, MobileSidebarToggle } from '../sidebar/components'; -import { SIDEBAR_FEATURE_FLAGS } from '../sidebar/constants'; +// Sidebar components +import { + SidebarNavigation, + CollapseToggleButton, + MobileSidebarToggle, + SidebarHeader, + SidebarFooter, +} from './components'; +import { SIDEBAR_FEATURE_FLAGS } from './constants'; import { useSidebarAutoCollapse, useRunningAgents, @@ -24,8 +30,8 @@ import { useSetupDialog, useTrashOperations, useUnviewedValidations, -} from '../sidebar/hooks'; -import { TrashDialog, OnboardingDialog } from '../sidebar/dialogs'; +} from './hooks'; +import { TrashDialog, OnboardingDialog } from './dialogs'; // Reuse dialogs from project-switcher import { ProjectContextMenu } from '../project-switcher/components/project-context-menu'; @@ -36,12 +42,9 @@ import { DeleteProjectDialog } from '@/components/views/settings-view/components import { NewProjectModal } from '@/components/dialogs/new-project-modal'; import { CreateSpecDialog } from '@/components/views/spec-view/dialogs'; -// Local components -import { SidebarHeader, SidebarFooter } from './components'; +const logger = createLogger('Sidebar'); -const logger = createLogger('UnifiedSidebar'); - -export function UnifiedSidebar() { +export function Sidebar() { const navigate = useNavigate(); const location = useLocation(); @@ -188,10 +191,13 @@ export function UnifiedSidebar() { setContextMenuPosition(null); }, []); - const handleEditProject = useCallback((project: Project) => { - setEditDialogProject(project); - handleCloseContextMenu(); - }, [handleCloseContextMenu]); + const handleEditProject = useCallback( + (project: Project) => { + setEditDialogProject(project); + handleCloseContextMenu(); + }, + [handleCloseContextMenu] + ); /** * Opens the system folder selection dialog and initializes the selected project. @@ -309,6 +315,9 @@ export function UnifiedSidebar() { return location.pathname === routePath; }; + // Track if nav can scroll down + const [canScrollDown, setCanScrollDown] = useState(false); + // Check if sidebar should be completely hidden on mobile const shouldHideSidebar = isCompact && mobileSidebarHidden; @@ -339,7 +348,9 @@ export function UnifiedSidebar() { shouldHideSidebar && 'hidden', // Width based on state !shouldHideSidebar && - (sidebarOpen ? 'fixed inset-y-0 left-0 w-72 lg:relative lg:w-72' : 'relative w-16') + (sidebarOpen + ? 'fixed inset-y-0 left-0 w-[17rem] lg:relative lg:w-[17rem]' + : 'relative w-14') )} data-testid="sidebar" > @@ -384,9 +395,17 @@ export function UnifiedSidebar() { navSections={navSections} isActiveRoute={isActiveRoute} navigate={navigate} + onScrollStateChange={setCanScrollDown} />
+ {/* Scroll indicator - shows there's more content below */} + {canScrollDown && sidebarOpen && ( +
+ +
+ )} + boolean; - navigate: (opts: NavigateOptions) => void; - hideRunningAgents: boolean; - hideWiki: boolean; - runningAgentsCount: number; - shortcuts: { - settings: string; - }; -} - -export function SidebarFooter({ - sidebarOpen, - isActiveRoute, - navigate, - hideRunningAgents, - hideWiki, - runningAgentsCount, - shortcuts, -}: SidebarFooterProps) { - const appVersion = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '0.0.0'; - const { os } = useOSDetection(); - const appMode = import.meta.env.VITE_APP_MODE || '?'; - const versionSuffix = `${getOSAbbreviation(os)}${appMode}`; - - const handleWikiClick = useCallback(() => { - navigate({ to: '/wiki' }); - }, [navigate]); - - const handleBugReportClick = useCallback(() => { - const api = getElectronAPI(); - api.openExternalLink('https://github.com/AutoMaker-Org/automaker/issues'); - }, []); - - // Collapsed state - if (!sidebarOpen) { - return ( -
-
- {/* Running Agents */} - {!hideRunningAgents && ( - - - - - - - Running Agents - {runningAgentsCount > 0 && ( - - {runningAgentsCount} - - )} - - - - )} - - {/* Settings */} - - - - - - - Global Settings - - {formatShortcut(shortcuts.settings, true)} - - - - - - {/* User Dropdown */} - - - - - - - - - - More options - - - - - {!hideWiki && ( - - - Documentation - - )} - - - Report Bug - - - -
- - v{appVersion} {versionSuffix} - -
-
-
-
-
- ); - } - - // Expanded state - return ( -
- {/* Running Agents Link */} - {!hideRunningAgents && ( -
- -
- )} - - {/* Settings Link */} -
- -
- - {/* User area with dropdown */} -
- - - - - - {!hideWiki && ( - - - Documentation - - )} - - - Report Bug / Feature Request - - - - -
-
- ); -} diff --git a/apps/ui/src/components/layout/unified-sidebar/components/sidebar-header.tsx b/apps/ui/src/components/layout/unified-sidebar/components/sidebar-header.tsx deleted file mode 100644 index 4a531718..00000000 --- a/apps/ui/src/components/layout/unified-sidebar/components/sidebar-header.tsx +++ /dev/null @@ -1,349 +0,0 @@ -import { useState, useCallback } from 'react'; -import { useNavigate } from '@tanstack/react-router'; -import { ChevronDown, Folder, Plus, FolderOpen, Check } from 'lucide-react'; -import * as LucideIcons from 'lucide-react'; -import type { LucideIcon } from 'lucide-react'; -import { cn, isMac } from '@/lib/utils'; -import { isElectron, type Project } from '@/lib/electron'; -import { getAuthenticatedImageUrl } from '@/lib/api-fetch'; -import { useAppStore } from '@/store/app-store'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from '@/components/ui/tooltip'; - -interface SidebarHeaderProps { - sidebarOpen: boolean; - currentProject: Project | null; - onNewProject: () => void; - onOpenFolder: () => void; - onProjectContextMenu: (project: Project, event: React.MouseEvent) => void; -} - -export function SidebarHeader({ - sidebarOpen, - currentProject, - onNewProject, - onOpenFolder, - onProjectContextMenu, -}: SidebarHeaderProps) { - const navigate = useNavigate(); - const { projects, setCurrentProject } = useAppStore(); - const [dropdownOpen, setDropdownOpen] = useState(false); - - const handleLogoClick = useCallback(() => { - navigate({ to: '/dashboard' }); - }, [navigate]); - - const handleProjectSelect = useCallback( - (project: Project) => { - setCurrentProject(project); - setDropdownOpen(false); - navigate({ to: '/board' }); - }, - [setCurrentProject, navigate] - ); - - const getIconComponent = (project: Project): LucideIcon => { - if (project?.icon && project.icon in LucideIcons) { - return (LucideIcons as unknown as Record)[project.icon]; - } - return Folder; - }; - - const renderProjectIcon = (project: Project, size: 'sm' | 'md' = 'md') => { - const IconComponent = getIconComponent(project); - const sizeClasses = size === 'sm' ? 'w-6 h-6' : 'w-8 h-8'; - const iconSizeClasses = size === 'sm' ? 'w-4 h-4' : 'w-5 h-5'; - - if (project.customIconPath) { - return ( - {project.name} - ); - } - - return ( -
- -
- ); - }; - - // Collapsed state - show logo only - if (!sidebarOpen) { - return ( -
- - - - - - - Go to Dashboard - - - - - {/* Collapsed project icon */} - {currentProject && ( - <> -
- - - - - - - {currentProject.name} - - - - - )} -
- ); - } - - // Expanded state - show logo + project dropdown - return ( -
- {/* Header with logo and project dropdown */} -
- {/* Logo */} - - - {/* Project Dropdown */} - {currentProject ? ( - - - - - -
- Projects -
- {projects.map((project, index) => { - const isActive = currentProject?.id === project.id; - const hotkeyLabel = index < 9 ? `${index + 1}` : index === 9 ? '0' : undefined; - - return ( - handleProjectSelect(project)} - onContextMenu={(e) => { - e.preventDefault(); - e.stopPropagation(); - setDropdownOpen(false); - onProjectContextMenu(project, e); - }} - className={cn( - 'flex items-center gap-3 cursor-pointer', - isActive && 'bg-brand-500/10' - )} - data-testid={`project-item-${project.id}`} - > - {renderProjectIcon(project, 'sm')} - {project.name} - {hotkeyLabel && ( - - {hotkeyLabel} - - )} - {isActive && } - - ); - })} - - { - setDropdownOpen(false); - onNewProject(); - }} - className="cursor-pointer" - data-testid="new-project-dropdown-item" - > - - New Project - - { - setDropdownOpen(false); - onOpenFolder(); - }} - className="cursor-pointer" - data-testid="open-project-dropdown-item" - > - - Open Project - -
-
- ) : ( -
- - -
- )} -
-
- ); -} diff --git a/apps/ui/src/components/layout/unified-sidebar/index.ts b/apps/ui/src/components/layout/unified-sidebar/index.ts deleted file mode 100644 index a88954e5..00000000 --- a/apps/ui/src/components/layout/unified-sidebar/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { UnifiedSidebar } from './unified-sidebar'; diff --git a/apps/ui/src/routes/__root.tsx b/apps/ui/src/routes/__root.tsx index f8379c70..f374b7dd 100644 --- a/apps/ui/src/routes/__root.tsx +++ b/apps/ui/src/routes/__root.tsx @@ -3,7 +3,7 @@ import { useEffect, useState, useCallback, useDeferredValue, useRef } from 'reac import { QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { createLogger } from '@automaker/utils/logger'; -import { UnifiedSidebar } from '@/components/layout/unified-sidebar'; +import { Sidebar } from '@/components/layout/sidebar'; import { FileBrowserProvider, useFileBrowser, @@ -860,7 +860,7 @@ function RootLayoutContent() { aria-hidden="true" /> )} - +