Files
tolaria/src/hooks/useMultiSelect.ts
Luca Rossi 9523bbdb54 feat: add multi-select notes with bulk archive and trash actions (#125)
* design: add multi-select notelist wireframes

Three frames: selected notes with bulk action bar, Shift+click range
select, and empty selection / hover states.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add multi-select notes with bulk archive and trash actions

Cmd/Ctrl+Click toggles individual note selection, Shift+Click selects
a range, Escape clears. A bulk action bar appears at the bottom with
Archive and Trash buttons. Includes useMultiSelect hook, BulkActionBar
component, Rust batch_archive_notes/batch_trash_notes commands, and
9 new tests covering the multi-select behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ci: trigger GitHub Actions run

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-27 14:14:21 +00:00

68 lines
1.8 KiB
TypeScript

import { useState, useCallback, useRef } from 'react'
import type { VaultEntry } from '../types'
export interface MultiSelectState {
selectedPaths: Set<string>
isMultiSelecting: boolean
toggle: (path: string) => void
selectRange: (toPath: string) => void
clear: () => void
selectAll: () => void
}
export function useMultiSelect(visibleEntries: VaultEntry[]): MultiSelectState {
const [selectedPaths, setSelectedPaths] = useState<Set<string>>(new Set())
const lastClickedRef = useRef<string | null>(null)
const toggle = useCallback((path: string) => {
setSelectedPaths((prev) => {
const next = new Set(prev)
if (next.has(path)) next.delete(path)
else next.add(path)
return next
})
lastClickedRef.current = path
}, [])
const selectRange = useCallback((toPath: string) => {
const fromPath = lastClickedRef.current
if (!fromPath) {
toggle(toPath)
return
}
const paths = visibleEntries.map((e) => e.path)
const fromIdx = paths.indexOf(fromPath)
const toIdx = paths.indexOf(toPath)
if (fromIdx === -1 || toIdx === -1) {
toggle(toPath)
return
}
const start = Math.min(fromIdx, toIdx)
const end = Math.max(fromIdx, toIdx)
setSelectedPaths((prev) => {
const next = new Set(prev)
for (let i = start; i <= end; i++) next.add(paths[i])
return next
})
lastClickedRef.current = toPath
}, [visibleEntries, toggle])
const clear = useCallback(() => {
setSelectedPaths(new Set())
lastClickedRef.current = null
}, [])
const selectAll = useCallback(() => {
setSelectedPaths(new Set(visibleEntries.map((e) => e.path)))
}, [visibleEntries])
return {
selectedPaths,
isMultiSelecting: selectedPaths.size > 0,
toggle,
selectRange,
clear,
selectAll,
}
}