feat: archive notes UI — filter, sidebar hiding, relationship indicators

- Add Archive filter in sidebar with count badge
- Hide archived notes from section groups and All Notes count
- Exclude archived notes from sectionGroup and topic filtering
- Show (archived) indicator on relationship chips and backlinks
- Show ARCHIVED badge on note list items
- Display 'Archive' header in NoteList when filter is active

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
lucaronin
2026-02-21 16:36:14 +01:00
parent 4d2f96dfab
commit c0bb2ded0e
3 changed files with 72 additions and 16 deletions

View File

@@ -62,18 +62,18 @@ function wikilinkTarget(ref: string): string {
return pipeIdx !== -1 ? inner.slice(0, pipeIdx) : inner
}
function resolveRefType(ref: string, entries: VaultEntry[]): string | undefined {
function resolveRef(ref: string, entries: VaultEntry[]): VaultEntry | undefined {
const target = wikilinkTarget(ref)
const match = entries.find((e) => {
return entries.find((e) => {
const stem = e.path.replace(/^.*\/Laputa\//, '').replace(/\.md$/, '')
if (stem === target) return true
const fileStem = e.filename.replace(/\.md$/, '')
if (fileStem === target.split('/').pop()) return true
return false
})
return match?.isA ?? undefined
}
function RelationshipGroup({ label, refs, entries, onNavigate }: { label: string; refs: string[]; entries: VaultEntry[]; onNavigate: (target: string) => void }) {
if (refs.length === 0) return null
return (
@@ -81,7 +81,9 @@ function RelationshipGroup({ label, refs, entries, onNavigate }: { label: string
<span className="font-mono-overline mb-1 block text-muted-foreground">{label}</span>
<div className="flex flex-col gap-1">
{refs.map((ref, idx) => {
const refType = resolveRefType(ref, entries)
const resolved = resolveRef(ref, entries)
const refType = resolved?.isA ?? undefined
const isArchived = resolved?.archived ?? false
const color = refType ? getTypeColor(refType) : 'var(--accent-blue)'
const bgColor = refType ? getTypeLightColor(refType) : 'var(--accent-blue-light)'
const TypeIcon = getTypeIcon(refType)
@@ -89,11 +91,20 @@ function RelationshipGroup({ label, refs, entries, onNavigate }: { label: string
<button
key={`${ref}-${idx}`}
className="flex items-center justify-between gap-2 border-none text-left cursor-pointer hover:opacity-80"
style={{ background: bgColor, color, borderRadius: 6, padding: '6px 10px', fontSize: 12, fontWeight: 500 }}
style={{
background: isArchived ? 'var(--muted)' : bgColor,
color: isArchived ? 'var(--muted-foreground)' : color,
borderRadius: 6, padding: '6px 10px', fontSize: 12, fontWeight: 500,
opacity: isArchived ? 0.7 : 1,
}}
onClick={() => onNavigate(wikilinkTarget(ref))}
title={isArchived ? 'Archived' : undefined}
>
<span className="flex-1 truncate">{wikilinkDisplay(ref)}</span>
<TypeIcon width={14} height={14} className="shrink-0" style={{ color }} />
<span className="flex-1 truncate">
{wikilinkDisplay(ref)}
{isArchived && <span style={{ marginLeft: 4, fontSize: 10, opacity: 0.8 }}>(archived)</span>}
</span>
<TypeIcon width={14} height={14} className="shrink-0" style={{ color: isArchived ? 'var(--muted-foreground)' : color }} />
</button>
)
})}
@@ -175,11 +186,15 @@ function BacklinksPanel({ backlinks, onNavigate }: { backlinks: VaultEntry[]; on
<button
key={e.path}
className="flex items-center justify-between gap-2 border-none bg-transparent p-0 py-1 text-left text-[13px] cursor-pointer hover:opacity-80"
style={{ color }}
style={{ color: e.archived ? 'var(--muted-foreground)' : color, opacity: e.archived ? 0.7 : 1 }}
onClick={() => onNavigate(e.title)}
title={e.archived ? 'Archived' : undefined}
>
<span className="flex-1 truncate">{e.title}</span>
{e.isA && <TypeIcon width={14} height={14} className="shrink-0" style={{ color }} />}
<span className="flex-1 truncate">
{e.title}
{e.archived && <span style={{ marginLeft: 4, fontSize: 10, opacity: 0.8 }}>(archived)</span>}
</span>
{e.isA && <TypeIcon width={14} height={14} className="shrink-0" style={{ color: e.archived ? 'var(--muted-foreground)' : color }} />}
</button>
)
})}

View File

@@ -201,18 +201,20 @@ export function filterEntries(entries: VaultEntry[], selection: SidebarSelection
case 'filter':
switch (selection.filter) {
case 'all':
return entries
return entries.filter((e) => !e.archived)
case 'favorites':
return []
case 'archived':
return entries.filter((e) => e.archived)
}
break
case 'sectionGroup':
return entries.filter((e) => e.isA === selection.type)
return entries.filter((e) => e.isA === selection.type && !e.archived)
case 'entity':
return []
case 'topic': {
const topic = selection.entry
return entries.filter((e) => refsMatch(e.relatedTo, topic))
return entries.filter((e) => refsMatch(e.relatedTo, topic) && !e.archived)
}
}
}
@@ -312,6 +314,14 @@ function NoteListInner({ entries, selection, selectedNote, allContent, modifiedF
isSelected ? "font-semibold" : "font-medium"
)}>
{entry.title}
{entry.archived && (
<span
className="ml-1.5 inline-block align-middle text-muted-foreground"
style={{ fontSize: 9, fontWeight: 500, background: 'var(--muted)', borderRadius: 4, padding: '1px 4px', verticalAlign: 'middle' }}
>
ARCHIVED
</span>
)}
</div>
</div>
<div className="mt-0.5 text-[12px] leading-[1.5] text-muted-foreground" style={{ display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden' }}>
@@ -394,7 +404,13 @@ function NoteListInner({ entries, selection, selectedNote, allContent, modifiedF
{/* Header */}
<div className="flex h-[45px] shrink-0 items-center justify-between border-b border-border px-4" data-tauri-drag-region style={{ WebkitAppRegion: 'drag' } as React.CSSProperties}>
<h3 className="m-0 min-w-0 flex-1 truncate text-[14px] font-semibold">
{isEntityView ? selection.entry.title : (typeDocument ? typeDocument.title : 'Notes')}
{isEntityView
? selection.entry.title
: typeDocument
? typeDocument.title
: selection.kind === 'filter' && (selection as { filter: string }).filter === 'archived'
? 'Archive'
: 'Notes'}
</h3>
<div className="flex items-center gap-3" style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}>
<button

View File

@@ -16,6 +16,7 @@ import {
TagSimple,
Trash,
StackSimple,
Archive,
type IconProps,
} from '@phosphor-icons/react'
@@ -88,8 +89,10 @@ export const Sidebar = memo(function Sidebar({ entries, selection, onSelect, onS
[customSectionGroups],
)
const archivedCount = useMemo(() => entries.filter((e) => e.archived).length, [entries])
const renderSection = ({ label, type, Icon }: SectionGroup) => {
const items = entries.filter((e) => e.isA === type)
const items = entries.filter((e) => e.isA === type && !e.archived)
const isCollapsed = collapsed[type] ?? false
const isTopic = type === 'Topic'
const isTypeSection = type === 'Type'
@@ -187,7 +190,7 @@ export const Sidebar = memo(function Sidebar({ entries, selection, onSelect, onS
{/* Top nav — All Notes + Favorites */}
<div className="border-b border-border" style={{ padding: '4px 6px' }}>
{TOP_NAV.map(({ label, filter, Icon }) => {
const count = filter === 'all' ? entries.length : 0
const count = filter === 'all' ? entries.filter((e) => !e.archived).length : 0
return (
<div
key={filter}
@@ -213,6 +216,28 @@ export const Sidebar = memo(function Sidebar({ entries, selection, onSelect, onS
</div>
)
})}
{/* Archive filter */}
<div
className={cn(
"flex cursor-pointer select-none items-center gap-2 rounded transition-colors",
isActive({ kind: 'filter', filter: 'archived' })
? "bg-primary/10 text-primary"
: "text-foreground hover:bg-accent"
)}
style={{ padding: '6px 16px', borderRadius: 4 }}
onClick={() => onSelect({ kind: 'filter', filter: 'archived' })}
>
<Archive size={16} />
<span className="flex-1 text-[13px] font-medium">Archive</span>
{archivedCount > 0 && (
<span
className="flex items-center justify-center text-muted-foreground"
style={{ height: 20, borderRadius: 9999, padding: '0 6px', fontSize: 10, background: 'var(--muted)' }}
>
{archivedCount}
</span>
)}
</div>
{/* Disabled placeholders */}
<div
className="flex select-none items-center gap-2 rounded text-foreground"