From 35652eaeb554418c374b388a90bd578623783dc7 Mon Sep 17 00:00:00 2001 From: Nicolas Varrot Date: Fri, 13 Feb 2026 02:10:53 +0000 Subject: [PATCH] feat: drag & drop session reordering in sidebar - Drag sessions to reorder within pinned/unpinned groups - Custom order persists in localStorage - Visual feedback: dragged item fades, drop target highlights - Disabled during search filtering - Works alongside existing pin feature (pinned group stays on top) --- src/components/Sidebar.tsx | 72 ++++++++++++++++++++++++++++++++++---- 1 file changed, 66 insertions(+), 6 deletions(-) diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index d89cbb9..82cb327 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -8,6 +8,7 @@ import { relativeTime } from '../lib/relativeTime'; const PINNED_KEY = 'pinchchat-pinned-sessions'; const WIDTH_KEY = 'pinchchat-sidebar-width'; +const ORDER_KEY = 'pinchchat-session-order'; const MIN_WIDTH = 220; const MAX_WIDTH = 480; const DEFAULT_WIDTH = 288; // w-72 @@ -37,6 +38,20 @@ function savePinnedSessions(pinned: Set) { } catch { /* noop */ } } +function getSavedOrder(): string[] { + try { + const raw = localStorage.getItem(ORDER_KEY); + if (raw) return JSON.parse(raw) as string[]; + } catch { /* noop */ } + return []; +} + +function saveOrder(order: string[]) { + try { + localStorage.setItem(ORDER_KEY, JSON.stringify(order)); + } catch { /* noop */ } +} + interface Props { sessions: Session[]; activeSession: string; @@ -54,6 +69,9 @@ export function Sidebar({ sessions, activeSession, onSwitch, onDelete, open, onC const [width, setWidth] = useState(getSavedWidth); const [dragging, setDragging] = useState(false); const [confirmDelete, setConfirmDelete] = useState(null); + const [customOrder, setCustomOrder] = useState(getSavedOrder); + const [dragKey, setDragKey] = useState(null); + const [dropTarget, setDropTarget] = useState(null); const searchRef = useRef(null); const listRef = useRef(null); const dragRef = useRef({ startX: 0, startW: 0 }); @@ -132,12 +150,20 @@ export function Sidebar({ sessions, activeSession, onSwitch, onDelete, open, onC // 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)); - // Sort each group by most recently updated - const byRecent = (a: Session, b: Session) => (b.updatedAt || 0) - (a.updatedAt || 0); - pinnedList.sort(byRecent); - unpinnedList.sort(byRecent); + // Sort each group: use custom order if set, then fall back to most recently updated + const orderMap = new Map(customOrder.map((k, i) => [k, i])); + const byCustomThenRecent = (a: Session, b: Session) => { + const aIdx = orderMap.get(a.key); + const bIdx = orderMap.get(b.key); + if (aIdx !== undefined && bIdx !== undefined) return aIdx - bIdx; + if (aIdx !== undefined) return -1; + if (bIdx !== undefined) return 1; + return (b.updatedAt || 0) - (a.updatedAt || 0); + }; + pinnedList.sort(byCustomThenRecent); + unpinnedList.sort(byCustomThenRecent); return [...pinnedList, ...unpinnedList]; - }, [sessions, filter, pinned]); + }, [sessions, filter, pinned, customOrder]); return ( <> @@ -224,6 +250,8 @@ export function Sidebar({ sessions, activeSession, onSwitch, onDelete, open, onC const isFocused = idx === focusIdx; const isPinned = pinned.has(s.key); const isFirstUnpinned = !isPinned && idx > 0 && pinned.has(filtered[idx - 1].key); + const isDragged = dragKey === s.key; + const isDropTarget = dropTarget === s.key && dragKey !== s.key; return (
{isFirstUnpinned && ( @@ -234,6 +262,38 @@ export function Sidebar({ sessions, activeSession, onSwitch, onDelete, open, onC