import React from 'react'; import { DndContext, closestCenter, PointerSensor, useSensor, useSensors, type DragEndEvent, type Modifier, } from '@dnd-kit/core'; import { SortableContext, useSortable, verticalListSortingStrategy, } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { RiFolderAddLine, RiSettings3Line, RiQuestionLine, RiDownloadLine, RiInformationLine, RiPencilLine, RiCloseLine, RiMenuFoldLine, RiMenuUnfoldLine, } from '@remixicon/react'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { toast } from '@/components/ui'; import { UpdateDialog } from '@/components/ui/UpdateDialog'; import { ProjectEditDialog } from '@/components/layout/ProjectEditDialog'; import { useUIStore } from '@/stores/useUIStore'; import { useProjectsStore } from '@/stores/useProjectsStore'; import { useSessionStore } from '@/stores/useSessionStore'; import { useDirectoryStore } from '@/stores/useDirectoryStore'; import { useUpdateStore } from '@/stores/useUpdateStore'; import { cn, formatDirectoryName, hasModifier } from '@/lib/utils'; import { PROJECT_ICON_MAP, PROJECT_COLOR_MAP, getProjectIconImageUrl } from '@/lib/projectMeta'; import { isDesktopLocalOriginActive, isDesktopShell, isTauriShell, requestDirectoryAccess } from '@/lib/desktop'; import { useLongPress } from '@/hooks/useLongPress'; import { formatShortcutForDisplay, getEffectiveShortcutCombo } from '@/lib/shortcuts'; import { sessionEvents } from '@/lib/sessionEvents'; import { useThemeSystem } from '@/contexts/useThemeSystem'; import type { ProjectEntry } from '@/lib/api/types'; const normalize = (value: string): string => { if (!value) return ''; const replaced = value.replace(/\\/g, '/'); return replaced === '/' ? '/' : replaced.replace(/\/+$/, ''); }; const NAV_RAIL_WIDTH = 56; const NAV_RAIL_EXPANDED_WIDTH = 200; const NAV_RAIL_TEXT_FADE_MS = 180; const PROJECT_TEXT_FADE_IN_DELAY_MS = 24; const ACTION_TEXT_FADE_IN_DELAY_MS = 60; type NavRailActionButtonProps = { onClick: () => void; disabled?: boolean; ariaLabel: string; icon: React.ReactNode; tooltipLabel: string; shortcutHint?: string; showExpandedShortcutHint?: boolean; buttonClassName: string; showExpandedContent: boolean; actionTextVisible: boolean; }; const NavRailActionButton: React.FC = ({ onClick, disabled = false, ariaLabel, icon, tooltipLabel, shortcutHint, showExpandedShortcutHint = true, buttonClassName, showExpandedContent, actionTextVisible, }) => { const pointerTriggeredRef = React.useRef(false); const pointerPressRef = React.useRef<{ active: boolean; pointerId: number | null }>({ active: false, pointerId: null, }); const handlePointerDown = React.useCallback((event: React.PointerEvent) => { if (disabled || event.button !== 0) { pointerPressRef.current = { active: false, pointerId: null }; return; } pointerPressRef.current = { active: true, pointerId: event.pointerId }; }, [disabled]); const clearPointerPress = React.useCallback(() => { pointerPressRef.current = { active: false, pointerId: null }; }, []); const handlePointerUp = React.useCallback((event: React.PointerEvent) => { if (disabled) return; if (event.button !== 0) return; const pointerPress = pointerPressRef.current; if (!pointerPress.active || pointerPress.pointerId !== event.pointerId) { return; } clearPointerPress(); pointerTriggeredRef.current = true; onClick(); }, [clearPointerPress, disabled, onClick]); const handleClick = React.useCallback(() => { if (disabled) return; if (pointerTriggeredRef.current) { pointerTriggeredRef.current = false; return; } onClick(); }, [disabled, onClick]); const btn = ( ); return ( {btn} {!showExpandedContent && (

{shortcutHint ? `${tooltipLabel} (${shortcutHint})` : tooltipLabel}

)}
); }; /** Tinted background for project tiles — uses project color at low opacity, or neutral fallback */ const TileBackground: React.FC<{ colorVar: string | null; children: React.ReactNode }> = ({ colorVar, children, }) => ( {colorVar && ( )} {children} ); /** First-letter avatar fallback */ const LetterAvatar: React.FC<{ label: string; color?: string | null }> = ({ label, color, }) => { const letter = label.charAt(0).toUpperCase() || '?'; const colorVar = color ? (PROJECT_COLOR_MAP[color] ?? null) : null; return ( {letter} ); }; const ProjectStatusDots: React.FC<{ color: string; variant?: 'streaming' | 'attention' | 'none'; size?: 'sm' | 'md'; }> = ({ color, variant = 'none', size = 'md' }) => (