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'}