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:
@@ -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>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user