From f34a1d1b0eaa63c6a5a1d84d585a42d6145be54a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Feb 2026 10:29:38 +0000 Subject: [PATCH 1/7] feat: add Linear-style keyboard shortcuts Add quick action shortcuts for power users: - Shift+T: Open tag edit modal for selected bookmark - Shift+L: Toggle read later status - c: Copy URL to clipboard - o: Open selected bookmark (alias for Enter) Also adds QuickTagModal component for fast tag editing and updates HelpModal to document all shortcuts including d, g i, undo/redo. https://claude.ai/code/session_011LNAQ1835Z6ACAzS3fDeMv --- src/components/bookmarks/BookmarkList.jsx | 71 ++++++++++++++++++++ src/components/ui/HelpModal.jsx | 13 ++++ src/components/ui/QuickTagModal.jsx | 81 +++++++++++++++++++++++ 3 files changed, 165 insertions(+) create mode 100644 src/components/ui/QuickTagModal.jsx diff --git a/src/components/bookmarks/BookmarkList.jsx b/src/components/bookmarks/BookmarkList.jsx index 9cfbb44..fdb6d29 100644 --- a/src/components/bookmarks/BookmarkList.jsx +++ b/src/components/bookmarks/BookmarkList.jsx @@ -11,12 +11,14 @@ import { FilterBar } from './FilterBar' import { SelectionActionBar } from './SelectionActionBar' import { SettingsView } from '../ui/SettingsView' import { HelpModal } from '../ui/HelpModal' +import { QuickTagModal } from '../ui/QuickTagModal' import { ToastContainer } from '../ui/Toast' import { PackageOpen } from '../ui/Icons' import { getAllBookmarks, deleteBookmark, bulkDeleteBookmarks, + toggleReadLater, } from '../../services/bookmarks' export function BookmarkList() { @@ -52,6 +54,8 @@ export function BookmarkList() { const [searchQuery, setSearchQuery] = useState('') const [isSidebarOpen, setIsSidebarOpen] = useState(false) const [isHelpOpen, setIsHelpOpen] = useState(false) + const [isTagModalOpen, setIsTagModalOpen] = useState(false) + const [tagModalBookmark, setTagModalBookmark] = useState(null) const [selectedIndex, setSelectedIndex] = useState(-1) const [selectionMode, setSelectionMode] = useState(false) const [selectedIds, setSelectedIds] = useState(new Set()) @@ -335,6 +339,63 @@ export function BookmarkList() { } }, [selectedIds, addToast, exitSelectionMode]) + // Get the currently selected bookmark + const getSelectedBookmark = useCallback(() => { + if (selectedIndex >= 0 && selectedIndex < filteredBookmarks.length) { + return filteredBookmarks[selectedIndex] + } + return null + }, [selectedIndex, filteredBookmarks]) + + // Shift+T: Open tag edit modal for selected bookmark + const openTagModal = useCallback(() => { + if (filterView === 'inbox') return + if (isAddingNew || editingBookmarkId) return + const bookmark = getSelectedBookmark() + if (bookmark) { + setTagModalBookmark(bookmark) + setIsTagModalOpen(true) + } + }, [filterView, isAddingNew, editingBookmarkId, getSelectedBookmark]) + + const closeTagModal = useCallback(() => { + setIsTagModalOpen(false) + setTagModalBookmark(null) + }, []) + + // Shift+L: Toggle read later for selected bookmark + const toggleReadLaterSelected = useCallback(() => { + if (filterView === 'inbox') return + if (isAddingNew || editingBookmarkId) return + const bookmark = getSelectedBookmark() + if (bookmark) { + try { + const newValue = toggleReadLater(bookmark._id) + addToast({ + message: newValue ? 'Added to Read Later' : 'Removed from Read Later', + duration: 2000, + }) + } catch (error) { + console.error('Failed to toggle read later:', error) + } + } + }, [filterView, isAddingNew, editingBookmarkId, getSelectedBookmark, addToast]) + + // c: Copy URL to clipboard + const copySelectedUrl = useCallback(() => { + if (filterView === 'inbox') return + if (isAddingNew || editingBookmarkId) return + const bookmark = getSelectedBookmark() + if (bookmark) { + navigator.clipboard.writeText(bookmark.url).then(() => { + addToast({ message: 'URL copied to clipboard', duration: 2000 }) + }).catch((error) => { + console.error('Failed to copy URL:', error) + addToast({ message: 'Failed to copy URL', duration: 2000 }) + }) + } + }, [filterView, isAddingNew, editingBookmarkId, getSelectedBookmark, addToast]) + useEffect(() => { setSelectedIndex(-1) }, [filteredBookmarks.length, filterView, selectedTag, debouncedSearchQuery]) @@ -364,8 +425,12 @@ export function BookmarkList() { 'shift+j': selectNextWithShift, 'shift+k': selectPrevWithShift, 'enter': openSelected, + 'o': openSelected, 'e': editSelected, 'd': selectionMode && selectedIds.size > 0 ? handleBulkDelete : deleteSelected, + 'shift+t': openTagModal, + 'shift+l': toggleReadLaterSelected, + 'c': copySelectedUrl, 'mod+k': focusSearch, 'q': exitInbox, 'mod+z': handleUndo, @@ -547,6 +612,12 @@ export function BookmarkList() { setIsHelpOpen(false)} /> + + { + if (isOpen) { + // Small delay to ensure dialog is rendered + setTimeout(() => { + tagInputRef.current?.focus() + }, 50) + } + }, [isOpen]) + + const handleTagsChange = useCallback((newTags) => { + if (!bookmark?._id) return + try { + updateBookmark(bookmark._id, { tags: newTags }) + } catch (error) { + console.error('Failed to update tags:', error) + } + }, [bookmark?._id]) + + const handleRemoveTag = useCallback((tagToRemove) => { + if (!bookmark?._id) return + const newTags = (bookmark.tags || []).filter(t => t !== tagToRemove) + handleTagsChange(newTags) + }, [bookmark?._id, bookmark?.tags, handleTagsChange]) + + if (!bookmark) return null + + const currentTags = bookmark.tags || [] + + return ( + !open && onClose()}> + + + + {bookmark.title} + + + +
+ {/* Current tags */} + {currentTags.length > 0 && ( +
+ {currentTags.map((tag) => ( + handleRemoveTag(tag)}> + {tag} + + ))} +
+ )} + + {/* Tag input */} +
+ +
+ + {/* Keyboard hint */} +

