feat: breadcrumb trash/restore buttons and cmd+del shortcut

- Added trash icon button in breadcrumb bar for non-trashed notes
- Added restore (arrow counter-clockwise) button for trashed notes
- Trash button sets trashed=true and trashed_at in frontmatter
- Restore button removes trashed/trashed_at from frontmatter
- Added Cmd+Delete keyboard shortcut to trash active note
- Wired handlers through Editor -> App with vault entry updates
- Toast notifications for trash/restore actions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
lucaronin
2026-02-21 17:37:15 +01:00
parent 3764bbd14b
commit d475143e8a
4 changed files with 55 additions and 2 deletions

View File

@@ -112,10 +112,26 @@ function App() {
vault.updateEntry(typeEntry.path, { icon, color })
}, [vault, notes])
const handleTrashNote = useCallback(async (path: string) => {
const now = new Date().toISOString().slice(0, 10)
await notes.handleUpdateFrontmatter(path, 'trashed', true)
await notes.handleUpdateFrontmatter(path, 'trashed_at', now)
vault.updateEntry(path, { trashed: true, trashedAt: Date.now() / 1000 })
setToastMessage('Note moved to trash')
}, [notes, vault, setToastMessage])
const handleRestoreNote = useCallback(async (path: string) => {
await notes.handleUpdateFrontmatter(path, 'trashed', false)
await notes.handleDeleteProperty(path, 'trashed_at')
vault.updateEntry(path, { trashed: false, trashedAt: null })
setToastMessage('Note restored from trash')
}, [notes, vault, setToastMessage])
useAppKeyboard({
onQuickOpen: () => setShowQuickOpen(true),
onCreateNote: openCreateDialog,
onSave: () => setToastMessage('Saved'),
onTrashNote: handleTrashNote,
activeTabPathRef: notes.activeTabPathRef,
handleCloseTabRef: notes.handleCloseTabRef,
})
@@ -193,6 +209,8 @@ function App() {
showAIChat={showAIChat}
onToggleAIChat={() => setShowAIChat(c => !c)}
vaultPath={vaultPath}
onTrashNote={handleTrashNote}
onRestoreNote={handleRestoreNote}
/>
</div>
</div>

View File

@@ -8,6 +8,8 @@ import {
Sparkle,
SlidersHorizontal,
DotsThree,
Trash,
ArrowCounterClockwise,
} from '@phosphor-icons/react'
interface BreadcrumbBarProps {
@@ -22,6 +24,8 @@ interface BreadcrumbBarProps {
onToggleAIChat?: () => void
inspectorCollapsed?: boolean
onToggleInspector?: () => void
onTrash?: () => void
onRestore?: () => void
}
const DISABLED_ICON_STYLE = { opacity: 0.4, cursor: 'not-allowed' } as const
@@ -29,6 +33,7 @@ const DISABLED_ICON_STYLE = { opacity: 0.4, cursor: 'not-allowed' } as const
export const BreadcrumbBar = memo(function BreadcrumbBar({
entry, wordCount, isModified, showDiffToggle, diffMode, diffLoading,
onToggleDiff, showAIChat, onToggleAIChat, inspectorCollapsed, onToggleInspector,
onTrash, onRestore,
}: BreadcrumbBarProps) {
return (
<div
@@ -104,6 +109,23 @@ export const BreadcrumbBar = memo(function BreadcrumbBar({
>
<Sparkle size={16} weight={showAIChat ? 'fill' : 'regular'} />
</button>
{entry.trashed ? (
<button
className="flex items-center justify-center border-none bg-transparent p-0 cursor-pointer transition-colors text-muted-foreground hover:text-foreground"
onClick={onRestore}
title="Restore from trash"
>
<ArrowCounterClockwise size={16} />
</button>
) : (
<button
className="flex items-center justify-center border-none bg-transparent p-0 cursor-pointer transition-colors text-muted-foreground hover:text-destructive"
onClick={onTrash}
title="Move to trash (Cmd+Delete)"
>
<Trash size={16} />
</button>
)}
{inspectorCollapsed && (
<button
className="flex items-center justify-center border-none bg-transparent p-0 text-muted-foreground cursor-pointer hover:text-foreground transition-colors"

View File

@@ -48,6 +48,8 @@ interface EditorProps {
showAIChat?: boolean
onToggleAIChat?: () => void
vaultPath?: string
onTrashNote?: (path: string) => void
onRestoreNote?: (path: string) => void
}
// --- Custom Inline Content: WikiLink ---
@@ -151,6 +153,7 @@ export const Editor = memo(function Editor({
onUpdateFrontmatter, onDeleteProperty, onAddProperty,
showAIChat, onToggleAIChat,
vaultPath,
onTrashNote, onRestoreNote,
}: EditorProps) {
const [diffMode, setDiffMode] = useState(false)
const [diffContent, setDiffContent] = useState<string | null>(null)
@@ -351,6 +354,8 @@ export const Editor = memo(function Editor({
onToggleAIChat={onToggleAIChat}
inspectorCollapsed={inspectorCollapsed}
onToggleInspector={onToggleInspector}
onTrash={onTrashNote ? () => onTrashNote(activeTab.entry.path) : undefined}
onRestore={onRestoreNote ? () => onRestoreNote(activeTab.entry.path) : undefined}
/>
) : null

View File

@@ -4,12 +4,13 @@ interface KeyboardActions {
onQuickOpen: () => void
onCreateNote: () => void
onSave: () => void
onTrashNote: (path: string) => void
activeTabPathRef: React.MutableRefObject<string | null>
handleCloseTabRef: React.MutableRefObject<(path: string) => void>
}
export function useAppKeyboard({
onQuickOpen, onCreateNote, onSave,
onQuickOpen, onCreateNote, onSave, onTrashNote,
activeTabPathRef, handleCloseTabRef,
}: KeyboardActions) {
useEffect(() => {
@@ -36,9 +37,16 @@ export function useAppKeyboard({
if (path) handleCloseTabRef.current(path)
break
}
case 'Backspace':
case 'Delete': {
e.preventDefault()
const path = activeTabPathRef.current
if (path) onTrashNote(path)
break
}
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [onQuickOpen, onCreateNote, onSave, activeTabPathRef, handleCloseTabRef])
}, [onQuickOpen, onCreateNote, onSave, onTrashNote, activeTabPathRef, handleCloseTabRef])
}