From 2a01f443740a595d3be9efefcf65ff7d3671c83d Mon Sep 17 00:00:00 2001 From: lucaronin Date: Sat, 21 Feb 2026 16:54:59 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20trash=20notes=20=E2=80=94=20sidebar=20f?= =?UTF-8?q?ilter,=20list=20view,=20relationship=20indicators?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Sidebar: replaced disabled Trash placeholder with active filter showing count badge and destructive color when selected - Sidebar: trashed notes excluded from section groups and All Notes count - NoteList: trash filter shows only trashed entries with TRASHED badge - NoteList: 30-day warning banner when expired trash notes exist - NoteList: per-note warning text for notes past 30-day retention - Inspector: trash icon shown before name in relationship pills for trashed entries, with (trashed) label and dimmed styling - Inspector: backlinks show trash icon for trashed entries Co-Authored-By: Claude Opus 4.6 --- src/components/Inspector.tsx | 33 +++++++++++++-------- src/components/NoteList.tsx | 57 +++++++++++++++++++++++++++++++----- src/components/Sidebar.tsx | 25 ++++++++++++---- 3 files changed, 89 insertions(+), 26 deletions(-) diff --git a/src/components/Inspector.tsx b/src/components/Inspector.tsx index 6dca232a..95a03645 100644 --- a/src/components/Inspector.tsx +++ b/src/components/Inspector.tsx @@ -4,7 +4,7 @@ import type { VaultEntry, GitCommit } from '../types' import { cn } from '@/lib/utils' import { SlidersHorizontal, X, Wrench, Flask, Target, ArrowsClockwise, - Users, CalendarBlank, Tag, FileText, StackSimple, + Users, CalendarBlank, Tag, FileText, StackSimple, Trash, } from '@phosphor-icons/react' import { parseFrontmatter, type ParsedFrontmatter } from '../utils/frontmatter' import { DynamicPropertiesPanel, RELATIONSHIP_KEYS, containsWikilinks } from './DynamicPropertiesPanel' @@ -84,6 +84,8 @@ function RelationshipGroup({ label, refs, entries, onNavigate }: { label: string const resolved = resolveRef(ref, entries) const refType = resolved?.isA ?? undefined const isArchived = resolved?.archived ?? false + const isTrashed = resolved?.trashed ?? false + const isDimmed = isArchived || isTrashed const color = refType ? getTypeColor(refType) : 'var(--accent-blue)' const bgColor = refType ? getTypeLightColor(refType) : 'var(--accent-blue-light)' const TypeIcon = getTypeIcon(refType) @@ -92,19 +94,21 @@ function RelationshipGroup({ label, refs, entries, onNavigate }: { label: string key={`${ref}-${idx}`} className="flex items-center justify-between gap-2 border-none text-left cursor-pointer hover:opacity-80" style={{ - background: isArchived ? 'var(--muted)' : bgColor, - color: isArchived ? 'var(--muted-foreground)' : color, + background: isDimmed ? 'var(--muted)' : bgColor, + color: isDimmed ? 'var(--muted-foreground)' : color, borderRadius: 6, padding: '6px 10px', fontSize: 12, fontWeight: 500, - opacity: isArchived ? 0.7 : 1, + opacity: isDimmed ? 0.7 : 1, }} onClick={() => onNavigate(wikilinkTarget(ref))} - title={isArchived ? 'Archived' : undefined} + title={isTrashed ? 'Trashed' : isArchived ? 'Archived' : undefined} > - + + {isTrashed && } {wikilinkDisplay(ref)} - {isArchived && (archived)} + {isTrashed && (trashed)} + {isArchived && !isTrashed && (archived)} - + ) })} @@ -182,19 +186,22 @@ function BacklinksPanel({ backlinks, onNavigate }: { backlinks: VaultEntry[]; on {backlinks.map((e) => { const color = e.isA ? getTypeColor(e.isA) : 'var(--accent-blue)' const TypeIcon = getTypeIcon(e.isA ?? undefined) + const isDimmed = e.archived || e.trashed return ( ) })} diff --git a/src/components/NoteList.tsx b/src/components/NoteList.tsx index 3ef91fcd..a54ea81d 100644 --- a/src/components/NoteList.tsx +++ b/src/components/NoteList.tsx @@ -6,6 +6,7 @@ import { Input } from '@/components/ui/input' import { MagnifyingGlass, Plus, Wrench, Flask, Target, ArrowsClockwise, Users, CalendarBlank, Tag, FileText, CaretDown, CaretRight, StackSimple, + Trash, Warning, } from '@phosphor-icons/react' import type { ComponentType, SVGAttributes } from 'react' import { getTypeColor, getTypeLightColor } from '../utils/typeColors' @@ -203,20 +204,22 @@ export function filterEntries(entries: VaultEntry[], selection: SidebarSelection case 'filter': switch (selection.filter) { case 'all': - return entries.filter((e) => !e.archived) + return entries.filter((e) => !e.archived && !e.trashed) case 'favorites': return [] case 'archived': - return entries.filter((e) => e.archived) + return entries.filter((e) => e.archived && !e.trashed) + case 'trash': + return entries.filter((e) => e.trashed) } break case 'sectionGroup': - return entries.filter((e) => e.isA === selection.type && !e.archived) + return entries.filter((e) => e.isA === selection.type && !e.archived && !e.trashed) case 'entity': return [] case 'topic': { const topic = selection.entry - return entries.filter((e) => refsMatch(e.relatedTo, topic) && !e.archived) + return entries.filter((e) => refsMatch(e.relatedTo, topic) && !e.archived && !e.trashed) } } } @@ -228,6 +231,7 @@ function NoteListInner({ entries, selection, selectedNote, allContent, modifiedF const isEntityView = selection.kind === 'entity' const isSectionGroup = selection.kind === 'sectionGroup' + const isTrashView = selection.kind === 'filter' && selection.filter === 'trash' const toggleGroup = useCallback((label: string) => { setCollapsedGroups((prev) => { @@ -289,6 +293,12 @@ function NoteListInner({ entries, selection, selectedNote, allContent, modifiedF ) + const expiredTrashCount = useMemo(() => { + if (!isTrashView) return 0 + const now = Date.now() / 1000 + return searched.filter((e) => e.trashedAt && (now - e.trashedAt) >= 86400 * 30).length + }, [isTrashView, searched]) + const renderItem = useCallback((entry: VaultEntry, isPinned = false) => { const isSelected = selectedNote?.path === entry.path && !isPinned const te = typeEntryMap[entry.isA ?? ''] @@ -334,13 +344,23 @@ function NoteListInner({ entries, selection, selectedNote, allContent, modifiedF ARCHIVED )} + {entry.trashed && ( + + TRASHED + + )}
{entry.snippet}
-
- {relativeDate(getDisplayDate(entry))} +
= 86400 * 30 ? { color: 'var(--destructive)', fontWeight: 500 } : undefined}> + {entry.trashed && entry.trashedAt + ? `Trashed ${relativeDate(entry.trashedAt)}${(Date.now() / 1000 - entry.trashedAt) >= 86400 * 30 ? ' — will be permanently deleted' : ''}` + : relativeDate(getDisplayDate(entry))}
) @@ -423,7 +443,9 @@ function NoteListInner({ entries, selection, selectedNote, allContent, modifiedF ? typeDocument.title : selection.kind === 'filter' && (selection as { filter: string }).filter === 'archived' ? 'Archive' - : 'Notes'} + : selection.kind === 'filter' && (selection as { filter: string }).filter === 'trash' + ? 'Trash' + : 'Notes'}