diff --git a/src/components/bookmarks/BookmarkItem.jsx b/src/components/bookmarks/BookmarkItem.jsx
index 983c1bb..852b7cc 100644
--- a/src/components/bookmarks/BookmarkItem.jsx
+++ b/src/components/bookmarks/BookmarkItem.jsx
@@ -1,8 +1,8 @@
import { forwardRef } from 'react'
-import { Pencil, Trash } from '../ui/Icons'
+import { Pencil, Trash, Square, CheckSquare } from '../ui/Icons'
export const BookmarkItem = forwardRef(function BookmarkItem(
- { bookmark, isSelected, onEdit, onDelete, onTagClick },
+ { bookmark, isSelected, isChecked, selectionMode, onEdit, onDelete, onTagClick, onToggleSelect },
ref
) {
const { title, url, tags } = bookmark
@@ -16,20 +16,51 @@ export const BookmarkItem = forwardRef(function BookmarkItem(
const faviconUrl = `https://www.google.com/s2/favicons?domain=${domain}&sz=32`
+ const handleClick = (e) => {
+ if (selectionMode) {
+ e.preventDefault()
+ onToggleSelect?.(bookmark._id)
+ } else if (e.shiftKey) {
+ e.preventDefault()
+ onToggleSelect?.(bookmark._id, true)
+ }
+ }
+
return (
-

{ e.target.style.opacity = 0 }}
+ {selectionMode && (
+
+ )}
+
+

{ e.target.style.opacity = 0 }}
/>
@@ -38,13 +69,14 @@ export const BookmarkItem = forwardRef(function BookmarkItem(
href={url}
target="_blank"
rel="noopener noreferrer"
+ onClick={(e) => selectionMode && e.preventDefault()}
className="font-medium text-sm text-foreground truncate hover:text-primary transition-colors"
>
{title}
{domain}
-
+
{tags && tags.length > 0 && (
{tags.map((tag) => (
@@ -52,7 +84,9 @@ export const BookmarkItem = forwardRef(function BookmarkItem(
key={tag}
onClick={(e) => {
e.stopPropagation()
- onTagClick && onTagClick(tag)
+ if (!selectionMode) {
+ onTagClick && onTagClick(tag)
+ }
}}
className="text-[10px] leading-tight px-1.5 py-0.5 rounded-[3px] bg-secondary text-secondary-foreground hover:text-primary hover:bg-accent cursor-pointer transition-colors"
>
@@ -63,22 +97,24 @@ export const BookmarkItem = forwardRef(function BookmarkItem(
)}
-
-
-
-
+ {!selectionMode && (
+
+
+
+
+ )}
)
})
diff --git a/src/components/bookmarks/BookmarkList.jsx b/src/components/bookmarks/BookmarkList.jsx
index f7c467f..b440ff8 100644
--- a/src/components/bookmarks/BookmarkList.jsx
+++ b/src/components/bookmarks/BookmarkList.jsx
@@ -8,6 +8,7 @@ import { BookmarkInlineCard } from './BookmarkInlineCard'
import { InboxView } from './InboxView'
import { TagSidebar } from './TagSidebar'
import { FilterBar } from './FilterBar'
+import { SelectionActionBar } from './SelectionActionBar'
import { SettingsView } from '../ui/SettingsView'
import { HelpModal } from '../ui/HelpModal'
import { ToastContainer } from '../ui/Toast'
@@ -15,6 +16,7 @@ import { PackageOpen } from '../ui/Icons'
import {
getAllBookmarks,
deleteBookmark,
+ bulkDeleteBookmarks,
} from '../../services/bookmarks'
export function BookmarkList() {
@@ -51,6 +53,8 @@ export function BookmarkList() {
const [isSidebarOpen, setIsSidebarOpen] = useState(false)
const [isHelpOpen, setIsHelpOpen] = useState(false)
const [selectedIndex, setSelectedIndex] = useState(-1)
+ const [selectionMode, setSelectionMode] = useState(false)
+ const [selectedIds, setSelectedIds] = useState(new Set())
const selectedItemRef = useRef(null)
const searchInputRef = useRef(null)
const inboxViewRef = useRef(null)
@@ -211,15 +215,133 @@ export function BookmarkList() {
}
}, [addToast])
+ const toggleSelectionMode = useCallback(() => {
+ setSelectionMode(prev => {
+ if (prev) {
+ setSelectedIds(new Set())
+ }
+ return !prev
+ })
+ }, [])
+
+ const exitSelectionMode = useCallback(() => {
+ setSelectionMode(false)
+ setSelectedIds(new Set())
+ }, [])
+
+ const toggleSelectBookmark = useCallback((id, initiateSelection = false) => {
+ if (initiateSelection && !selectionMode) {
+ setSelectionMode(true)
+ setSelectedIds(new Set([id]))
+ return
+ }
+
+ setSelectedIds(prev => {
+ const next = new Set(prev)
+ if (next.has(id)) {
+ next.delete(id)
+ } else {
+ next.add(id)
+ }
+ return next
+ })
+ }, [selectionMode])
+
+ const toggleSelectCurrent = useCallback(() => {
+ if (!selectionMode) return
+ if (selectedIndex >= 0 && selectedIndex < filteredBookmarks.length) {
+ const bookmark = filteredBookmarks[selectedIndex]
+ toggleSelectBookmark(bookmark._id)
+ }
+ }, [selectionMode, selectedIndex, filteredBookmarks, toggleSelectBookmark])
+
+ const selectAllBookmarks = useCallback(() => {
+ if (!selectionMode) {
+ setSelectionMode(true)
+ }
+ setSelectedIds(new Set(filteredBookmarks.map(b => b._id)))
+ }, [selectionMode, filteredBookmarks])
+
+ const selectNextWithShift = useCallback(() => {
+ if (filteredBookmarks.length === 0) return
+
+ if (!selectionMode) {
+ setSelectionMode(true)
+ }
+
+ // Select current item before moving
+ if (selectedIndex >= 0 && selectedIndex < filteredBookmarks.length) {
+ const currentBookmark = filteredBookmarks[selectedIndex]
+ setSelectedIds(prev => new Set(prev).add(currentBookmark._id))
+ }
+
+ // Move to next and select it
+ setSelectedIndex(prev => {
+ const maxIndex = filteredBookmarks.length - 1
+ const nextIndex = prev < maxIndex ? prev + 1 : prev
+ if (nextIndex >= 0 && nextIndex < filteredBookmarks.length) {
+ const nextBookmark = filteredBookmarks[nextIndex]
+ setSelectedIds(p => new Set(p).add(nextBookmark._id))
+ }
+ return nextIndex
+ })
+ }, [selectionMode, selectedIndex, filteredBookmarks])
+
+ const selectPrevWithShift = useCallback(() => {
+ if (filteredBookmarks.length === 0) return
+
+ if (!selectionMode) {
+ setSelectionMode(true)
+ }
+
+ // Select current item before moving
+ if (selectedIndex >= 0 && selectedIndex < filteredBookmarks.length) {
+ const currentBookmark = filteredBookmarks[selectedIndex]
+ setSelectedIds(prev => new Set(prev).add(currentBookmark._id))
+ }
+
+ // Move to prev and select it
+ setSelectedIndex(prev => {
+ const prevIndex = prev > 0 ? prev - 1 : 0
+ if (prevIndex >= 0 && prevIndex < filteredBookmarks.length) {
+ const prevBookmark = filteredBookmarks[prevIndex]
+ setSelectedIds(p => new Set(p).add(prevBookmark._id))
+ }
+ return prevIndex
+ })
+ }, [selectionMode, selectedIndex, filteredBookmarks])
+
+ const handleBulkDelete = useCallback(() => {
+ if (selectedIds.size === 0) return
+
+ const count = selectedIds.size
+ try {
+ bulkDeleteBookmarks(Array.from(selectedIds))
+ addToast({
+ message: `Deleted ${count} bookmark${count > 1 ? 's' : ''}`,
+ action: () => {
+ undo()
+ },
+ actionLabel: 'Undo',
+ duration: 5000,
+ })
+ exitSelectionMode()
+ } catch (error) {
+ console.error('Failed to delete bookmarks:', error)
+ addToast({ message: 'Failed to delete bookmarks', duration: 3000 })
+ }
+ }, [selectedIds, addToast, exitSelectionMode])
+
useEffect(() => {
setSelectedIndex(-1)
}, [filteredBookmarks.length, filterView, selectedTag, debouncedSearchQuery])
- // Close inline card when view/filter changes
+ // Close inline card and exit selection mode when view/filter changes
useEffect(() => {
setIsAddingNew(false)
setEditingBookmarkId(null)
- }, [filterView, selectedTag, currentView])
+ exitSelectionMode()
+ }, [filterView, selectedTag, currentView, exitSelectionMode])
useEffect(() => {
if (selectedIndex >= 0 && selectedItemRef.current) {
@@ -236,13 +358,18 @@ export function BookmarkList() {
'shift+?': showHelp,
'j': selectNext,
'k': selectPrev,
+ 'shift+j': selectNextWithShift,
+ 'shift+k': selectPrevWithShift,
'enter': openSelected,
'e': editSelected,
- 'd': deleteSelected,
+ 'd': selectionMode && selectedIds.size > 0 ? handleBulkDelete : deleteSelected,
'mod+k': focusSearch,
'q': exitInbox,
'mod+z': handleUndo,
'mod+shift+z': handleRedo,
+ 'escape': exitSelectionMode,
+ 'space': toggleSelectCurrent,
+ 'mod+a': selectAllBookmarks,
})
const handleAddNew = openNewBookmarkForm
@@ -338,6 +465,8 @@ export function BookmarkList() {
onToggleSidebar={() => setIsSidebarOpen(!isSidebarOpen)}
onAddNew={handleAddNew}
searchInputRef={searchInputRef}
+ selectionMode={selectionMode}
+ onToggleSelectionMode={toggleSelectionMode}
/>
@@ -389,9 +518,12 @@ export function BookmarkList() {
ref={index === selectedIndex ? selectedItemRef : null}
bookmark={bookmark}
isSelected={index === selectedIndex}
+ isChecked={selectedIds.has(bookmark._id)}
+ selectionMode={selectionMode}
onEdit={handleEdit}
onDelete={handleDelete}
onTagClick={handleTagClick}
+ onToggleSelect={toggleSelectBookmark}
/>
)
))
@@ -412,6 +544,12 @@ export function BookmarkList() {
setIsHelpOpen(false)} />
+
+
)
diff --git a/src/components/bookmarks/FilterBar.jsx b/src/components/bookmarks/FilterBar.jsx
index 1fe7448..68f2917 100644
--- a/src/components/bookmarks/FilterBar.jsx
+++ b/src/components/bookmarks/FilterBar.jsx
@@ -1,4 +1,4 @@
-import { X, ChevronDown, Menu, Search, Plus } from '../ui/Icons'
+import { X, ChevronDown, Menu, Search, Plus, ListChecks } from '../ui/Icons'
export function FilterBar({
searchQuery,
@@ -9,6 +9,8 @@ export function FilterBar({
onToggleSidebar,
onAddNew,
searchInputRef,
+ selectionMode,
+ onToggleSelectionMode,
}) {
const handleKeyDown = (e) => {
if (e.key === 'Enter') {
@@ -69,9 +71,23 @@ export function FilterBar({
+
+