chore: drop static shortcut drawer; inline contextual hints

The bottom-left KeyboardShortcuts drawer duplicated information that
the contextual KeyboardHints pill already shows for the current
selection state. Removing it in favor of the contextual hints alone.

KeyboardHints was previously a fixed top-14 overlay, which collided
with the FilterBar when it was opened — the hints panel covered the
filter controls. Refactored it to render inline in the App header
stack (TopBar / FilterBar / ActiveFilterChips / KeyboardHints /
Timeline) so it flows naturally and never overlaps.

Also:
- Hide hints in loupe mode (the loupe has its own context)
- Replace the deleted shortcuts (Ctrl+A, Trash) with the newly wired
  ones (\\ Filters, / Search, E Loupe) so the hints surface them

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-07 22:15:31 +02:00
parent 6e6672f225
commit 3e322576f2
3 changed files with 26 additions and 167 deletions

View File

@@ -6,7 +6,6 @@ import { RightSidebar } from './components/layout/RightSidebar'
import { TopBar } from './components/layout/TopBar'
import { ScanProgress } from './components/ScanProgress'
import { ToastContainer } from './components/ToastContainer'
import { KeyboardShortcuts } from './components/KeyboardShortcuts'
import { KeyboardHints } from './components/KeyboardHints'
import { LoupeView } from './components/loupe/LoupeView'
import { FilterBar } from './components/filter/FilterBar'
@@ -53,6 +52,7 @@ function App() {
<TopBar />
<FilterBar />
<ActiveFilterChips />
<KeyboardHints />
<div className="flex flex-1 overflow-hidden">
{/* Left Sidebar */}
@@ -79,12 +79,6 @@ function App() {
</div>
</div>
{/* Contextual Keyboard Hints */}
<KeyboardHints />
{/* Keyboard Shortcuts Legend */}
<KeyboardShortcuts />
{/* Scan Progress Indicator */}
<ScanProgress />

View File

@@ -2,27 +2,35 @@ import { usePhotoStore } from '../store/photoStore'
export function KeyboardHints() {
const selectedCount = usePhotoStore((state) => state.selectedPhotos.length)
const viewMode = usePhotoStore((state) => state.viewMode)
const hints = selectedCount > 0 ? [
{ key: '1-5', action: 'Rate' },
{ key: 'P', action: 'Pick' },
{ key: 'X', action: 'Reject' },
{ key: 'Delete', action: 'Trash' },
{ key: 'Esc', action: 'Deselect' },
] : [
{ key: '↑↓←→', action: 'Navigate' },
{ key: 'Click', action: 'Select' },
{ key: 'Shift+Click', action: 'Range' },
{ key: 'Ctrl+A', action: 'Select All' },
{ key: 'Space', action: 'Preview' },
]
// In loupe mode the photo viewer has its own context, so the grid hints
// would just be confusing. Hide them.
if (viewMode === 'loupe') return null
const hints = selectedCount > 0
? [
{ key: '1-5', action: 'Rate' },
{ key: 'P', action: 'Pick' },
{ key: 'X', action: 'Trash' },
{ key: 'E', action: 'Loupe' },
{ key: 'Esc', action: 'Deselect' },
]
: [
{ key: '↑↓←→', action: 'Navigate' },
{ key: 'Click', action: 'Select' },
{ key: 'Shift+Click', action: 'Range' },
{ key: 'Space', action: 'Preview' },
{ key: '\\', action: 'Filters' },
{ key: '/', action: 'Search' },
]
return (
<div className="fixed top-14 left-1/2 z-20 -translate-x-1/2">
<div className="flex items-center gap-3 rounded-full border border-border bg-surface/90 px-4 py-2 shadow-lg backdrop-blur-sm">
<div className="flex justify-center border-b border-border bg-surface/60 px-4 py-1.5">
<div className="flex items-center gap-3">
{hints.map((hint, i) => (
<div key={i} className="flex items-center gap-1.5">
<kbd className="rounded bg-surface-offset px-2 py-0.5 text-xs font-medium text-text">
<kbd className="rounded bg-surface-offset px-2 py-0.5 text-[11px] font-medium text-text">
{hint.key}
</kbd>
<span className="text-xs text-text-muted">{hint.action}</span>

View File

@@ -1,143 +0,0 @@
import { useState } from 'react'
import { Keyboard, ChevronRight, ChevronDown, X } from 'lucide-react'
import clsx from 'clsx'
interface Shortcut {
keys: string[]
description: string
category: 'navigation' | 'selection' | 'actions' | 'view'
}
const shortcuts: Shortcut[] = [
// Navigation
{ keys: ['↑', '↓', '←', '→'], description: 'Navigate photos', category: 'navigation' },
{ keys: ['Space'], description: 'Quick preview', category: 'navigation' },
{ keys: ['Enter'], description: 'Open in loupe view', category: 'navigation' },
// Selection
{ keys: ['Click'], description: 'Select photo', category: 'selection' },
{ keys: ['Shift', 'Click'], description: 'Select range', category: 'selection' },
{ keys: ['Ctrl/Cmd', 'Click'], description: 'Add to selection', category: 'selection' },
{ keys: ['Ctrl/Cmd', 'A'], description: 'Select all', category: 'selection' },
{ keys: ['Escape'], description: 'Clear selection', category: 'selection' },
// Actions
{ keys: ['1-5'], description: 'Set rating', category: 'actions' },
{ keys: ['0'], description: 'Remove rating', category: 'actions' },
{ keys: ['P'], description: 'Pick photo', category: 'actions' },
{ keys: ['X'], description: 'Reject photo', category: 'actions' },
{ keys: ['U'], description: 'Unflag photo', category: 'actions' },
{ keys: ['Delete'], description: 'Move to trash', category: 'actions' },
// View
{ keys: ['Tab'], description: 'Toggle left sidebar', category: 'view' },
{ keys: ['I'], description: 'Toggle info panel', category: 'view' },
{ keys: ['G'], description: 'Grid view', category: 'view' },
{ keys: ['E'], description: 'Loupe view', category: 'view' },
{ keys: ['F'], description: 'Fullscreen', category: 'view' },
]
export function KeyboardShortcuts() {
const [isExpanded, setIsExpanded] = useState(true)
const [isMinimized, setIsMinimized] = useState(false)
const categories = {
navigation: { label: 'Navigation', color: 'text-primary' },
selection: { label: 'Selection', color: 'text-pick' },
actions: { label: 'Actions', color: 'text-star' },
view: { label: 'View', color: 'text-text' },
}
if (isMinimized) {
return (
<div className="fixed bottom-4 left-4 z-30">
<button
onClick={() => setIsMinimized(false)}
className="flex items-center gap-2 rounded-lg border border-border bg-surface/90 px-3 py-2 text-sm backdrop-blur-sm hover:bg-surface"
title="Show keyboard shortcuts"
>
<Keyboard className="h-4 w-4 text-primary" />
<span className="text-text-muted">Shortcuts</span>
</button>
</div>
)
}
return (
<div className="fixed bottom-4 left-4 z-30 w-80 overflow-hidden rounded-lg border border-border bg-surface/95 shadow-xl backdrop-blur-sm">
{/* Header */}
<div className="flex items-center justify-between bg-surface-2 px-3 py-2">
<div className="flex items-center gap-2">
<Keyboard className="h-4 w-4 text-primary" />
<span className="text-sm font-medium text-text">Keyboard Shortcuts</span>
</div>
<div className="flex items-center gap-1">
<button
onClick={() => setIsExpanded(!isExpanded)}
className="rounded p-1 text-text-muted hover:bg-surface-offset hover:text-text"
title={isExpanded ? 'Collapse' : 'Expand'}
>
{isExpanded ? (
<ChevronDown className="h-3 w-3" />
) : (
<ChevronRight className="h-3 w-3" />
)}
</button>
<button
onClick={() => setIsMinimized(true)}
className="rounded p-1 text-text-muted hover:bg-surface-offset hover:text-text"
title="Minimize"
>
<X className="h-3 w-3" />
</button>
</div>
</div>
{/* Content */}
{isExpanded && (
<div className="max-h-96 overflow-y-auto p-2">
{Object.entries(categories).map(([category, { label, color }]) => (
<div key={category} className="mb-3">
<h3 className={clsx('mb-1.5 text-xs font-semibold uppercase', color)}>
{label}
</h3>
<div className="space-y-1">
{shortcuts
.filter(s => s.category === category)
.map((shortcut, i) => (
<div
key={i}
className="flex items-center justify-between rounded px-2 py-1 hover:bg-surface-2"
>
<span className="text-xs text-text-muted">
{shortcut.description}
</span>
<div className="flex items-center gap-1">
{shortcut.keys.map((key, j) => (
<span key={j} className="flex items-center">
<kbd className="rounded bg-surface-offset px-1.5 py-0.5 text-[10px] font-medium text-text">
{key}
</kbd>
{j < shortcut.keys.length - 1 && (
<span className="mx-0.5 text-[10px] text-text-muted">+</span>
)}
</span>
))}
</div>
</div>
))}
</div>
</div>
))}
</div>
)}
{/* Footer Hint */}
{!isExpanded && (
<div className="px-3 pb-2 pt-1">
<p className="text-xs text-text-muted">Click to expand shortcuts list</p>
</div>
)}
</div>
)
}