Files
tolaria/src/components/BreadcrumbBar.tsx
lucaronin 4b75eb46b6 feat: archive button in breadcrumbs bar + cmd+e shortcut
Add archive/unarchive toggle button to the BreadcrumbBar (Archive icon
for normal notes, ArrowUUpLeft icon for archived notes). Add cmd+e
keyboard shortcut to toggle archive state of the active note.

Refactored useAppKeyboard to use action map pattern (reduces cyclomatic
complexity) and extracted BreadcrumbActions from BreadcrumbBar to split
rendering complexity across two functions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 18:45:25 +01:00

178 lines
6.1 KiB
TypeScript

import { memo } from 'react'
import type { VaultEntry } from '../types'
import { cn } from '@/lib/utils'
import {
MagnifyingGlass,
GitBranch,
CursorText,
Sparkle,
SlidersHorizontal,
DotsThree,
Trash,
ArrowCounterClockwise,
Archive,
ArrowUUpLeft,
} from '@phosphor-icons/react'
interface BreadcrumbBarProps {
entry: VaultEntry
wordCount: number
isModified: boolean
showDiffToggle: boolean
diffMode: boolean
diffLoading: boolean
onToggleDiff: () => void
showAIChat?: boolean
onToggleAIChat?: () => void
inspectorCollapsed?: boolean
onToggleInspector?: () => void
onTrash?: () => void
onRestore?: () => void
onArchive?: () => void
onUnarchive?: () => void
}
const DISABLED_ICON_STYLE = { opacity: 0.4, cursor: 'not-allowed' } as const
function BreadcrumbActions({ entry, showDiffToggle, diffMode, diffLoading, onToggleDiff,
showAIChat, onToggleAIChat, inspectorCollapsed, onToggleInspector,
onTrash, onRestore, onArchive, onUnarchive,
}: Omit<BreadcrumbBarProps, 'wordCount' | 'isModified'>) {
return (
<div className="flex items-center" style={{ gap: 12 }}>
<button
className="flex items-center justify-center border-none bg-transparent p-0 text-muted-foreground cursor-pointer hover:text-foreground transition-colors"
title="Search in file"
>
<MagnifyingGlass size={16} />
</button>
{showDiffToggle ? (
<button
className={cn(
"flex items-center justify-center border-none bg-transparent p-0 cursor-pointer transition-colors",
diffMode ? "text-foreground" : "text-muted-foreground hover:text-foreground"
)}
onClick={onToggleDiff}
disabled={diffLoading}
title={diffLoading ? 'Loading diff...' : diffMode ? 'Back to editor' : 'Show diff'}
>
<GitBranch size={16} />
</button>
) : (
<button
className="flex items-center justify-center border-none bg-transparent p-0 text-muted-foreground"
style={DISABLED_ICON_STYLE}
title="No changes"
tabIndex={-1}
>
<GitBranch size={16} />
</button>
)}
<button
className="flex items-center justify-center border-none bg-transparent p-0 text-muted-foreground"
style={DISABLED_ICON_STYLE}
title="Coming soon"
tabIndex={-1}
>
<CursorText size={16} />
</button>
<button
className={cn(
"flex items-center justify-center border-none bg-transparent p-0 cursor-pointer transition-colors",
showAIChat ? "" : "text-muted-foreground hover:text-foreground"
)}
style={showAIChat ? { color: 'var(--primary)' } : undefined}
onClick={onToggleAIChat}
title={showAIChat ? 'Close AI Chat' : 'Open AI Chat'}
>
<Sparkle size={16} weight={showAIChat ? 'fill' : 'regular'} />
</button>
{entry.archived ? (
<button
className="flex items-center justify-center border-none bg-transparent p-0 cursor-pointer transition-colors text-muted-foreground hover:text-foreground"
onClick={onUnarchive}
title="Unarchive (Cmd+E)"
>
<ArrowUUpLeft 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-foreground"
onClick={onArchive}
title="Archive (Cmd+E)"
>
<Archive size={16} />
</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"
onClick={onToggleInspector}
title="Open Properties"
>
<SlidersHorizontal size={16} />
</button>
)}
<button
className="flex items-center justify-center border-none bg-transparent p-0 text-muted-foreground"
style={DISABLED_ICON_STYLE}
title="Coming soon"
tabIndex={-1}
>
<DotsThree size={16} />
</button>
</div>
)
}
export const BreadcrumbBar = memo(function BreadcrumbBar({
entry, wordCount, isModified, ...actionProps
}: BreadcrumbBarProps) {
return (
<div
className="flex shrink-0 items-center justify-between"
style={{
height: 45,
background: 'var(--background)',
borderBottom: '1px solid var(--border)',
padding: '6px 16px',
}}
>
{/* Left: breadcrumb */}
<div className="flex items-center gap-1" style={{ fontSize: 12 }}>
<span className="text-muted-foreground">{entry.isA || 'Note'}</span>
<span className="text-muted-foreground" style={{ margin: '0 2px' }}>&rsaquo;</span>
<span className="font-medium text-foreground">{entry.title}</span>
<span className="text-muted-foreground" style={{ margin: '0 4px' }}>&middot;</span>
<span className="text-muted-foreground">{wordCount.toLocaleString()} words</span>
{isModified && (
<>
<span className="text-muted-foreground" style={{ margin: '0 4px' }}>&middot;</span>
<span className="font-semibold" style={{ color: 'var(--accent-yellow)' }}>M</span>
</>
)}
</div>
{/* Right: action icons */}
<BreadcrumbActions entry={entry} {...actionProps} />
</div>
)
})