The search box on the right edge of the filter bar wasn't pulling its weight — kills it entirely along with the supporting plumbing: - FilterBar: remove input + Search icon import + local/debounced state - filterStore: drop `q`, `setQ`, plus all references in INITIAL_FILTERS, filtersToParams, hasActiveFilters, snapshotFilters - usePhotosQuery: stop passing q through filtersToParams - useFilterUrlSync: drop the `q` URL param read/write - PhotoThumbnail + PreviewView: remove the search-match banner/chip and findSearchMatch helper imports - Timeline + MemoriesView: stop subscribing to / forwarding the prop - useKeyboardShortcuts: drop the `/` and Cmd+F focus hotkeys - KeyboardHints: drop the `/` hint and the now-stale `?` collision note - delete hooks/useSearchQuery.ts (no callers) and lib/searchMatch.ts Backend /photos/search endpoint left untouched — no UI reaches it now. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
137 lines
4.7 KiB
TypeScript
137 lines
4.7 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import { useHotkeys } from 'react-hotkeys-hook'
|
|
import { ChevronDown, ChevronUp } from 'lucide-react'
|
|
import { usePhotoStore } from '../store/photoStore'
|
|
import { useFilterStore } from '../store/filterStore'
|
|
|
|
const STORAGE_KEY = 'keyboard-hints-collapsed'
|
|
|
|
interface Hint {
|
|
key: string
|
|
action: string
|
|
}
|
|
|
|
/** Build the hint list for the current context. Returns an empty array
|
|
* when no shortcuts apply, which lets the caller hide the panel
|
|
* entirely instead of rendering an empty pill. */
|
|
function getHints(opts: {
|
|
selectedCount: number
|
|
currentSection: string
|
|
viewMode: string
|
|
}): Hint[] {
|
|
const { selectedCount, currentSection, viewMode } = opts
|
|
|
|
// Preview mode: culling shortcuts apply to the photo on screen, plus
|
|
// arrow nav between photos and Esc to close.
|
|
if (viewMode === 'preview') {
|
|
const preview: Hint[] = [
|
|
{ key: '←→', action: 'Navigate' },
|
|
{ key: '1-5', action: 'Rate' },
|
|
{ key: 'S', action: 'Select → heap' },
|
|
]
|
|
if (currentSection === 'discarded') {
|
|
preview.push({ key: 'U', action: 'Restore' })
|
|
} else {
|
|
preview.push({ key: 'X', action: 'Discard' })
|
|
}
|
|
preview.push(
|
|
{ key: 'I', action: 'Info panel' },
|
|
{ key: 'Space', action: 'Close' },
|
|
{ key: 'Esc', action: 'Close' }
|
|
)
|
|
return preview
|
|
}
|
|
|
|
if (selectedCount > 0) {
|
|
const base: Hint[] = [
|
|
{ key: '1-5', action: 'Rate' },
|
|
{ key: 'S', action: 'Select → heap' },
|
|
]
|
|
if (currentSection === 'discarded') {
|
|
base.push({ key: 'U', action: 'Restore' })
|
|
} else {
|
|
base.push({ key: 'X', action: 'Discard' })
|
|
}
|
|
base.push(
|
|
{ key: 'Space', action: 'Preview' },
|
|
{ key: 'I', action: 'Info panel' },
|
|
{ key: 'Esc', action: 'Deselect' }
|
|
)
|
|
return base
|
|
}
|
|
|
|
return [
|
|
{ key: '↑↓←→', action: 'Navigate' },
|
|
{ key: 'Space', action: 'Preview' },
|
|
{ key: 'Tab', action: 'Library panel' },
|
|
{ key: 'I', action: 'Info panel' },
|
|
]
|
|
}
|
|
|
|
export function KeyboardHints() {
|
|
const selectedCount = usePhotoStore((s) => s.selectedPhotos.length)
|
|
const viewMode = usePhotoStore((s) => s.viewMode)
|
|
const currentSection = useFilterStore((s) => s.currentSection)
|
|
|
|
const [collapsed, setCollapsed] = useState(
|
|
() => typeof window !== 'undefined' && localStorage.getItem(STORAGE_KEY) === '1'
|
|
)
|
|
useEffect(() => {
|
|
localStorage.setItem(STORAGE_KEY, collapsed ? '1' : '0')
|
|
}, [collapsed])
|
|
|
|
// `H` toggles the panel.
|
|
useHotkeys('h', () => setCollapsed((c) => !c), { preventDefault: true })
|
|
|
|
const hints = getHints({ selectedCount, currentSection, viewMode })
|
|
|
|
// Nothing relevant to show — hide entirely.
|
|
if (hints.length === 0) return null
|
|
|
|
return (
|
|
<div className="pointer-events-none absolute bottom-0 left-1/2 z-30 -translate-x-1/2 pb-4">
|
|
{collapsed ? (
|
|
// Collapsed handle: a small pill peeking from the bottom so the
|
|
// user can re-open the panel without remembering the shortcut.
|
|
<button
|
|
type="button"
|
|
onClick={() => setCollapsed(false)}
|
|
className="pointer-events-auto flex items-center gap-1.5 rounded-full border border-white/15 bg-black/80 px-3 py-1 text-[11px] text-white/80 shadow-xl backdrop-blur-md transition-colors hover:bg-black/90 hover:text-white"
|
|
title="Show shortcuts (H)"
|
|
>
|
|
<ChevronUp className="h-3 w-3" />
|
|
Shortcuts
|
|
<kbd className="rounded bg-white/15 px-1 py-0.5 text-[10px] font-medium text-white">
|
|
H
|
|
</kbd>
|
|
</button>
|
|
) : (
|
|
<div className="pointer-events-auto flex items-center gap-3 whitespace-nowrap rounded-full border border-white/15 bg-black/80 px-4 py-1.5 shadow-xl ring-1 ring-black/40 backdrop-blur-md">
|
|
{hints.map((hint, i) => (
|
|
<div key={i} className="flex items-center gap-1.5">
|
|
<kbd className="rounded bg-white/15 px-1.5 py-0.5 text-[11px] font-medium text-white shadow-sm">
|
|
{hint.key}
|
|
</kbd>
|
|
<span className="whitespace-nowrap text-xs text-white/85">
|
|
{hint.action}
|
|
</span>
|
|
<span className="ml-1 text-white/30">•</span>
|
|
</div>
|
|
))}
|
|
<button
|
|
type="button"
|
|
onClick={() => setCollapsed(true)}
|
|
className="-mr-1 flex items-center gap-1 rounded-full px-1.5 py-0.5 text-[11px] text-white/60 transition-colors hover:bg-white/10 hover:text-white"
|
|
title="Hide shortcuts (H)"
|
|
>
|
|
<kbd className="rounded bg-white/15 px-1 py-0.5 text-[10px] font-medium text-white">
|
|
H
|
|
</kbd>
|
|
<ChevronDown className="h-3 w-3" />
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|