import { useState, useMemo, useRef, useEffect, useCallback } from 'react'; import { X, Sparkles, Search, Pin, Trash2 } from 'lucide-react'; import type { Session } from '../types'; import { useT } from '../hooks/useLocale'; import { SessionIcon } from './SessionIcon'; const PINNED_KEY = 'pinchchat-pinned-sessions'; const WIDTH_KEY = 'pinchchat-sidebar-width'; const MIN_WIDTH = 220; const MAX_WIDTH = 480; const DEFAULT_WIDTH = 288; // w-72 function getSavedWidth(): number { try { const v = localStorage.getItem(WIDTH_KEY); if (v) { const n = Number(v); if (n >= MIN_WIDTH && n <= MAX_WIDTH) return n; } } catch { /* noop */ } return DEFAULT_WIDTH; } function getPinnedSessions(): Set { try { const raw = localStorage.getItem(PINNED_KEY); if (raw) return new Set(JSON.parse(raw) as string[]); } catch { /* noop */ } return new Set(); } function savePinnedSessions(pinned: Set) { try { localStorage.setItem(PINNED_KEY, JSON.stringify([...pinned])); } catch { /* noop */ } } interface Props { sessions: Session[]; activeSession: string; onSwitch: (key: string) => void; onDelete: (key: string) => void; open: boolean; onClose: () => void; } export function Sidebar({ sessions, activeSession, onSwitch, onDelete, open, onClose }: Props) { const t = useT(); const [filter, setFilter] = useState(''); const [focusIdx, setFocusIdx] = useState(-1); const [pinned, setPinned] = useState(getPinnedSessions); const [width, setWidth] = useState(getSavedWidth); const [dragging, setDragging] = useState(false); const [confirmDelete, setConfirmDelete] = useState(null); const searchRef = useRef(null); const listRef = useRef(null); const dragRef = useRef({ startX: 0, startW: 0 }); // Drag-to-resize logic useEffect(() => { if (!dragging) return; const onMove = (e: MouseEvent | TouchEvent) => { const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX; const newW = Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, dragRef.current.startW + (clientX - dragRef.current.startX))); setWidth(newW); }; const onUp = () => { setDragging(false); // persist on release localStorage.setItem(WIDTH_KEY, String(width)); }; document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); document.addEventListener('touchmove', onMove); document.addEventListener('touchend', onUp); return () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); document.removeEventListener('touchmove', onMove); document.removeEventListener('touchend', onUp); }; }, [dragging, width]); // Save width when it changes (debounced via drag end above, but also on unmount) useEffect(() => { return () => { try { localStorage.setItem(WIDTH_KEY, String(width)); } catch { /* noop */ } }; }, [width]); const startDrag = useCallback((e: React.MouseEvent | React.TouchEvent) => { e.preventDefault(); const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX; dragRef.current = { startX: clientX, startW: width }; setDragging(true); }, [width]); const togglePin = useCallback((key: string, e: React.MouseEvent) => { e.stopPropagation(); setPinned(prev => { const next = new Set(prev); if (next.has(key)) next.delete(key); else next.add(key); savePinnedSessions(next); return next; }); }, []); // Keyboard shortcut: Ctrl+K or Cmd+K to focus search when sidebar is open useEffect(() => { const handler = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); searchRef.current?.focus(); } }; window.addEventListener('keydown', handler); return () => window.removeEventListener('keydown', handler); }, []); const updateFilter = useCallback((value: string) => { setFilter(value); setFocusIdx(-1); }, []); const filtered = useMemo(() => { let list = sessions; if (filter.trim()) { const q = filter.toLowerCase(); list = sessions.filter(s => (s.label || s.key).toLowerCase().includes(q)); } // Sort pinned sessions to top (preserving relative order within each group) const pinnedList = list.filter(s => pinned.has(s.key)); const unpinnedList = list.filter(s => !pinned.has(s.key)); return [...pinnedList, ...unpinnedList]; }, [sessions, filter, pinned]); return ( <> {open &&
} {/* Prevent text selection while dragging */} {dragging &&
} {/* Delete confirmation dialog */} {confirmDelete && ( <>
setConfirmDelete(null)} />

{t('sidebar.deleteConfirm')}

)} ); }