diff --git a/src/components/NoteList.tsx b/src/components/NoteList.tsx index 657f2fe7..b8edff0c 100644 --- a/src/components/NoteList.tsx +++ b/src/components/NoteList.tsx @@ -196,7 +196,7 @@ function countExpiredTrash(entries: VaultEntry[]): number { // --- Click routing --- -type MultiSelectActions = { toggle: (path: string) => void; selectRange: (path: string) => void; clear: () => void } +type MultiSelectActions = { toggle: (path: string) => void; selectRange: (path: string) => void; clear: () => void; setAnchor: (path: string) => void } function routeNoteClick( entry: VaultEntry, e: React.MouseEvent, @@ -205,7 +205,7 @@ function routeNoteClick( ) { if (e.shiftKey) { multiSelect.selectRange(entry.path) } else if (e.metaKey || e.ctrlKey) { multiSelect.toggle(entry.path) } - else { multiSelect.clear(); onReplaceActiveTab(entry) } + else { multiSelect.clear(); multiSelect.setAnchor(entry.path); onReplaceActiveTab(entry) } } // --- Pure helpers extracted from NoteListInner to reduce cyclomatic complexity --- @@ -307,7 +307,7 @@ function NoteListInner({ entries, selection, selectedNote, allContent, modifiedF const listDirection = listConfig.direction const { isEntityView, isTrashView, isChangesView, typeDocument, searched, searchedGroups, expiredTrashCount } = useNoteListData({ entries, selection, allContent, query, listSort, listDirection, modifiedPathSet }) - const multiSelect = useMultiSelect(searched) + const multiSelect = useMultiSelect(searched, selectedNote?.path ?? null) // Clear multi-select when sidebar selection changes useEffect(() => { multiSelect.clear() }, [selection]) // eslint-disable-line react-hooks/exhaustive-deps -- clear on selection change only @@ -353,7 +353,7 @@ function NoteListInner({ entries, selection, selectedNote, allContent, modifiedF ), [selectedNote?.path, handleClickNote, typeEntryMap, resolvedGetNoteStatus, multiSelect.selectedPaths]) return ( -
+

{resolveHeaderTitle(selection, typeDocument)}

diff --git a/src/hooks/useMultiSelect.ts b/src/hooks/useMultiSelect.ts index d091c46c..970e7ba2 100644 --- a/src/hooks/useMultiSelect.ts +++ b/src/hooks/useMultiSelect.ts @@ -7,10 +7,11 @@ export interface MultiSelectState { toggle: (path: string) => void selectRange: (toPath: string) => void clear: () => void + setAnchor: (path: string) => void selectAll: () => void } -export function useMultiSelect(visibleEntries: VaultEntry[]): MultiSelectState { +export function useMultiSelect(visibleEntries: VaultEntry[], activePath: string | null = null): MultiSelectState { const [selectedPaths, setSelectedPaths] = useState>(new Set()) const lastClickedRef = useRef(null) @@ -25,7 +26,7 @@ export function useMultiSelect(visibleEntries: VaultEntry[]): MultiSelectState { }, []) const selectRange = useCallback((toPath: string) => { - const fromPath = lastClickedRef.current + const fromPath = lastClickedRef.current ?? activePath if (!fromPath) { toggle(toPath) return @@ -45,13 +46,17 @@ export function useMultiSelect(visibleEntries: VaultEntry[]): MultiSelectState { return next }) lastClickedRef.current = toPath - }, [visibleEntries, toggle]) + }, [visibleEntries, activePath, toggle]) const clear = useCallback(() => { setSelectedPaths(new Set()) lastClickedRef.current = null }, []) + const setAnchor = useCallback((path: string) => { + lastClickedRef.current = path + }, []) + const selectAll = useCallback(() => { setSelectedPaths(new Set(visibleEntries.map((e) => e.path))) }, [visibleEntries]) @@ -62,6 +67,7 @@ export function useMultiSelect(visibleEntries: VaultEntry[]): MultiSelectState { toggle, selectRange, clear, + setAnchor, selectAll, } }