+ Press Esc to close +

+
+
+
+ ) +} From ab9ce7655886a45637a5280a37cecf1c2efd07c6 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Feb 2026 10:34:36 +0000 Subject: [PATCH 2/7] feat: redesign tag selector with Linear-style command palette Replace the basic tag modal with a Linear-inspired design: - Bookmark title header at top - Search/filter input field - List of tags with hash icons - Checkmarks for currently applied tags - Number keys (1-9) for instant tag toggling - j/k and arrow key navigation - Enter to toggle, Escape to close - Option to create new tags inline https://claude.ai/code/session_011LNAQ1835Z6ACAzS3fDeMv --- src/components/ui/QuickTagModal.jsx | 261 ++++++++++++++++++++++------ 1 file changed, 207 insertions(+), 54 deletions(-) diff --git a/src/components/ui/QuickTagModal.jsx b/src/components/ui/QuickTagModal.jsx index 36d423e..7505d94 100644 --- a/src/components/ui/QuickTagModal.jsx +++ b/src/components/ui/QuickTagModal.jsx @@ -1,81 +1,234 @@ -import { useEffect, useRef, useCallback } from 'react' -import { Dialog, DialogContent, DialogHeader, DialogTitle } from './dialog' -import { TagInput } from './TagInput' -import { Tag } from './Tag' +import { useState, useEffect, useRef, useMemo, useCallback } from 'react' +import { Hash, Check, Plus } from './Icons' import { getAllTags, updateBookmark } from '../../services/bookmarks' +import { cn } from '@/utils/cn' +/** + * Linear-style tag selector modal + * - Shows bookmark title at top + * - Search/filter input + * - List of tags with checkmarks for selected, number keys for quick select + * - Keyboard navigation (j/k, arrows, numbers 1-9) + */ export function QuickTagModal({ isOpen, onClose, bookmark }) { - const tagInputRef = useRef(null) + const [searchQuery, setSearchQuery] = useState('') + const [selectedIndex, setSelectedIndex] = useState(0) + const inputRef = useRef(null) + const listRef = useRef(null) + const allTags = getAllTags() + const currentTags = bookmark?.tags || [] + + // Filter tags based on search, and include "create new" option + const filteredOptions = useMemo(() => { + const query = searchQuery.toLowerCase().trim() - // Focus tag input when modal opens + // Filter existing tags + const matchingTags = allTags + .filter(tag => !query || tag.toLowerCase().includes(query)) + .map(tag => ({ + type: 'existing', + value: tag, + isSelected: currentTags.includes(tag), + })) + + // Add "create new" option if query doesn't match existing tag exactly + const exactMatch = allTags.some(tag => tag.toLowerCase() === query) + if (query && !exactMatch) { + matchingTags.push({ + type: 'create', + value: query, + isSelected: false, + }) + } + + return matchingTags + }, [allTags, currentTags, searchQuery]) + + // Reset state when modal opens useEffect(() => { if (isOpen) { - // Small delay to ensure dialog is rendered - setTimeout(() => { - tagInputRef.current?.focus() - }, 50) + setSearchQuery('') + setSelectedIndex(0) + setTimeout(() => inputRef.current?.focus(), 50) } }, [isOpen]) - const handleTagsChange = useCallback((newTags) => { + // Keep selected index in bounds + useEffect(() => { + if (selectedIndex >= filteredOptions.length) { + setSelectedIndex(Math.max(0, filteredOptions.length - 1)) + } + }, [filteredOptions.length, selectedIndex]) + + // Scroll selected item into view + useEffect(() => { + if (listRef.current && filteredOptions.length > 0) { + const selectedItem = listRef.current.children[selectedIndex] + selectedItem?.scrollIntoView({ block: 'nearest' }) + } + }, [selectedIndex, filteredOptions.length]) + + const toggleTag = useCallback((tagValue) => { if (!bookmark?._id) return + + const newTags = currentTags.includes(tagValue) + ? currentTags.filter(t => t !== tagValue) + : [...currentTags, tagValue.toLowerCase()] + try { updateBookmark(bookmark._id, { tags: newTags }) } catch (error) { console.error('Failed to update tags:', error) } - }, [bookmark?._id]) - - const handleRemoveTag = useCallback((tagToRemove) => { - if (!bookmark?._id) return - const newTags = (bookmark.tags || []).filter(t => t !== tagToRemove) - handleTagsChange(newTags) - }, [bookmark?._id, bookmark?.tags, handleTagsChange]) + }, [bookmark?._id, currentTags]) - if (!bookmark) return null + const handleKeyDown = useCallback((e) => { + switch (e.key) { + case 'ArrowDown': + case 'j': + e.preventDefault() + setSelectedIndex(prev => + prev < filteredOptions.length - 1 ? prev + 1 : 0 + ) + break + case 'ArrowUp': + case 'k': + e.preventDefault() + setSelectedIndex(prev => + prev > 0 ? prev - 1 : filteredOptions.length - 1 + ) + break + case 'Enter': + e.preventDefault() + if (filteredOptions[selectedIndex]) { + toggleTag(filteredOptions[selectedIndex].value) + } + break + case 'Escape': + e.preventDefault() + onClose() + break + default: + // Number keys 1-9 for quick selection + if (e.key >= '1' && e.key <= '9') { + const index = parseInt(e.key) - 1 + if (index < filteredOptions.length) { + e.preventDefault() + toggleTag(filteredOptions[index].value) + } + } + } + }, [filteredOptions, selectedIndex, toggleTag, onClose]) - const currentTags = bookmark.tags || [] + if (!isOpen || !bookmark) return null return ( - !open && onClose()}> - - - +
+ {/* Backdrop */} +
+ + {/* Modal */} +
e.stopPropagation()} + onKeyDown={handleKeyDown} + > + {/* Header - Bookmark title */} +
+ {bookmark.title} - - - -
- {/* Current tags */} - {currentTags.length > 0 && ( -
- {currentTags.map((tag) => ( - handleRemoveTag(tag)}> - {tag} - - ))} + +
+ + {/* Search input */} +
+ setSearchQuery(e.target.value)} + placeholder="Change tags..." + className="w-full bg-transparent text-foreground placeholder:text-muted-foreground text-sm outline-none" + autoComplete="off" + /> +
+ + {/* Options list */} +
+ {filteredOptions.length === 0 ? ( +
+ No tags found
+ ) : ( + filteredOptions.map((option, index) => ( +
toggleTag(option.value)} + onMouseEnter={() => setSelectedIndex(index)} + className={cn( + 'flex items-center gap-3 px-3 py-2.5 cursor-pointer transition-colors', + index === selectedIndex + ? 'bg-accent' + : 'hover:bg-accent/50' + )} + > + {/* Icon */} +
+ {option.type === 'create' ? ( + + ) : ( + + )} +
+ + {/* Label */} + + {option.type === 'create' ? ( + <>Create "{option.value}" + ) : ( + option.value + )} + + + {/* Checkmark for selected */} + {option.isSelected && ( + + )} + + {/* Number key hint (1-9) */} + {index < 9 && ( + + {index + 1} + + )} +
+ )) )} +
- {/* Tag input */} -
- -
- - {/* Keyboard hint */} -

