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:
@@ -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 />
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -42,4 +50,4 @@ export function KeyboardHints() {
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user