- Press Esc to close -

+ {/* Footer hints */} +
+ + ↑↓ + navigate + + + Enter + toggle + + + Esc + close +
- -
+ + ) } From bf97579da76c2ec57420be8ab1ef2a0a2aea91fa Mon Sep 17 00:00:00 2001 From: pheuberger Date: Thu, 5 Feb 2026 12:54:12 +0200 Subject: [PATCH 3/7] fix: improve tag selector keyboard behavior - Use Ctrl+j/k instead of j/k for navigation (avoids conflict with typing) - Add Space key to toggle tags (Linear-style) - Add checkbox UI on left side for selected tags - Keep arrow keys and number keys (1-9) for navigation/quick toggle --- src/components/ui/QuickTagModal.jsx | 97 +++++++++++++++++------------ 1 file changed, 56 insertions(+), 41 deletions(-) diff --git a/src/components/ui/QuickTagModal.jsx b/src/components/ui/QuickTagModal.jsx index 7505d94..7d0b3bf 100644 --- a/src/components/ui/QuickTagModal.jsx +++ b/src/components/ui/QuickTagModal.jsx @@ -7,8 +7,8 @@ import { cn } from '@/utils/cn' * Linear-style tag selector modal * - Shows bookmark title at top * - Search/filter input - * - List of tags with checkmarks for selected, number keys for quick select - * - Keyboard navigation (j/k, arrows, numbers 1-9) + * - List of tags with checkboxes for selected + * - Keyboard navigation: Ctrl+j/k or arrows, Space to toggle, 1-9 for quick select */ export function QuickTagModal({ isOpen, onClose, bookmark }) { const [searchQuery, setSearchQuery] = useState('') @@ -84,40 +84,48 @@ export function QuickTagModal({ isOpen, onClose, bookmark }) { }, [bookmark?._id, currentTags]) const handleKeyDown = useCallback((e) => { - switch (e.key) { - case 'ArrowDown': - case 'j': - e.preventDefault() - setSelectedIndex(prev => - prev < filteredOptions.length - 1 ? prev + 1 : 0 - ) - break - case 'ArrowUp': - case 'k': - e.preventDefault() - setSelectedIndex(prev => - prev > 0 ? prev - 1 : filteredOptions.length - 1 - ) - break - case 'Enter': - e.preventDefault() - if (filteredOptions[selectedIndex]) { - toggleTag(filteredOptions[selectedIndex].value) - } - break - case 'Escape': + const isModified = e.ctrlKey || e.metaKey + + // Ctrl+j / Ctrl+k or Arrow keys for navigation + if (e.key === 'ArrowDown' || (isModified && e.key === 'j')) { + e.preventDefault() + setSelectedIndex(prev => + prev < filteredOptions.length - 1 ? prev + 1 : 0 + ) + return + } + + if (e.key === 'ArrowUp' || (isModified && e.key === 'k')) { + e.preventDefault() + setSelectedIndex(prev => + prev > 0 ? prev - 1 : filteredOptions.length - 1 + ) + return + } + + // Space or Enter to toggle selected tag + if (e.key === ' ' || e.key === 'Enter') { + e.preventDefault() + if (filteredOptions[selectedIndex]) { + toggleTag(filteredOptions[selectedIndex].value) + } + return + } + + // Escape to close + if (e.key === 'Escape') { + e.preventDefault() + onClose() + return + } + + // Number keys 1-9 for quick toggle + if (e.key >= '1' && e.key <= '9' && !isModified) { + const index = parseInt(e.key) - 1 + if (index < filteredOptions.length) { e.preventDefault() - onClose() - break - default: - // Number keys 1-9 for quick selection - if (e.key >= '1' && e.key <= '9') { - const index = parseInt(e.key) - 1 - if (index < filteredOptions.length) { - e.preventDefault() - toggleTag(filteredOptions[index].value) - } - } + toggleTag(filteredOptions[index].value) + } } }, [filteredOptions, selectedIndex, toggleTag, onClose]) @@ -179,6 +187,18 @@ export function QuickTagModal({ isOpen, onClose, bookmark }) { : 'hover:bg-accent/50' )} > + {/* Checkbox */} +
+ {option.isSelected && ( + + )} +
+ {/* Icon */}
{option.type === 'create' ? ( @@ -197,11 +217,6 @@ export function QuickTagModal({ isOpen, onClose, bookmark }) { )} - {/* Checkmark for selected */} - {option.isSelected && ( - - )} - {/* Number key hint (1-9) */} {index < 9 && ( @@ -220,7 +235,7 @@ export function QuickTagModal({ isOpen, onClose, bookmark }) { navigate - Enter + Space toggle From 49f7bd8dbd1ef5eb00d7323d7ea20f15c0a6ef69 Mon Sep 17 00:00:00 2001 From: pheuberger Date: Thu, 5 Feb 2026 12:58:31 +0200 Subject: [PATCH 4/7] fix: batch tag changes and save on Enter Tag selector now works like Linear's label picker: - Track tag selections locally (not applied immediately) - Space: toggle tag selection - Enter: save all changes and close - Escape: discard changes and close - Arrow keys or Ctrl+j/k: navigate - 1-9: quick toggle by position This fixes the stale state issues and provides cleaner UX. --- src/components/ui/QuickTagModal.jsx | 52 +++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/src/components/ui/QuickTagModal.jsx b/src/components/ui/QuickTagModal.jsx index 7d0b3bf..7d0d563 100644 --- a/src/components/ui/QuickTagModal.jsx +++ b/src/components/ui/QuickTagModal.jsx @@ -13,11 +13,18 @@ import { cn } from '@/utils/cn' export function QuickTagModal({ isOpen, onClose, bookmark }) { const [searchQuery, setSearchQuery] = useState('') const [selectedIndex, setSelectedIndex] = useState(0) + const [localTags, setLocalTags] = useState([]) const inputRef = useRef(null) const listRef = useRef(null) const allTags = getAllTags() - const currentTags = bookmark?.tags || [] + + // Sync local tags when modal opens or bookmark changes + useEffect(() => { + if (isOpen && bookmark) { + setLocalTags(bookmark.tags || []) + } + }, [isOpen, bookmark?._id]) // Filter tags based on search, and include "create new" option const filteredOptions = useMemo(() => { @@ -29,7 +36,7 @@ export function QuickTagModal({ isOpen, onClose, bookmark }) { .map(tag => ({ type: 'existing', value: tag, - isSelected: currentTags.includes(tag), + isSelected: localTags.includes(tag), })) // Add "create new" option if query doesn't match existing tag exactly @@ -43,7 +50,7 @@ export function QuickTagModal({ isOpen, onClose, bookmark }) { } return matchingTags - }, [allTags, currentTags, searchQuery]) + }, [allTags, localTags, searchQuery]) // Reset state when modal opens useEffect(() => { @@ -70,18 +77,24 @@ export function QuickTagModal({ isOpen, onClose, bookmark }) { }, [selectedIndex, filteredOptions.length]) const toggleTag = useCallback((tagValue) => { - if (!bookmark?._id) return + const normalizedTag = tagValue.toLowerCase() + setLocalTags(prev => + prev.includes(normalizedTag) + ? prev.filter(t => t !== normalizedTag) + : [...prev, normalizedTag] + ) + }, []) - const newTags = currentTags.includes(tagValue) - ? currentTags.filter(t => t !== tagValue) - : [...currentTags, tagValue.toLowerCase()] + const saveAndClose = useCallback(() => { + if (!bookmark?._id) return try { - updateBookmark(bookmark._id, { tags: newTags }) + updateBookmark(bookmark._id, { tags: localTags }) } catch (error) { console.error('Failed to update tags:', error) } - }, [bookmark?._id, currentTags]) + onClose() + }, [bookmark?._id, localTags, onClose]) const handleKeyDown = useCallback((e) => { const isModified = e.ctrlKey || e.metaKey @@ -103,8 +116,8 @@ export function QuickTagModal({ isOpen, onClose, bookmark }) { return } - // Space or Enter to toggle selected tag - if (e.key === ' ' || e.key === 'Enter') { + // Space to toggle selected tag + if (e.key === ' ') { e.preventDefault() if (filteredOptions[selectedIndex]) { toggleTag(filteredOptions[selectedIndex].value) @@ -112,7 +125,14 @@ export function QuickTagModal({ isOpen, onClose, bookmark }) { return } - // Escape to close + // Enter to save and close + if (e.key === 'Enter') { + e.preventDefault() + saveAndClose() + return + } + + // Escape to close without saving if (e.key === 'Escape') { e.preventDefault() onClose() @@ -127,7 +147,7 @@ export function QuickTagModal({ isOpen, onClose, bookmark }) { toggleTag(filteredOptions[index].value) } } - }, [filteredOptions, selectedIndex, toggleTag, onClose]) + }, [filteredOptions, selectedIndex, toggleTag, saveAndClose, onClose]) if (!isOpen || !bookmark) return null @@ -238,9 +258,13 @@ export function QuickTagModal({ isOpen, onClose, bookmark }) { Space toggle + + Enter + save + Esc - close + discard
From 697e0294c8018508a594ba14f9da8b7dfe2df5b0 Mon Sep 17 00:00:00 2001 From: pheuberger Date: Thu, 5 Feb 2026 13:01:03 +0200 Subject: [PATCH 5/7] feat: simplify tag and read later shortcuts to t and l --- src/components/bookmarks/BookmarkList.jsx | 4 ++-- src/components/ui/HelpModal.jsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/bookmarks/BookmarkList.jsx b/src/components/bookmarks/BookmarkList.jsx index fdb6d29..0e6caff 100644 --- a/src/components/bookmarks/BookmarkList.jsx +++ b/src/components/bookmarks/BookmarkList.jsx @@ -428,8 +428,8 @@ export function BookmarkList() { 'o': openSelected, 'e': editSelected, 'd': selectionMode && selectedIds.size > 0 ? handleBulkDelete : deleteSelected, - 'shift+t': openTagModal, - 'shift+l': toggleReadLaterSelected, + 't': openTagModal, + 'l': toggleReadLaterSelected, 'c': copySelectedUrl, 'mod+k': focusSearch, 'q': exitInbox, diff --git a/src/components/ui/HelpModal.jsx b/src/components/ui/HelpModal.jsx index fcf7da8..4d9c297 100644 --- a/src/components/ui/HelpModal.jsx +++ b/src/components/ui/HelpModal.jsx @@ -21,8 +21,8 @@ const HOTKEY_GROUPS = [ { title: 'Quick Actions', hotkeys: [ - { keys: ['Shift', 't'], description: 'Edit tags', modifier: true }, - { keys: ['Shift', 'l'], description: 'Toggle read later', modifier: true }, + { keys: ['t'], description: 'Edit tags' }, + { keys: ['l'], description: 'Toggle read later' }, { keys: ['c'], description: 'Copy URL' }, ], }, From 96454b17c60b08561fdfc77492dd7ee5e52f1ae8 Mon Sep 17 00:00:00 2001 From: pheuberger Date: Thu, 5 Feb 2026 13:03:59 +0200 Subject: [PATCH 6/7] fix: allow creating new tags in tag selector modal --- src/components/ui/QuickTagModal.jsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/ui/QuickTagModal.jsx b/src/components/ui/QuickTagModal.jsx index 7d0d563..71fa8d3 100644 --- a/src/components/ui/QuickTagModal.jsx +++ b/src/components/ui/QuickTagModal.jsx @@ -30,8 +30,11 @@ export function QuickTagModal({ isOpen, onClose, bookmark }) { const filteredOptions = useMemo(() => { const query = searchQuery.toLowerCase().trim() - // Filter existing tags - const matchingTags = allTags + // Combine store tags with any new tags in localTags + const allAvailableTags = [...new Set([...allTags, ...localTags])] + + // Filter tags that match the query + const matchingTags = allAvailableTags .filter(tag => !query || tag.toLowerCase().includes(query)) .map(tag => ({ type: 'existing', @@ -39,8 +42,8 @@ export function QuickTagModal({ isOpen, onClose, bookmark }) { isSelected: localTags.includes(tag), })) - // Add "create new" option if query doesn't match existing tag exactly - const exactMatch = allTags.some(tag => tag.toLowerCase() === query) + // Add "create new" option if query doesn't match any available tag exactly + const exactMatch = allAvailableTags.some(tag => tag.toLowerCase() === query) if (query && !exactMatch) { matchingTags.push({ type: 'create', From 4fa59b9a15a5573e288d88b5ae20a99ff5a11e9e Mon Sep 17 00:00:00 2001 From: pheuberger Date: Thu, 5 Feb 2026 13:05:52 +0200 Subject: [PATCH 7/7] fix: Enter creates and saves new tag when Create option is selected --- src/components/ui/QuickTagModal.jsx | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/components/ui/QuickTagModal.jsx b/src/components/ui/QuickTagModal.jsx index 71fa8d3..49085ac 100644 --- a/src/components/ui/QuickTagModal.jsx +++ b/src/components/ui/QuickTagModal.jsx @@ -128,10 +128,26 @@ export function QuickTagModal({ isOpen, onClose, bookmark }) { return } - // Enter to save and close + // Enter to save and close (also adds the tag if "Create" option is selected) if (e.key === 'Enter') { e.preventDefault() - saveAndClose() + const selectedOption = filteredOptions[selectedIndex] + if (selectedOption?.type === 'create') { + // Add the new tag before saving + const newTags = [...localTags, selectedOption.value.toLowerCase()] + setLocalTags(newTags) + // Save with the new tag included + if (bookmark?._id) { + try { + updateBookmark(bookmark._id, { tags: newTags }) + } catch (error) { + console.error('Failed to update tags:', error) + } + } + onClose() + } else { + saveAndClose() + } return }